Ver código fonte

node: add admin command to sign existing VAAs (#2183)

* node: add admin command to sign existing VAAs

Change-Id: Ia59c077db1817a3f35ec30544c307448e01455ca

* node: add tests for signing of existing VAAs

Change-Id: I16fca1181fc9d96abb4ebdfad91bc686da517090
Hendrik Hofstadt 2 anos atrás
pai
commit
fc64658ce8

+ 151 - 0
node/cmd/guardiand/adminclient.go

@@ -2,14 +2,18 @@ package guardiand
 
 import (
 	"context"
+	"encoding/csv"
 	"encoding/hex"
 	"fmt"
+	"io"
 	"log"
 	"os"
 	"strconv"
 	"strings"
 	"time"
 
+	ethcommon "github.com/ethereum/go-ethereum/common"
+
 	"github.com/davecgh/go-spew/spew"
 	"github.com/ethereum/go-ethereum/crypto"
 	"github.com/mr-tron/base58"
@@ -58,6 +62,8 @@ func init() {
 	ClientChainGovernorReleasePendingVAACmd.Flags().AddFlagSet(pf)
 	ClientChainGovernorResetReleaseTimerCmd.Flags().AddFlagSet(pf)
 	PurgePythNetVaasCmd.Flags().AddFlagSet(pf)
+	SignExistingVaaCmd.Flags().AddFlagSet(pf)
+	SignExistingVaasFromCSVCmd.Flags().AddFlagSet(pf)
 
 	AdminCmd.AddCommand(AdminClientInjectGuardianSetUpdateCmd)
 	AdminCmd.AddCommand(AdminClientFindMissingMessagesCmd)
@@ -72,6 +78,8 @@ func init() {
 	AdminCmd.AddCommand(ClientChainGovernorReleasePendingVAACmd)
 	AdminCmd.AddCommand(ClientChainGovernorResetReleaseTimerCmd)
 	AdminCmd.AddCommand(PurgePythNetVaasCmd)
+	AdminCmd.AddCommand(SignExistingVaaCmd)
+	AdminCmd.AddCommand(SignExistingVaasFromCSVCmd)
 }
 
 var AdminCmd = &cobra.Command{
@@ -156,6 +164,20 @@ var PurgePythNetVaasCmd = &cobra.Command{
 	Args:  cobra.RangeArgs(1, 2),
 }
 
+var SignExistingVaaCmd = &cobra.Command{
+	Use:   "sign-existing-vaa [VAA] [NEW_GUARDIANS] [NEW_GUARDIAN_SET_INDEX]",
+	Short: "Signs an existing VAA for a new guardian set using the local guardian key. This only works if the new VAA would have quorum.",
+	Run:   runSignExistingVaa,
+	Args:  cobra.ExactArgs(3),
+}
+
+var SignExistingVaasFromCSVCmd = &cobra.Command{
+	Use:   "sign-existing-vaas-csv [IN_FILE] [OUT_FILE] [NEW_GUARDIANS] [NEW_GUARDIAN_SET_INDEX]",
+	Short: "Signs a CSV [VAA_ID,VAA_HEX] of existing VAAs for a new guardian set using the local guardian key and writes it to a new CSV. VAAs that don't have quorum on the new set will be dropped.",
+	Run:   runSignExistingVaasFromCSV,
+	Args:  cobra.ExactArgs(4),
+}
+
 func getAdminClient(ctx context.Context, addr string) (*grpc.ClientConn, nodev1.NodePrivilegedServiceClient, error) {
 	conn, err := grpc.DialContext(ctx, fmt.Sprintf("unix:///%s", addr), grpc.WithTransportCredentials(insecure.NewCredentials()))
 
@@ -491,3 +513,132 @@ func runPurgePythNetVaas(cmd *cobra.Command, args []string) {
 
 	fmt.Println(resp.Response)
 }
+
+func runSignExistingVaa(cmd *cobra.Command, args []string) {
+	existingVAA := ethcommon.Hex2Bytes(args[0])
+	if len(existingVAA) == 0 {
+		log.Fatalf("vaa hex invalid")
+	}
+
+	newGsStrings := strings.Split(args[1], ",")
+
+	newGsIndex, err := strconv.ParseUint(args[2], 10, 32)
+	if err != nil {
+		log.Fatalf("invalid new guardian set index")
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	conn, c, err := getAdminClient(ctx, *clientSocketPath)
+	if err != nil {
+		log.Fatalf("failed to get admin client: %v", err)
+	}
+	defer conn.Close()
+
+	msg := nodev1.SignExistingVAARequest{
+		Vaa:                 existingVAA,
+		NewGuardianAddrs:    newGsStrings,
+		NewGuardianSetIndex: uint32(newGsIndex),
+	}
+	resp, err := c.SignExistingVAA(ctx, &msg)
+	if err != nil {
+		log.Fatalf("failed to run SignExistingVAA RPC: %s", err)
+	}
+
+	fmt.Println(hex.EncodeToString(resp.Vaa))
+}
+
+func runSignExistingVaasFromCSV(cmd *cobra.Command, args []string) {
+	oldVAAFile, err := os.Open(args[0])
+	if err != nil {
+		log.Fatalf("failed to read old VAA db: %v", err)
+	}
+	defer oldVAAFile.Close()
+
+	newVAAFile, err := os.OpenFile(args[1], os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
+	if err != nil {
+		log.Fatalf("failed to create new VAA db: %v", err)
+	}
+	defer newVAAFile.Close()
+	newVAAWriter := csv.NewWriter(newVAAFile)
+
+	newGsStrings := strings.Split(args[2], ",")
+
+	newGsIndex, err := strconv.ParseUint(args[3], 10, 32)
+	if err != nil {
+		log.Fatalf("invalid new guardian set index")
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	conn, c, err := getAdminClient(ctx, *clientSocketPath)
+	if err != nil {
+		log.Fatalf("failed to get admin client: %v", err)
+	}
+	defer conn.Close()
+
+	// Scan the CSV once to make sure it won't fail while reading unless raced
+	oldVAAReader := csv.NewReader(oldVAAFile)
+	numOldVAAs := 0
+	for {
+		row, err := oldVAAReader.Read()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			log.Fatalf("failed to parse VAA CSV: %v", err)
+		}
+		if len(row) != 2 {
+			log.Fatalf("row [%d] does not have 2 elements", numOldVAAs)
+		}
+		numOldVAAs++
+	}
+
+	// Reset reader
+	_, err = oldVAAFile.Seek(0, io.SeekStart)
+	if err != nil {
+		log.Fatalf("failed to seek back in CSV file: %v", err)
+	}
+	oldVAAReader = csv.NewReader(oldVAAFile)
+
+	counter, i := 0, 0
+	for {
+		row, err := oldVAAReader.Read()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			log.Fatalf("failed to parse VAA CSV: %v", err)
+		}
+		if len(row) != 2 {
+			log.Fatalf("row [%d] does not have 2 elements", i)
+		}
+		i++
+
+		if i%10 == 0 {
+			log.Printf("Processing VAA %d/%d", i, numOldVAAs)
+		}
+
+		vaaBytes := ethcommon.Hex2Bytes(row[1])
+		msg := nodev1.SignExistingVAARequest{
+			Vaa:                 vaaBytes,
+			NewGuardianAddrs:    newGsStrings,
+			NewGuardianSetIndex: uint32(newGsIndex),
+		}
+		resp, err := c.SignExistingVAA(ctx, &msg)
+		if err != nil {
+			log.Printf("signing VAA (%s)[%d] failed - skipping: %v", row[0], i, err)
+			continue
+		}
+		err = newVAAWriter.Write([]string{row[0], hex.EncodeToString(resp.Vaa)})
+		if err != nil {
+			log.Fatalf("failed to write new VAA to out db: %v", err)
+		}
+		counter++
+	}
+
+	log.Printf("Successfully signed %d out of %d VAAs", counter, numOldVAAs)
+	newVAAWriter.Flush()
+}

+ 152 - 13
node/cmd/guardiand/adminserver.go

@@ -1,7 +1,9 @@
 package guardiand
 
 import (
+	"bytes"
 	"context"
+	"crypto/ecdsa"
 	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
@@ -12,8 +14,13 @@ import (
 	"net"
 	"net/http"
 	"os"
+	"sync"
 	"time"
 
+	"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
+	ethcrypto "github.com/ethereum/go-ethereum/crypto"
+	"golang.org/x/exp/slices"
+
 	"github.com/certusone/wormhole/node/pkg/db"
 	"github.com/certusone/wormhole/node/pkg/governor"
 	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
@@ -32,12 +39,16 @@ import (
 
 type nodePrivilegedService struct {
 	nodev1.UnimplementedNodePrivilegedServiceServer
-	db           *db.Database
-	injectC      chan<- *vaa.VAA
-	obsvReqSendC chan *gossipv1.ObservationRequest
-	logger       *zap.Logger
-	signedInC    chan *gossipv1.SignedVAAWithQuorum
-	governor     *governor.ChainGovernor
+	db              *db.Database
+	injectC         chan<- *vaa.VAA
+	obsvReqSendC    chan *gossipv1.ObservationRequest
+	logger          *zap.Logger
+	signedInC       chan *gossipv1.SignedVAAWithQuorum
+	governor        *governor.ChainGovernor
+	evmConnector    connectors.Connector
+	gsCache         sync.Map
+	gk              *ecdsa.PrivateKey
+	guardianAddress ethcommon.Address
 }
 
 // adminGuardianSetUpdateToVAA converts a nodev1.GuardianSetUpdate message to its canonical VAA representation.
@@ -387,7 +398,7 @@ func (s *nodePrivilegedService) FindMissingMessages(ctx context.Context, req *no
 }
 
 func adminServiceRunnable(logger *zap.Logger, socketPath string, injectC chan<- *vaa.VAA, signedInC chan *gossipv1.SignedVAAWithQuorum, obsvReqSendC chan *gossipv1.ObservationRequest,
-	db *db.Database, gst *common.GuardianSetState, gov *governor.ChainGovernor) (supervisor.Runnable, error) {
+	db *db.Database, gst *common.GuardianSetState, gov *governor.ChainGovernor, gk *ecdsa.PrivateKey, ethRpc *string, ethContract *string) (supervisor.Runnable, error) {
 	// Delete existing UNIX socket, if present.
 	fi, err := os.Stat(socketPath)
 	if err == nil {
@@ -419,13 +430,28 @@ func adminServiceRunnable(logger *zap.Logger, socketPath string, injectC chan<-
 
 	logger.Info("admin server listening on", zap.String("path", socketPath))
 
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	var evmConnector connectors.Connector
+	if ethRPC != nil && ethContract != nil {
+		contract := ethcommon.HexToAddress(*ethContract)
+		evmConnector, err = connectors.NewEthereumConnector(ctx, "eth", *ethRpc, contract, logger)
+		if err != nil {
+			return nil, fmt.Errorf("failed to connecto to ethereum")
+		}
+	}
+
 	nodeService := &nodePrivilegedService{
-		injectC:      injectC,
-		obsvReqSendC: obsvReqSendC,
-		db:           db,
-		logger:       logger.Named("adminservice"),
-		signedInC:    signedInC,
-		governor:     gov,
+		db:              db,
+		injectC:         injectC,
+		obsvReqSendC:    obsvReqSendC,
+		logger:          logger.Named("adminservice"),
+		signedInC:       signedInC,
+		governor:        gov,
+		gk:              gk,
+		guardianAddress: ethcrypto.PubkeyToAddress(gk.PublicKey),
+		evmConnector:    evmConnector,
 	}
 
 	publicrpcService := publicrpc.NewPublicrpcServer(logger, db, gst, gov)
@@ -539,3 +565,116 @@ func (s *nodePrivilegedService) PurgePythNetVaas(ctx context.Context, req *nodev
 		Response: resp,
 	}, nil
 }
+
+func (s *nodePrivilegedService) SignExistingVAA(ctx context.Context, req *nodev1.SignExistingVAARequest) (*nodev1.SignExistingVAAResponse, error) {
+	v, err := vaa.Unmarshal(req.Vaa)
+	if err != nil {
+		return nil, fmt.Errorf("failed to unmarshal VAA: %w", err)
+	}
+
+	if req.NewGuardianSetIndex <= v.GuardianSetIndex {
+		return nil, errors.New("new guardian set index must be higher than provided VAA")
+	}
+
+	if s.evmConnector == nil {
+		return nil, errors.New("the node needs to have an Ethereum connection configured to sign existing VAAs")
+	}
+
+	var gs *common.GuardianSet
+	if cachedGs, exists := s.gsCache.Load(v.GuardianSetIndex); exists {
+		gs = cachedGs.(*common.GuardianSet)
+	} else {
+		evmGs, err := s.evmConnector.GetGuardianSet(ctx, v.GuardianSetIndex)
+		if err != nil {
+			return nil, fmt.Errorf("failed to load guardian set [%d]: %w", v.GuardianSetIndex, err)
+		}
+		gs = &common.GuardianSet{
+			Keys:  evmGs.Keys,
+			Index: v.GuardianSetIndex,
+		}
+		s.gsCache.Store(v.GuardianSetIndex, gs)
+	}
+
+	if slices.Index(gs.Keys, s.guardianAddress) != -1 {
+		return nil, fmt.Errorf("local guardian is already on the old set")
+	}
+
+	// Verify VAA
+	err = v.Verify(gs.Keys)
+	if err != nil {
+		return nil, fmt.Errorf("failed to verify existing VAA: %w", err)
+	}
+
+	if len(req.NewGuardianAddrs) > 255 {
+		return nil, errors.New("new guardian set has too many guardians")
+	}
+	newGS := make([]ethcommon.Address, len(req.NewGuardianAddrs))
+	for i, guardianString := range req.NewGuardianAddrs {
+		guardianAddress := ethcommon.HexToAddress(guardianString)
+		newGS[i] = guardianAddress
+	}
+
+	// Make sure there are no duplicates. Compact needs to take a sorted slice to remove all duplicates.
+	newGSSorted := slices.Clone(newGS)
+	slices.SortFunc(newGSSorted, func(a, b ethcommon.Address) bool {
+		return bytes.Compare(a[:], b[:]) < 0
+	})
+	newGsLen := len(newGSSorted)
+	if len(slices.Compact(newGSSorted)) != newGsLen {
+		return nil, fmt.Errorf("duplicate guardians in the guardian set")
+	}
+
+	localGuardianIndex := slices.Index(newGS, s.guardianAddress)
+	if localGuardianIndex == -1 {
+		return nil, fmt.Errorf("local guardian is not a member of the new guardian set")
+	}
+
+	newVAA := &vaa.VAA{
+		Version: v.Version,
+		// Set the new guardian set index
+		GuardianSetIndex: req.NewGuardianSetIndex,
+		// Signatures will be repopulated
+		Signatures:       nil,
+		Timestamp:        v.Timestamp,
+		Nonce:            v.Nonce,
+		Sequence:         v.Sequence,
+		ConsistencyLevel: v.ConsistencyLevel,
+		EmitterChain:     v.EmitterChain,
+		EmitterAddress:   v.EmitterAddress,
+		Payload:          v.Payload,
+	}
+
+	// Copy original VAA signatures
+	for _, sig := range v.Signatures {
+		signerAddress := gs.Keys[sig.Index]
+		newIndex := slices.Index(newGS, signerAddress)
+		// Guardian is not part of the new set
+		if newIndex == -1 {
+			continue
+		}
+		newVAA.Signatures = append(newVAA.Signatures, &vaa.Signature{
+			Index:     uint8(newIndex),
+			Signature: sig.Signature,
+		})
+	}
+
+	// Add our own signature only if the new guardian set would reach quorum
+	if vaa.CalculateQuorum(len(newGS)) > len(newVAA.Signatures)+1 {
+		return nil, errors.New("cannot reach quorum on new guardian set with the local signature")
+	}
+
+	// Add local signature
+	newVAA.AddSignature(s.gk, uint8(localGuardianIndex))
+
+	// Sort VAA signatures by guardian ID
+	slices.SortFunc(newVAA.Signatures, func(a, b *vaa.Signature) bool {
+		return a.Index < b.Index
+	})
+
+	newVAABytes, err := newVAA.Marshal()
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal new VAA: %w", err)
+	}
+
+	return &nodev1.SignExistingVAAResponse{Vaa: newVAABytes}, nil
+}

+ 257 - 0
node/cmd/guardiand/adminserver_test.go

@@ -0,0 +1,257 @@
+package guardiand
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"testing"
+	"time"
+
+	nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1"
+	"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
+	"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/ethabi"
+	ethereum "github.com/ethereum/go-ethereum"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	ethcrypto "github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/event"
+	"github.com/stretchr/testify/require"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+	"go.uber.org/zap"
+)
+
+type mockEVMConnector struct {
+	guardianAddrs    []common.Address
+	guardianSetIndex uint32
+}
+
+func (m mockEVMConnector) GetCurrentGuardianSetIndex(ctx context.Context) (uint32, error) {
+	return m.guardianSetIndex, nil
+}
+
+func (m mockEVMConnector) GetGuardianSet(ctx context.Context, index uint32) (ethabi.StructsGuardianSet, error) {
+	return ethabi.StructsGuardianSet{
+		Keys:           m.guardianAddrs,
+		ExpirationTime: 0,
+	}, nil
+}
+
+func (m mockEVMConnector) NetworkName() string {
+	panic("unimplemented")
+}
+
+func (m mockEVMConnector) ContractAddress() common.Address {
+	panic("unimplemented")
+}
+
+func (m mockEVMConnector) WatchLogMessagePublished(ctx context.Context, sink chan<- *ethabi.AbiLogMessagePublished) (event.Subscription, error) {
+	panic("unimplemented")
+}
+
+func (m mockEVMConnector) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
+	panic("unimplemented")
+}
+
+func (m mockEVMConnector) TimeOfBlockByHash(ctx context.Context, hash common.Hash) (uint64, error) {
+	panic("unimplemented")
+}
+
+func (m mockEVMConnector) ParseLogMessagePublished(log types.Log) (*ethabi.AbiLogMessagePublished, error) {
+	panic("unimplemented")
+}
+
+func (m mockEVMConnector) SubscribeForBlocks(ctx context.Context, sink chan<- *connectors.NewBlock) (ethereum.Subscription, error) {
+	panic("unimplemented")
+}
+
+func (m mockEVMConnector) RawCallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
+	panic("unimplemented")
+}
+
+func generateGS(num int) (keys []*ecdsa.PrivateKey, addrs []common.Address) {
+	for i := 0; i < num; i++ {
+		key, err := ethcrypto.GenerateKey()
+		if err != nil {
+			panic(err)
+		}
+		keys = append(keys, key)
+		addrs = append(addrs, ethcrypto.PubkeyToAddress(key.PublicKey))
+	}
+	return
+}
+
+func addrsToHexStrings(addrs []common.Address) (out []string) {
+	for _, addr := range addrs {
+		out = append(out, addr.String())
+	}
+	return
+}
+
+func generateMockVAA(gsIndex uint32, gsKeys []*ecdsa.PrivateKey) []byte {
+	v := &vaa.VAA{
+		Version:          1,
+		GuardianSetIndex: gsIndex,
+		Signatures:       nil,
+		Timestamp:        time.Now(),
+		Nonce:            3,
+		Sequence:         79,
+		ConsistencyLevel: 1,
+		EmitterChain:     1,
+		EmitterAddress:   vaa.Address{},
+		Payload:          []byte("test"),
+	}
+	for i, key := range gsKeys {
+		v.AddSignature(key, uint8(i))
+	}
+
+	vBytes, err := v.Marshal()
+	if err != nil {
+		panic(err)
+	}
+	return vBytes
+}
+
+func setupAdminServerForVAASigning(gsIndex uint32, gsAddrs []common.Address) *nodePrivilegedService {
+	gk, err := ethcrypto.GenerateKey()
+	if err != nil {
+		panic(err)
+	}
+
+	connector := mockEVMConnector{
+		guardianAddrs:    gsAddrs,
+		guardianSetIndex: gsIndex,
+	}
+
+	return &nodePrivilegedService{
+		db:              nil,
+		injectC:         nil,
+		obsvReqSendC:    nil,
+		logger:          zap.L(),
+		signedInC:       nil,
+		governor:        nil,
+		evmConnector:    connector,
+		gk:              gk,
+		guardianAddress: ethcrypto.PubkeyToAddress(gk.PublicKey),
+	}
+}
+
+func TestSignExistingVAA_NoVAA(t *testing.T) {
+	s := setupAdminServerForVAASigning(0, []common.Address{})
+
+	_, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 nil,
+		NewGuardianAddrs:    nil,
+		NewGuardianSetIndex: 0,
+	})
+	require.ErrorContains(t, err, "failed to unmarshal VAA")
+}
+
+func TestSignExistingVAA_NotGuardian(t *testing.T) {
+	gsKeys, gsAddrs := generateGS(5)
+	s := setupAdminServerForVAASigning(0, gsAddrs)
+
+	v := generateMockVAA(0, gsKeys)
+
+	_, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 v,
+		NewGuardianAddrs:    addrsToHexStrings(gsAddrs),
+		NewGuardianSetIndex: 1,
+	})
+	require.ErrorContains(t, err, "local guardian is not a member of the new guardian set")
+}
+
+func TestSignExistingVAA_InvalidVAA(t *testing.T) {
+	gsKeys, gsAddrs := generateGS(5)
+	s := setupAdminServerForVAASigning(0, gsAddrs)
+
+	v := generateMockVAA(0, gsKeys[:2])
+
+	gsAddrs = append(gsAddrs, s.guardianAddress)
+	_, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 v,
+		NewGuardianAddrs:    addrsToHexStrings(gsAddrs),
+		NewGuardianSetIndex: 1,
+	})
+	require.ErrorContains(t, err, "failed to verify existing VAA")
+}
+
+func TestSignExistingVAA_DuplicateGuardian(t *testing.T) {
+	gsKeys, gsAddrs := generateGS(5)
+	s := setupAdminServerForVAASigning(0, gsAddrs)
+
+	v := generateMockVAA(0, gsKeys)
+
+	gsAddrs = append(gsAddrs, s.guardianAddress)
+	gsAddrs = append(gsAddrs, s.guardianAddress)
+	_, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 v,
+		NewGuardianAddrs:    addrsToHexStrings(gsAddrs),
+		NewGuardianSetIndex: 1,
+	})
+	require.ErrorContains(t, err, "duplicate guardians in the guardian set")
+}
+
+func TestSignExistingVAA_AlreadyGuardian(t *testing.T) {
+	gsKeys, gsAddrs := generateGS(5)
+	s := setupAdminServerForVAASigning(0, gsAddrs)
+	s.evmConnector = mockEVMConnector{
+		guardianAddrs:    append(gsAddrs, s.guardianAddress),
+		guardianSetIndex: 0,
+	}
+
+	v := generateMockVAA(0, append(gsKeys, s.gk))
+
+	gsAddrs = append(gsAddrs, s.guardianAddress)
+	_, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 v,
+		NewGuardianAddrs:    addrsToHexStrings(gsAddrs),
+		NewGuardianSetIndex: 1,
+	})
+	require.ErrorContains(t, err, "local guardian is already on the old set")
+}
+
+func TestSignExistingVAA_NotAFutureGuardian(t *testing.T) {
+	gsKeys, gsAddrs := generateGS(5)
+	s := setupAdminServerForVAASigning(0, gsAddrs)
+
+	v := generateMockVAA(0, gsKeys)
+
+	_, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 v,
+		NewGuardianAddrs:    addrsToHexStrings(gsAddrs),
+		NewGuardianSetIndex: 1,
+	})
+	require.ErrorContains(t, err, "local guardian is not a member of the new guardian set")
+}
+
+func TestSignExistingVAA_CantReachQuorum(t *testing.T) {
+	gsKeys, gsAddrs := generateGS(5)
+	s := setupAdminServerForVAASigning(0, gsAddrs)
+
+	v := generateMockVAA(0, gsKeys)
+
+	gsAddrs = append(gsAddrs, s.guardianAddress)
+	_, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 v,
+		NewGuardianAddrs:    addrsToHexStrings(append(gsAddrs, common.Address{0, 1}, common.Address{3, 1}, common.Address{8, 1})),
+		NewGuardianSetIndex: 1,
+	})
+	require.ErrorContains(t, err, "cannot reach quorum on new guardian set with the local signature")
+}
+
+func TestSignExistingVAA_Valid(t *testing.T) {
+	gsKeys, gsAddrs := generateGS(5)
+	s := setupAdminServerForVAASigning(0, gsAddrs)
+
+	v := generateMockVAA(0, gsKeys)
+
+	gsAddrs = append(gsAddrs, s.guardianAddress)
+	res, err := s.SignExistingVAA(context.Background(), &nodev1.SignExistingVAARequest{
+		Vaa:                 v,
+		NewGuardianAddrs:    addrsToHexStrings(gsAddrs),
+		NewGuardianSetIndex: 1,
+	})
+
+	require.NoError(t, err)
+	v2 := generateMockVAA(1, append(gsKeys, s.gk))
+	require.Equal(t, v2, res.Vaa)
+}

