diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 2422fe1aff..9dc3b1eb23 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -13,20 +13,17 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/core/rawdb" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/precompile/precompileconfig" "github.com/ava-labs/subnet-evm/predicate" - "github.com/ava-labs/subnet-evm/x/warp" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) var ( @@ -130,21 +127,6 @@ func (b *Block) handlePrecompileAccept(rules *params.Rules, sharedMemoryWriter * } } - // If Warp is enabled, add the block hash as an unsigned message to the warp backend. - if rules.IsPrecompileEnabled(warp.ContractAddress) { - blockHashPayload, err := payload.NewHash(ids.ID(b.ethBlock.Hash())) - if err != nil { - return fmt.Errorf("failed to create block hash payload: %w", err) - } - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(b.vm.ctx.NetworkID, b.vm.ctx.ChainID, blockHashPayload.Bytes()) - if err != nil { - return fmt.Errorf("failed to create unsigned message for block hash payload: %w", err) - } - if err := b.vm.warpBackend.AddMessage(unsignedMessage); err != nil { - return fmt.Errorf("failed to add block hash payload unsigned message: %w", err) - } - } - return nil } diff --git a/plugin/evm/message/codec.go b/plugin/evm/message/codec.go index 8b9a84d43a..91db9633ab 100644 --- a/plugin/evm/message/codec.go +++ b/plugin/evm/message/codec.go @@ -41,7 +41,8 @@ func init() { c.RegisterType(CodeResponse{}), // Warp request types - c.RegisterType(SignatureRequest{}), + c.RegisterType(MessageSignatureRequest{}), + c.RegisterType(BlockSignatureRequest{}), c.RegisterType(SignatureResponse{}), Codec.RegisterCodec(Version, c), diff --git a/plugin/evm/message/handler.go b/plugin/evm/message/handler.go index 659908aaee..b5933f28f3 100644 --- a/plugin/evm/message/handler.go +++ b/plugin/evm/message/handler.go @@ -38,7 +38,8 @@ type RequestHandler interface { HandleTrieLeafsRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, leafsRequest LeafsRequest) ([]byte, error) HandleBlockRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, blockRequest BlockRequest) ([]byte, error) HandleCodeRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, codeRequest CodeRequest) ([]byte, error) - HandleSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest SignatureRequest) ([]byte, error) + HandleMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest MessageSignatureRequest) ([]byte, error) + HandleBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest BlockSignatureRequest) ([]byte, error) } // ResponseHandler handles response for a sent request @@ -64,7 +65,11 @@ func (NoopRequestHandler) HandleCodeRequest(ctx context.Context, nodeID ids.Node return nil, nil } -func (NoopRequestHandler) HandleSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest SignatureRequest) ([]byte, error) { +func (NoopRequestHandler) HandleMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest MessageSignatureRequest) ([]byte, error) { + return nil, nil +} + +func (NoopRequestHandler) HandleBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest BlockSignatureRequest) ([]byte, error) { return nil, nil } diff --git a/plugin/evm/message/signature_request.go b/plugin/evm/message/signature_request.go index 3ed8b64829..127716617f 100644 --- a/plugin/evm/message/signature_request.go +++ b/plugin/evm/message/signature_request.go @@ -11,22 +11,38 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/bls" ) -var _ Request = SignatureRequest{} +var ( + _ Request = MessageSignatureRequest{} + _ Request = BlockSignatureRequest{} +) -// SignatureRequest is used to request a warp message's signature. -type SignatureRequest struct { +// MessageSignatureRequest is used to request a warp message's signature. +type MessageSignatureRequest struct { MessageID ids.ID `serialize:"true"` } -func (s SignatureRequest) String() string { - return fmt.Sprintf("SignatureRequest(MessageID=%s)", s.MessageID.String()) +func (s MessageSignatureRequest) String() string { + return fmt.Sprintf("MessageSignatureRequest(MessageID=%s)", s.MessageID.String()) +} + +func (s MessageSignatureRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) { + return handler.HandleMessageSignatureRequest(ctx, nodeID, requestID, s) +} + +// BlockSignatureRequest is used to request a warp message's signature. +type BlockSignatureRequest struct { + BlockID ids.ID `serialize:"true"` +} + +func (s BlockSignatureRequest) String() string { + return fmt.Sprintf("BlockSignatureRequest(BlockID=%s)", s.BlockID.String()) } -func (s SignatureRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) { - return handler.HandleSignatureRequest(ctx, nodeID, requestID, s) +func (s BlockSignatureRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) { + return handler.HandleBlockSignatureRequest(ctx, nodeID, requestID, s) } -// SignatureResponse is the response to a SignatureRequest. +// SignatureResponse is the response to a BlockSignatureRequest or MessageSignatureRequest. // The response contains a BLS signature of the requested message, signed by the responding node's BLS private key. type SignatureResponse struct { Signature [bls.SignatureLen]byte `serialize:"true"` diff --git a/plugin/evm/message/signature_request_test.go b/plugin/evm/message/signature_request_test.go index 9e4c2fd96e..59614fbb2e 100644 --- a/plugin/evm/message/signature_request_test.go +++ b/plugin/evm/message/signature_request_test.go @@ -13,27 +13,40 @@ import ( "github.com/stretchr/testify/require" ) -// TestMarshalSignatureRequest asserts that the structure or serialization logic hasn't changed, primarily to +// TestMarshalMessageSignatureRequest asserts that the structure or serialization logic hasn't changed, primarily to // ensure compatibility with the network. -func TestMarshalSignatureRequest(t *testing.T) { - messageIDBytes, err := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000000000") +func TestMarshalMessageSignatureRequest(t *testing.T) { + signatureRequest := MessageSignatureRequest{ + MessageID: ids.ID{68, 79, 70, 65, 72, 73, 64, 107}, + } + + base64MessageSignatureRequest := "AABET0ZBSElAawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + signatureRequestBytes, err := Codec.Marshal(Version, signatureRequest) require.NoError(t, err) - messageID, err := ids.ToID(messageIDBytes) + require.Equal(t, base64MessageSignatureRequest, base64.StdEncoding.EncodeToString(signatureRequestBytes)) + + var s MessageSignatureRequest + _, err = Codec.Unmarshal(signatureRequestBytes, &s) require.NoError(t, err) + require.Equal(t, signatureRequest.MessageID, s.MessageID) +} - signatureRequest := SignatureRequest{ - MessageID: messageID, +// TestMarshalBlockSignatureRequest asserts that the structure or serialization logic hasn't changed, primarily to +// ensure compatibility with the network. +func TestMarshalBlockSignatureRequest(t *testing.T) { + signatureRequest := BlockSignatureRequest{ + BlockID: ids.ID{68, 79, 70, 65, 72, 73, 64, 107}, } - base64SignatureRequest := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + base64BlockSignatureRequest := "AABET0ZBSElAawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" signatureRequestBytes, err := Codec.Marshal(Version, signatureRequest) require.NoError(t, err) - require.Equal(t, base64SignatureRequest, base64.StdEncoding.EncodeToString(signatureRequestBytes)) + require.Equal(t, base64BlockSignatureRequest, base64.StdEncoding.EncodeToString(signatureRequestBytes)) - var s SignatureRequest + var s BlockSignatureRequest _, err = Codec.Unmarshal(signatureRequestBytes, &s) require.NoError(t, err) - require.Equal(t, signatureRequest.MessageID, s.MessageID) + require.Equal(t, signatureRequest.BlockID, s.BlockID) } // TestMarshalSignatureResponse asserts that the structure or serialization logic hasn't changed, primarily to diff --git a/plugin/evm/network_handler.go b/plugin/evm/network_handler.go index f0d68bc4d6..21bbf83d7e 100644 --- a/plugin/evm/network_handler.go +++ b/plugin/evm/network_handler.go @@ -56,6 +56,10 @@ func (n networkHandler) HandleCodeRequest(ctx context.Context, nodeID ids.NodeID return n.codeRequestHandler.OnCodeRequest(ctx, nodeID, requestID, codeRequest) } -func (n networkHandler) HandleSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest message.SignatureRequest) ([]byte, error) { - return n.signatureRequestHandler.OnSignatureRequest(ctx, nodeID, requestID, signatureRequest) +func (n networkHandler) HandleMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, messageSignatureRequest message.MessageSignatureRequest) ([]byte, error) { + return n.signatureRequestHandler.OnMessageSignatureRequest(ctx, nodeID, requestID, messageSignatureRequest) +} + +func (n networkHandler) HandleBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, blockSignatureRequest message.BlockSignatureRequest) ([]byte, error) { + return n.signatureRequestHandler.OnBlockSignatureRequest(ctx, nodeID, requestID, blockSignatureRequest) } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index c1e561a9a1..adebe9157a 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -42,7 +42,6 @@ import ( "github.com/ava-labs/subnet-evm/sync/client/stats" "github.com/ava-labs/subnet-evm/trie" "github.com/ava-labs/subnet-evm/warp" - "github.com/ava-labs/subnet-evm/warp/aggregator" warpValidators "github.com/ava-labs/subnet-evm/warp/validators" // Force-load tracer engine to trigger registration @@ -472,7 +471,7 @@ func (vm *VM) Initialize( vm.client = peer.NewNetworkClient(vm.Network) // initialize warp backend - vm.warpBackend = warp.NewBackend(vm.ctx.WarpSigner, vm.warpDB, warpSignatureCacheSize) + vm.warpBackend = warp.NewBackend(vm.ctx.NetworkID, vm.ctx.ChainID, vm.ctx.WarpSigner, vm, vm.warpDB, warpSignatureCacheSize) // clear warpdb on initialization if config enabled if vm.config.PruneWarpDB { @@ -936,9 +935,7 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { if vm.config.WarpAPIEnabled { validatorsState := warpValidators.NewState(vm.ctx) - signatureGetter := &aggregator.NetworkSignatureGetter{Client: vm.client} - warpAggregator := aggregator.New(vm.ctx.SubnetID, validatorsState, signatureGetter) - if err := handler.RegisterName("warp", warp.NewAPI(vm.warpBackend, warpAggregator)); err != nil { + if err := handler.RegisterName("warp", warp.NewAPI(vm.ctx.NetworkID, vm.ctx.SubnetID, vm.ctx.ChainID, validatorsState, vm.warpBackend, vm.client)); err != nil { return nil, err } enabledAPIs = append(enabledAPIs, "warp") diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 2914eeb2b3..2c71ebac15 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -3273,7 +3273,7 @@ func TestCrossChainMessagestoVM(t *testing.T) { require.True(calledSendCrossChainAppResponseFn, "sendCrossChainAppResponseFn was not called") } -func TestSignatureRequestsToVM(t *testing.T) { +func TestMessageSignatureRequestsToVM(t *testing.T) { _, vm, _, appSender := GenesisVM(t, true, genesisJSONSubnetEVM, "", "") defer func() { @@ -3288,7 +3288,7 @@ func TestSignatureRequestsToVM(t *testing.T) { // Add the known message and get its signature to confirm. err = vm.warpBackend.AddMessage(warpMessage) require.NoError(t, err) - signature, err := vm.warpBackend.GetSignature(warpMessage.ID()) + signature, err := vm.warpBackend.GetMessageSignature(warpMessage.ID()) require.NoError(t, err) tests := map[string]struct { @@ -3317,7 +3317,7 @@ func TestSignatureRequestsToVM(t *testing.T) { return nil } t.Run(name, func(t *testing.T) { - var signatureRequest message.Request = message.SignatureRequest{ + var signatureRequest message.Request = message.MessageSignatureRequest{ MessageID: test.messageID, } @@ -3332,3 +3332,59 @@ func TestSignatureRequestsToVM(t *testing.T) { }) } } + +func TestBlockSignatureRequestsToVM(t *testing.T) { + _, vm, _, appSender := GenesisVM(t, true, genesisJSONSubnetEVM, "", "") + + defer func() { + err := vm.Shutdown(context.Background()) + require.NoError(t, err) + }() + + lastAcceptedID, err := vm.LastAccepted(context.Background()) + require.NoError(t, err) + + signature, err := vm.warpBackend.GetBlockSignature(lastAcceptedID) + require.NoError(t, err) + + tests := map[string]struct { + blockID ids.ID + expectedResponse [bls.SignatureLen]byte + }{ + "known": { + blockID: lastAcceptedID, + expectedResponse: signature, + }, + "unknown": { + blockID: ids.GenerateTestID(), + expectedResponse: [bls.SignatureLen]byte{}, + }, + } + + for name, test := range tests { + calledSendAppResponseFn := false + appSender.SendAppResponseF = func(ctx context.Context, nodeID ids.NodeID, requestID uint32, responseBytes []byte) error { + calledSendAppResponseFn = true + var response message.SignatureResponse + _, err := message.Codec.Unmarshal(responseBytes, &response) + require.NoError(t, err) + require.Equal(t, test.expectedResponse, response.Signature) + + return nil + } + t.Run(name, func(t *testing.T) { + var signatureRequest message.Request = message.BlockSignatureRequest{ + BlockID: test.blockID, + } + + requestBytes, err := message.Codec.Marshal(message.Version, &signatureRequest) + require.NoError(t, err) + + // Send the app request and make sure we called SendAppResponseFn + deadline := time.Now().Add(60 * time.Second) + err = vm.Network.AppRequest(context.Background(), ids.GenerateTestNodeID(), 1, deadline, requestBytes) + require.NoError(t, err) + require.True(t, calledSendAppResponseFn) + }) + } +} diff --git a/plugin/evm/vm_warp_test.go b/plugin/evm/vm_warp_test.go index 0d0745b984..4e1321a8d5 100644 --- a/plugin/evm/vm_warp_test.go +++ b/plugin/evm/vm_warp_test.go @@ -113,13 +113,17 @@ func TestSendWarpMessage(t *testing.T) { unsignedMessageID := unsignedMessage.ID() // Verify the signature cannot be fetched before the block is accepted - _, err = vm.warpBackend.GetSignature(unsignedMessageID) + _, err = vm.warpBackend.GetMessageSignature(unsignedMessageID) + require.Error(err) + _, err = vm.warpBackend.GetBlockSignature(blk.ID()) require.Error(err) require.NoError(vm.SetPreference(context.Background(), blk.ID())) require.NoError(blk.Accept(context.Background())) vm.blockChain.DrainAcceptorQueue() - rawSignatureBytes, err := vm.warpBackend.GetSignature(unsignedMessageID) + + // Verify the message signature after accepting the block. + rawSignatureBytes, err := vm.warpBackend.GetMessageSignature(unsignedMessageID) require.NoError(err) blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes[:]) require.NoError(err) @@ -132,7 +136,21 @@ func TestSendWarpMessage(t *testing.T) { require.Fail("Failed to read accepted logs from subscription") } - // Verify the produced signature is valid + // Verify the produced message signature is valid + require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) + + // Verify the blockID will now be signed by the backend and produces a valid signature. + rawSignatureBytes, err = vm.warpBackend.GetBlockSignature(blk.ID()) + require.NoError(err) + blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes[:]) + require.NoError(err) + + blockHashPayload, err := payload.NewHash(blk.ID()) + require.NoError(err) + unsignedMessage, err = avalancheWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, blockHashPayload.Bytes()) + require.NoError(err) + + // Verify the produced message signature is valid require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) } diff --git a/tests/warp/warp_test.go b/tests/warp/warp_test.go index 2311bcd974..23d2a821ad 100644 --- a/tests/warp/warp_test.go +++ b/tests/warp/warp_test.go @@ -18,8 +18,7 @@ import ( "github.com/ava-labs/avalanche-network-runner/rpcpb" "github.com/ava-labs/avalanchego/api/info" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/core/types" @@ -31,6 +30,7 @@ import ( "github.com/ava-labs/subnet-evm/tests/utils" "github.com/ava-labs/subnet-evm/tests/utils/runner" warpBackend "github.com/ava-labs/subnet-evm/warp" + "github.com/ava-labs/subnet-evm/warp/aggregator" "github.com/ava-labs/subnet-evm/x/warp" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -45,10 +45,16 @@ var ( config = runner.NewDefaultANRConfig() manager = runner.NewNetworkManager(config) warpChainConfigPath string + networkID uint32 unsignedWarpMsg *avalancheWarp.UnsignedMessage unsignedWarpMessageID ids.ID signedWarpMsg *avalancheWarp.Message + warpBlockID ids.ID + warpBlockHashPayload *payload.Hash + warpBlockHashUnsignedMsg *avalancheWarp.UnsignedMessage + warpBlockHashSignedMsg *avalancheWarp.Message blockchainIDA, blockchainIDB ids.ID + subnetIDA, subnetIDB ids.ID chainAURIs, chainBURIs []string chainAWSClient, chainBWSClient ethclient.Client chainID = big.NewInt(99999) @@ -56,6 +62,7 @@ var ( fundedAddress common.Address testPayload = []byte{1, 2, 3} txSigner = types.LatestSignerForChainID(chainID) + nodesPerSubnet = 5 ) func TestE2E(t *testing.T) { @@ -76,15 +83,12 @@ var _ = ginkgo.BeforeSuite(func() { var err error // Name 10 new validators (which should have BLS key registered) subnetANodeNames := make([]string, 0) - subnetBNodeNames := []string{} - for i := 1; i <= 10; i++ { - n := fmt.Sprintf("node%d-bls", i) - if i <= 5 { - subnetANodeNames = append(subnetANodeNames, n) - } else { - subnetBNodeNames = append(subnetBNodeNames, n) - } + subnetBNodeNames := make([]string, 0) + for i := 0; i < nodesPerSubnet; i++ { + subnetANodeNames = append(subnetANodeNames, fmt.Sprintf("node%d-subnetA-bls", i)) + subnetBNodeNames = append(subnetBNodeNames, fmt.Sprintf("node%d-subnetB-bls", i)) } + f, err := os.CreateTemp(os.TempDir(), "config.json") gomega.Expect(err).Should(gomega.BeNil()) _, err = f.Write([]byte(`{"warp-api-enabled": true}`)) @@ -144,17 +148,23 @@ var _ = ginkgo.BeforeSuite(func() { subnetADetails, ok := manager.GetSubnet(subnetA) gomega.Expect(ok).Should(gomega.BeTrue()) blockchainIDA = subnetADetails.BlockchainID - gomega.Expect(len(subnetADetails.ValidatorURIs)).Should(gomega.Equal(5)) + subnetIDA = subnetADetails.SubnetID + gomega.Expect(len(subnetADetails.ValidatorURIs)).Should(gomega.Equal(nodesPerSubnet)) chainAURIs = append(chainAURIs, subnetADetails.ValidatorURIs...) + infoClient := info.NewClient(chainAURIs[0]) + networkID, err = infoClient.GetNetworkID(context.Background()) + gomega.Expect(err).Should(gomega.BeNil()) + subnetB = subnetIDs[1] subnetBDetails, ok = manager.GetSubnet(subnetB) gomega.Expect(ok).Should(gomega.BeTrue()) blockchainIDB = subnetBDetails.BlockchainID - gomega.Expect(len(subnetBDetails.ValidatorURIs)).Should(gomega.Equal(5)) + subnetIDB = subnetBDetails.SubnetID + gomega.Expect(len(subnetBDetails.ValidatorURIs)).Should(gomega.Equal(nodesPerSubnet)) chainBURIs = append(chainBURIs, subnetBDetails.ValidatorURIs...) - log.Info("Created URIs for both subnets", "ChainAURIs", chainAURIs, "ChainBURIs", chainBURIs, "blockchainIDA", blockchainIDA, "blockchainIDB", blockchainIDB) + log.Info("Created URIs for both subnets", "ChainAURIs", chainAURIs, "ChainBURIs", chainBURIs, "blockchainIDA", blockchainIDA, "subnetIDA", subnetIDA, "blockchainIDB", blockchainIDB, "subnetIDB", subnetIDB) chainAWSURI := toWebsocketURI(chainAURIs[0], blockchainIDA.String()) log.Info("Creating ethclient for blockchainA", "wsURI", chainAWSURI) @@ -210,6 +220,13 @@ var _ = ginkgo.Describe("[Warp]", ginkgo.Ordered, func() { newHead := <-newHeads blockHash := newHead.Hash() + log.Info("Constructing warp block hash unsigned message", "blockHash", blockHash) + warpBlockID = ids.ID(blockHash) // Set warpBlockID to construct a warp message containing a block hash payload later + warpBlockHashPayload, err = payload.NewHash(warpBlockID) + gomega.Expect(err).Should(gomega.BeNil()) + warpBlockHashUnsignedMsg, err = avalancheWarp.NewUnsignedMessage(networkID, blockchainIDA, warpBlockHashPayload.Bytes()) + gomega.Expect(err).Should(gomega.BeNil()) + log.Info("Fetching relevant warp logs from the newly produced block") logs, err := chainAWSClient.FilterLogs(ctx, interfaces.FilterQuery{ BlockHash: &blockHash, @@ -256,53 +273,55 @@ var _ = ginkgo.Describe("[Warp]", ginkgo.Ordered, func() { ginkgo.It("Aggregate Warp Signature via API", ginkgo.Label("Warp", "ReceiveWarp", "AggregateWarpManually"), func() { ctx := context.Background() - blsSignatures := make([]*bls.Signature, 0, len(chainAURIs)) - for i, uri := range chainAURIs { + warpAPIs := make(map[ids.NodeID]warpBackend.Client, len(chainAURIs)) + for _, uri := range chainAURIs { client, err := warpBackend.NewClient(uri, blockchainIDA.String()) gomega.Expect(err).Should(gomega.BeNil()) - log.Info("Fetching warp signature from node") - rawSignatureBytes, err := client.GetSignature(ctx, unsignedWarpMessageID) - gomega.Expect(err).Should(gomega.BeNil()) - - blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes) - gomega.Expect(err).Should(gomega.BeNil()) infoClient := info.NewClient(uri) - nodeID, blsSigner, err := infoClient.GetNodeID(ctx) + nodeID, _, err := infoClient.GetNodeID(ctx) gomega.Expect(err).Should(gomega.BeNil()) - - blsSignatures = append(blsSignatures, blsSignature) - - blsPublicKey := blsSigner.Key() - log.Info("Verifying BLS Signature from node", "nodeID", nodeID, "nodeIndex", i) - gomega.Expect(bls.Verify(blsPublicKey, blsSignature, unsignedWarpMsg.Bytes())).Should(gomega.BeTrue()) + warpAPIs[nodeID] = client } - blsAggregatedSignature, err := bls.AggregateSignatures(blsSignatures) + pChainClient := platformvm.NewClient(chainAURIs[0]) + pChainHeight, err := pChainClient.GetHeight(ctx) gomega.Expect(err).Should(gomega.BeNil()) - - signersBitSet := set.NewBits() - for i := 0; i < len(blsSignatures); i++ { - signersBitSet.Add(i) - } - warpSignature := &avalancheWarp.BitSetSignature{ - Signers: signersBitSet.Bytes(), + validators, err := pChainClient.GetValidatorsAt(ctx, subnetIDA, pChainHeight) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(len(validators)).Should(gomega.Equal(nodesPerSubnet)) + totalWeight := uint64(0) + warpValidators := make([]*avalancheWarp.Validator, 0, len(validators)) + for nodeID, validator := range validators { + warpValidators = append(warpValidators, &avalancheWarp.Validator{ + PublicKey: validator.PublicKey, + Weight: validator.Weight, + NodeIDs: []ids.NodeID{nodeID}, + }) + totalWeight += validator.Weight } - blsAggregatedSignatureBytes := bls.SignatureToBytes(blsAggregatedSignature) - copy(warpSignature.Signature[:], blsAggregatedSignatureBytes) + log.Info("Aggregating signatures from validator set", "numValidators", len(warpValidators), "totalWeight", totalWeight) + apiSignatureGetter := warpBackend.NewAPIFetcher(warpAPIs) + signatureResult, err := aggregator.New(apiSignatureGetter, warpValidators, totalWeight).AggregateSignatures(ctx, unsignedWarpMsg, 100) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(signatureResult.SignatureWeight).Should(gomega.Equal(signatureResult.TotalWeight)) + gomega.Expect(signatureResult.SignatureWeight).Should(gomega.Equal(totalWeight)) - warpMsg, err := avalancheWarp.NewMessage( - unsignedWarpMsg, - warpSignature, - ) + signedWarpMsg = signatureResult.Message + + signatureResult, err = aggregator.New(apiSignatureGetter, warpValidators, totalWeight).AggregateSignatures(ctx, warpBlockHashUnsignedMsg, 100) gomega.Expect(err).Should(gomega.BeNil()) - signedWarpMsg = warpMsg + gomega.Expect(signatureResult.SignatureWeight).Should(gomega.Equal(signatureResult.TotalWeight)) + gomega.Expect(signatureResult.SignatureWeight).Should(gomega.Equal(totalWeight)) + warpBlockHashSignedMsg = signatureResult.Message + + log.Info("Aggregated signatures for warp messages", "signedWarpMsg", common.Bytes2Hex(signedWarpMsg.Bytes()), "warpBlockHashSignedMsg", common.Bytes2Hex(warpBlockHashSignedMsg.Bytes())) }) - // Aggregate a Warp Signature using the node's Signature Aggregation API call and verifying that its output matches the + // Aggregate a Warp Message Signature using the node's Signature Aggregation API call and verifying that its output matches the // the manual construction - ginkgo.It("Aggregate Warp Signature via Aggregator", ginkgo.Label("Warp", "ReceiveWarp", "AggregatorWarp"), func() { + ginkgo.It("Aggregate Warp Message Signature via Aggregator", ginkgo.Label("Warp", "ReceiveWarp", "AggregatorWarp"), func() { ctx := context.Background() // Verify that the signature aggregation matches the results of manually constructing the warp message @@ -310,11 +329,26 @@ var _ = ginkgo.Describe("[Warp]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) // Specify WarpQuorumDenominator to retrieve signatures from every validator - signedWarpMessageBytes, err := client.GetAggregateSignature(ctx, unsignedWarpMessageID, params.WarpQuorumDenominator) + signedWarpMessageBytes, err := client.GetMessageAggregateSignature(ctx, unsignedWarpMessageID, params.WarpQuorumDenominator) gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(signedWarpMessageBytes).Should(gomega.Equal(signedWarpMsg.Bytes())) }) + // Aggregate a Warp Block Signature using the node's Signature Aggregation API call and verifying that its output matches the + // the manual construction + ginkgo.It("Aggregate Warp Block Signature via Aggregator", ginkgo.Label("Warp", "ReceiveWarp", "AggregatorWarp"), func() { + ctx := context.Background() + + // Verify that the signature aggregation matches the results of manually constructing the warp message + client, err := warpBackend.NewClient(chainAURIs[0], blockchainIDA.String()) + gomega.Expect(err).Should(gomega.BeNil()) + + // Specify WarpQuorumDenominator to retrieve signatures from every validator + signedWarpBlockBytes, err := client.GetBlockAggregateSignature(ctx, warpBlockID, params.WarpQuorumDenominator) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(signedWarpBlockBytes).Should(gomega.Equal(warpBlockHashSignedMsg.Bytes())) + }) + // Verify successful delivery of the Avalanche Warp Message from Chain A to Chain B ginkgo.It("Verify Message from A to B", ginkgo.Label("Warp", "VerifyMessage"), func() { ctx := context.Background() @@ -366,6 +400,57 @@ var _ = ginkgo.Describe("[Warp]", ginkgo.Ordered, func() { gomega.Expect(receipt.Status).Should(gomega.Equal(types.ReceiptStatusSuccessful)) }) + // Verify successful delivery of the Avalanche Warp Block Hash from Chain A to Chain B + ginkgo.It("Verify Block Hash from A to B", ginkgo.Label("Warp", "VerifyMessage"), func() { + ctx := context.Background() + + log.Info("Subscribing to new heads") + newHeads := make(chan *types.Header, 10) + sub, err := chainBWSClient.SubscribeNewHead(ctx, newHeads) + gomega.Expect(err).Should(gomega.BeNil()) + defer sub.Unsubscribe() + + nonce, err := chainBWSClient.NonceAt(ctx, fundedAddress, nil) + gomega.Expect(err).Should(gomega.BeNil()) + + packedInput, err := warp.PackGetVerifiedWarpBlockHash(0) + gomega.Expect(err).Should(gomega.BeNil()) + tx := predicate.NewPredicateTx( + chainID, + nonce, + &warp.Module.Address, + 5_000_000, + big.NewInt(225*params.GWei), + big.NewInt(params.GWei), + common.Big0, + packedInput, + types.AccessList{}, + warp.ContractAddress, + warpBlockHashSignedMsg.Bytes(), + ) + signedTx, err := types.SignTx(tx, txSigner, fundedKey) + gomega.Expect(err).Should(gomega.BeNil()) + txBytes, err := signedTx.MarshalBinary() + gomega.Expect(err).Should(gomega.BeNil()) + log.Info("Sending getVerifiedWarpBlockHash transaction", "txHash", signedTx.Hash(), "txBytes", common.Bytes2Hex(txBytes)) + err = chainBWSClient.SendTransaction(ctx, signedTx) + gomega.Expect(err).Should(gomega.BeNil()) + + log.Info("Waiting for new block confirmation") + newHead := <-newHeads + blockHash := newHead.Hash() + log.Info("Fetching relevant warp logs and receipts from new block") + logs, err := chainBWSClient.FilterLogs(ctx, interfaces.FilterQuery{ + BlockHash: &blockHash, + Addresses: []common.Address{warp.Module.Address}, + }) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(len(logs)).Should(gomega.Equal(0)) + receipt, err := chainBWSClient.TransactionReceipt(ctx, signedTx.Hash()) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(receipt.Status).Should(gomega.Equal(types.ReceiptStatusSuccessful)) + }) + ginkgo.It("Send Message from A to B from Hardhat", ginkgo.Label("Warp", "IWarpMessenger", "SendWarpMessage"), func() { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() diff --git a/warp/aggregator/aggregator.go b/warp/aggregator/aggregator.go index d5c98fb50f..7f53075c55 100644 --- a/warp/aggregator/aggregator.go +++ b/warp/aggregator/aggregator.go @@ -5,22 +5,17 @@ package aggregator import ( "context" - "errors" "fmt" "github.com/ava-labs/subnet-evm/params" "github.com/ethereum/go-ethereum/log" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/set" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) -var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") - type AggregateSignatureResult struct { // Weight of validators included in the aggregate signature. SignatureWeight uint64 @@ -30,63 +25,40 @@ type AggregateSignatureResult struct { Message *avalancheWarp.Message } +type signatureFetchResult struct { + sig *bls.Signature + index int + weight uint64 +} + // Aggregator requests signatures from validators and // aggregates them into a single signature. type Aggregator struct { - // Aggregating signatures for a chain validated by this subnet. - subnetID ids.ID - // Fetches signatures from validators. - client SignatureGetter - // Validator state for this chain. - state validators.State + validators []*avalancheWarp.Validator + totalWeight uint64 + client SignatureGetter } // New returns a signature aggregator for the chain with the given [state] on the // given [subnetID], and where [client] can be used to fetch signatures from validators. -func New(subnetID ids.ID, state validators.State, client SignatureGetter) *Aggregator { +func New(client SignatureGetter, validators []*avalancheWarp.Validator, totalWeight uint64) *Aggregator { return &Aggregator{ - subnetID: subnetID, - client: client, - state: state, + client: client, + validators: validators, + totalWeight: totalWeight, } } // Returns an aggregate signature over [unsignedMessage]. // The returned signature's weight exceeds the threshold given by [quorumNum]. func (a *Aggregator) AggregateSignatures(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage, quorumNum uint64) (*AggregateSignatureResult, error) { - // Note: we use the current height as a best guess of the canonical validator set when the aggregated signature will be verified - // by the recipient chain. If the validator set changes from [pChainHeight] to the P-Chain height that is actually specified by the - // ProposerVM header when this message is verified, then the aggregate signature could become outdated and require re-aggregation. - pChainHeight, err := a.state.GetCurrentHeight(ctx) - if err != nil { - return nil, err - } - - log.Debug("Fetching signature", - "a.subnetID", a.subnetID, - "height", pChainHeight, - ) - validators, totalWeight, err := avalancheWarp.GetCanonicalValidatorSet(ctx, a.state, pChainHeight, a.subnetID) - if err != nil { - return nil, fmt.Errorf("failed to get validator set: %w", err) - } - if len(validators) == 0 { - return nil, fmt.Errorf("%w (SubnetID: %s, Height: %d)", errNoValidators, a.subnetID, pChainHeight) - } - - type signatureFetchResult struct { - sig *bls.Signature - index int - weight uint64 - } - // Create a child context to cancel signature fetching if we reach signature threshold. signatureFetchCtx, signatureFetchCancel := context.WithCancel(ctx) defer signatureFetchCancel() // Fetch signatures from validators concurrently. signatureFetchResultChan := make(chan *signatureFetchResult) - for i, validator := range validators { + for i, validator := range a.validators { var ( i = i validator = validator @@ -137,13 +109,13 @@ func (a *Aggregator) AggregateSignatures(ctx context.Context, unsignedMessage *a } var ( - signatures = make([]*bls.Signature, 0, len(validators)) + signatures = make([]*bls.Signature, 0, len(a.validators)) signersBitset = set.NewBits() signaturesWeight = uint64(0) signaturesPassedThreshold = false ) - for i := 0; i < len(validators); i++ { + for i := 0; i < len(a.validators); i++ { signatureFetchResult := <-signatureFetchResultChan if signatureFetchResult == nil { continue @@ -159,10 +131,10 @@ func (a *Aggregator) AggregateSignatures(ctx context.Context, unsignedMessage *a ) // If the signature weight meets the requested threshold, cancel signature fetching - if err := avalancheWarp.VerifyWeight(signaturesWeight, totalWeight, quorumNum, params.WarpQuorumDenominator); err == nil { + if err := avalancheWarp.VerifyWeight(signaturesWeight, a.totalWeight, quorumNum, params.WarpQuorumDenominator); err == nil { log.Debug("Verify weight passed, exiting aggregation early", "quorumNum", quorumNum, - "totalWeight", totalWeight, + "totalWeight", a.totalWeight, "signatureWeight", signaturesWeight, "msgID", unsignedMessage.ID(), ) @@ -196,6 +168,6 @@ func (a *Aggregator) AggregateSignatures(ctx context.Context, unsignedMessage *a return &AggregateSignatureResult{ Message: msg, SignatureWeight: signaturesWeight, - TotalWeight: totalWeight, + TotalWeight: a.totalWeight, }, nil } diff --git a/warp/aggregator/aggregator_test.go b/warp/aggregator/aggregator_test.go index 6668eba766..07d2020756 100644 --- a/warp/aggregator/aggregator_test.go +++ b/warp/aggregator/aggregator_test.go @@ -13,7 +13,6 @@ import ( "go.uber.org/mock/gomock" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) @@ -31,9 +30,7 @@ func newValidator(t testing.TB, weight uint64) (*bls.SecretKey, *avalancheWarp.V } func TestAggregateSignatures(t *testing.T) { - subnetID := ids.GenerateTestID() errTest := errors.New("test error") - pChainHeight := uint64(1337) unsignedMsg := &avalancheWarp.UnsignedMessage{ NetworkID: 1338, SourceChainID: ids.ID{'y', 'e', 'e', 't'}, @@ -57,20 +54,20 @@ func TestAggregateSignatures(t *testing.T) { nonVdrSk, err := bls.NewSecretKey() require.NoError(t, err) nonVdrSig := bls.Sign(nonVdrSk, unsignedMsg.Bytes()) - vdrSet := map[ids.NodeID]*validators.GetValidatorOutput{ - nodeID1: { - NodeID: nodeID1, + vdrs := []*avalancheWarp.Validator{ + { PublicKey: vdr1.PublicKey, + NodeIDs: []ids.NodeID{nodeID1}, Weight: vdr1.Weight, }, - nodeID2: { - NodeID: nodeID2, + { PublicKey: vdr2.PublicKey, + NodeIDs: []ids.NodeID{nodeID2}, Weight: vdr2.Weight, }, - nodeID3: { - NodeID: nodeID3, + { PublicKey: vdr3.PublicKey, + NodeIDs: []ids.NodeID{nodeID3}, Weight: vdr3.Weight, }, } @@ -86,64 +83,15 @@ func TestAggregateSignatures(t *testing.T) { } tests := []test{ - { - name: "can't get height", - contextWithCancelFunc: func() (context.Context, context.CancelFunc) { - return context.Background(), nil - }, - aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(uint64(0), errTest) - return New(subnetID, state, nil) - }, - unsignedMsg: nil, - quorumNum: 0, - expectedErr: errTest, - }, - { - name: "can't get validator set", - contextWithCancelFunc: func() (context.Context, context.CancelFunc) { - return context.Background(), nil - }, - aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errTest) - return New(subnetID, state, nil) - }, - unsignedMsg: nil, - expectedErr: errTest, - }, - { - name: "no validators exist", - contextWithCancelFunc: func() (context.Context, context.CancelFunc) { - return context.Background(), nil - }, - aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil) - return New(subnetID, state, nil) - }, - unsignedMsg: nil, - quorumNum: 0, - expectedErr: errNoValidators, - }, { name: "0/3 validators reply with signature", contextWithCancelFunc: func() (context.Context, context.CancelFunc) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) - client.EXPECT().GetSignature(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errTest).Times(len(vdrSet)) - return New(subnetID, state, client) + client.EXPECT().GetSignature(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errTest).Times(len(vdrs)) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 1, @@ -155,17 +103,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(sig1, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(nil, errTest).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(nil, errTest).Times(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 35, // Require >1/3 of weight @@ -177,17 +119,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(sig1, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(sig2, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(nil, errTest).Times(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 69, // Require >2/3 of weight @@ -199,17 +135,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(sig1, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(sig2, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(nil, errTest).MaxTimes(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 65, // Require <2/3 of weight @@ -222,17 +152,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(sig1, nil).MaxTimes(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(sig2, nil).MaxTimes(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(sig3, nil).MaxTimes(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 100, // Require all weight @@ -245,17 +169,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(nonVdrSig, nil).MaxTimes(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(sig2, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(sig3, nil).Times(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 64, @@ -268,17 +186,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(nonVdrSig, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(nonVdrSig, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(nonVdrSig, nil).Times(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 1, @@ -290,17 +202,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(nonVdrSig, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(nonVdrSig, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(sig3, nil).Times(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 40, @@ -312,17 +218,11 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(nonVdrSig, nil).MaxTimes(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(nil, errTest).MaxTimes(1) client.EXPECT().GetSignature(gomock.Any(), nodeID3, gomock.Any()).Return(sig3, nil).Times(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 30, @@ -337,12 +237,6 @@ func TestAggregateSignatures(t *testing.T) { return ctx, cancel }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - // Assert that the context passed into each goroutine is canceled // because the parent context is canceled. client := NewMockSignatureGetter(ctrl) @@ -370,7 +264,7 @@ func TestAggregateSignatures(t *testing.T) { return nil, err }, ).MaxTimes(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 60, // Require 2/3 validators @@ -384,12 +278,6 @@ func TestAggregateSignatures(t *testing.T) { return ctx, cancel }, aggregatorFunc: func(ctrl *gomock.Controller, cancel context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).DoAndReturn( func(ctx context.Context, _ ids.NodeID, _ *avalancheWarp.UnsignedMessage) (*bls.Signature, error) { @@ -416,7 +304,7 @@ func TestAggregateSignatures(t *testing.T) { return nil, err }, ).MaxTimes(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 33, // 1/3 Should have gotten one signature before cancellation @@ -429,12 +317,6 @@ func TestAggregateSignatures(t *testing.T) { return context.Background(), nil }, aggregatorFunc: func(ctrl *gomock.Controller, _ context.CancelFunc) *Aggregator { - state := validators.NewMockState(ctrl) - state.EXPECT().GetCurrentHeight(gomock.Any()).Return(pChainHeight, nil) - state.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), gomock.Any()).Return( - vdrSet, nil, - ) - client := NewMockSignatureGetter(ctrl) client.EXPECT().GetSignature(gomock.Any(), nodeID1, gomock.Any()).Return(sig1, nil).Times(1) client.EXPECT().GetSignature(gomock.Any(), nodeID2, gomock.Any()).Return(sig2, nil).Times(1) @@ -448,7 +330,7 @@ func TestAggregateSignatures(t *testing.T) { return nil, err }, ).MaxTimes(1) - return New(subnetID, state, client) + return New(client, vdrs, vdrWeight*uint64(len(vdrs))) }, unsignedMsg: unsignedMsg, quorumNum: 60, // Require 2/3 validators diff --git a/warp/aggregator/signature_getter.go b/warp/aggregator/signature_getter.go index 526280bc9b..4fdb2219e0 100644 --- a/warp/aggregator/signature_getter.go +++ b/warp/aggregator/signature_getter.go @@ -11,6 +11,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/plugin/evm/message" ) @@ -38,17 +39,39 @@ type NetworkSignatureGetter struct { Client NetworkClient } +func NewSignatureGetter(client NetworkClient) *NetworkSignatureGetter { + return &NetworkSignatureGetter{ + Client: client, + } +} + // GetSignature attempts to fetch a BLS Signature of [unsignedWarpMessage] from [nodeID] until it succeeds or receives an invalid response // // Note: this function will continue attempting to fetch the signature from [nodeID] until it receives an invalid value or [ctx] is cancelled. // The caller is responsible to cancel [ctx] if it no longer needs to fetch this signature. func (s *NetworkSignatureGetter) GetSignature(ctx context.Context, nodeID ids.NodeID, unsignedWarpMessage *avalancheWarp.UnsignedMessage) (*bls.Signature, error) { - signatureReq := message.SignatureRequest{ - MessageID: unsignedWarpMessage.ID(), - } - signatureReqBytes, err := message.RequestToBytes(message.Codec, signatureReq) + var signatureReqBytes []byte + parsedPayload, err := payload.Parse(unsignedWarpMessage.Payload) if err != nil { - return nil, fmt.Errorf("failed to marshal signature request: %w", err) + return nil, fmt.Errorf("failed to parse unsigned message payload: %w", err) + } + switch p := parsedPayload.(type) { + case *payload.AddressedCall: + signatureReq := message.MessageSignatureRequest{ + MessageID: unsignedWarpMessage.ID(), + } + signatureReqBytes, err = message.RequestToBytes(message.Codec, signatureReq) + if err != nil { + return nil, fmt.Errorf("failed to marshal signature request: %w", err) + } + case *payload.Hash: + signatureReq := message.BlockSignatureRequest{ + BlockID: p.Hash, + } + signatureReqBytes, err = message.RequestToBytes(message.Codec, signatureReq) + if err != nil { + return nil, fmt.Errorf("failed to marshal signature request: %w", err) + } } delay := initialRetryFetchSignatureDelay diff --git a/warp/backend.go b/warp/backend.go index fac81248c1..e40d21c426 100644 --- a/warp/backend.go +++ b/warp/backend.go @@ -4,13 +4,17 @@ package warp import ( + "context" "fmt" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/ethdb" "github.com/ethereum/go-ethereum/log" ) @@ -19,14 +23,21 @@ var _ Backend = &backend{} const batchSize = ethdb.IdealBatchSize +type BlockClient interface { + GetBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) +} + // Backend tracks signature-eligible warp messages and provides an interface to fetch them. // The backend is also used to query for warp message signatures by the signature request handler. type Backend interface { // AddMessage signs [unsignedMessage] and adds it to the warp backend database AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) error - // GetSignature returns the signature of the requested message hash. - GetSignature(messageHash ids.ID) ([bls.SignatureLen]byte, error) + // GetMessageSignature returns the signature of the requested message hash. + GetMessageSignature(messageID ids.ID) ([bls.SignatureLen]byte, error) + + // GetBlockSignature returns the signature of the requested message hash. + GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) // GetMessage retrieves the [unsignedMessage] from the warp backend database if available GetMessage(messageHash ids.ID) (*avalancheWarp.UnsignedMessage, error) @@ -37,24 +48,33 @@ type Backend interface { // backend implements Backend, keeps track of warp messages, and generates message signatures. type backend struct { - db database.Database - warpSigner avalancheWarp.Signer - signatureCache *cache.LRU[ids.ID, [bls.SignatureLen]byte] - messageCache *cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage] + networkID uint32 + sourceChainID ids.ID + db database.Database + warpSigner avalancheWarp.Signer + blockClient BlockClient + messageSignatureCache *cache.LRU[ids.ID, [bls.SignatureLen]byte] + blockSignatureCache *cache.LRU[ids.ID, [bls.SignatureLen]byte] + messageCache *cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage] } // NewBackend creates a new Backend, and initializes the signature cache and message tracking database. -func NewBackend(warpSigner avalancheWarp.Signer, db database.Database, cacheSize int) Backend { +func NewBackend(networkID uint32, sourceChainID ids.ID, warpSigner avalancheWarp.Signer, blockClient BlockClient, db database.Database, cacheSize int) Backend { return &backend{ - db: db, - warpSigner: warpSigner, - signatureCache: &cache.LRU[ids.ID, [bls.SignatureLen]byte]{Size: cacheSize}, - messageCache: &cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage]{Size: cacheSize}, + networkID: networkID, + sourceChainID: sourceChainID, + db: db, + warpSigner: warpSigner, + blockClient: blockClient, + messageSignatureCache: &cache.LRU[ids.ID, [bls.SignatureLen]byte]{Size: cacheSize}, + blockSignatureCache: &cache.LRU[ids.ID, [bls.SignatureLen]byte]{Size: cacheSize}, + messageCache: &cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage]{Size: cacheSize}, } } func (b *backend) Clear() error { - b.signatureCache.Flush() + b.messageSignatureCache.Flush() + b.blockSignatureCache.Flush() b.messageCache.Flush() return database.Clear(b.db, batchSize) } @@ -76,14 +96,14 @@ func (b *backend) AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) err } copy(signature[:], sig) - b.signatureCache.Put(messageID, signature) + b.messageSignatureCache.Put(messageID, signature) log.Debug("Adding warp message to backend", "messageID", messageID) return nil } -func (b *backend) GetSignature(messageID ids.ID) ([bls.SignatureLen]byte, error) { +func (b *backend) GetMessageSignature(messageID ids.ID) ([bls.SignatureLen]byte, error) { log.Debug("Getting warp message from backend", "messageID", messageID) - if sig, ok := b.signatureCache.Get(messageID); ok { + if sig, ok := b.messageSignatureCache.Get(messageID); ok { return sig, nil } @@ -99,7 +119,40 @@ func (b *backend) GetSignature(messageID ids.ID) ([bls.SignatureLen]byte, error) } copy(signature[:], sig) - b.signatureCache.Put(messageID, signature) + b.messageSignatureCache.Put(messageID, signature) + return signature, nil +} + +func (b *backend) GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) { + log.Debug("Getting block from backend", "blockID", blockID) + if sig, ok := b.blockSignatureCache.Get(blockID); ok { + return sig, nil + } + + block, err := b.blockClient.GetBlock(context.TODO(), blockID) + if err != nil { + return [bls.SignatureLen]byte{}, fmt.Errorf("failed to get block %s: %w", blockID, err) + } + if block.Status() != choices.Accepted { + return [bls.SignatureLen]byte{}, fmt.Errorf("block %s was not accepted", blockID) + } + + var signature [bls.SignatureLen]byte + blockHashPayload, err := payload.NewHash(blockID) + if err != nil { + return [bls.SignatureLen]byte{}, fmt.Errorf("failed to create new block hash payload: %w", err) + } + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(b.networkID, b.sourceChainID, blockHashPayload.Bytes()) + if err != nil { + return [bls.SignatureLen]byte{}, fmt.Errorf("failed to create new unsigned warp message: %w", err) + } + sig, err := b.warpSigner.Sign(unsignedMessage) + if err != nil { + return [bls.SignatureLen]byte{}, fmt.Errorf("failed to sign warp message: %w", err) + } + + copy(signature[:], sig) + b.blockSignatureCache.Put(blockID, signature) return signature, nil } diff --git a/warp/backend_test.go b/warp/backend_test.go index 9f92234b44..024375a308 100644 --- a/warp/backend_test.go +++ b/warp/backend_test.go @@ -4,20 +4,27 @@ package warp import ( + "context" + "errors" "testing" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/hashing" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/stretchr/testify/require" ) var ( networkID uint32 = 54321 sourceChainID = ids.GenerateTestID() - payload = []byte("test") + testPayload = []byte("test") ) func TestClearDB(t *testing.T) { @@ -26,7 +33,7 @@ func TestClearDB(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backendIntf := NewBackend(warpSigner, db, 500) + backendIntf := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500) backend, ok := backendIntf.(*backend) require.True(t, ok) @@ -43,21 +50,22 @@ func TestClearDB(t *testing.T) { err = backend.AddMessage(unsignedMsg) require.NoError(t, err) // ensure that the message was added - _, err = backend.GetSignature(messageID) + _, err = backend.GetMessageSignature(messageID) require.NoError(t, err) } err = backend.Clear() require.NoError(t, err) require.Zero(t, backend.messageCache.Len()) - require.Zero(t, backend.signatureCache.Len()) + require.Zero(t, backend.messageSignatureCache.Len()) + require.Zero(t, backend.blockSignatureCache.Len()) it := db.NewIterator() defer it.Release() require.False(t, it.Next()) // ensure all messages have been deleted for _, messageID := range messageIDs { - _, err := backend.GetSignature(messageID) + _, err := backend.GetMessageSignature(messageID) require.ErrorContains(t, err, "failed to get warp message") } } @@ -68,17 +76,17 @@ func TestAddAndGetValidMessage(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend := NewBackend(warpSigner, db, 500) + backend := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500) // Create a new unsigned message and add it to the warp backend. - unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, payload) + unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testPayload) require.NoError(t, err) err = backend.AddMessage(unsignedMsg) require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. messageID := unsignedMsg.ID() - signature, err := backend.GetSignature(messageID) + signature, err := backend.GetMessageSignature(messageID) require.NoError(t, err) expectedSig, err := warpSigner.Sign(unsignedMsg) @@ -92,16 +100,56 @@ func TestAddAndGetUnknownMessage(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend := NewBackend(warpSigner, db, 500) - unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, payload) + backend := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500) + unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testPayload) require.NoError(t, err) // Try getting a signature for a message that was not added. messageID := unsignedMsg.ID() - _, err = backend.GetSignature(messageID) + _, err = backend.GetMessageSignature(messageID) require.Error(t, err) } +func TestGetBlockSignature(t *testing.T) { + require := require.New(t) + + blkID := ids.GenerateTestID() + testVM := &block.TestVM{ + TestVM: common.TestVM{T: t}, + GetBlockF: func(ctx context.Context, i ids.ID) (snowman.Block, error) { + if i == blkID { + return &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: blkID, + StatusV: choices.Accepted, + }, + }, nil + } + return nil, errors.New("invalid blockID") + }, + } + db := memdb.New() + + sk, err := bls.NewSecretKey() + require.NoError(err) + warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) + backend := NewBackend(networkID, sourceChainID, warpSigner, testVM, db, 500) + + blockHashPayload, err := payload.NewHash(blkID) + require.NoError(err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) + require.NoError(err) + expectedSig, err := warpSigner.Sign(unsignedMessage) + require.NoError(err) + + signature, err := backend.GetBlockSignature(blkID) + require.NoError(err) + require.Equal(expectedSig, signature[:]) + + _, err = backend.GetBlockSignature(ids.GenerateTestID()) + require.Error(err) +} + func TestZeroSizedCache(t *testing.T) { db := memdb.New() @@ -110,17 +158,17 @@ func TestZeroSizedCache(t *testing.T) { warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. - backend := NewBackend(warpSigner, db, 0) + backend := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 0) // Create a new unsigned message and add it to the warp backend. - unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, payload) + unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testPayload) require.NoError(t, err) err = backend.AddMessage(unsignedMsg) require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. messageID := unsignedMsg.ID() - signature, err := backend.GetSignature(messageID) + signature, err := backend.GetMessageSignature(messageID) require.NoError(t, err) expectedSig, err := warpSigner.Sign(unsignedMsg) diff --git a/warp/client.go b/warp/client.go index c9e116db56..7e23c7e9a1 100644 --- a/warp/client.go +++ b/warp/client.go @@ -15,10 +15,10 @@ import ( var _ Client = (*client)(nil) type Client interface { - // GetSignature requests the BLS signature associated with a messageID - GetSignature(ctx context.Context, messageID ids.ID) ([]byte, error) - // GetAggregateSignature requests the aggregate signature associated with messageID - GetAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) ([]byte, error) + GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) + GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) ([]byte, error) + GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) + GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64) ([]byte, error) } // client implementation for interacting with EVM [chain] @@ -37,18 +37,34 @@ func NewClient(uri, chain string) (Client, error) { }, nil } -func (c *client) GetSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { +func (c *client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) { var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getSignature", messageID); err != nil { - return nil, fmt.Errorf("call to warp_getSignature failed. err: %w", err) + if err := c.client.CallContext(ctx, &res, "warp_getMessageSignature", messageID); err != nil { + return nil, fmt.Errorf("call to warp_getMessageSignature failed. err: %w", err) } return res, nil } -func (c *client) GetAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) ([]byte, error) { +func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) ([]byte, error) { var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getAggregateSignature", messageID, quorumNum); err != nil { - return nil, fmt.Errorf("call to warp_getAggregateSignature failed. err: %w", err) + if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum); err != nil { + return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed. err: %w", err) + } + return res, nil +} + +func (c *client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { + var res hexutil.Bytes + if err := c.client.CallContext(ctx, &res, "warp_getBlockSignature", blockID); err != nil { + return nil, fmt.Errorf("call to warp_getBlockSignature failed. err: %w", err) + } + return res, nil +} + +func (c *client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64) ([]byte, error) { + var res hexutil.Bytes + if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum); err != nil { + return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed. err: %w", err) } return res, nil } diff --git a/warp/fetcher.go b/warp/fetcher.go index eba0bc7949..fcf014650b 100644 --- a/warp/fetcher.go +++ b/warp/fetcher.go @@ -10,8 +10,12 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/warp/aggregator" ) +var _ aggregator.SignatureGetter = (*apiFetcher)(nil) + type apiFetcher struct { clients map[ids.NodeID]Client } @@ -22,13 +26,22 @@ func NewAPIFetcher(clients map[ids.NodeID]Client) *apiFetcher { } } -func (f *apiFetcher) FetchWarpSignature(ctx context.Context, nodeID ids.NodeID, unsignedWarpMessage *avalancheWarp.UnsignedMessage) (*bls.Signature, error) { +func (f *apiFetcher) GetSignature(ctx context.Context, nodeID ids.NodeID, unsignedWarpMessage *avalancheWarp.UnsignedMessage) (*bls.Signature, error) { client, ok := f.clients[nodeID] if !ok { return nil, fmt.Errorf("no warp client for nodeID: %s", nodeID) } - - signatureBytes, err := client.GetSignature(ctx, unsignedWarpMessage.ID()) + var signatureBytes []byte + parsedPayload, err := payload.Parse(unsignedWarpMessage.Payload) + if err != nil { + return nil, fmt.Errorf("failed to parse unsigned message payload: %w", err) + } + switch p := parsedPayload.(type) { + case *payload.AddressedCall: + signatureBytes, err = client.GetMessageSignature(ctx, unsignedWarpMessage.ID()) + case *payload.Hash: + signatureBytes, err = client.GetBlockSignature(ctx, p.Hash) + } if err != nil { return nil, err } diff --git a/warp/handlers/signature_request.go b/warp/handlers/signature_request.go index 30be346327..c307d284d1 100644 --- a/warp/handlers/signature_request.go +++ b/warp/handlers/signature_request.go @@ -15,7 +15,7 @@ import ( "github.com/ethereum/go-ethereum/log" ) -// SignatureRequestHandler serves warp signature requests. It is a peer.RequestHandler for message.SignatureRequest. +// SignatureRequestHandler serves warp signature requests. It is a peer.RequestHandler for message.MessageSignatureRequest. type SignatureRequestHandler struct { backend warp.Backend codec codec.Manager @@ -30,27 +30,55 @@ func NewSignatureRequestHandler(backend warp.Backend, codec codec.Manager) *Sign } } -// OnSignatureRequest handles message.SignatureRequest, and retrieves a warp signature for the requested message ID. +// OnMessageSignatureRequest handles message.MessageSignatureRequest, and retrieves a warp signature for the requested message ID. // Never returns an error // Expects returned errors to be treated as FATAL // Returns empty response if signature is not found // Assumes ctx is active -func (s *SignatureRequestHandler) OnSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest message.SignatureRequest) ([]byte, error) { +func (s *SignatureRequestHandler) OnMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest message.MessageSignatureRequest) ([]byte, error) { startTime := time.Now() - s.stats.IncSignatureRequest() + s.stats.IncMessageSignatureRequest() // Always report signature request time defer func() { - s.stats.UpdateSignatureRequestTime(time.Since(startTime)) + s.stats.UpdateMessageSignatureRequestTime(time.Since(startTime)) }() - signature, err := s.backend.GetSignature(signatureRequest.MessageID) + signature, err := s.backend.GetMessageSignature(signatureRequest.MessageID) if err != nil { log.Debug("Unknown warp signature requested", "messageID", signatureRequest.MessageID) - s.stats.IncSignatureMiss() + s.stats.IncMessageSignatureMiss() signature = [bls.SignatureLen]byte{} } else { - s.stats.IncSignatureHit() + s.stats.IncMessageSignatureHit() + } + + response := message.SignatureResponse{Signature: signature} + responseBytes, err := s.codec.Marshal(message.Version, &response) + if err != nil { + log.Error("could not marshal SignatureResponse, dropping request", "nodeID", nodeID, "requestID", requestID, "err", err) + return nil, nil + } + + return responseBytes, nil +} + +func (s *SignatureRequestHandler) OnBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request message.BlockSignatureRequest) ([]byte, error) { + startTime := time.Now() + s.stats.IncBlockSignatureRequest() + + // Always report signature request time + defer func() { + s.stats.UpdateBlockSignatureRequestTime(time.Since(startTime)) + }() + + signature, err := s.backend.GetBlockSignature(request.BlockID) + if err != nil { + log.Debug("Unknown warp signature requested", "blockID", request.BlockID) + s.stats.IncBlockSignatureMiss() + signature = [bls.SignatureLen]byte{} + } else { + s.stats.IncBlockSignatureHit() } response := message.SignatureResponse{Signature: signature} @@ -65,6 +93,10 @@ func (s *SignatureRequestHandler) OnSignatureRequest(ctx context.Context, nodeID type NoopSignatureRequestHandler struct{} -func (s *NoopSignatureRequestHandler) OnSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest message.SignatureRequest) ([]byte, error) { +func (s *NoopSignatureRequestHandler) OnMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest message.MessageSignatureRequest) ([]byte, error) { + return nil, nil +} + +func (s *NoopSignatureRequestHandler) OnBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest message.BlockSignatureRequest) ([]byte, error) { return nil, nil } diff --git a/warp/handlers/signature_request_test.go b/warp/handlers/signature_request_test.go index 0d4a1fb373..7a672bde8e 100644 --- a/warp/handlers/signature_request_test.go +++ b/warp/handlers/signature_request_test.go @@ -5,12 +5,17 @@ package handlers import ( "context" + "errors" "testing" "time" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/subnet-evm/plugin/evm/message" @@ -18,54 +23,62 @@ import ( "github.com/stretchr/testify/require" ) -func TestSignatureHandler(t *testing.T) { +func TestMessageSignatureHandler(t *testing.T) { database := memdb.New() snowCtx := snow.DefaultContextTest() blsSecretKey, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) - backend := warp.NewBackend(warpSigner, database, 100) + backend := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, &block.TestVM{TestVM: common.TestVM{T: t}}, database, 100) msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, []byte("test")) require.NoError(t, err) messageID := msg.ID() require.NoError(t, backend.AddMessage(msg)) - signature, err := backend.GetSignature(messageID) + signature, err := backend.GetMessageSignature(messageID) require.NoError(t, err) unknownMessageID := ids.GenerateTestID() emptySignature := [bls.SignatureLen]byte{} tests := map[string]struct { - setup func() (request message.SignatureRequest, expectedResponse []byte) + setup func() (request message.MessageSignatureRequest, expectedResponse []byte) verifyStats func(t *testing.T, stats *handlerStats) }{ - "normal": { - setup: func() (request message.SignatureRequest, expectedResponse []byte) { - return message.SignatureRequest{ + "known message": { + setup: func() (request message.MessageSignatureRequest, expectedResponse []byte) { + return message.MessageSignatureRequest{ MessageID: messageID, }, signature[:] }, verifyStats: func(t *testing.T, stats *handlerStats) { - require.EqualValues(t, 1, stats.signatureRequest.Count()) - require.EqualValues(t, 1, stats.signatureHit.Count()) - require.EqualValues(t, 0, stats.signatureMiss.Count()) - require.Greater(t, stats.signatureRequestDuration.Value(), time.Duration(0)) + require.EqualValues(t, 1, stats.messageSignatureRequest.Count()) + require.EqualValues(t, 1, stats.messageSignatureHit.Count()) + require.EqualValues(t, 0, stats.messageSignatureMiss.Count()) + require.Greater(t, stats.messageSignatureRequestDuration.Value(), time.Duration(0)) + require.EqualValues(t, 0, stats.blockSignatureRequest.Count()) + require.EqualValues(t, 0, stats.blockSignatureHit.Count()) + require.EqualValues(t, 0, stats.blockSignatureMiss.Count()) + require.EqualValues(t, stats.blockSignatureRequestDuration.Value(), time.Duration(0)) }, }, - "unknown": { - setup: func() (request message.SignatureRequest, expectedResponse []byte) { - return message.SignatureRequest{ + "unknown message": { + setup: func() (request message.MessageSignatureRequest, expectedResponse []byte) { + return message.MessageSignatureRequest{ MessageID: unknownMessageID, }, emptySignature[:] }, verifyStats: func(t *testing.T, stats *handlerStats) { - require.EqualValues(t, 1, stats.signatureRequest.Count()) - require.EqualValues(t, 0, stats.signatureHit.Count()) - require.EqualValues(t, 1, stats.signatureMiss.Count()) - require.Greater(t, stats.signatureRequestDuration.Value(), time.Duration(0)) + require.EqualValues(t, 1, stats.messageSignatureRequest.Count()) + require.EqualValues(t, 0, stats.messageSignatureHit.Count()) + require.EqualValues(t, 1, stats.messageSignatureMiss.Count()) + require.Greater(t, stats.messageSignatureRequestDuration.Value(), time.Duration(0)) + require.EqualValues(t, 0, stats.blockSignatureRequest.Count()) + require.EqualValues(t, 0, stats.blockSignatureHit.Count()) + require.EqualValues(t, 0, stats.blockSignatureMiss.Count()) + require.EqualValues(t, stats.blockSignatureRequestDuration.Value(), time.Duration(0)) }, }, } @@ -76,7 +89,109 @@ func TestSignatureHandler(t *testing.T) { handler.stats.Clear() request, expectedResponse := test.setup() - responseBytes, err := handler.OnSignatureRequest(context.Background(), ids.GenerateTestNodeID(), 1, request) + responseBytes, err := handler.OnMessageSignatureRequest(context.Background(), ids.GenerateTestNodeID(), 1, request) + require.NoError(t, err) + + test.verifyStats(t, handler.stats) + + // If the expected response is empty, assert that the handler returns an empty response and return early. + if len(expectedResponse) == 0 { + require.Len(t, responseBytes, 0, "expected response to be empty") + return + } + var response message.SignatureResponse + _, err = message.Codec.Unmarshal(responseBytes, &response) + require.NoError(t, err, "error unmarshalling SignatureResponse") + + require.Equal(t, expectedResponse, response.Signature[:]) + }) + } +} + +func TestBlockSignatureHandler(t *testing.T) { + database := memdb.New() + snowCtx := snow.DefaultContextTest() + blsSecretKey, err := bls.NewSecretKey() + require.NoError(t, err) + + warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) + blkID := ids.GenerateTestID() + testVM := &block.TestVM{ + TestVM: common.TestVM{T: t}, + GetBlockF: func(ctx context.Context, i ids.ID) (snowman.Block, error) { + if i == blkID { + return &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: blkID, + StatusV: choices.Accepted, + }, + }, nil + } + return nil, errors.New("invalid blockID") + }, + } + backend := warp.NewBackend( + snowCtx.NetworkID, + snowCtx.ChainID, + warpSigner, + testVM, + database, + 100, + ) + + signature, err := backend.GetBlockSignature(blkID) + require.NoError(t, err) + unknownMessageID := ids.GenerateTestID() + + emptySignature := [bls.SignatureLen]byte{} + + tests := map[string]struct { + setup func() (request message.BlockSignatureRequest, expectedResponse []byte) + verifyStats func(t *testing.T, stats *handlerStats) + }{ + "known block": { + setup: func() (request message.BlockSignatureRequest, expectedResponse []byte) { + return message.BlockSignatureRequest{ + BlockID: blkID, + }, signature[:] + }, + verifyStats: func(t *testing.T, stats *handlerStats) { + require.EqualValues(t, 0, stats.messageSignatureRequest.Count()) + require.EqualValues(t, 0, stats.messageSignatureHit.Count()) + require.EqualValues(t, 0, stats.messageSignatureMiss.Count()) + require.EqualValues(t, stats.messageSignatureRequestDuration.Value(), time.Duration(0)) + require.EqualValues(t, 1, stats.blockSignatureRequest.Count()) + require.EqualValues(t, 1, stats.blockSignatureHit.Count()) + require.EqualValues(t, 0, stats.blockSignatureMiss.Count()) + require.Greater(t, stats.blockSignatureRequestDuration.Value(), time.Duration(0)) + }, + }, + "unknown block": { + setup: func() (request message.BlockSignatureRequest, expectedResponse []byte) { + return message.BlockSignatureRequest{ + BlockID: unknownMessageID, + }, emptySignature[:] + }, + verifyStats: func(t *testing.T, stats *handlerStats) { + require.EqualValues(t, 0, stats.messageSignatureRequest.Count()) + require.EqualValues(t, 0, stats.messageSignatureHit.Count()) + require.EqualValues(t, 0, stats.messageSignatureMiss.Count()) + require.EqualValues(t, stats.messageSignatureRequestDuration.Value(), time.Duration(0)) + require.EqualValues(t, 1, stats.blockSignatureRequest.Count()) + require.EqualValues(t, 0, stats.blockSignatureHit.Count()) + require.EqualValues(t, 1, stats.blockSignatureMiss.Count()) + require.Greater(t, stats.blockSignatureRequestDuration.Value(), time.Duration(0)) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + handler := NewSignatureRequestHandler(backend, message.Codec) + handler.stats.Clear() + + request, expectedResponse := test.setup() + responseBytes, err := handler.OnBlockSignatureRequest(context.Background(), ids.GenerateTestNodeID(), 1, request) require.NoError(t, err) test.verifyStats(t, handler.stats) diff --git a/warp/handlers/stats.go b/warp/handlers/stats.go index dfb261f9a3..481f2aaac0 100644 --- a/warp/handlers/stats.go +++ b/warp/handlers/stats.go @@ -10,31 +10,50 @@ import ( ) type handlerStats struct { - // SignatureRequestHandler metrics - signatureRequest metrics.Counter - signatureHit metrics.Counter - signatureMiss metrics.Counter - signatureRequestDuration metrics.Gauge + // MessageSignatureRequestHandler metrics + messageSignatureRequest metrics.Counter + messageSignatureHit metrics.Counter + messageSignatureMiss metrics.Counter + messageSignatureRequestDuration metrics.Gauge + // BlockSignatureRequestHandler metrics + blockSignatureRequest metrics.Counter + blockSignatureHit metrics.Counter + blockSignatureMiss metrics.Counter + blockSignatureRequestDuration metrics.Gauge } func newStats() *handlerStats { return &handlerStats{ - signatureRequest: metrics.GetOrRegisterCounter("signature_request_count", nil), - signatureHit: metrics.GetOrRegisterCounter("signature_request_hit", nil), - signatureMiss: metrics.GetOrRegisterCounter("signature_request_miss", nil), - signatureRequestDuration: metrics.GetOrRegisterGauge("signature_request_duration", nil), + messageSignatureRequest: metrics.GetOrRegisterCounter("message_signature_request_count", nil), + messageSignatureHit: metrics.GetOrRegisterCounter("message_signature_request_hit", nil), + messageSignatureMiss: metrics.GetOrRegisterCounter("message_signature_request_miss", nil), + messageSignatureRequestDuration: metrics.GetOrRegisterGauge("message_signature_request_duration", nil), + blockSignatureRequest: metrics.GetOrRegisterCounter("block_signature_request_count", nil), + blockSignatureHit: metrics.GetOrRegisterCounter("block_signature_request_hit", nil), + blockSignatureMiss: metrics.GetOrRegisterCounter("block_signature_request_miss", nil), + blockSignatureRequestDuration: metrics.GetOrRegisterGauge("block_signature_request_duration", nil), } } -func (h *handlerStats) IncSignatureRequest() { h.signatureRequest.Inc(1) } -func (h *handlerStats) IncSignatureHit() { h.signatureHit.Inc(1) } -func (h *handlerStats) IncSignatureMiss() { h.signatureMiss.Inc(1) } -func (h *handlerStats) UpdateSignatureRequestTime(duration time.Duration) { - h.signatureRequestDuration.Inc(int64(duration)) +func (h *handlerStats) IncMessageSignatureRequest() { h.messageSignatureRequest.Inc(1) } +func (h *handlerStats) IncMessageSignatureHit() { h.messageSignatureHit.Inc(1) } +func (h *handlerStats) IncMessageSignatureMiss() { h.messageSignatureMiss.Inc(1) } +func (h *handlerStats) UpdateMessageSignatureRequestTime(duration time.Duration) { + h.messageSignatureRequestDuration.Inc(int64(duration)) +} +func (h *handlerStats) IncBlockSignatureRequest() { h.blockSignatureRequest.Inc(1) } +func (h *handlerStats) IncBlockSignatureHit() { h.blockSignatureHit.Inc(1) } +func (h *handlerStats) IncBlockSignatureMiss() { h.blockSignatureMiss.Inc(1) } +func (h *handlerStats) UpdateBlockSignatureRequestTime(duration time.Duration) { + h.blockSignatureRequestDuration.Inc(int64(duration)) } func (h *handlerStats) Clear() { - h.signatureRequest.Clear() - h.signatureHit.Clear() - h.signatureMiss.Clear() - h.signatureRequestDuration.Update(0) + h.messageSignatureRequest.Clear() + h.messageSignatureHit.Clear() + h.messageSignatureMiss.Clear() + h.messageSignatureRequestDuration.Update(0) + h.blockSignatureRequest.Clear() + h.blockSignatureHit.Clear() + h.blockSignatureMiss.Clear() + h.blockSignatureRequestDuration.Update(0) } diff --git a/warp/service.go b/warp/service.go index ee907ead30..a3e632a470 100644 --- a/warp/service.go +++ b/warp/service.go @@ -5,43 +5,102 @@ package warp import ( "context" + "errors" "fmt" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/peer" "github.com/ava-labs/subnet-evm/warp/aggregator" + "github.com/ava-labs/subnet-evm/warp/validators" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/log" ) +var errNoValidators = errors.New("cannot aggregate signatures from subnet with no validators") + // API introduces snowman specific functionality to the evm type API struct { - backend Backend - aggregator *aggregator.Aggregator + networkID uint32 + sourceSubnetID, sourceChainID ids.ID + backend Backend + state *validators.State + client peer.NetworkClient } -func NewAPI(backend Backend, aggregator *aggregator.Aggregator) *API { +func NewAPI(networkID uint32, sourceSubnetID ids.ID, sourceChainID ids.ID, state *validators.State, backend Backend, client peer.NetworkClient) *API { return &API{ - backend: backend, - aggregator: aggregator, + networkID: networkID, + sourceSubnetID: sourceSubnetID, + sourceChainID: sourceChainID, + backend: backend, + state: state, + client: client, + } +} + +// GetMessageSignature returns the BLS signature associated with a messageID. +func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { + signature, err := a.backend.GetMessageSignature(messageID) + if err != nil { + return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) } + return signature[:], nil } -// GetSignature returns the BLS signature associated with a messageID. -func (a *API) GetSignature(ctx context.Context, messageID ids.ID) (hexutil.Bytes, error) { - signature, err := a.backend.GetSignature(messageID) +// GetBlockSignature returns the BLS signature associated with a blockID. +func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { + signature, err := a.backend.GetBlockSignature(blockID) if err != nil { - return nil, fmt.Errorf("failed to get signature for with error %w", err) + return nil, fmt.Errorf("failed to get signature for block %s with error %w", blockID, err) } return signature[:], nil } -// GetAggregateSignature fetches the aggregate signature for the requested [messageID] -func (a *API) GetAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) (signedMessageBytes hexutil.Bytes, err error) { +// GetMessageAggregateSignature fetches the aggregate signature for the requested [messageID] +func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) (signedMessageBytes hexutil.Bytes, err error) { unsignedMessage, err := a.backend.GetMessage(messageID) if err != nil { return nil, err } + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum) +} + +// GetBlockAggregateSignature fetches the aggregate signature for the requested [blockID] +func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64) (signedMessageBytes hexutil.Bytes, err error) { + blockHashPayload, err := payload.NewHash(blockID) + if err != nil { + return nil, err + } + unsignedMessage, err := warp.NewUnsignedMessage(a.networkID, a.sourceChainID, blockHashPayload.Bytes()) + if err != nil { + return nil, err + } + + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum) +} + +func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64) (hexutil.Bytes, error) { + pChainHeight, err := a.state.GetCurrentHeight(ctx) + if err != nil { + return nil, err + } + + log.Debug("Fetching signature", + "a.subnetID", a.sourceSubnetID, + "height", pChainHeight, + ) + validators, totalWeight, err := warp.GetCanonicalValidatorSet(ctx, a.state, pChainHeight, a.sourceSubnetID) + if err != nil { + return nil, fmt.Errorf("failed to get validator set: %w", err) + } + if len(validators) == 0 { + return nil, fmt.Errorf("%w (SubnetID: %s, Height: %d)", errNoValidators, a.sourceSubnetID, pChainHeight) + } - signatureResult, err := a.aggregator.AggregateSignatures(ctx, unsignedMessage, quorumNum) + agg := aggregator.New(aggregator.NewSignatureGetter(a.client), validators, totalWeight) + signatureResult, err := agg.AggregateSignatures(ctx, unsignedMessage, quorumNum) if err != nil { return nil, err }