diff --git a/plugin/evm/config.go b/plugin/evm/config.go index 7f4ca902d6..4ad45eb035 100644 --- a/plugin/evm/config.go +++ b/plugin/evm/config.go @@ -11,6 +11,7 @@ import ( "github.com/ava-labs/subnet-evm/core/txpool" "github.com/ava-labs/subnet-evm/eth" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/spf13/cast" ) @@ -208,6 +209,12 @@ type Config struct { // * 0: means no limit // * N: means N block limit [HEAD-N+1, HEAD] and delete extra indexes TxLookupLimit uint64 `json:"tx-lookup-limit"` + + // WarpOffChainMessages encodes off-chain messages (unrelated to any on-chain event ie. block or AddressedCall) + // that the node should be willing to sign. + // Note: only supports AddressedCall payloads as defined here: + // https://github.com/ava-labs/avalanchego/tree/7623ffd4be915a5185c9ed5e11fa9be15a6e1f00/vms/platformvm/warp/payload#addressedcall + WarpOffChainMessages []hexutil.Bytes `json:"warp-off-chain-messages"` } // EthAPIs returns an array of strings representing the Eth APIs that should be enabled diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 4abd15a008..f92eaea1ea 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -468,8 +468,15 @@ func (vm *VM) Initialize( vm.Network = peer.NewNetwork(p2pNetwork, appSender, vm.networkCodec, message.CrossChainCodec, chainCtx.NodeID, vm.config.MaxOutboundActiveRequests, vm.config.MaxOutboundActiveCrossChainRequests) vm.client = peer.NewNetworkClient(vm.Network) - // initialize warp backend - vm.warpBackend = warp.NewBackend(vm.ctx.NetworkID, vm.ctx.ChainID, vm.ctx.WarpSigner, vm, vm.warpDB, warpSignatureCacheSize) + // Initialize warp backend + offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages)) + for i, hexMsg := range vm.config.WarpOffChainMessages { + offchainWarpMessages[i] = []byte(hexMsg) + } + vm.warpBackend, err = warp.NewBackend(vm.ctx.NetworkID, vm.ctx.ChainID, vm.ctx.WarpSigner, vm, vm.warpDB, warpSignatureCacheSize, offchainWarpMessages) + if err != nil { + return err + } // clear warpdb on initialization if config enabled if vm.config.PruneWarpDB { diff --git a/precompile/precompileconfig/config.go b/precompile/precompileconfig/config.go index 1f37507a1c..be23f3570a 100644 --- a/precompile/precompileconfig/config.go +++ b/precompile/precompileconfig/config.go @@ -41,12 +41,14 @@ type PredicateContext struct { } // Predicater is an optional interface for StatefulPrecompileContracts to implement. -// If implemented, the predicate will be enforced on every transaction in a block, prior to -// the block's execution. -// If VerifyPredicate returns an error, the block will fail verification with no further processing. -// WARNING: If you are implementing a custom precompile, beware that subnet-evm -// will not maintain backwards compatibility of this interface and your code should not -// rely on this. Designed for use only by precompiles that ship with subnet-evm. +// If implemented, the predicate will be called for each predicate included in the +// access list of a transaction. +// PredicateGas will be called while calculating the IntrinsicGas of a transaction +// causing it to be dropped if the total gas goes above the tx gas limit. +// VerifyPredicate is used to populate a bit set of predicates verified prior to +// block execution, which can be accessed via the StateDB during execution. +// The bitset is stored in the block, so that historical blocks can be re-verified +// without calling VerifyPredicate. type Predicater interface { PredicateGas(predicateBytes []byte) (uint64, error) VerifyPredicate(predicateContext *PredicateContext, predicateBytes []byte) error diff --git a/warp/backend.go b/warp/backend.go index 4c7e1892e5..8c8a0aa330 100644 --- a/warp/backend.go +++ b/warp/backend.go @@ -5,6 +5,7 @@ package warp import ( "context" + "errors" "fmt" "github.com/ava-labs/avalanchego/cache" @@ -19,7 +20,10 @@ import ( "github.com/ethereum/go-ethereum/log" ) -var _ Backend = &backend{} +var ( + _ Backend = &backend{} + errParsingOffChainMessage = errors.New("failed to parse off-chain message") +) const batchSize = ethdb.IdealBatchSize @@ -48,28 +52,56 @@ type Backend interface { // backend implements Backend, keeps track of warp messages, and generates message signatures. type backend struct { - 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] + 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] + offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.UnsignedMessage } // NewBackend creates a new Backend, and initializes the signature cache and message tracking database. -func NewBackend(networkID uint32, sourceChainID ids.ID, warpSigner avalancheWarp.Signer, blockClient BlockClient, db database.Database, cacheSize int) Backend { - return &backend{ - 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 NewBackend( + networkID uint32, + sourceChainID ids.ID, + warpSigner avalancheWarp.Signer, + blockClient BlockClient, + db database.Database, + cacheSize int, + offchainMessages [][]byte, +) (Backend, error) { + b := &backend{ + 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}, + offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), + } + return b, b.initOffChainMessages(offchainMessages) +} + +func (b *backend) initOffChainMessages(offchainMessages [][]byte) error { + for i, offchainMsg := range offchainMessages { + unsignedMsg, err := avalancheWarp.ParseUnsignedMessage(offchainMsg) + if err != nil { + return fmt.Errorf("%w at index %d: %w", errParsingOffChainMessage, i, err) + } + + _, err = payload.ParseAddressedCall(unsignedMsg.Payload) + if err != nil { + return fmt.Errorf("%w at index %d as AddressedCall: %w", errParsingOffChainMessage, i, err) + } + b.offchainAddressedCallMsgs[unsignedMsg.ID()] = unsignedMsg } + + return nil } func (b *backend) Clear() error { @@ -160,6 +192,9 @@ func (b *backend) GetMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, if message, ok := b.messageCache.Get(messageID); ok { return message, nil } + if message, ok := b.offchainAddressedCallMsgs[messageID]; ok { + return message, nil + } unsignedMessageBytes, err := b.db.Get(messageID[:]) if err != nil { diff --git a/warp/backend_test.go b/warp/backend_test.go index 024375a308..8bba70f42b 100644 --- a/warp/backend_test.go +++ b/warp/backend_test.go @@ -14,6 +14,7 @@ import ( "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" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/hashing" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" @@ -22,18 +23,32 @@ import ( ) var ( - networkID uint32 = 54321 - sourceChainID = ids.GenerateTestID() - testPayload = []byte("test") + networkID uint32 = 54321 + sourceChainID = ids.GenerateTestID() + testSourceAddress = utils.RandomBytes(20) + testPayload = []byte("test") + testUnsignedMessage *avalancheWarp.UnsignedMessage ) +func init() { + testAddressedCallPayload, err := payload.NewAddressedCall(testSourceAddress, testPayload) + if err != nil { + panic(err) + } + testUnsignedMessage, err = avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testAddressedCallPayload.Bytes()) + if err != nil { + panic(err) + } +} + func TestClearDB(t *testing.T) { db := memdb.New() sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backendIntf := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500) + backendIntf, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500, nil) + require.NoError(t, err) backend, ok := backendIntf.(*backend) require.True(t, ok) @@ -76,20 +91,19 @@ func TestAddAndGetValidMessage(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - 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, testPayload) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500, nil) require.NoError(t, err) - err = backend.AddMessage(unsignedMsg) + + // Add testUnsignedMessage to the warp backend + err = backend.AddMessage(testUnsignedMessage) require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. - messageID := unsignedMsg.ID() + messageID := testUnsignedMessage.ID() signature, err := backend.GetMessageSignature(messageID) require.NoError(t, err) - expectedSig, err := warpSigner.Sign(unsignedMsg) + expectedSig, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) require.Equal(t, expectedSig, signature[:]) } @@ -100,12 +114,11 @@ func TestAddAndGetUnknownMessage(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500) - unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testPayload) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500, nil) require.NoError(t, err) // Try getting a signature for a message that was not added. - messageID := unsignedMsg.ID() + messageID := testUnsignedMessage.ID() _, err = backend.GetMessageSignature(messageID) require.Error(t, err) } @@ -133,7 +146,8 @@ func TestGetBlockSignature(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend := NewBackend(networkID, sourceChainID, warpSigner, testVM, db, 500) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, testVM, db, 500, nil) + require.NoError(err) blockHashPayload, err := payload.NewHash(blkID) require.NoError(err) @@ -158,20 +172,65 @@ 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(networkID, sourceChainID, warpSigner, nil, db, 0) - - // Create a new unsigned message and add it to the warp backend. - unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, testPayload) + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 0, nil) require.NoError(t, err) - err = backend.AddMessage(unsignedMsg) + + // Add testUnsignedMessage to the warp backend + err = backend.AddMessage(testUnsignedMessage) require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. - messageID := unsignedMsg.ID() + messageID := testUnsignedMessage.ID() signature, err := backend.GetMessageSignature(messageID) require.NoError(t, err) - expectedSig, err := warpSigner.Sign(unsignedMsg) + expectedSig, err := warpSigner.Sign(testUnsignedMessage) require.NoError(t, err) require.Equal(t, expectedSig, signature[:]) } + +func TestOffChainMessages(t *testing.T) { + type test struct { + offchainMessages [][]byte + check func(require *require.Assertions, b Backend) + err error + } + sk, err := bls.NewSecretKey() + require.NoError(t, err) + warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) + + for name, test := range map[string]test{ + "no offchain messages": {}, + "single off-chain message": { + offchainMessages: [][]byte{ + testUnsignedMessage.Bytes(), + }, + check: func(require *require.Assertions, b Backend) { + msg, err := b.GetMessage(testUnsignedMessage.ID()) + require.NoError(err) + require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) + + signature, err := b.GetMessageSignature(testUnsignedMessage.ID()) + require.NoError(err) + expectedSignatureBytes, err := warpSigner.Sign(msg) + require.NoError(err) + require.Equal(expectedSignatureBytes, signature[:]) + }, + }, + "invalid message": { + offchainMessages: [][]byte{{1, 2, 3}}, + err: errParsingOffChainMessage, + }, + } { + t.Run(name, func(t *testing.T) { + require := require.New(t) + db := memdb.New() + + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 0, test.offchainMessages) + require.ErrorIs(err, test.err) + if test.check != nil { + test.check(require, backend) + } + }) + } +} diff --git a/warp/handlers/signature_request_test.go b/warp/handlers/signature_request_test.go index e98ea1b929..cffd2dbd81 100644 --- a/warp/handlers/signature_request_test.go +++ b/warp/handlers/signature_request_test.go @@ -16,6 +16,7 @@ import ( "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/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/plugin/evm/message" "github.com/ava-labs/subnet-evm/utils" "github.com/ava-labs/subnet-evm/warp" @@ -27,17 +28,25 @@ func TestMessageSignatureHandler(t *testing.T) { snowCtx := utils.TestSnowContext() blsSecretKey, err := bls.NewSecretKey() require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) - 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")) + addressedPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) + require.NoError(t, err) + offchainMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedPayload.Bytes()) require.NoError(t, err) + backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, &block.TestVM{TestVM: common.TestVM{T: t}}, database, 100, [][]byte{offchainMessage.Bytes()}) + require.NoError(t, err) + + 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.GetMessageSignature(messageID) require.NoError(t, err) + offchainSignature, err := backend.GetMessageSignature(offchainMessage.ID()) + require.NoError(t, err) + unknownMessageID := ids.GenerateTestID() emptySignature := [bls.SignatureLen]byte{} @@ -61,6 +70,21 @@ func TestMessageSignatureHandler(t *testing.T) { require.EqualValues(t, 0, stats.blockSignatureMiss.Count()) }, }, + "offchain message": { + setup: func() (request message.MessageSignatureRequest, expectedResponse []byte) { + return message.MessageSignatureRequest{ + MessageID: offchainMessage.ID(), + }, offchainSignature[:] + }, + verifyStats: func(t *testing.T, stats *handlerStats) { + require.EqualValues(t, 1, stats.messageSignatureRequest.Count()) + require.EqualValues(t, 1, stats.messageSignatureHit.Count()) + require.EqualValues(t, 0, stats.messageSignatureMiss.Count()) + require.EqualValues(t, 0, stats.blockSignatureRequest.Count()) + require.EqualValues(t, 0, stats.blockSignatureHit.Count()) + require.EqualValues(t, 0, stats.blockSignatureMiss.Count()) + }, + }, "unknown message": { setup: func() (request message.MessageSignatureRequest, expectedResponse []byte) { return message.MessageSignatureRequest{ @@ -125,14 +149,16 @@ func TestBlockSignatureHandler(t *testing.T) { return nil, errors.New("invalid blockID") }, } - backend := warp.NewBackend( + backend, err := warp.NewBackend( snowCtx.NetworkID, snowCtx.ChainID, warpSigner, testVM, database, 100, + nil, ) + require.NoError(t, err) signature, err := backend.GetBlockSignature(blkID) require.NoError(t, err)