+ 1 - 1
node/cmd/guardiand/node.go

@@ -917,7 +917,7 @@ func runNode(cmd *cobra.Command, args []string) {
 	}
 
 	// local admin service socket
-	adminService, err := adminServiceRunnable(logger, *adminSocketPath, injectC, signedInC, obsvReqSendC, db, gst, gov)
+	adminService, err := adminServiceRunnable(logger, *adminSocketPath, injectC, signedInC, obsvReqSendC, db, gst, gov, gk, ethRPC, ethContract)
 	if err != nil {
 		logger.Fatal("failed to create admin service socket", zap.Error(err))
 	}

+ 1 - 0
node/go.mod

@@ -56,6 +56,7 @@ require (
 	github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
 	github.com/test-go/testify v1.1.4
 	github.com/wormhole-foundation/wormhole/sdk v0.0.0-00010101000000-000000000000
+	golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5
 	golang.org/x/text v0.4.0
 )
 

+ 2 - 0
node/go.sum

@@ -1338,6 +1338,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
+golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 h1:rxKZ2gOnYxjfmakvUUqh9Gyb6KXfrj7JWTxORTYqb0E=
+golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=

+ 240 - 85
node/pkg/proto/node/v1/node.pb.go

@@ -1485,6 +1485,116 @@ func (x *PurgePythNetVaasResponse) GetResponse() string {
 	return ""
 }
 
