bridge: implement guardian set update submission node admin service
Tested on a live devnet via `scripts/test-injection.sh 0`. ghstack-source-id: 92489c2455e677433414dfa66c6917a577e4c4a5 Pull Request resolved: https://github.com/certusone/wormhole/pull/104
This commit is contained in:
parent
7545d2b803
commit
66430cb5be
|
@ -0,0 +1,81 @@
|
||||||
|
package guardiand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/status-im/keycard-go/hexutils"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/protobuf/encoding/prototext"
|
||||||
|
|
||||||
|
nodev1 "github.com/certusone/wormhole/bridge/pkg/proto/node/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clientSocketPath *string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pf := AdminClientInjectGuardianSetUpdateCmd.Flags()
|
||||||
|
clientSocketPath = pf.String("socket", "", "gRPC admin server socket to connect to")
|
||||||
|
err := cobra.MarkFlagRequired(pf, "socket")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminCmd.AddCommand(AdminClientInjectGuardianSetUpdateCmd)
|
||||||
|
AdminCmd.AddCommand(AdminClientGuardianSetTemplateCmd)
|
||||||
|
AdminCmd.AddCommand(AdminClientGuardianSetVerifyCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var AdminCmd = &cobra.Command{
|
||||||
|
Use: "admin",
|
||||||
|
Short: "Guardian node admin commands",
|
||||||
|
}
|
||||||
|
|
||||||
|
var AdminClientInjectGuardianSetUpdateCmd = &cobra.Command{
|
||||||
|
Use: "guardian-set-update-inject",
|
||||||
|
Short: "Inject and sign a guardian set update from a prototxt file (see docs!)",
|
||||||
|
Run: runInjectGuardianSetUpdate,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAdminClient(ctx context.Context, addr string) (*grpc.ClientConn, error, nodev1.NodePrivilegedClient) {
|
||||||
|
conn, err := grpc.DialContext(ctx, fmt.Sprintf("unix:///%s", addr), grpc.WithInsecure())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect to %s: %v", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := nodev1.NewNodePrivilegedClient(conn)
|
||||||
|
return conn, err, c
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInjectGuardianSetUpdate(cmd *cobra.Command, args []string) {
|
||||||
|
path := args[0]
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err, c := getAdminClient(ctx, *clientSocketPath)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg nodev1.GuardianSetUpdate
|
||||||
|
err = prototext.Unmarshal(b, &msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to deserialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SubmitGuardianSetVAA(ctx, &nodev1.SubmitGuardianSetVAARequest{GuardianSet: &msg})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to submit guardian set update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("VAA successfully injected with digest %s", hexutils.BytesToHex(resp.Digest))
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package guardiand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/certusone/wormhole/bridge/pkg/common"
|
||||||
|
nodev1 "github.com/certusone/wormhole/bridge/pkg/proto/node/v1"
|
||||||
|
"github.com/certusone/wormhole/bridge/pkg/supervisor"
|
||||||
|
"github.com/certusone/wormhole/bridge/pkg/vaa"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodePrivilegedService struct {
|
||||||
|
nodev1.UnimplementedNodePrivilegedServer
|
||||||
|
injectC chan<- *vaa.VAA
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// adminGuardianSetUpdateToVAA converts a nodev1.GuardianSetUpdate message to its canonical VAA representation.
|
||||||
|
// Returns an error if the data is invalid.
|
||||||
|
func adminGuardianSetUpdateToVAA(req *nodev1.GuardianSetUpdate) (*vaa.VAA, error) {
|
||||||
|
if len(req.Guardians) == 0 {
|
||||||
|
return nil, errors.New("empty guardian set specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Guardians) > common.MaxGuardianCount {
|
||||||
|
return nil, fmt.Errorf("too many guardians - %d, maximum is %d", len(req.Guardians), common.MaxGuardianCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs := make([]ethcommon.Address, len(req.Guardians))
|
||||||
|
for i, g := range req.Guardians {
|
||||||
|
if !ethcommon.IsHexAddress(g.Pubkey) {
|
||||||
|
return nil, fmt.Errorf("invalid pubkey format at index %d (%s)", i, g.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs[i] = ethcommon.HexToAddress(g.Pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := &vaa.VAA{
|
||||||
|
Version: vaa.SupportedVAAVersion,
|
||||||
|
GuardianSetIndex: req.CurrentSetIndex,
|
||||||
|
Timestamp: time.Unix(int64(req.Timestamp), 0),
|
||||||
|
Payload: &vaa.BodyGuardianSetUpdate{
|
||||||
|
Keys: addrs,
|
||||||
|
NewIndex: req.CurrentSetIndex + 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *nodePrivilegedService) SubmitGuardianSetVAA(ctx context.Context, req *nodev1.SubmitGuardianSetVAARequest) (*nodev1.SubmitGuardianSetVAAResponse, error) {
|
||||||
|
s.logger.Info("guardian set injected via admin socket", zap.String("request", req.String()))
|
||||||
|
|
||||||
|
v, err := adminGuardianSetUpdateToVAA(req.GuardianSet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate digest of the unsigned VAA.
|
||||||
|
digest, err := v.SigningMsg()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("guardian set VAA constructed",
|
||||||
|
zap.Any("vaa", v),
|
||||||
|
zap.String("digest", digest.String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.injectC <- v
|
||||||
|
|
||||||
|
return &nodev1.SubmitGuardianSetVAAResponse{Digest: digest.Bytes()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminServiceRunnable(logger *zap.Logger, socketPath string, injectC chan<- *vaa.VAA) (supervisor.Runnable, error) {
|
||||||
|
// Delete existing UNIX socket, if present.
|
||||||
|
fi, err := os.Stat(socketPath)
|
||||||
|
if err == nil {
|
||||||
|
fmode := fi.Mode()
|
||||||
|
if fmode&os.ModeType == os.ModeSocket {
|
||||||
|
err = os.Remove(socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to remove existing socket at %s: %w", socketPath, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("%s is not a UNIX socket", socketPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new UNIX socket and listen to it.
|
||||||
|
|
||||||
|
// The socket is created with the default umask. We set a restrictive umask in setRestrictiveUmask
|
||||||
|
// to ensure that any files we create are only readable by the user - this is much harder to mess up.
|
||||||
|
// The umask avoids a race condition between file creation and chmod.
|
||||||
|
|
||||||
|
laddr, err := net.ResolveUnixAddr("unix", socketPath)
|
||||||
|
l, err := net.ListenUnix("unix", laddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to listen on %s: %w", socketPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("listening on", zap.String("path", socketPath))
|
||||||
|
|
||||||
|
nodeService := &nodePrivilegedService{
|
||||||
|
injectC: injectC,
|
||||||
|
logger: logger.Named("adminservice"),
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer()
|
||||||
|
nodev1.RegisterNodePrivilegedServer(grpcServer, nodeService)
|
||||||
|
return supervisor.GRPCServer(grpcServer, l, false), nil
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package guardiand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/protobuf/encoding/prototext"
|
||||||
|
|
||||||
|
"github.com/certusone/wormhole/bridge/pkg/devnet"
|
||||||
|
nodev1 "github.com/certusone/wormhole/bridge/pkg/proto/node/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var templateNumGuardians *int
|
||||||
|
var templateGuardianIndex *int
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
templateNumGuardians = AdminClientGuardianSetTemplateCmd.Flags().Int("num", 1, "Number of devnet guardians in example file")
|
||||||
|
templateGuardianIndex = AdminClientGuardianSetTemplateCmd.Flags().Int("idx", 0, "Default current guardian set index")
|
||||||
|
}
|
||||||
|
|
||||||
|
var AdminClientGuardianSetTemplateCmd = &cobra.Command{
|
||||||
|
Use: "guardian-set-update-template",
|
||||||
|
Short: "Generate an empty guardian set template at specified path (offline)",
|
||||||
|
Run: runGuardianSetTemplate,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGuardianSetTemplate(cmd *cobra.Command, args []string) {
|
||||||
|
path := args[0]
|
||||||
|
|
||||||
|
// Use deterministic devnet addresses as examples in the template, such that this doubles as a test fixture.
|
||||||
|
guardians := make([]*nodev1.GuardianSetUpdate_Guardian, *templateNumGuardians)
|
||||||
|
for i := 0; i < *templateNumGuardians; i++ {
|
||||||
|
k := devnet.DeterministicEcdsaKeyByIndex(crypto.S256(), uint64(i))
|
||||||
|
guardians[i] = &nodev1.GuardianSetUpdate_Guardian{
|
||||||
|
Pubkey: crypto.PubkeyToAddress(k.PublicKey).Hex(),
|
||||||
|
Name: fmt.Sprintf("Example validator %d", i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &nodev1.GuardianSetUpdate{
|
||||||
|
CurrentSetIndex: uint32(*templateGuardianIndex),
|
||||||
|
// Timestamp is hardcoded to make it reproducible on different devnet nodes.
|
||||||
|
// In production, a real UNIX timestamp should be used (see node.proto).
|
||||||
|
Timestamp: 1605744545,
|
||||||
|
Guardians: guardians,
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := prototext.MarshalOptions{Multiline: true}.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(path, b, 0640)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package guardiand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/protobuf/encoding/prototext"
|
||||||
|
|
||||||
|
nodev1 "github.com/certusone/wormhole/bridge/pkg/proto/node/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AdminClientGuardianSetVerifyCmd = &cobra.Command{
|
||||||
|
Use: "guardian-set-update-verify",
|
||||||
|
Short: "Verify guardian set update in prototxt format (offline)",
|
||||||
|
Run: runGuardianSetVerify,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGuardianSetVerify(cmd *cobra.Command, args []string) {
|
||||||
|
path := args[0]
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg nodev1.GuardianSetUpdate
|
||||||
|
err = prototext.Unmarshal(b, &msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to deserialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := adminGuardianSetUpdateToVAA(&msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
digest, err := v.SigningMsg()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("VAA with digest %s: %+v", digest.Hex(), spew.Sdump(v))
|
||||||
|
}
|
|
@ -38,6 +38,8 @@ var (
|
||||||
|
|
||||||
nodeKeyPath *string
|
nodeKeyPath *string
|
||||||
|
|
||||||
|
adminSocketPath *string
|
||||||
|
|
||||||
bridgeKeyPath *string
|
bridgeKeyPath *string
|
||||||
|
|
||||||
ethRPC *string
|
ethRPC *string
|
||||||
|
@ -67,6 +69,8 @@ func init() {
|
||||||
|
|
||||||
nodeKeyPath = BridgeCmd.Flags().String("nodeKey", "", "Path to node key (will be generated if it doesn't exist)")
|
nodeKeyPath = BridgeCmd.Flags().String("nodeKey", "", "Path to node key (will be generated if it doesn't exist)")
|
||||||
|
|
||||||
|
adminSocketPath = BridgeCmd.Flags().String("adminSocket", "", "Admin gRPC service UNIX domain socket path")
|
||||||
|
|
||||||
bridgeKeyPath = BridgeCmd.Flags().String("bridgeKey", "", "Path to guardian key (required)")
|
bridgeKeyPath = BridgeCmd.Flags().String("bridgeKey", "", "Path to guardian key (required)")
|
||||||
|
|
||||||
ethRPC = BridgeCmd.Flags().String("ethRPC", "", "Ethereum RPC URL")
|
ethRPC = BridgeCmd.Flags().String("ethRPC", "", "Ethereum RPC URL")
|
||||||
|
@ -133,6 +137,12 @@ func lockMemory() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setRestrictiveUmask masks the group and world bits. This ensures that key material
|
||||||
|
// and sockets we create aren't accidentally group- or world-readable.
|
||||||
|
func setRestrictiveUmask() {
|
||||||
|
syscall.Umask(0077) // cannot fail
|
||||||
|
}
|
||||||
|
|
||||||
// BridgeCmd represents the bridge command
|
// BridgeCmd represents the bridge command
|
||||||
var BridgeCmd = &cobra.Command{
|
var BridgeCmd = &cobra.Command{
|
||||||
Use: "bridge",
|
Use: "bridge",
|
||||||
|
@ -146,6 +156,7 @@ func runBridge(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
lockMemory()
|
lockMemory()
|
||||||
|
setRestrictiveUmask()
|
||||||
|
|
||||||
// Set up logging. The go-log zap wrapper that libp2p uses is compatible with our
|
// Set up logging. The go-log zap wrapper that libp2p uses is compatible with our
|
||||||
// usage of zap in supervisor, which is nice.
|
// usage of zap in supervisor, which is nice.
|
||||||
|
@ -196,6 +207,9 @@ func runBridge(cmd *cobra.Command, args []string) {
|
||||||
if *bridgeKeyPath == "" {
|
if *bridgeKeyPath == "" {
|
||||||
logger.Fatal("Please specify -bridgeKey")
|
logger.Fatal("Please specify -bridgeKey")
|
||||||
}
|
}
|
||||||
|
if *adminSocketPath == "" {
|
||||||
|
logger.Fatal("Please specify -adminSocket")
|
||||||
|
}
|
||||||
if *agentRPC == "" {
|
if *agentRPC == "" {
|
||||||
logger.Fatal("Please specify -agentRPC")
|
logger.Fatal("Please specify -agentRPC")
|
||||||
}
|
}
|
||||||
|
@ -273,6 +287,9 @@ func runBridge(cmd *cobra.Command, args []string) {
|
||||||
// VAAs to submit to Solana
|
// VAAs to submit to Solana
|
||||||
solanaVaaC := make(chan *vaa.VAA)
|
solanaVaaC := make(chan *vaa.VAA)
|
||||||
|
|
||||||
|
// Injected VAAs (manually generated rather than created via observation)
|
||||||
|
injectC := make(chan *vaa.VAA)
|
||||||
|
|
||||||
// Load p2p private key
|
// Load p2p private key
|
||||||
var priv crypto.PrivKey
|
var priv crypto.PrivKey
|
||||||
if *unsafeDevMode {
|
if *unsafeDevMode {
|
||||||
|
@ -288,6 +305,11 @@ func runBridge(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adminService, err := adminServiceRunnable(logger, *adminSocketPath, injectC)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("failed to create admin service socket", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
// Run supervisor.
|
// Run supervisor.
|
||||||
supervisor.New(rootCtx, logger, func(ctx context.Context) error {
|
supervisor.New(rootCtx, logger, func(ctx context.Context) error {
|
||||||
if err := supervisor.Run(ctx, "p2p", p2p.Run(
|
if err := supervisor.Run(ctx, "p2p", p2p.Run(
|
||||||
|
@ -314,11 +336,15 @@ func runBridge(cmd *cobra.Command, args []string) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := processor.NewProcessor(ctx, lockC, setC, sendC, obsvC, solanaVaaC, gk, *unsafeDevMode, *devNumGuardians, *ethRPC, *terraLCD, *terraChaidID, *terraContract, *terraFeePayer)
|
p := processor.NewProcessor(ctx, lockC, setC, sendC, obsvC, solanaVaaC, injectC, gk, *unsafeDevMode, *devNumGuardians, *ethRPC, *terraLCD, *terraChaidID, *terraContract, *terraFeePayer)
|
||||||
if err := supervisor.Run(ctx, "processor", p.Run); err != nil {
|
if err := supervisor.Run(ctx, "processor", p.Run); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := supervisor.Run(ctx, "admin", adminService); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Started internal services")
|
logger.Info("Started internal services")
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -32,6 +32,7 @@ var KeygenCmd = &cobra.Command{
|
||||||
|
|
||||||
func runKeygen(cmd *cobra.Command, args []string) {
|
func runKeygen(cmd *cobra.Command, args []string) {
|
||||||
lockMemory()
|
lockMemory()
|
||||||
|
setRestrictiveUmask()
|
||||||
|
|
||||||
log.Print("Creating new key at ", args[0])
|
log.Print("Creating new key at ", args[0])
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ func init() {
|
||||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.guardiand.yaml)")
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.guardiand.yaml)")
|
||||||
rootCmd.AddCommand(guardiand.BridgeCmd)
|
rootCmd.AddCommand(guardiand.BridgeCmd)
|
||||||
rootCmd.AddCommand(guardiand.KeygenCmd)
|
rootCmd.AddCommand(guardiand.KeygenCmd)
|
||||||
|
rootCmd.AddCommand(guardiand.AdminCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initConfig reads in config file and ENV variables if set.
|
// initConfig reads in config file and ENV variables if set.
|
||||||
|
|
|
@ -7,6 +7,7 @@ require (
|
||||||
github.com/aristanetworks/goarista v0.0.0-20201012165903-2cb20defcd66 // indirect
|
github.com/aristanetworks/goarista v0.0.0-20201012165903-2cb20defcd66 // indirect
|
||||||
github.com/btcsuite/btcd v0.21.0-beta // indirect
|
github.com/btcsuite/btcd v0.21.0-beta // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.0
|
github.com/cenkalti/backoff/v4 v4.1.0
|
||||||
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||||
github.com/ethereum/go-ethereum v1.9.23
|
github.com/ethereum/go-ethereum v1.9.23
|
||||||
|
@ -45,7 +46,7 @@ require (
|
||||||
github.com/shirou/gopsutil v2.20.9+incompatible // indirect
|
github.com/shirou/gopsutil v2.20.9+incompatible // indirect
|
||||||
github.com/spf13/cobra v1.0.0
|
github.com/spf13/cobra v1.0.0
|
||||||
github.com/spf13/viper v1.6.3
|
github.com/spf13/viper v1.6.3
|
||||||
github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969 // indirect
|
github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.6.1
|
||||||
github.com/tendermint/tendermint v0.33.8 // indirect
|
github.com/tendermint/tendermint v0.33.8 // indirect
|
||||||
github.com/terra-project/terra.go v1.0.1-0.20201113170042-b3bffdc6fd06
|
github.com/terra-project/terra.go v1.0.1-0.20201113170042-b3bffdc6fd06
|
||||||
|
|
|
@ -4,6 +4,15 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: this should be 20, https://github.com/certusone/wormhole/issues/86
|
||||||
|
//
|
||||||
|
// Matching constants:
|
||||||
|
// - MAX_LEN_GUARDIAN_KEYS in Solana contract
|
||||||
|
//
|
||||||
|
// The Eth and Terra contracts do not specify a maximum number and support more than 20,
|
||||||
|
// but presumably, chain-specific transaction size limits will apply at some point (untested).
|
||||||
|
const MaxGuardianCount = 19
|
||||||
|
|
||||||
type GuardianSet struct {
|
type GuardianSet struct {
|
||||||
// Guardian's public keys truncated by the ETH standard hashing mechanism (20 bytes).
|
// Guardian's public keys truncated by the ETH standard hashing mechanism (20 bytes).
|
||||||
Keys []common.Address
|
Keys []common.Address
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/certusone/wormhole/bridge/pkg/supervisor"
|
||||||
|
"github.com/certusone/wormhole/bridge/pkg/vaa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleInjection processes a pre-populated VAA injected locally.
|
||||||
|
func (p *Processor) handleInjection(ctx context.Context, v *vaa.VAA) {
|
||||||
|
// Check if we're in the guardian set.
|
||||||
|
us, ok := p.gs.KeyIndex(p.ourAddr)
|
||||||
|
if !ok {
|
||||||
|
p.logger.Error("we're not in the guardian set - refusing to sign",
|
||||||
|
zap.Uint32("index", p.gs.Index),
|
||||||
|
zap.Stringer("our_addr", p.ourAddr),
|
||||||
|
zap.Any("set", p.gs.KeysAsHexStrings()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate digest of the unsigned VAA.
|
||||||
|
digest, err := v.SigningMsg()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The internal originator is responsible for logging the full VAA, just log the digest here.
|
||||||
|
supervisor.Logger(ctx).Info("signing injected VAA",
|
||||||
|
zap.Stringer("digest", digest))
|
||||||
|
|
||||||
|
// Sign the digest using our node's guardian key.
|
||||||
|
s, err := crypto.Sign(digest.Bytes(), p.gk)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.logger.Info("observed and signed injected VAA",
|
||||||
|
zap.String("digest", hex.EncodeToString(digest.Bytes())),
|
||||||
|
zap.String("signature", hex.EncodeToString(s)),
|
||||||
|
zap.Int("our_index", us))
|
||||||
|
|
||||||
|
p.broadcastSignature(v, s)
|
||||||
|
}
|
|
@ -138,43 +138,50 @@ func (p *Processor) handleObservation(ctx context.Context, m *gossipv1.LockupObs
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if t, ok := v.Payload.(*vaa.BodyTransfer); ok {
|
// Submit every VAA to Solana for data availability.
|
||||||
|
p.logger.Info("submitting signed VAA to Solana",
|
||||||
|
zap.String("digest", hash),
|
||||||
|
zap.Any("vaa", signed),
|
||||||
|
zap.String("bytes", hex.EncodeToString(vaaBytes)))
|
||||||
|
p.vaaC <- signed
|
||||||
|
|
||||||
|
switch t := v.Payload.(type) {
|
||||||
|
case *vaa.BodyTransfer:
|
||||||
|
// Depending on the target chain, guardians submit VAAs directly to the chain.
|
||||||
|
|
||||||
switch t.TargetChain {
|
switch t.TargetChain {
|
||||||
case vaa.ChainIDEthereum,
|
case vaa.ChainIDSolana:
|
||||||
vaa.ChainIDSolana,
|
// No-op.
|
||||||
vaa.ChainIDTerra:
|
case vaa.ChainIDEthereum:
|
||||||
// Submit to Solana if target is Solana, but also cross-submit all other targets to Solana for data availability
|
// Ethereum is special because it's expensive, and guardians cannot
|
||||||
p.logger.Info("submitting signed VAA to Solana",
|
// be expected to pay the fees. We only submit to Ethereum in devnet mode.
|
||||||
zap.String("digest", hash),
|
p.devnetVAASubmission(ctx, signed, hash)
|
||||||
zap.Any("vaa", signed),
|
case vaa.ChainIDTerra:
|
||||||
zap.String("bytes", hex.EncodeToString(vaaBytes)))
|
p.terraVAASubmission(ctx, signed, hash)
|
||||||
|
|
||||||
// Check whether we run in devmode and submit the VAA ourselves, if so.
|
|
||||||
switch t.TargetChain {
|
|
||||||
case vaa.ChainIDEthereum:
|
|
||||||
p.devnetVAASubmission(ctx, signed, hash)
|
|
||||||
case vaa.ChainIDTerra:
|
|
||||||
p.terraVAASubmission(ctx, signed, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.vaaC <- signed
|
|
||||||
default:
|
default:
|
||||||
p.logger.Error("we don't know how to submit this VAA",
|
p.logger.Error("unknown target chain ID",
|
||||||
zap.String("digest", hash),
|
zap.String("digest", hash),
|
||||||
zap.Any("vaa", signed),
|
zap.Any("vaa", signed),
|
||||||
zap.String("bytes", hex.EncodeToString(vaaBytes)),
|
zap.String("bytes", hex.EncodeToString(vaaBytes)),
|
||||||
zap.Stringer("target_chain", t.TargetChain))
|
zap.Stringer("target_chain", t.TargetChain))
|
||||||
}
|
}
|
||||||
|
case *vaa.BodyGuardianSetUpdate:
|
||||||
p.state.vaaSignatures[hash].submitted = true
|
// A guardian set update is broadcast to every chain that we talk to.
|
||||||
} else {
|
p.devnetVAASubmission(ctx, signed, hash)
|
||||||
|
p.terraVAASubmission(ctx, signed, hash)
|
||||||
|
default:
|
||||||
panic(fmt.Sprintf("unknown VAA payload type: %+v", v))
|
panic(fmt.Sprintf("unknown VAA payload type: %+v", v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.state.vaaSignatures[hash].submitted = true
|
||||||
} else {
|
} else {
|
||||||
p.logger.Info("quorum not met or already submitted, doing nothing",
|
p.logger.Info("quorum not met or already submitted, doing nothing",
|
||||||
zap.String("digest", hash))
|
zap.String("digest", hash))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
p.logger.Info("we have not yet seen this VAA - temporarily storing signature",
|
||||||
|
zap.String("digest", hash))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,9 @@ type Processor struct {
|
||||||
// vaaC is a channel of VAAs to submit to store on Solana (either as target, or for data availability)
|
// vaaC is a channel of VAAs to submit to store on Solana (either as target, or for data availability)
|
||||||
vaaC chan *vaa.VAA
|
vaaC chan *vaa.VAA
|
||||||
|
|
||||||
|
// injectC is a channel of VAAs injected locally.
|
||||||
|
injectC chan *vaa.VAA
|
||||||
|
|
||||||
// gk is the node's guardian private key
|
// gk is the node's guardian private key
|
||||||
gk *ecdsa.PrivateKey
|
gk *ecdsa.PrivateKey
|
||||||
|
|
||||||
|
@ -84,6 +87,7 @@ func NewProcessor(
|
||||||
sendC chan []byte,
|
sendC chan []byte,
|
||||||
obsvC chan *gossipv1.LockupObservation,
|
obsvC chan *gossipv1.LockupObservation,
|
||||||
vaaC chan *vaa.VAA,
|
vaaC chan *vaa.VAA,
|
||||||
|
injectC chan *vaa.VAA,
|
||||||
gk *ecdsa.PrivateKey,
|
gk *ecdsa.PrivateKey,
|
||||||
devnetMode bool,
|
devnetMode bool,
|
||||||
devnetNumGuardians uint,
|
devnetNumGuardians uint,
|
||||||
|
@ -99,6 +103,7 @@ func NewProcessor(
|
||||||
sendC: sendC,
|
sendC: sendC,
|
||||||
obsvC: obsvC,
|
obsvC: obsvC,
|
||||||
vaaC: vaaC,
|
vaaC: vaaC,
|
||||||
|
injectC: injectC,
|
||||||
gk: gk,
|
gk: gk,
|
||||||
devnetMode: devnetMode,
|
devnetMode: devnetMode,
|
||||||
devnetNumGuardians: devnetNumGuardians,
|
devnetNumGuardians: devnetNumGuardians,
|
||||||
|
@ -134,6 +139,8 @@ func (p *Processor) Run(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
case k := <-p.lockC:
|
case k := <-p.lockC:
|
||||||
p.handleLockup(ctx, k)
|
p.handleLockup(ctx, k)
|
||||||
|
case v := <-p.injectC:
|
||||||
|
p.handleInjection(ctx, v)
|
||||||
case m := <-p.obsvC:
|
case m := <-p.obsvC:
|
||||||
p.handleObservation(ctx, m)
|
p.handleObservation(ctx, m)
|
||||||
case <-p.cleanup.C:
|
case <-p.cleanup.C:
|
||||||
|
|
|
@ -40,6 +40,8 @@ message Heartbeat {
|
||||||
// guardians submitting valid signatures for a given hash, they can be assembled into a VAA.
|
// guardians submitting valid signatures for a given hash, they can be assembled into a VAA.
|
||||||
//
|
//
|
||||||
// Messages without valid signature are dropped unceremoniously.
|
// Messages without valid signature are dropped unceremoniously.
|
||||||
|
//
|
||||||
|
// TODO: rename? we also use it for governance VAAs
|
||||||
message LockupObservation {
|
message LockupObservation {
|
||||||
// Guardian pubkey as truncated eth address.
|
// Guardian pubkey as truncated eth address.
|
||||||
bytes addr = 1;
|
bytes addr = 1;
|
||||||
|
|
|
@ -6,9 +6,58 @@ option go_package = "proto/node/v1;nodev1";
|
||||||
|
|
||||||
import "google/api/annotations.proto";
|
import "google/api/annotations.proto";
|
||||||
|
|
||||||
service Node {
|
// NodePrivileged exposes an administrative API. It runs on a UNIX socket and is authenticated
|
||||||
|
// using Linux filesystem permissions.
|
||||||
|
service NodePrivileged {
|
||||||
|
// SubmitGuardianSetVAA injects a guardian set change VAA into the guardian node.
|
||||||
|
// The node will inject the VAA into the aggregator and sign/broadcast the VAA signature.
|
||||||
|
//
|
||||||
|
// A consensus majority of nodes on the network will have to inject the VAA within the
|
||||||
|
// VAA timeout window for it to reach consensus.
|
||||||
|
//
|
||||||
|
rpc SubmitGuardianSetVAA (SubmitGuardianSetVAARequest) returns (SubmitGuardianSetVAAResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GuardianSet represents a new guardian set to be submitted to and signed by the node.
|
||||||
|
// During the genesis procedure, this data structure will be assembled using off-chain collaborative tooling
|
||||||
|
// like GitHub using a human-readable encoding, so readability is a concern.
|
||||||
|
message GuardianSetUpdate {
|
||||||
|
// Index of the current guardian set to be replaced.
|
||||||
|
uint32 current_set_index = 1;
|
||||||
|
|
||||||
|
// UNIX timestamp (s) of the VAA to be created. The timestamp is informational and will be part
|
||||||
|
// of the VAA submitted to the chain. It's part of the VAA digest and has to be identical across nodes.
|
||||||
|
//
|
||||||
|
// For lockups, the timestamp identifies the block that the lockup belongs to. For guardian set updates,
|
||||||
|
// we create the VAA manually. Best practice is to pick a timestamp which roughly matches the expected
|
||||||
|
// genesis ceremony data.
|
||||||
|
//
|
||||||
|
// The actual on-chain guardian set creation timestamp will be set when the VAA is accepted on each chain.
|
||||||
|
//
|
||||||
|
// This is a uint32 to match the on-chain timestamp representation. This becomes a problem in 2106 (sorry).
|
||||||
|
uint32 timestamp = 2;
|
||||||
|
|
||||||
|
// List of guardian set members.
|
||||||
|
message Guardian {
|
||||||
|
// Guardian key pubkey. Stored as hex string with 0x prefix for human readability -
|
||||||
|
// this is the canonical Ethereum representation.
|
||||||
|
string pubkey = 1;
|
||||||
|
// Optional descriptive name. Not stored on any chain, purely informational.
|
||||||
|
string name = 2;
|
||||||
|
};
|
||||||
|
repeated Guardian guardians = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubmitGuardianSetVAARequest {
|
||||||
|
GuardianSetUpdate guardian_set = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubmitGuardianSetVAAResponse {
|
||||||
|
// Canonical digest of the submitted VAA.
|
||||||
|
bytes digest = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GuardianKey specifies the on-disk format for a node's guardian key.
|
||||||
message GuardianKey {
|
message GuardianKey {
|
||||||
// description is an optional, free-form description text set by the operator.
|
// description is an optional, free-form description text set by the operator.
|
||||||
string description = 1;
|
string description = 1;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# This script submits a guardian set update using the VAA injection admin command.
|
||||||
|
# First argument is node to submit to. Second argument is current set index.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
node=$1
|
||||||
|
idx=$2
|
||||||
|
path=/tmp/new-guardianset.prototxt
|
||||||
|
sock=/tmp/admin.sock
|
||||||
|
|
||||||
|
# Create a no-op update that sets the same 1-node guardian set again.
|
||||||
|
kubectl exec guardian-${node} -c guardiand -- /guardiand admin guardian-set-update-template --num=1 --idx=${idx} $path
|
||||||
|
|
||||||
|
# Verify and print resulting result. The digest incorporates the current time and is NOT deterministic.
|
||||||
|
kubectl exec guardian-${node} -c guardiand -- /guardiand admin guardian-set-update-verify $path
|
||||||
|
|
||||||
|
# Submit to node
|
||||||
|
kubectl exec guardian-${node} -c guardiand -- /guardiand admin guardian-set-update-inject --socket $sock $path
|
Loading…
Reference in New Issue