+type SignExistingVAARequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Vaa                 []byte   `protobuf:"bytes,1,opt,name=vaa,proto3" json:"vaa,omitempty"`
+	NewGuardianAddrs    []string `protobuf:"bytes,2,rep,name=new_guardian_addrs,json=newGuardianAddrs,proto3" json:"new_guardian_addrs,omitempty"`
+	NewGuardianSetIndex uint32   `protobuf:"varint,3,opt,name=new_guardian_set_index,json=newGuardianSetIndex,proto3" json:"new_guardian_set_index,omitempty"`
+}
+
+func (x *SignExistingVAARequest) Reset() {
+	*x = SignExistingVAARequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_node_v1_node_proto_msgTypes[26]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SignExistingVAARequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SignExistingVAARequest) ProtoMessage() {}
+
+func (x *SignExistingVAARequest) ProtoReflect() protoreflect.Message {
+	mi := &file_node_v1_node_proto_msgTypes[26]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SignExistingVAARequest.ProtoReflect.Descriptor instead.
+func (*SignExistingVAARequest) Descriptor() ([]byte, []int) {
+	return file_node_v1_node_proto_rawDescGZIP(), []int{26}
+}
+
+func (x *SignExistingVAARequest) GetVaa() []byte {
+	if x != nil {
+		return x.Vaa
+	}
+	return nil
+}
+
+func (x *SignExistingVAARequest) GetNewGuardianAddrs() []string {
+	if x != nil {
+		return x.NewGuardianAddrs
+	}
+	return nil
+}
+
+func (x *SignExistingVAARequest) GetNewGuardianSetIndex() uint32 {
+	if x != nil {
+		return x.NewGuardianSetIndex
+	}
+	return 0
+}
+
+type SignExistingVAAResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Vaa []byte `protobuf:"bytes,1,opt,name=vaa,proto3" json:"vaa,omitempty"`
+}
+
+func (x *SignExistingVAAResponse) Reset() {
+	*x = SignExistingVAAResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_node_v1_node_proto_msgTypes[27]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SignExistingVAAResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SignExistingVAAResponse) ProtoMessage() {}
+
+func (x *SignExistingVAAResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_node_v1_node_proto_msgTypes[27]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SignExistingVAAResponse.ProtoReflect.Descriptor instead.
+func (*SignExistingVAAResponse) Descriptor() ([]byte, []int) {
+	return file_node_v1_node_proto_rawDescGZIP(), []int{27}
+}
+
+func (x *SignExistingVAAResponse) GetVaa() []byte {
+	if x != nil {
+		return x.Vaa
+	}
+	return nil
+}
+
 // List of guardian set members.
 type GuardianSetUpdate_Guardian struct {
 	state         protoimpl.MessageState
@@ -1501,7 +1611,7 @@ type GuardianSetUpdate_Guardian struct {
 func (x *GuardianSetUpdate_Guardian) Reset() {
 	*x = GuardianSetUpdate_Guardian{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_node_v1_node_proto_msgTypes[26]
+		mi := &file_node_v1_node_proto_msgTypes[28]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1514,7 +1624,7 @@ func (x *GuardianSetUpdate_Guardian) String() string {
 func (*GuardianSetUpdate_Guardian) ProtoMessage() {}
 
 func (x *GuardianSetUpdate_Guardian) ProtoReflect() protoreflect.Message {
-	mi := &file_node_v1_node_proto_msgTypes[26]
+	mi := &file_node_v1_node_proto_msgTypes[28]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1720,74 +1830,91 @@ var file_node_v1_node_proto_rawDesc = []byte{
 	0x67, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x36, 0x0a, 0x18, 0x50, 0x75, 0x72, 0x67, 0x65, 0x50, 0x79,
 	0x74, 0x68, 0x4e, 0x65, 0x74, 0x56, 0x61, 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
 	0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe5, 0x07,
-	0x0a, 0x15, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x64,
-	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x49, 0x6e, 0x6a, 0x65, 0x63,
-	0x74, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x41, 0x41, 0x12, 0x23,
-	0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x47,
-	0x6f, 0x76, 0x65, 0x72, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x41, 0x41, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e,
-	0x6a, 0x65, 0x63, 0x74, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x41,
-	0x41, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x46, 0x69, 0x6e,
+	0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8d, 0x01,
+	0x0a, 0x16, 0x53, 0x69, 0x67, 0x6e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x56, 0x41,
+	0x41, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x61, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x76, 0x61, 0x61, 0x12, 0x2c, 0x0a, 0x12, 0x6e, 0x65,
+	0x77, 0x5f, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73,
+	0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x65, 0x77, 0x47, 0x75, 0x61, 0x72, 0x64,
+	0x69, 0x61, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, 0x33, 0x0a, 0x16, 0x6e, 0x65, 0x77, 0x5f,
+	0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x64,
+	0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x6e, 0x65, 0x77, 0x47, 0x75, 0x61,
+	0x72, 0x64, 0x69, 0x61, 0x6e, 0x53, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x2b, 0x0a,
+	0x17, 0x53, 0x69, 0x67, 0x6e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61, 0x61, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x76, 0x61, 0x61, 0x32, 0xbb, 0x08, 0x0a, 0x15, 0x4e,
+	0x6f, 0x64, 0x65, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x64, 0x53, 0x65, 0x72,
+	0x76, 0x69, 0x63, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x49, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x47, 0x6f,
+	0x76, 0x65, 0x72, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x41, 0x41, 0x12, 0x23, 0x2e, 0x6e, 0x6f,
+	0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x47, 0x6f, 0x76, 0x65,
+	0x72, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x41, 0x41, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x1a, 0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x6a, 0x65, 0x63,
+	0x74, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x41, 0x41, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x69,
+	0x73, 0x73, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x23, 0x2e,
+	0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x69, 0x73, 0x73,
+	0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e,
 	0x64, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73,
-	0x12, 0x23, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d,
-	0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x46, 0x69, 0x6e, 0x64, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61,
-	0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x16, 0x53,
-	0x65, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x53, 0x65, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e,
-	0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65,
-	0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47,
-	0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x2e,
-	0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76,
-	0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61,
-	0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69,
-	0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x12,
-	0x23, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47,
-	0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43,
-	0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x6f,
-	0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x1b, 0x43, 0x68,
-	0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x44, 0x72, 0x6f, 0x70, 0x50,
-	0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x12, 0x2b, 0x2e, 0x6e, 0x6f, 0x64, 0x65,
-	0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f,
-	0x72, 0x44, 0x72, 0x6f, 0x70, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x16, 0x53, 0x65, 0x6e, 0x64,
+	0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x12, 0x26, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x6e,
+	0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6e, 0x6f, 0x64,
+	0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61,
+	0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65,
+	0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x2e, 0x6e, 0x6f, 0x64,
+	0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e,
+	0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+	0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47,
+	0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f,
+	0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x23, 0x2e, 0x6e,
+	0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65,
+	0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x1a, 0x24, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69,
+	0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x1b, 0x43, 0x68, 0x61, 0x69, 0x6e,
+	0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x44, 0x72, 0x6f, 0x70, 0x50, 0x65, 0x6e, 0x64,
+	0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x12, 0x2b, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31,
 	0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x44, 0x72,
-	0x6f, 0x70, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x1e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f,
-	0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x50, 0x65, 0x6e,
-	0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x12, 0x2e, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76,
-	0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52,
-	0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76,
-	0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52,
-	0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x1e, 0x43, 0x68, 0x61,
-	0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52,
-	0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x72, 0x12, 0x2e, 0x2e, 0x6e, 0x6f,
-	0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72,
-	0x6e, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54,
-	0x69, 0x6d, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x6e, 0x6f,
-	0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72,
-	0x6e, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54,
-	0x69, 0x6d, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10,
-	0x50, 0x75, 0x72, 0x67, 0x65, 0x50, 0x79, 0x74, 0x68, 0x4e, 0x65, 0x74, 0x56, 0x61, 0x61, 0x73,
-	0x12, 0x20, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x72, 0x67, 0x65,
-	0x50, 0x79, 0x74, 0x68, 0x4e, 0x65, 0x74, 0x56, 0x61, 0x61, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x72,
-	0x67, 0x65, 0x50, 0x79, 0x74, 0x68, 0x4e, 0x65, 0x74, 0x56, 0x61, 0x61, 0x73, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
-	0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65, 0x2f, 0x77, 0x6f,
-	0x72, 0x6d, 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x6e, 0x6f,
-	0x64, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6f, 0x70, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68,
+	0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x44, 0x72, 0x6f, 0x70, 0x50,
+	0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x81, 0x01, 0x0a, 0x1e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72,
+	0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e,
+	0x67, 0x56, 0x41, 0x41, 0x12, 0x2e, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43,
+	0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x65,
+	0x61, 0x73, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43,
+	0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x6c, 0x65,
+	0x61, 0x73, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x1e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47,
+	0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65,
+	0x61, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x72, 0x12, 0x2e, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e,
+	0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72,
+	0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x65,
+	0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e,
+	0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72,
+	0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x65,
+	0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10, 0x50, 0x75, 0x72,
+	0x67, 0x65, 0x50, 0x79, 0x74, 0x68, 0x4e, 0x65, 0x74, 0x56, 0x61, 0x61, 0x73, 0x12, 0x20, 0x2e,
+	0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x50, 0x79, 0x74,
+	0x68, 0x4e, 0x65, 0x74, 0x56, 0x61, 0x61, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+	0x21, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x50,
+	0x79, 0x74, 0x68, 0x4e, 0x65, 0x74, 0x56, 0x61, 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69,
+	0x6e, 0x67, 0x56, 0x41, 0x41, 0x12, 0x1f, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31, 0x2e,
+	0x53, 0x69, 0x67, 0x6e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x76, 0x31,
+	0x2e, 0x53, 0x69, 0x67, 0x6e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x56, 0x41, 0x41,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68,
+	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x75, 0x73, 0x6f, 0x6e, 0x65,
+	0x2f, 0x77, 0x6f, 0x72, 0x6d, 0x68, 0x6f, 0x6c, 0x65, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70,
+	0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x76, 0x31,
+	0x3b, 0x6e, 0x6f, 0x64, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1802,7 +1929,7 @@ func file_node_v1_node_proto_rawDescGZIP() []byte {
 	return file_node_v1_node_proto_rawDescData
 }
 
-var file_node_v1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
+var file_node_v1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
 var file_node_v1_node_proto_goTypes = []interface{}{
 	(*InjectGovernanceVAARequest)(nil),             // 0: node.v1.InjectGovernanceVAARequest
 	(*GovernanceMessage)(nil),                      // 1: node.v1.GovernanceMessage
@@ -1830,8 +1957,10 @@ var file_node_v1_node_proto_goTypes = []interface{}{
 	(*ChainGovernorResetReleaseTimerResponse)(nil), // 23: node.v1.ChainGovernorResetReleaseTimerResponse
 	(*PurgePythNetVaasRequest)(nil),                // 24: node.v1.PurgePythNetVaasRequest
 	(*PurgePythNetVaasResponse)(nil),               // 25: node.v1.PurgePythNetVaasResponse
-	(*GuardianSetUpdate_Guardian)(nil),             // 26: node.v1.GuardianSetUpdate.Guardian
-	(*v1.ObservationRequest)(nil),                  // 27: gossip.v1.ObservationRequest
+	(*SignExistingVAARequest)(nil),                 // 26: node.v1.SignExistingVAARequest
+	(*SignExistingVAAResponse)(nil),                // 27: node.v1.SignExistingVAAResponse
+	(*GuardianSetUpdate_Guardian)(nil),             // 28: node.v1.GuardianSetUpdate.Guardian
+	(*v1.ObservationRequest)(nil),                  // 29: gossip.v1.ObservationRequest
 }
 var file_node_v1_node_proto_depIdxs = []int32{
 	1,  // 0: node.v1.InjectGovernanceVAARequest.messages:type_name -> node.v1.GovernanceMessage
@@ -1841,8 +1970,8 @@ var file_node_v1_node_proto_depIdxs = []int32{
 	7,  // 4: node.v1.GovernanceMessage.bridge_contract_upgrade:type_name -> node.v1.BridgeUpgradeContract
 	8,  // 5: node.v1.GovernanceMessage.wormchain_store_code:type_name -> node.v1.WormchainStoreCode
 	9,  // 6: node.v1.GovernanceMessage.wormchain_instantiate_contract:type_name -> node.v1.WormchainInstantiateContract
-	26, // 7: node.v1.GuardianSetUpdate.guardians:type_name -> node.v1.GuardianSetUpdate.Guardian
-	27, // 8: node.v1.SendObservationRequestRequest.observation_request:type_name -> gossip.v1.ObservationRequest
+	28, // 7: node.v1.GuardianSetUpdate.guardians:type_name -> node.v1.GuardianSetUpdate.Guardian
+	29, // 8: node.v1.SendObservationRequestRequest.observation_request:type_name -> gossip.v1.ObservationRequest
 	0,  // 9: node.v1.NodePrivilegedService.InjectGovernanceVAA:input_type -> node.v1.InjectGovernanceVAARequest
 	10, // 10: node.v1.NodePrivilegedService.FindMissingMessages:input_type -> node.v1.FindMissingMessagesRequest
 	12, // 11: node.v1.NodePrivilegedService.SendObservationRequest:input_type -> node.v1.SendObservationRequestRequest
@@ -1852,17 +1981,19 @@ var file_node_v1_node_proto_depIdxs = []int32{
 	20, // 15: node.v1.NodePrivilegedService.ChainGovernorReleasePendingVAA:input_type -> node.v1.ChainGovernorReleasePendingVAARequest
 	22, // 16: node.v1.NodePrivilegedService.ChainGovernorResetReleaseTimer:input_type -> node.v1.ChainGovernorResetReleaseTimerRequest
 	24, // 17: node.v1.NodePrivilegedService.PurgePythNetVaas:input_type -> node.v1.PurgePythNetVaasRequest
-	2,  // 18: node.v1.NodePrivilegedService.InjectGovernanceVAA:output_type -> node.v1.InjectGovernanceVAAResponse
-	11, // 19: node.v1.NodePrivilegedService.FindMissingMessages:output_type -> node.v1.FindMissingMessagesResponse
-	13, // 20: node.v1.NodePrivilegedService.SendObservationRequest:output_type -> node.v1.SendObservationRequestResponse
-	15, // 21: node.v1.NodePrivilegedService.ChainGovernorStatus:output_type -> node.v1.ChainGovernorStatusResponse
-	17, // 22: node.v1.NodePrivilegedService.ChainGovernorReload:output_type -> node.v1.ChainGovernorReloadResponse
-	19, // 23: node.v1.NodePrivilegedService.ChainGovernorDropPendingVAA:output_type -> node.v1.ChainGovernorDropPendingVAAResponse
-	21, // 24: node.v1.NodePrivilegedService.ChainGovernorReleasePendingVAA:output_type -> node.v1.ChainGovernorReleasePendingVAAResponse
-	23, // 25: node.v1.NodePrivilegedService.ChainGovernorResetReleaseTimer:output_type -> node.v1.ChainGovernorResetReleaseTimerResponse
-	25, // 26: node.v1.NodePrivilegedService.PurgePythNetVaas:output_type -> node.v1.PurgePythNetVaasResponse
-	18, // [18:27] is the sub-list for method output_type
-	9,  // [9:18] is the sub-list for method input_type
+	26, // 18: node.v1.NodePrivilegedService.SignExistingVAA:input_type -> node.v1.SignExistingVAARequest
+	2,  // 19: node.v1.NodePrivilegedService.InjectGovernanceVAA:output_type -> node.v1.InjectGovernanceVAAResponse
+	11, // 20: node.v1.NodePrivilegedService.FindMissingMessages:output_type -> node.v1.FindMissingMessagesResponse
+	13, // 21: node.v1.NodePrivilegedService.SendObservationRequest:output_type -> node.v1.SendObservationRequestResponse
+	15, // 22: node.v1.NodePrivilegedService.ChainGovernorStatus:output_type -> node.v1.ChainGovernorStatusResponse
+	17, // 23: node.v1.NodePrivilegedService.ChainGovernorReload:output_type -> node.v1.ChainGovernorReloadResponse
+	19, // 24: node.v1.NodePrivilegedService.ChainGovernorDropPendingVAA:output_type -> node.v1.ChainGovernorDropPendingVAAResponse
+	21, // 25: node.v1.NodePrivilegedService.ChainGovernorReleasePendingVAA:output_type -> node.v1.ChainGovernorReleasePendingVAAResponse
+	23, // 26: node.v1.NodePrivilegedService.ChainGovernorResetReleaseTimer:output_type -> node.v1.ChainGovernorResetReleaseTimerResponse
+	25, // 27: node.v1.NodePrivilegedService.PurgePythNetVaas:output_type -> node.v1.PurgePythNetVaasResponse
+	27, // 28: node.v1.NodePrivilegedService.SignExistingVAA:output_type -> node.v1.SignExistingVAAResponse
+	19, // [19:29] is the sub-list for method output_type
+	9,  // [9:19] is the sub-list for method input_type
 	9,  // [9:9] is the sub-list for extension type_name
 	9,  // [9:9] is the sub-list for extension extendee
 	0,  // [0:9] is the sub-list for field type_name
@@ -2187,6 +2318,30 @@ func file_node_v1_node_proto_init() {
 			}
 		}
 		file_node_v1_node_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*SignExistingVAARequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_node_v1_node_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*SignExistingVAAResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_node_v1_node_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*GuardianSetUpdate_Guardian); i {
 			case 0:
 				return &v.state
@@ -2213,7 +2368,7 @@ func file_node_v1_node_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_node_v1_node_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   27,
+			NumMessages:   29,
 			NumExtensions: 0,
 			NumServices:   1,
 		},

+ 81 - 0
node/pkg/proto/node/v1/node.pb.gw.go

@@ -337,6 +337,40 @@ func local_request_NodePrivilegedService_PurgePythNetVaas_0(ctx context.Context,
 
 }
 
+func request_NodePrivilegedService_SignExistingVAA_0(ctx context.Context, marshaler runtime.Marshaler, client NodePrivilegedServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq SignExistingVAARequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.SignExistingVAA(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_NodePrivilegedService_SignExistingVAA_0(ctx context.Context, marshaler runtime.Marshaler, server NodePrivilegedServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq SignExistingVAARequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.SignExistingVAA(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
 // RegisterNodePrivilegedServiceHandlerServer registers the http handlers for service NodePrivilegedService to "mux".
 // UnaryRPC     :call NodePrivilegedServiceServer directly.
 // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@@ -550,6 +584,29 @@ func RegisterNodePrivilegedServiceHandlerServer(ctx context.Context, mux *runtim
 
 	})
 
+	mux.Handle("POST", pattern_NodePrivilegedService_SignExistingVAA_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/node.v1.NodePrivilegedService/SignExistingVAA", runtime.WithHTTPPathPattern("/node.v1.NodePrivilegedService/SignExistingVAA"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_NodePrivilegedService_SignExistingVAA_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_NodePrivilegedService_SignExistingVAA_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	return nil
 }
 
@@ -771,6 +828,26 @@ func RegisterNodePrivilegedServiceHandlerClient(ctx context.Context, mux *runtim
 
 	})
 
+	mux.Handle("POST", pattern_NodePrivilegedService_SignExistingVAA_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/node.v1.NodePrivilegedService/SignExistingVAA", runtime.WithHTTPPathPattern("/node.v1.NodePrivilegedService/SignExistingVAA"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_NodePrivilegedService_SignExistingVAA_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_NodePrivilegedService_SignExistingVAA_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	return nil
 }
 
@@ -792,6 +869,8 @@ var (
 	pattern_NodePrivilegedService_ChainGovernorResetReleaseTimer_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"node.v1.NodePrivilegedService", "ChainGovernorResetReleaseTimer"}, ""))
 
 	pattern_NodePrivilegedService_PurgePythNetVaas_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"node.v1.NodePrivilegedService", "PurgePythNetVaas"}, ""))
+
+	pattern_NodePrivilegedService_SignExistingVAA_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"node.v1.NodePrivilegedService", "SignExistingVAA"}, ""))
 )
 
 var (
@@ -812,4 +891,6 @@ var (
 	forward_NodePrivilegedService_ChainGovernorResetReleaseTimer_0 = runtime.ForwardResponseMessage
 
 	forward_NodePrivilegedService_PurgePythNetVaas_0 = runtime.ForwardResponseMessage
+
+	forward_NodePrivilegedService_SignExistingVAA_0 = runtime.ForwardResponseMessage
 )

+ 38 - 0
node/pkg/proto/node/v1/node_grpc.pb.go

@@ -46,6 +46,8 @@ type NodePrivilegedServiceClient interface {
 	ChainGovernorResetReleaseTimer(ctx context.Context, in *ChainGovernorResetReleaseTimerRequest, opts ...grpc.CallOption) (*ChainGovernorResetReleaseTimerResponse, error)
 	// PurgePythNetVaas deletes PythNet VAAs from the database that are more than the specified number of days old.
 	PurgePythNetVaas(ctx context.Context, in *PurgePythNetVaasRequest, opts ...grpc.CallOption) (*PurgePythNetVaasResponse, error)
+	// SignExistingVAA signs an existing VAA for a new guardian set using the local guardian key.
+	SignExistingVAA(ctx context.Context, in *SignExistingVAARequest, opts ...grpc.CallOption) (*SignExistingVAAResponse, error)
 }
 
 type nodePrivilegedServiceClient struct {
@@ -137,6 +139,15 @@ func (c *nodePrivilegedServiceClient) PurgePythNetVaas(ctx context.Context, in *
 	return out, nil
 }
 
+func (c *nodePrivilegedServiceClient) SignExistingVAA(ctx context.Context, in *SignExistingVAARequest, opts ...grpc.CallOption) (*SignExistingVAAResponse, error) {
+	out := new(SignExistingVAAResponse)
+	err := c.cc.Invoke(ctx, "/node.v1.NodePrivilegedService/SignExistingVAA", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 // NodePrivilegedServiceServer is the server API for NodePrivilegedService service.
 // All implementations must embed UnimplementedNodePrivilegedServiceServer
 // for forward compatibility
@@ -169,6 +180,8 @@ type NodePrivilegedServiceServer interface {
 	ChainGovernorResetReleaseTimer(context.Context, *ChainGovernorResetReleaseTimerRequest) (*ChainGovernorResetReleaseTimerResponse, error)
 	// PurgePythNetVaas deletes PythNet VAAs from the database that are more than the specified number of days old.
 	PurgePythNetVaas(context.Context, *PurgePythNetVaasRequest) (*PurgePythNetVaasResponse, error)
+	// SignExistingVAA signs an existing VAA for a new guardian set using the local guardian key.
+	SignExistingVAA(context.Context, *SignExistingVAARequest) (*SignExistingVAAResponse, error)
 	mustEmbedUnimplementedNodePrivilegedServiceServer()
 }
 
@@ -203,6 +216,9 @@ func (UnimplementedNodePrivilegedServiceServer) ChainGovernorResetReleaseTimer(c
 func (UnimplementedNodePrivilegedServiceServer) PurgePythNetVaas(context.Context, *PurgePythNetVaasRequest) (*PurgePythNetVaasResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method PurgePythNetVaas not implemented")
 }
+func (UnimplementedNodePrivilegedServiceServer) SignExistingVAA(context.Context, *SignExistingVAARequest) (*SignExistingVAAResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SignExistingVAA not implemented")
+}
 func (UnimplementedNodePrivilegedServiceServer) mustEmbedUnimplementedNodePrivilegedServiceServer() {}
 
 // UnsafeNodePrivilegedServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -378,6 +394,24 @@ func _NodePrivilegedService_PurgePythNetVaas_Handler(srv interface{}, ctx contex
 	return interceptor(ctx, in, info, handler)
 }
 
+func _NodePrivilegedService_SignExistingVAA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SignExistingVAARequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(NodePrivilegedServiceServer).SignExistingVAA(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/node.v1.NodePrivilegedService/SignExistingVAA",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(NodePrivilegedServiceServer).SignExistingVAA(ctx, req.(*SignExistingVAARequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 // NodePrivilegedService_ServiceDesc is the grpc.ServiceDesc for NodePrivilegedService service.
 // It's only intended for direct use with grpc.RegisterService,
 // and not to be introspected or modified (even as a copy)
@@ -421,6 +455,10 @@ var NodePrivilegedService_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "PurgePythNetVaas",
 			Handler:    _NodePrivilegedService_PurgePythNetVaas_Handler,
 		},
+		{
+			MethodName: "SignExistingVAA",
+			Handler:    _NodePrivilegedService_SignExistingVAA_Handler,
+		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: "node/v1/node.proto",

+ 14 - 1
proto/node/v1/node.proto

@@ -45,7 +45,10 @@ service NodePrivilegedService {
   rpc ChainGovernorResetReleaseTimer (ChainGovernorResetReleaseTimerRequest) returns (ChainGovernorResetReleaseTimerResponse);
 
   // PurgePythNetVaas deletes PythNet VAAs from the database that are more than the specified number of days old.
-  rpc PurgePythNetVaas (PurgePythNetVaasRequest) returns (PurgePythNetVaasResponse);  
+  rpc PurgePythNetVaas (PurgePythNetVaasRequest) returns (PurgePythNetVaasResponse);
+
+  // SignExistingVAA signs an existing VAA for a new guardian set using the local guardian key.
+  rpc SignExistingVAA (SignExistingVAARequest) returns (SignExistingVAAResponse);
 }
 
 message InjectGovernanceVAARequest {
@@ -233,3 +236,13 @@ message PurgePythNetVaasRequest {
 message PurgePythNetVaasResponse {
   string response = 1;
 }
+
+message SignExistingVAARequest {
+  bytes vaa = 1;
+  repeated string new_guardian_addrs = 2;
+  uint32 new_guardian_set_index = 3;
+}
+
+message SignExistingVAAResponse {
+  bytes vaa = 1;
+}