diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index 6ca45a3a1f..fd200cc6f2 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -91,6 +91,55 @@ func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error) { return append(method.ID, arguments...), nil } +// PackEvent packs the given event name and arguments to conform the ABI. +// Returns the topics for the event including the event signature (if non-anonymous event) and +// hashes derived from indexed arguments and the packed data of non-indexed args according to +// the event ABI specification. +// The order of arguments must match the order of the event definition. +// https://docs.soliditylang.org/en/v0.8.17/abi-spec.html#indexed-event-encoding. +// Note: PackEvent does not support array (fixed or dynamic-size) or struct types. +func (abi ABI) PackEvent(name string, args ...interface{}) ([]common.Hash, []byte, error) { + event, exist := abi.Events[name] + if !exist { + return nil, nil, fmt.Errorf("event '%s' not found", name) + } + if len(args) != len(event.Inputs) { + return nil, nil, fmt.Errorf("event '%s' unexpected number of inputs %d", name, len(args)) + } + + var ( + nonIndexedInputs = make([]interface{}, 0) + indexedInputs = make([]interface{}, 0) + nonIndexedArgs Arguments + indexedArgs Arguments + ) + + for i, arg := range event.Inputs { + if arg.Indexed { + indexedArgs = append(indexedArgs, arg) + indexedInputs = append(indexedInputs, args[i]) + } else { + nonIndexedArgs = append(nonIndexedArgs, arg) + nonIndexedInputs = append(nonIndexedInputs, args[i]) + } + } + + packedArguments, err := nonIndexedArgs.Pack(nonIndexedInputs...) + if err != nil { + return nil, nil, err + } + topics := make([]common.Hash, 0, len(indexedArgs)+1) + if !event.Anonymous { + topics = append(topics, event.ID) + } + indexedTopics, err := PackTopics(indexedInputs) + if err != nil { + return nil, nil, err + } + + return append(topics, indexedTopics...), packedArguments, nil +} + // PackOutput packs the given [args] as the output of given method [name] to conform the ABI. // This does not include method ID. func (abi ABI) PackOutput(name string, args ...interface{}) ([]byte, error) { diff --git a/accounts/abi/abi_test.go b/accounts/abi/abi_test.go index 5bd8b17e55..669fe2e69c 100644 --- a/accounts/abi/abi_test.go +++ b/accounts/abi/abi_test.go @@ -39,6 +39,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" ) const jsondata = ` @@ -1202,3 +1203,98 @@ func TestUnpackRevert(t *testing.T) { }) } } + +func TestABI_PackEvent(t *testing.T) { + tests := []struct { + name string + json string + event string + args []interface{} + expectedTopics []common.Hash + expectedData []byte + }{ + { + name: "received", + json: `[ + {"type":"event","name":"received","anonymous":false,"inputs":[ + {"indexed":false,"name":"sender","type":"address"}, + {"indexed":false,"name":"amount","type":"uint256"}, + {"indexed":false,"name":"memo","type":"bytes"} + ] + }]`, + event: "received(address,uint256,bytes)", + args: []interface{}{ + common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), + big.NewInt(1), + []byte{0x88}, + }, + expectedTopics: []common.Hash{ + common.HexToHash("0x75fd880d39c1daf53b6547ab6cb59451fc6452d27caa90e5b6649dd8293b9eed"), + }, + expectedData: common.Hex2Bytes("000000000000000000000000376c47978271565f56deb45495afa69e59c16ab20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000018800000000000000000000000000000000000000000000000000000000000000"), + }, + { + name: "received", + json: `[ + {"type":"event","name":"received","anonymous":true,"inputs":[ + {"indexed":false,"name":"sender","type":"address"}, + {"indexed":false,"name":"amount","type":"uint256"}, + {"indexed":false,"name":"memo","type":"bytes"} + ] + }]`, + event: "received(address,uint256,bytes)", + args: []interface{}{ + common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), + big.NewInt(1), + []byte{0x88}, + }, + expectedTopics: []common.Hash{}, + expectedData: common.Hex2Bytes("000000000000000000000000376c47978271565f56deb45495afa69e59c16ab20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000018800000000000000000000000000000000000000000000000000000000000000"), + }, { + name: "Transfer", + json: `[ + { "constant": true, "inputs": [], "name": "name", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, + { "constant": false, "inputs": [ { "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "approve", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, + { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, + { "constant": false, "inputs": [ { "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transferFrom", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, + { "constant": true, "inputs": [], "name": "decimals", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, + { "constant": true, "inputs": [ { "name": "_owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "balance", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, + { "constant": true, "inputs": [], "name": "symbol", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, + { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, + { "constant": true, "inputs": [ { "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" } ], "name": "allowance", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, + { "payable": true, "stateMutability": "payable", "type": "fallback" }, + { "anonymous": false, "inputs": [ { "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Approval", "type": "event" }, + { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } + ]`, + event: "Transfer(address,address,uint256)", + args: []interface{}{ + common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), + big.NewInt(100), + }, + expectedTopics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + common.HexToHash("0x0000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fc"), + common.HexToHash("0x000000000000000000000000376c47978271565f56deb45495afa69e59c16ab2"), + }, + expectedData: common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000064"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + abi, err := JSON(strings.NewReader(test.json)) + if err != nil { + t.Error(err) + } + + topics, data, err := abi.PackEvent(test.name, test.args...) + if err != nil { + t.Fatal(err) + } + + assert.EqualValues(t, test.expectedTopics, topics) + assert.EqualValues(t, test.expectedData, data) + }) + } +} diff --git a/accounts/abi/topics.go b/accounts/abi/topics.go index 5fe7071b66..ef99de0b9e 100644 --- a/accounts/abi/topics.go +++ b/accounts/abi/topics.go @@ -37,70 +37,97 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) +// packTopic packs rule into the corresponding hash value for a log's topic +// according to the Solidity documentation: +// https://docs.soliditylang.org/en/v0.8.17/abi-spec.html#indexed-event-encoding. +func packTopic(rule interface{}) (common.Hash, error) { + var topic common.Hash + + // Try to generate the topic based on simple types + switch rule := rule.(type) { + case common.Hash: + copy(topic[:], rule[:]) + case common.Address: + copy(topic[common.HashLength-common.AddressLength:], rule[:]) + case *big.Int: + blob := rule.Bytes() + copy(topic[common.HashLength-len(blob):], blob) + case bool: + if rule { + topic[common.HashLength-1] = 1 + } + case int8: + copy(topic[:], genIntType(int64(rule), 1)) + case int16: + copy(topic[:], genIntType(int64(rule), 2)) + case int32: + copy(topic[:], genIntType(int64(rule), 4)) + case int64: + copy(topic[:], genIntType(rule, 8)) + case uint8: + blob := new(big.Int).SetUint64(uint64(rule)).Bytes() + copy(topic[common.HashLength-len(blob):], blob) + case uint16: + blob := new(big.Int).SetUint64(uint64(rule)).Bytes() + copy(topic[common.HashLength-len(blob):], blob) + case uint32: + blob := new(big.Int).SetUint64(uint64(rule)).Bytes() + copy(topic[common.HashLength-len(blob):], blob) + case uint64: + blob := new(big.Int).SetUint64(rule).Bytes() + copy(topic[common.HashLength-len(blob):], blob) + case string: + hash := crypto.Keccak256Hash([]byte(rule)) + copy(topic[:], hash[:]) + case []byte: + hash := crypto.Keccak256Hash(rule) + copy(topic[:], hash[:]) + + default: + // todo(rjl493456442) according solidity documentation, indexed event + // parameters that are not value types i.e. arrays and structs are not + // stored directly but instead a keccak256-hash of an encoding is stored. + // + // We only convert strings and bytes to hash, still need to deal with + // array(both fixed-size and dynamic-size) and struct. + + // Attempt to generate the topic from funky types + val := reflect.ValueOf(rule) + switch { + // static byte array + case val.Kind() == reflect.Array && reflect.TypeOf(rule).Elem().Kind() == reflect.Uint8: + reflect.Copy(reflect.ValueOf(topic[:val.Len()]), val) + default: + return common.Hash{}, fmt.Errorf("unsupported indexed type: %T", rule) + } + } + return topic, nil +} + +// PackTopics packs the array of filters into an array of corresponding topics +// according to the Solidity documentation. +// Note: PackTopics does not support array (fixed or dynamic-size) or struct types. +func PackTopics(filter []interface{}) ([]common.Hash, error) { + topics := make([]common.Hash, len(filter)) + for i, rule := range filter { + topic, err := packTopic(rule) + if err != nil { + return nil, err + } + topics[i] = topic + } + + return topics, nil +} + // MakeTopics converts a filter query argument list into a filter topic set. func MakeTopics(query ...[]interface{}) ([][]common.Hash, error) { topics := make([][]common.Hash, len(query)) for i, filter := range query { for _, rule := range filter { - var topic common.Hash - - // Try to generate the topic based on simple types - switch rule := rule.(type) { - case common.Hash: - copy(topic[:], rule[:]) - case common.Address: - copy(topic[common.HashLength-common.AddressLength:], rule[:]) - case *big.Int: - blob := rule.Bytes() - copy(topic[common.HashLength-len(blob):], blob) - case bool: - if rule { - topic[common.HashLength-1] = 1 - } - case int8: - copy(topic[:], genIntType(int64(rule), 1)) - case int16: - copy(topic[:], genIntType(int64(rule), 2)) - case int32: - copy(topic[:], genIntType(int64(rule), 4)) - case int64: - copy(topic[:], genIntType(rule, 8)) - case uint8: - blob := new(big.Int).SetUint64(uint64(rule)).Bytes() - copy(topic[common.HashLength-len(blob):], blob) - case uint16: - blob := new(big.Int).SetUint64(uint64(rule)).Bytes() - copy(topic[common.HashLength-len(blob):], blob) - case uint32: - blob := new(big.Int).SetUint64(uint64(rule)).Bytes() - copy(topic[common.HashLength-len(blob):], blob) - case uint64: - blob := new(big.Int).SetUint64(rule).Bytes() - copy(topic[common.HashLength-len(blob):], blob) - case string: - hash := crypto.Keccak256Hash([]byte(rule)) - copy(topic[:], hash[:]) - case []byte: - hash := crypto.Keccak256Hash(rule) - copy(topic[:], hash[:]) - - default: - // todo(rjl493456442) according solidity documentation, indexed event - // parameters that are not value types i.e. arrays and structs are not - // stored directly but instead a keccak256-hash of an encoding is stored. - // - // We only convert stringS and bytes to hash, still need to deal with - // array(both fixed-size and dynamic-size) and struct. - - // Attempt to generate the topic from funky types - val := reflect.ValueOf(rule) - switch { - // static byte array - case val.Kind() == reflect.Array && reflect.TypeOf(rule).Elem().Kind() == reflect.Uint8: - reflect.Copy(reflect.ValueOf(topic[:val.Len()]), val) - default: - return nil, fmt.Errorf("unsupported indexed type: %T", rule) - } + topic, err := packTopic(rule) + if err != nil { + return nil, err } topics[i] = append(topics[i], topic) } diff --git a/contracts/contracts/ExampleWarp.sol b/contracts/contracts/ExampleWarp.sol new file mode 100644 index 0000000000..b6247058ef --- /dev/null +++ b/contracts/contracts/ExampleWarp.sol @@ -0,0 +1,58 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "./interfaces/IWarpMessenger.sol"; + +contract ExampleWarp { + address constant WARP_ADDRESS = 0x0200000000000000000000000000000000000005; + IWarpMessenger warp = IWarpMessenger(WARP_ADDRESS); + + // sendWarpMessage sends a warp message containing the payload + function sendWarpMessage(bytes calldata payload) external { + warp.sendWarpMessage(payload); + } + + // validateWarpMessage retrieves the warp message attached to the transaction and verifies all of its attributes. + function validateWarpMessage( + uint32 index, + bytes32 sourceChainID, + address originSenderAddress, + bytes calldata payload + ) external view { + (WarpMessage memory message, bool valid) = warp.getVerifiedWarpMessage(index); + require(valid); + require(message.sourceChainID == sourceChainID); + require(message.originSenderAddress == originSenderAddress); + require(keccak256(message.payload) == keccak256(payload)); + } + + function validateInvalidWarpMessage(uint32 index) external view { + (WarpMessage memory message, bool valid) = warp.getVerifiedWarpMessage(index); + require(!valid); + require(message.sourceChainID == bytes32(0)); + require(message.originSenderAddress == address(0)); + require(keccak256(message.payload) == keccak256(bytes(""))); + } + + // validateWarpBlockHash retrieves the warp block hash attached to the transaction and verifies it matches the + // expected block hash. + function validateWarpBlockHash(uint32 index, bytes32 sourceChainID, bytes32 blockHash) external view { + (WarpBlockHash memory warpBlockHash, bool valid) = warp.getVerifiedWarpBlockHash(index); + require(valid); + require(warpBlockHash.sourceChainID == sourceChainID); + require(warpBlockHash.blockHash == blockHash); + } + + function validateInvalidWarpBlockHash(uint32 index) external view { + (WarpBlockHash memory warpBlockHash, bool valid) = warp.getVerifiedWarpBlockHash(index); + require(!valid); + require(warpBlockHash.sourceChainID == bytes32(0)); + require(warpBlockHash.blockHash == bytes32(0)); + } + + // validateGetBlockchainID checks that the blockchainID returned by warp matches the argument + function validateGetBlockchainID(bytes32 blockchainID) external view { + require(blockchainID == warp.getBlockchainID()); + } +} diff --git a/contracts/contracts/interfaces/IWarpMessenger.sol b/contracts/contracts/interfaces/IWarpMessenger.sol new file mode 100644 index 0000000000..0a77d36640 --- /dev/null +++ b/contracts/contracts/interfaces/IWarpMessenger.sol @@ -0,0 +1,51 @@ +// (c) 2022-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +struct WarpMessage { + bytes32 sourceChainID; + address originSenderAddress; + bytes payload; +} + +struct WarpBlockHash { + bytes32 sourceChainID; + bytes32 blockHash; +} + +interface IWarpMessenger { + event SendWarpMessage(address indexed sender, bytes32 indexed messageID, bytes message); + + // sendWarpMessage emits a request for the subnet to send a warp message from [msg.sender] + // with the specified parameters. + // This emits a SendWarpMessage log from the precompile. When the corresponding block is accepted + // the Accept hook of the Warp precompile is invoked with all accepted logs emitted by the Warp + // precompile. + // Each validator then adds the UnsignedWarpMessage encoded in the log to the set of messages + // it is willing to sign for an off-chain relayer to aggregate Warp signatures. + function sendWarpMessage(bytes calldata payload) external returns (bytes32 messageID); + + // getVerifiedWarpMessage parses the pre-verified warp message in the + // predicate storage slots as a WarpMessage and returns it to the caller. + // If the message exists and passes verification, returns the verified message + // and true. + // Otherwise, returns false and the empty value for the message. + function getVerifiedWarpMessage(uint32 index) external view returns (WarpMessage calldata message, bool valid); + + // getVerifiedWarpBlockHash parses the pre-verified WarpBlockHash message in the + // predicate storage slots as a WarpBlockHash message and returns it to the caller. + // If the message exists and passes verification, returns the verified message + // and true. + // Otherwise, returns false and the empty value for the message. + function getVerifiedWarpBlockHash( + uint32 index + ) external view returns (WarpBlockHash calldata warpBlockHash, bool valid); + + // getBlockchainID returns the snow.Context BlockchainID of this chain. + // This blockchainID is the hash of the transaction that created this blockchain on the P-Chain + // and is not related to the Ethereum ChainID. + function getBlockchainID() external view returns (bytes32 blockchainID); +} diff --git a/plugin/evm/ExampleWarp.abi b/plugin/evm/ExampleWarp.abi new file mode 100644 index 0000000000..9d4b442caa --- /dev/null +++ b/plugin/evm/ExampleWarp.abi @@ -0,0 +1,105 @@ +[ + { + "inputs": [ + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "name": "sendWarpMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "blockchainID", + "type": "bytes32" + } + ], + "name": "validateGetBlockchainID", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "index", + "type": "uint32" + } + ], + "name": "validateInvalidWarpBlockHash", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "index", + "type": "uint32" + } + ], + "name": "validateInvalidWarpMessage", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "index", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "sourceChainID", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "name": "validateWarpBlockHash", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "index", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "sourceChainID", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "originSenderAddress", + "type": "address" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "name": "validateWarpMessage", + "outputs": [], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/plugin/evm/ExampleWarp.bin b/plugin/evm/ExampleWarp.bin new file mode 100644 index 0000000000..c5963ac7ea --- /dev/null +++ b/plugin/evm/ExampleWarp.bin @@ -0,0 +1 @@ +60806040527302000000000000000000000000000000000000055f806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550348015610062575f80fd5b50610d15806100705f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c806315f0c959146100645780635bd05f061461008057806377ca84db1461009c578063e519286f146100b8578063ee5b48eb146100d4578063f25ec06a146100f0575b5f80fd5b61007e60048036038101906100799190610658565b61010c565b005b61009a60048036038101906100959190610777565b6101a5565b005b6100b660048036038101906100b191906107fb565b6102cd565b005b6100d260048036038101906100cd9190610826565b61039a565b005b6100ee60048036038101906100e99190610876565b610464565b005b61010a600480360381019061010591906107fb565b6104ef565b005b5f8054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16634213cf786040518163ffffffff1660e01b8152600401602060405180830381865afa158015610174573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061019891906108d5565b81146101a2575f80fd5b50565b5f805f8054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16636f825350886040518263ffffffff1660e01b81526004016101ff919061090f565b5f60405180830381865afa158015610219573d5f803e3d5ffd5b505050506040513d5f823e3d601f19601f820116820180604052508101906102419190610b48565b915091508061024e575f80fd5b85825f01511461025c575f80fd5b8473ffffffffffffffffffffffffffffffffffffffff16826020015173ffffffffffffffffffffffffffffffffffffffff1614610297575f80fd5b83836040516102a7929190610bde565b6040518091039020826040015180519060200120146102c4575f80fd5b50505050505050565b5f805f8054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663ce7f5929846040518263ffffffff1660e01b8152600401610327919061090f565b606060405180830381865afa158015610342573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103669190610c43565b915091508015610374575f80fd5b5f801b825f015114610384575f80fd5b5f801b826020015114610395575f80fd5b505050565b5f805f8054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663ce7f5929866040518263ffffffff1660e01b81526004016103f4919061090f565b606060405180830381865afa15801561040f573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906104339190610c43565b9150915080610440575f80fd5b83825f01511461044e575f80fd5b8282602001511461045d575f80fd5b5050505050565b5f8054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663ee5b48eb83836040518363ffffffff1660e01b81526004016104be929190610cbd565b5f604051808303815f87803b1580156104d5575f80fd5b505af11580156104e7573d5f803e3d5ffd5b505050505050565b5f805f8054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16636f825350846040518263ffffffff1660e01b8152600401610549919061090f565b5f60405180830381865afa158015610563573d5f803e3d5ffd5b505050506040513d5f823e3d601f19601f8201168201806040525081019061058b9190610b48565b915091508015610599575f80fd5b5f801b825f0151146105a9575f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff16826020015173ffffffffffffffffffffffffffffffffffffffff16146105e4575f80fd5b60405180602001604052805f815250805190602001208260400151805190602001201461060f575f80fd5b505050565b5f604051905090565b5f80fd5b5f80fd5b5f819050919050565b61063781610625565b8114610641575f80fd5b50565b5f813590506106528161062e565b92915050565b5f6020828403121561066d5761066c61061d565b5b5f61067a84828501610644565b91505092915050565b5f63ffffffff82169050919050565b61069b81610683565b81146106a5575f80fd5b50565b5f813590506106b681610692565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6106e5826106bc565b9050919050565b6106f5816106db565b81146106ff575f80fd5b50565b5f81359050610710816106ec565b92915050565b5f80fd5b5f80fd5b5f80fd5b5f8083601f84011261073757610736610716565b5b8235905067ffffffffffffffff8111156107545761075361071a565b5b6020830191508360018202830111156107705761076f61071e565b5b9250929050565b5f805f805f608086880312156107905761078f61061d565b5b5f61079d888289016106a8565b95505060206107ae88828901610644565b94505060406107bf88828901610702565b935050606086013567ffffffffffffffff8111156107e0576107df610621565b5b6107ec88828901610722565b92509250509295509295909350565b5f602082840312156108105761080f61061d565b5b5f61081d848285016106a8565b91505092915050565b5f805f6060848603121561083d5761083c61061d565b5b5f61084a868287016106a8565b935050602061085b86828701610644565b925050604061086c86828701610644565b9150509250925092565b5f806020838503121561088c5761088b61061d565b5b5f83013567ffffffffffffffff8111156108a9576108a8610621565b5b6108b585828601610722565b92509250509250929050565b5f815190506108cf8161062e565b92915050565b5f602082840312156108ea576108e961061d565b5b5f6108f7848285016108c1565b91505092915050565b61090981610683565b82525050565b5f6020820190506109225f830184610900565b92915050565b5f80fd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6109728261092c565b810181811067ffffffffffffffff821117156109915761099061093c565b5b80604052505050565b5f6109a3610614565b90506109af8282610969565b919050565b5f80fd5b5f815190506109c6816106ec565b92915050565b5f80fd5b5f67ffffffffffffffff8211156109ea576109e961093c565b5b6109f38261092c565b9050602081019050919050565b5f5b83811015610a1d578082015181840152602081019050610a02565b5f8484015250505050565b5f610a3a610a35846109d0565b61099a565b905082815260208101848484011115610a5657610a556109cc565b5b610a61848285610a00565b509392505050565b5f82601f830112610a7d57610a7c610716565b5b8151610a8d848260208601610a28565b91505092915050565b5f60608284031215610aab57610aaa610928565b5b610ab5606061099a565b90505f610ac4848285016108c1565b5f830152506020610ad7848285016109b8565b602083015250604082015167ffffffffffffffff811115610afb57610afa6109b4565b5b610b0784828501610a69565b60408301525092915050565b5f8115159050919050565b610b2781610b13565b8114610b31575f80fd5b50565b5f81519050610b4281610b1e565b92915050565b5f8060408385031215610b5e57610b5d61061d565b5b5f83015167ffffffffffffffff811115610b7b57610b7a610621565b5b610b8785828601610a96565b9250506020610b9885828601610b34565b9150509250929050565b5f81905092915050565b828183375f83830152505050565b5f610bc58385610ba2565b9350610bd2838584610bac565b82840190509392505050565b5f610bea828486610bba565b91508190509392505050565b5f60408284031215610c0b57610c0a610928565b5b610c15604061099a565b90505f610c24848285016108c1565b5f830152506020610c37848285016108c1565b60208301525092915050565b5f8060608385031215610c5957610c5861061d565b5b5f610c6685828601610bf6565b9250506040610c7785828601610b34565b9150509250929050565b5f82825260208201905092915050565b5f610c9c8385610c81565b9350610ca9838584610bac565b610cb28361092c565b840190509392505050565b5f6020820190508181035f830152610cd6818486610c91565b9050939250505056fea2646970667358221220d2f09e48f2e77361389456025f7337767127dc73767d50ff2f46bc5273493cec64736f6c63430008150033 \ No newline at end of file diff --git a/plugin/evm/atomic_state.go b/plugin/evm/atomic_state.go index 0cb7d2fea7..667e4c2517 100644 --- a/plugin/evm/atomic_state.go +++ b/plugin/evm/atomic_state.go @@ -25,7 +25,7 @@ type AtomicState interface { Root() common.Hash // Accept applies the state change to VM's persistent storage // Changes are persisted atomically along with the provided [commitBatch]. - Accept(commitBatch database.Batch) error + Accept(commitBatch database.Batch, requests map[ids.ID]*atomic.Requests) error // Reject frees memory associated with the state change. Reject() error } @@ -46,7 +46,11 @@ func (a *atomicState) Root() common.Hash { } // Accept applies the state change to VM's persistent storage. -func (a *atomicState) Accept(commitBatch database.Batch) error { +func (a *atomicState) Accept(commitBatch database.Batch, requests map[ids.ID]*atomic.Requests) error { + // Add the new requests to the batch to be accepted + for chainID, requests := range requests { + mergeAtomicOpsToMap(a.atomicOps, chainID, requests) + } // Update the atomic tx repository. Note it is necessary to invoke // the correct method taking bonus blocks into consideration. if a.backend.IsBonus(a.blockHeight, a.blockHash) { diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 08a2ef33c2..15fd547811 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -15,7 +15,9 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/core/rawdb" "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/precompile/precompileconfig" "github.com/ava-labs/coreth/predicate" @@ -147,6 +149,18 @@ func (b *Block) Accept(context.Context) error { b.status = choices.Accepted log.Debug(fmt.Sprintf("Accepting block %s (%s) at height %d", b.ID().Hex(), b.ID(), b.Height())) + + // Call Accept for relevant precompile logs. Note we do this prior to + // calling Accept on the blockChain so any side effects (eg warp signatures) + // take place before the accepted log is emitted to subscribers. Use of the + // sharedMemoryWriter ensures shared memory requests generated by + // precompiles are committed atomically with the vm's lastAcceptedKey. + rules := b.vm.chainConfig.AvalancheRules(b.ethBlock.Number(), b.ethBlock.Timestamp()) + sharedMemoryWriter := NewSharedMemoryWriter() + if err := b.handlePrecompileAccept(rules, sharedMemoryWriter); err != nil { + return err + } + if err := vm.blockChain.Accept(b.ethBlock); err != nil { return fmt.Errorf("chain could not accept %s: %w", b.ID(), err) } @@ -170,7 +184,44 @@ func (b *Block) Accept(context.Context) error { if err != nil { return fmt.Errorf("could not create commit batch processing block[%s]: %w", b.ID(), err) } - return atomicState.Accept(commitBatch) + return atomicState.Accept(commitBatch, sharedMemoryWriter.requests) +} + +// handlePrecompileAccept calls Accept on any logs generated with an active precompile address that implements +// contract.Accepter +// This function assumes that the Accept function will ONLY operate on state maintained in the VM's versiondb. +// This ensures that any DB operations are performed atomically with marking the block as accepted. +func (b *Block) handlePrecompileAccept(rules params.Rules, sharedMemoryWriter *sharedMemoryWriter) error { + // Short circuit early if there are no precompile accepters to execute + if len(rules.AccepterPrecompiles) == 0 { + return nil + } + + // Read receipts from disk + receipts := rawdb.ReadReceipts(b.vm.chaindb, b.ethBlock.Hash(), b.ethBlock.NumberU64(), b.ethBlock.Time(), b.vm.chainConfig) + // If there are no receipts, ReadReceipts may be nil, so we check the length and confirm the ReceiptHash + // is empty to ensure that missing receipts results in an error on accept. + if len(receipts) == 0 && b.ethBlock.ReceiptHash() != types.EmptyRootHash { + return fmt.Errorf("failed to fetch receipts for accepted block with non-empty root hash (%s) (Block: %s, Height: %d)", b.ethBlock.ReceiptHash(), b.ethBlock.Hash(), b.ethBlock.NumberU64()) + } + acceptCtx := &precompileconfig.AcceptContext{ + SnowCtx: b.vm.ctx, + SharedMemory: sharedMemoryWriter, + Warp: b.vm.warpBackend, + } + for _, receipt := range receipts { + for logIdx, log := range receipt.Logs { + accepter, ok := rules.AccepterPrecompiles[log.Address] + if !ok { + continue + } + if err := accepter.Accept(acceptCtx, log.BlockHash, log.BlockNumber, log.TxHash, logIdx, log.Topics, log.Data); err != nil { + return err + } + } + } + + return nil } // Reject implements the snowman.Block interface diff --git a/plugin/evm/message/codec.go b/plugin/evm/message/codec.go index e32670cedb..ea37200665 100644 --- a/plugin/evm/message/codec.go +++ b/plugin/evm/message/codec.go @@ -41,6 +41,11 @@ func init() { c.RegisterType(CodeRequest{}), c.RegisterType(CodeResponse{}), + // Warp request types + c.RegisterType(MessageSignatureRequest{}), + c.RegisterType(BlockSignatureRequest{}), + c.RegisterType(SignatureResponse{}), + Codec.RegisterCodec(Version, c), ) diff --git a/plugin/evm/shared_memory_writer.go b/plugin/evm/shared_memory_writer.go new file mode 100644 index 0000000000..34b9428989 --- /dev/null +++ b/plugin/evm/shared_memory_writer.go @@ -0,0 +1,26 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/coreth/precompile/precompileconfig" +) + +var _ precompileconfig.SharedMemoryWriter = &sharedMemoryWriter{} + +type sharedMemoryWriter struct { + requests map[ids.ID]*atomic.Requests +} + +func NewSharedMemoryWriter() *sharedMemoryWriter { + return &sharedMemoryWriter{ + requests: make(map[ids.ID]*atomic.Requests), + } +} + +func (s *sharedMemoryWriter) AddSharedMemoryRequests(chainID ids.ID, requests *atomic.Requests) { + mergeAtomicOpsToMap(s.requests, chainID, requests) +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 1e97132103..027567c6f8 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -37,7 +37,11 @@ import ( "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/peer" "github.com/ava-labs/coreth/plugin/evm/message" + + // Force-load precompiles to trigger registration + warpPrecompile "github.com/ava-labs/coreth/precompile/contracts/warp" "github.com/ava-labs/coreth/precompile/precompileconfig" + _ "github.com/ava-labs/coreth/precompile/registry" "github.com/ava-labs/coreth/rpc" statesyncclient "github.com/ava-labs/coreth/sync/client" "github.com/ava-labs/coreth/sync/client/stats" @@ -454,13 +458,22 @@ func (vm *VM) Initialize( // Set the chain config for mainnet/fuji chain IDs switch { case g.Config.ChainID.Cmp(params.AvalancheMainnetChainID) == 0: - g.Config = params.AvalancheMainnetChainConfig + config := *params.AvalancheMainnetChainConfig + g.Config = &config extDataHashes = mainnetExtDataHashes case g.Config.ChainID.Cmp(params.AvalancheFujiChainID) == 0: - g.Config = params.AvalancheFujiChainConfig + config := *params.AvalancheFujiChainConfig + g.Config = &config extDataHashes = fujiExtDataHashes case g.Config.ChainID.Cmp(params.AvalancheLocalChainID) == 0: - g.Config = params.AvalancheLocalChainConfig + config := *params.AvalancheLocalChainConfig + g.Config = &config + } + // If the DUpgrade is activated, activate the Warp Precompile at the same time + if g.Config.DUpgradeBlockTimestamp != nil { + g.Config.PrecompileUpgrades = append(g.Config.PrecompileUpgrades, params.PrecompileUpgrade{ + Config: warpPrecompile.NewDefaultConfig(g.Config.DUpgradeBlockTimestamp), + }) } // Set the Avalanche Context on the ChainConfig g.Config.AvalancheContext = params.AvalancheContext{ diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 9b5dc59599..0ba3b17c87 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -45,6 +45,7 @@ import ( "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/cb58" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/utils/hashing" @@ -64,6 +65,7 @@ import ( "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/rpc" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/coreth/accounts/abi" accountKeystore "github.com/ava-labs/coreth/accounts/keystore" ) @@ -94,7 +96,7 @@ var ( genesisJSONBanff = "{\"config\":{\"chainId\":43111,\"homesteadBlock\":0,\"daoForkBlock\":0,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"apricotPhase1BlockTimestamp\":0,\"apricotPhase2BlockTimestamp\":0,\"apricotPhase3BlockTimestamp\":0,\"apricotPhase4BlockTimestamp\":0,\"apricotPhase5BlockTimestamp\":0,\"apricotPhasePre6BlockTimestamp\":0,\"apricotPhase6BlockTimestamp\":0,\"apricotPhasePost6BlockTimestamp\":0,\"banffBlockTimestamp\":0},\"nonce\":\"0x0\",\"timestamp\":\"0x0\",\"extraData\":\"0x00\",\"gasLimit\":\"0x5f5e100\",\"difficulty\":\"0x0\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"coinbase\":\"0x0000000000000000000000000000000000000000\",\"alloc\":{\"0100000000000000000000000000000000000000\":{\"code\":\"0x7300000000000000000000000000000000000000003014608060405260043610603d5760003560e01c80631e010439146042578063b6510bb314606e575b600080fd5b605c60048036036020811015605657600080fd5b503560b1565b60408051918252519081900360200190f35b818015607957600080fd5b5060af60048036036080811015608e57600080fd5b506001600160a01b03813516906020810135906040810135906060013560b6565b005b30cd90565b836001600160a01b031681836108fc8690811502906040516000604051808303818888878c8acf9550505050505015801560f4573d6000803e3d6000fd5b505050505056fea26469706673582212201eebce970fe3f5cb96bf8ac6ba5f5c133fc2908ae3dcd51082cfee8f583429d064736f6c634300060a0033\",\"balance\":\"0x0\"}},\"number\":\"0x0\",\"gasUsed\":\"0x0\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}" genesisJSONCortina = "{\"config\":{\"chainId\":43111,\"homesteadBlock\":0,\"daoForkBlock\":0,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"apricotPhase1BlockTimestamp\":0,\"apricotPhase2BlockTimestamp\":0,\"apricotPhase3BlockTimestamp\":0,\"apricotPhase4BlockTimestamp\":0,\"apricotPhase5BlockTimestamp\":0,\"apricotPhasePre6BlockTimestamp\":0,\"apricotPhase6BlockTimestamp\":0,\"apricotPhasePost6BlockTimestamp\":0,\"banffBlockTimestamp\":0,\"cortinaBlockTimestamp\":0},\"nonce\":\"0x0\",\"timestamp\":\"0x0\",\"extraData\":\"0x00\",\"gasLimit\":\"0x5f5e100\",\"difficulty\":\"0x0\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"coinbase\":\"0x0000000000000000000000000000000000000000\",\"alloc\":{\"0100000000000000000000000000000000000000\":{\"code\":\"0x7300000000000000000000000000000000000000003014608060405260043610603d5760003560e01c80631e010439146042578063b6510bb314606e575b600080fd5b605c60048036036020811015605657600080fd5b503560b1565b60408051918252519081900360200190f35b818015607957600080fd5b5060af60048036036080811015608e57600080fd5b506001600160a01b03813516906020810135906040810135906060013560b6565b005b30cd90565b836001600160a01b031681836108fc8690811502906040516000604051808303818888878c8acf9550505050505015801560f4573d6000803e3d6000fd5b505050505056fea26469706673582212201eebce970fe3f5cb96bf8ac6ba5f5c133fc2908ae3dcd51082cfee8f583429d064736f6c634300060a0033\",\"balance\":\"0x0\"}},\"number\":\"0x0\",\"gasUsed\":\"0x0\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}" - genesisJSONDUpgrade = "{\"config\":{\"chainId\":43111,\"homesteadBlock\":0,\"daoForkBlock\":0,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"apricotPhase1BlockTimestamp\":0,\"apricotPhase2BlockTimestamp\":0,\"apricotPhase3BlockTimestamp\":0,\"apricotPhase4BlockTimestamp\":0,\"apricotPhase5BlockTimestamp\":0,\"apricotPhasePre6BlockTimestamp\":0,\"apricotPhase6BlockTimestamp\":0,\"apricotPhasePost6BlockTimestamp\":0,\"banffBlockTimestamp\":0,\"cortinaBlockTimestamp\":0,\"dUpgradeBlockTimestamp\":0},\"nonce\":\"0x0\",\"timestamp\":\"0x0\",\"extraData\":\"0x00\",\"gasLimit\":\"0x5f5e100\",\"difficulty\":\"0x0\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"coinbase\":\"0x0000000000000000000000000000000000000000\",\"alloc\":{\"0100000000000000000000000000000000000000\":{\"code\":\"0x7300000000000000000000000000000000000000003014608060405260043610603d5760003560e01c80631e010439146042578063b6510bb314606e575b600080fd5b605c60048036036020811015605657600080fd5b503560b1565b60408051918252519081900360200190f35b818015607957600080fd5b5060af60048036036080811015608e57600080fd5b506001600160a01b03813516906020810135906040810135906060013560b6565b005b30cd90565b836001600160a01b031681836108fc8690811502906040516000604051808303818888878c8acf9550505050505015801560f4573d6000803e3d6000fd5b505050505056fea26469706673582212201eebce970fe3f5cb96bf8ac6ba5f5c133fc2908ae3dcd51082cfee8f583429d064736f6c634300060a0033\",\"balance\":\"0x0\"}},\"number\":\"0x0\",\"gasUsed\":\"0x0\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}" + genesisJSONDUpgrade = "{\"config\":{\"chainId\":43111,\"homesteadBlock\":0,\"daoForkBlock\":0,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"apricotPhase1BlockTimestamp\":0,\"apricotPhase2BlockTimestamp\":0,\"apricotPhase3BlockTimestamp\":0,\"apricotPhase4BlockTimestamp\":0,\"apricotPhase5BlockTimestamp\":0,\"apricotPhasePre6BlockTimestamp\":0,\"apricotPhase6BlockTimestamp\":0,\"apricotPhasePost6BlockTimestamp\":0,\"banffBlockTimestamp\":0,\"cortinaBlockTimestamp\":0,\"dUpgradeBlockTimestamp\":0},\"nonce\":\"0x0\",\"timestamp\":\"0x0\",\"extraData\":\"0x00\",\"gasLimit\":\"0x5f5e100\",\"difficulty\":\"0x0\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"coinbase\":\"0x0000000000000000000000000000000000000000\",\"alloc\":{\"0x99b9DEA54C48Dfea6aA9A4Ca4623633EE04ddbB5\":{\"balance\":\"0x56bc75e2d63100000\"},\"0100000000000000000000000000000000000000\":{\"code\":\"0x7300000000000000000000000000000000000000003014608060405260043610603d5760003560e01c80631e010439146042578063b6510bb314606e575b600080fd5b605c60048036036020811015605657600080fd5b503560b1565b60408051918252519081900360200190f35b818015607957600080fd5b5060af60048036036080811015608e57600080fd5b506001600160a01b03813516906020810135906040810135906060013560b6565b005b30cd90565b836001600160a01b031681836108fc8690811502906040516000604051808303818888878c8acf9550505050505015801560f4573d6000803e3d6000fd5b505050505056fea26469706673582212201eebce970fe3f5cb96bf8ac6ba5f5c133fc2908ae3dcd51082cfee8f583429d064736f6c634300060a0033\",\"balance\":\"0x0\"}},\"number\":\"0x0\",\"gasUsed\":\"0x0\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}" genesisJSONLatest = genesisJSONDUpgrade apricotRulesPhase0 = params.Rules{} @@ -169,6 +171,13 @@ func NewContext() *snow.Context { return subnetID, nil }, } + blsSecretKey, err := bls.NewSecretKey() + if err != nil { + panic(err) + } + ctx.WarpSigner = avalancheWarp.NewSigner(blsSecretKey, ctx.NetworkID, ctx.ChainID) + ctx.PublicKey = bls.PublicFromSecretKey(blsSecretKey) + return ctx } diff --git a/plugin/evm/vm_warp_test.go b/plugin/evm/vm_warp_test.go new file mode 100644 index 0000000000..53bf4a7283 --- /dev/null +++ b/plugin/evm/vm_warp_test.go @@ -0,0 +1,679 @@ +// See the file LICENSE for licensing terms. + +package evm + +import ( + "context" + "encoding/json" + "errors" + "math/big" + "testing" + "time" + + _ "embed" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/chain" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/coreth/core/rawdb" + "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/coreth/eth/tracers" + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/plugin/evm/message" + "github.com/ava-labs/coreth/precompile/contract" + "github.com/ava-labs/coreth/precompile/contracts/warp" + "github.com/ava-labs/coreth/predicate" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +var ( + //go:embed ExampleWarp.bin + exampleWarpBin string + //go:embed ExampleWarp.abi + exampleWarpABI string +) + +func TestSendWarpMessage(t *testing.T) { + require := require.New(t) + issuer, vm, _, _, _ := GenesisVM(t, true, genesisJSONDUpgrade, "", "") + + defer func() { + require.NoError(vm.Shutdown(context.Background())) + }() + + acceptedLogsChan := make(chan []*types.Log, 10) + logsSub := vm.eth.APIBackend.SubscribeAcceptedLogsEvent(acceptedLogsChan) + defer logsSub.Unsubscribe() + + payloadData := utils.RandomBytes(100) + + warpSendMessageInput, err := warp.PackSendWarpMessage(payloadData) + require.NoError(err) + addressedPayload, err := payload.NewAddressedCall( + testEthAddrs[0].Bytes(), + payloadData, + ) + require.NoError(err) + expectedUnsignedMessage, err := avalancheWarp.NewUnsignedMessage( + vm.ctx.NetworkID, + vm.ctx.ChainID, + addressedPayload.Bytes(), + ) + require.NoError(err) + + // Submit a transaction to trigger sending a warp message + tx0 := types.NewTransaction(uint64(0), warp.ContractAddress, big.NewInt(1), 100_000, big.NewInt(params.LaunchMinGasPrice), warpSendMessageInput) + signedTx0, err := types.SignTx(tx0, types.LatestSignerForChainID(vm.chainConfig.ChainID), testKeys[0].ToECDSA()) + require.NoError(err) + + errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0}) + require.NoError(errs[0]) + + <-issuer + blk, err := vm.BuildBlock(context.Background()) + require.NoError(err) + + require.NoError(blk.Verify(context.Background())) + + require.Equal(choices.Processing, blk.Status()) + + // Verify that the constructed block contains the expected log with an unsigned warp message in the log data + ethBlock1 := blk.(*chain.BlockWrapper).Block.(*Block).ethBlock + require.Len(ethBlock1.Transactions(), 1) + receipts := rawdb.ReadReceipts(vm.chaindb, ethBlock1.Hash(), ethBlock1.NumberU64(), ethBlock1.Time(), vm.chainConfig) + require.Len(receipts, 1) + + require.Len(receipts[0].Logs, 1) + expectedTopics := []common.Hash{ + warp.WarpABI.Events["SendWarpMessage"].ID, + testEthAddrs[0].Hash(), + common.Hash(expectedUnsignedMessage.ID()), + } + require.Equal(expectedTopics, receipts[0].Logs[0].Topics) + logData := receipts[0].Logs[0].Data + unsignedMessage, err := warp.UnpackSendWarpEventDataToMessage(logData) + require.NoError(err) + unsignedMessageID := unsignedMessage.ID() + + // Verify the signature cannot be fetched before the block is accepted + _, 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() + + // 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) + + select { + case acceptedLogs := <-acceptedLogsChan: + require.Len(acceptedLogs, 1, "unexpected length of accepted logs") + require.Equal(acceptedLogs[0], receipts[0].Logs[0]) + case <-time.After(time.Second): + require.Fail("Failed to read accepted logs from subscription") + } + + // 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())) +} + +func TestValidateWarpMessage(t *testing.T) { + require := require.New(t) + sourceChainID := ids.GenerateTestID() + sourceAddress := common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2") + payloadData := []byte{1, 2, 3} + addressedPayload, err := payload.NewAddressedCall( + sourceAddress.Bytes(), + payloadData, + ) + require.NoError(err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(testNetworkID, sourceChainID, addressedPayload.Bytes()) + require.NoError(err) + + exampleWarpABI := contract.ParseABI(exampleWarpABI) + exampleWarpPayload, err := exampleWarpABI.Pack( + "validateWarpMessage", + uint32(0), + sourceChainID, + sourceAddress, + payloadData, + ) + require.NoError(err) + + testWarpVMTransaction(t, unsignedMessage, true, exampleWarpPayload) +} + +func TestValidateInvalidWarpMessage(t *testing.T) { + require := require.New(t) + sourceChainID := ids.GenerateTestID() + sourceAddress := common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2") + payloadData := []byte{1, 2, 3} + addressedPayload, err := payload.NewAddressedCall( + sourceAddress.Bytes(), + payloadData, + ) + require.NoError(err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(testNetworkID, sourceChainID, addressedPayload.Bytes()) + require.NoError(err) + + exampleWarpABI := contract.ParseABI(exampleWarpABI) + exampleWarpPayload, err := exampleWarpABI.Pack( + "validateInvalidWarpMessage", + uint32(0), + ) + require.NoError(err) + + testWarpVMTransaction(t, unsignedMessage, false, exampleWarpPayload) +} + +func TestValidateWarpBlockHash(t *testing.T) { + require := require.New(t) + sourceChainID := ids.GenerateTestID() + blockHash := ids.GenerateTestID() + blockHashPayload, err := payload.NewHash(blockHash) + require.NoError(err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(testNetworkID, sourceChainID, blockHashPayload.Bytes()) + require.NoError(err) + + exampleWarpABI := contract.ParseABI(exampleWarpABI) + exampleWarpPayload, err := exampleWarpABI.Pack( + "validateWarpBlockHash", + uint32(0), + sourceChainID, + blockHash, + ) + require.NoError(err) + + testWarpVMTransaction(t, unsignedMessage, true, exampleWarpPayload) +} + +func TestValidateInvalidWarpBlockHash(t *testing.T) { + require := require.New(t) + sourceChainID := ids.GenerateTestID() + blockHash := ids.GenerateTestID() + blockHashPayload, err := payload.NewHash(blockHash) + require.NoError(err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(testNetworkID, sourceChainID, blockHashPayload.Bytes()) + require.NoError(err) + + exampleWarpABI := contract.ParseABI(exampleWarpABI) + exampleWarpPayload, err := exampleWarpABI.Pack( + "validateInvalidWarpBlockHash", + uint32(0), + ) + require.NoError(err) + + testWarpVMTransaction(t, unsignedMessage, false, exampleWarpPayload) +} + +func testWarpVMTransaction(t *testing.T, unsignedMessage *avalancheWarp.UnsignedMessage, validSignature bool, txPayload []byte) { + require := require.New(t) + issuer, vm, _, _, _ := GenesisVM(t, true, genesisJSONDUpgrade, "", "") + + defer func() { + require.NoError(vm.Shutdown(context.Background())) + }() + + acceptedLogsChan := make(chan []*types.Log, 10) + logsSub := vm.eth.APIBackend.SubscribeAcceptedLogsEvent(acceptedLogsChan) + defer logsSub.Unsubscribe() + + nodeID1 := ids.GenerateTestNodeID() + blsSecretKey1, err := bls.NewSecretKey() + require.NoError(err) + blsPublicKey1 := bls.PublicFromSecretKey(blsSecretKey1) + blsSignature1 := bls.Sign(blsSecretKey1, unsignedMessage.Bytes()) + + nodeID2 := ids.GenerateTestNodeID() + blsSecretKey2, err := bls.NewSecretKey() + require.NoError(err) + blsPublicKey2 := bls.PublicFromSecretKey(blsSecretKey2) + blsSignature2 := bls.Sign(blsSecretKey2, unsignedMessage.Bytes()) + + blsAggregatedSignature, err := bls.AggregateSignatures([]*bls.Signature{blsSignature1, blsSignature2}) + require.NoError(err) + + minimumValidPChainHeight := uint64(10) + getValidatorSetTestErr := errors.New("can't get validator set test error") + + vm.ctx.ValidatorState = &validators.TestState{ + // TODO: test both Primary Network / C-Chain and non-Primary Network + GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) { + return ids.Empty, nil + }, + GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + if height < minimumValidPChainHeight { + return nil, getValidatorSetTestErr + } + return map[ids.NodeID]*validators.GetValidatorOutput{ + nodeID1: { + NodeID: nodeID1, + PublicKey: blsPublicKey1, + Weight: 50, + }, + nodeID2: { + NodeID: nodeID2, + PublicKey: blsPublicKey2, + Weight: 50, + }, + }, nil + }, + } + + signersBitSet := set.NewBits() + signersBitSet.Add(0) + signersBitSet.Add(1) + + warpSignature := &avalancheWarp.BitSetSignature{ + Signers: signersBitSet.Bytes(), + } + + blsAggregatedSignatureBytes := bls.SignatureToBytes(blsAggregatedSignature) + copy(warpSignature.Signature[:], blsAggregatedSignatureBytes) + + signedMessage, err := avalancheWarp.NewMessage( + unsignedMessage, + warpSignature, + ) + require.NoError(err) + + createTx, err := types.SignTx( + types.NewContractCreation(0, common.Big0, 7_000_000, big.NewInt(225*params.GWei), common.Hex2Bytes(exampleWarpBin)), + types.LatestSignerForChainID(vm.chainConfig.ChainID), + testKeys[0].ToECDSA(), + ) + require.NoError(err) + exampleWarpAddress := crypto.CreateAddress(testEthAddrs[0], 0) + + tx, err := types.SignTx( + predicate.NewPredicateTx( + vm.chainConfig.ChainID, + 1, + &exampleWarpAddress, + 1_000_000, + big.NewInt(225*params.GWei), + big.NewInt(params.GWei), + common.Big0, + txPayload, + types.AccessList{}, + warp.ContractAddress, + signedMessage.Bytes(), + ), + types.LatestSignerForChainID(vm.chainConfig.ChainID), + testKeys[0].ToECDSA(), + ) + require.NoError(err) + errs := vm.txPool.AddRemotesSync([]*types.Transaction{createTx, tx}) + for i, err := range errs { + require.NoError(err, "failed to add tx at index %d", i) + } + + // If [validSignature] set the signature to be considered valid at the verified height. + blockCtx := &block.Context{ + PChainHeight: minimumValidPChainHeight - 1, + } + if validSignature { + blockCtx.PChainHeight = minimumValidPChainHeight + } + vm.clock.Set(vm.clock.Time().Add(2 * time.Second)) + <-issuer + + warpBlock, err := vm.BuildBlockWithContext(context.Background(), blockCtx) + require.NoError(err) + + warpBlockVerifyWithCtx, ok := warpBlock.(block.WithVerifyContext) + require.True(ok) + shouldVerifyWithCtx, err := warpBlockVerifyWithCtx.ShouldVerifyWithContext(context.Background()) + require.NoError(err) + require.True(shouldVerifyWithCtx) + require.NoError(warpBlockVerifyWithCtx.VerifyWithContext(context.Background(), blockCtx)) + require.Equal(choices.Processing, warpBlock.Status()) + require.NoError(vm.SetPreference(context.Background(), warpBlock.ID())) + require.NoError(warpBlock.Accept(context.Background())) + vm.blockChain.DrainAcceptorQueue() + + ethBlock := warpBlock.(*chain.BlockWrapper).Block.(*Block).ethBlock + verifiedMessageReceipts := vm.blockChain.GetReceiptsByHash(ethBlock.Hash()) + require.Len(verifiedMessageReceipts, 2) + for i, receipt := range verifiedMessageReceipts { + require.Equal(types.ReceiptStatusSuccessful, receipt.Status, "index: %d", i) + } + + tracerAPI := tracers.NewAPI(vm.eth.APIBackend) + txTraceResults, err := tracerAPI.TraceBlockByHash(context.Background(), ethBlock.Hash(), nil) + require.NoError(err) + require.Len(txTraceResults, 2) + blockTxTraceResultBytes, err := json.Marshal(txTraceResults[1].Result) + require.NoError(err) + unmarshalResults := make(map[string]interface{}) + require.NoError(json.Unmarshal(blockTxTraceResultBytes, &unmarshalResults)) + require.Equal("", unmarshalResults["returnValue"]) + + txTraceResult, err := tracerAPI.TraceTransaction(context.Background(), tx.Hash(), nil) + require.NoError(err) + txTraceResultBytes, err := json.Marshal(txTraceResult) + require.NoError(err) + require.JSONEq(string(txTraceResultBytes), string(blockTxTraceResultBytes)) +} + +func TestReceiveWarpMessage(t *testing.T) { + require := require.New(t) + issuer, vm, _, _, _ := GenesisVM(t, true, genesisJSONDUpgrade, "", "") + + defer func() { + require.NoError(vm.Shutdown(context.Background())) + }() + + acceptedLogsChan := make(chan []*types.Log, 10) + logsSub := vm.eth.APIBackend.SubscribeAcceptedLogsEvent(acceptedLogsChan) + defer logsSub.Unsubscribe() + + payloadData := utils.RandomBytes(100) + + addressedPayload, err := payload.NewAddressedCall( + testEthAddrs[0].Bytes(), + payloadData, + ) + require.NoError(err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage( + vm.ctx.NetworkID, + vm.ctx.ChainID, + addressedPayload.Bytes(), + ) + require.NoError(err) + + nodeID1 := ids.GenerateTestNodeID() + blsSecretKey1, err := bls.NewSecretKey() + require.NoError(err) + blsPublicKey1 := bls.PublicFromSecretKey(blsSecretKey1) + blsSignature1 := bls.Sign(blsSecretKey1, unsignedMessage.Bytes()) + + nodeID2 := ids.GenerateTestNodeID() + blsSecretKey2, err := bls.NewSecretKey() + require.NoError(err) + blsPublicKey2 := bls.PublicFromSecretKey(blsSecretKey2) + blsSignature2 := bls.Sign(blsSecretKey2, unsignedMessage.Bytes()) + + blsAggregatedSignature, err := bls.AggregateSignatures([]*bls.Signature{blsSignature1, blsSignature2}) + require.NoError(err) + + minimumValidPChainHeight := uint64(10) + getValidatorSetTestErr := errors.New("can't get validator set test error") + + vm.ctx.ValidatorState = &validators.TestState{ + GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) { + return ids.Empty, nil + }, + GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + if height < minimumValidPChainHeight { + return nil, getValidatorSetTestErr + } + return map[ids.NodeID]*validators.GetValidatorOutput{ + nodeID1: { + NodeID: nodeID1, + PublicKey: blsPublicKey1, + Weight: 50, + }, + nodeID2: { + NodeID: nodeID2, + PublicKey: blsPublicKey2, + Weight: 50, + }, + }, nil + }, + } + + signersBitSet := set.NewBits() + signersBitSet.Add(0) + signersBitSet.Add(1) + + warpSignature := &avalancheWarp.BitSetSignature{ + Signers: signersBitSet.Bytes(), + } + + blsAggregatedSignatureBytes := bls.SignatureToBytes(blsAggregatedSignature) + copy(warpSignature.Signature[:], blsAggregatedSignatureBytes) + + signedMessage, err := avalancheWarp.NewMessage( + unsignedMessage, + warpSignature, + ) + require.NoError(err) + + getWarpMsgInput, err := warp.PackGetVerifiedWarpMessage(0) + require.NoError(err) + getVerifiedWarpMessageTx, err := types.SignTx( + predicate.NewPredicateTx( + vm.chainConfig.ChainID, + 0, + &warp.Module.Address, + 1_000_000, + big.NewInt(225*params.GWei), + big.NewInt(params.GWei), + common.Big0, + getWarpMsgInput, + types.AccessList{}, + warp.ContractAddress, + signedMessage.Bytes(), + ), + types.LatestSignerForChainID(vm.chainConfig.ChainID), + testKeys[0].ToECDSA(), + ) + require.NoError(err) + errs := vm.txPool.AddRemotesSync([]*types.Transaction{getVerifiedWarpMessageTx}) + for i, err := range errs { + require.NoError(err, "failed to add tx at index %d", i) + } + + // Build, verify, and accept block with valid proposer context. + validProposerCtx := &block.Context{ + PChainHeight: minimumValidPChainHeight, + } + vm.clock.Set(vm.clock.Time().Add(2 * time.Second)) + <-issuer + + block2, err := vm.BuildBlockWithContext(context.Background(), validProposerCtx) + require.NoError(err) + + block2VerifyWithCtx, ok := block2.(block.WithVerifyContext) + require.True(ok) + shouldVerifyWithCtx, err := block2VerifyWithCtx.ShouldVerifyWithContext(context.Background()) + require.NoError(err) + require.True(shouldVerifyWithCtx) + require.NoError(block2VerifyWithCtx.VerifyWithContext(context.Background(), validProposerCtx)) + require.Equal(choices.Processing, block2.Status()) + require.NoError(vm.SetPreference(context.Background(), block2.ID())) + + // Verify the block with another valid context with identical predicate results + require.NoError(block2VerifyWithCtx.VerifyWithContext(context.Background(), &block.Context{ + PChainHeight: minimumValidPChainHeight + 1, + })) + require.Equal(choices.Processing, block2.Status()) + + // Verify the block in a different context causing the warp message to fail verification changing + // the expected header predicate results. + require.ErrorIs(block2VerifyWithCtx.VerifyWithContext(context.Background(), &block.Context{ + PChainHeight: minimumValidPChainHeight - 1, + }), errInvalidHeaderPredicateResults) + + // Accept the block after performing multiple VerifyWithContext operations + require.NoError(block2.Accept(context.Background())) + vm.blockChain.DrainAcceptorQueue() + + ethBlock := block2.(*chain.BlockWrapper).Block.(*Block).ethBlock + verifiedMessageReceipts := vm.blockChain.GetReceiptsByHash(ethBlock.Hash()) + require.Len(verifiedMessageReceipts, 1) + verifiedMessageTxReceipt := verifiedMessageReceipts[0] + require.Equal(types.ReceiptStatusSuccessful, verifiedMessageTxReceipt.Status) + + expectedOutput, err := warp.PackGetVerifiedWarpMessageOutput(warp.GetVerifiedWarpMessageOutput{ + Message: warp.WarpMessage{ + SourceChainID: common.Hash(vm.ctx.ChainID), + OriginSenderAddress: testEthAddrs[0], + Payload: payloadData, + }, + Valid: true, + }) + require.NoError(err) + + tracerAPI := tracers.NewAPI(vm.eth.APIBackend) + txTraceResults, err := tracerAPI.TraceBlockByHash(context.Background(), ethBlock.Hash(), nil) + require.NoError(err) + require.Len(txTraceResults, 1) + blockTxTraceResultBytes, err := json.Marshal(txTraceResults[0].Result) + require.NoError(err) + unmarshalResults := make(map[string]interface{}) + require.NoError(json.Unmarshal(blockTxTraceResultBytes, &unmarshalResults)) + require.Equal(common.Bytes2Hex(expectedOutput), unmarshalResults["returnValue"]) + + txTraceResult, err := tracerAPI.TraceTransaction(context.Background(), getVerifiedWarpMessageTx.Hash(), nil) + require.NoError(err) + txTraceResultBytes, err := json.Marshal(txTraceResult) + require.NoError(err) + require.JSONEq(string(txTraceResultBytes), string(blockTxTraceResultBytes)) +} + +func TestMessageSignatureRequestsToVM(t *testing.T) { + _, vm, _, _, appSender := GenesisVM(t, true, genesisJSONDUpgrade, "", "") + + defer func() { + err := vm.Shutdown(context.Background()) + require.NoError(t, err) + }() + + // Generate a new warp unsigned message and add to warp backend + warpMessage, err := avalancheWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, []byte{1, 2, 3}) + require.NoError(t, err) + + // Add the known message and get its signature to confirm. + err = vm.warpBackend.AddMessage(warpMessage) + require.NoError(t, err) + signature, err := vm.warpBackend.GetMessageSignature(warpMessage.ID()) + require.NoError(t, err) + + tests := map[string]struct { + messageID ids.ID + expectedResponse [bls.SignatureLen]byte + }{ + "known": { + messageID: warpMessage.ID(), + expectedResponse: signature, + }, + "unknown": { + messageID: 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.MessageSignatureRequest{ + MessageID: test.messageID, + } + + 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) + }) + } +} + +func TestBlockSignatureRequestsToVM(t *testing.T) { + _, vm, _, _, appSender := GenesisVM(t, true, genesisJSONDUpgrade, "", "") + + 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/precompile/contracts/warp/README.md b/precompile/contracts/warp/README.md new file mode 100644 index 0000000000..9c702ed049 --- /dev/null +++ b/precompile/contracts/warp/README.md @@ -0,0 +1,153 @@ +# Avalanche Warp Messaging + +Avalanche Warp Messaging offers a basic primitive to enable Cross-Subnet communication on the Avalanche Network. + +It is intended to allow communication between arbitrary Custom Virtual Machines (including, but not limited to Subnet-EVM and Coreth). + +## How does Avalanche Warp Messaging Work + +Avalanche Warp Messaging uses BLS Multi-Signatures with Public-Key Aggregation where every Avalanche validator registers a public key alongside its NodeID on the Avalanche P-Chain. + +Every node tracking a Subnet has read access to the Avalanche P-Chain. This provides weighted sets of BLS Public Keys that correspond to the validator sets of each Subnet on the Avalanche Network. Avalanche Warp Messaging provides a basic primitive for signing and verifying messages between Subnets: the receiving network can verify whether an aggregation of signatures from a set of source Subnet validators represents a threshold of stake large enough for the receiving network to process the message. + +For more details on Avalanche Warp Messaging, see the AvalancheGo [Warp README](https://github.com/ava-labs/avalanchego/blob/warp-readme/vms/platformvm/warp/README.md). + +## Integrating Avalanche Warp Messaging into the EVM + +### Flow of Sending / Receiving a Warp Message within the EVM + +The Avalanche Warp Precompile enables this flow to send a message from blockchain A to blockchain B: + +1. Call the Warp Precompile `sendWarpMessage` function with the arguments for the `UnsignedMessage` +2. Warp Precompile emits an event / log containing the `UnsignedMessage` specified by the caller of `sendWarpMessage` +3. Network accepts the block containing the `UnsignedMessage` in the log, so that validators are willing to sign the message +4. An off-chain relayer queries the validators for their signatures of the message and aggregate the signatures to create a `SignedMessage` +5. The off-chain relayer encodes the `SignedMessage` as the [predicate](#predicate-encoding) in the AccessList of a transaction to deliver on blockchain B +6. The transaction is delivered on blockchain B, the signature is verified prior to executing the block, and the message is accessible via the Warp Precompile's `getVerifiedWarpMessage` during the execution of that transaction + +### Warp Precompile + +The Warp Precompile is broken down into three functions defined in the Solidity interface file [here](../../../contracts/contracts/interfaces/IWarpMessenger.sol). + +#### sendWarpMessage + +`sendWarpMessage` is used to send a verifiable message. Calling this function results in sending a message with the following contents: + +- `SourceChainID` - blockchainID of the sourceChain on the Avalanche P-Chain +- `SourceAddress` - `msg.sender` encoded as a 32 byte value that calls `sendWarpMessage` +- `Payload` - `payload` argument specified in the call to `sendWarpMessage` emitted as the unindexed data of the resulting log + +Calling this function will issue a `SendWarpMessage` event from the Warp Precompile. Since the EVM limits the number of topics to 4 including the EventID, this message includes only the topics that would be expected to help filter messages emitted from the Warp Precompile the most. + +Specifically, the `payload` is not emitted as a topic because each topic must be encoded as a hash. Therefore, we opt to take advantage of each possible topic to maximize the possible filtering for emitted Warp Messages. + +Additionally, the `SourceChainID` is excluded because anyone parsing the chain can be expected to already know the blockchainID. Therefore, the `SendWarpMessage` event includes the indexable attributes: + +- `sender` +- The `messageID` of the unsigned message (sha256 of the unsigned message) + +The actual `message` is the entire [Avalanche Warp Unsigned Message](https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/warp/unsigned_message.go#L14) including an [AddressedCall](https://github.com/ava-labs/avalanchego/tree/v1.10.15/vms/platformvm/warp/payload). The unsigned message is emitted as the unindexed data in the log. + +#### getVerifiedMessage + +`getVerifiedMessage` is used to read the contents of the delivered Avalanche Warp Message into the expected format. + +It returns the message if present and a boolean indicating if a message is present. + +To use this function, the transaction must include the signed Avalanche Warp Message encoded in the [predicate](#predicate-encoding) of the transaction. Prior to executing a block, the VM iterates through transactions and pre-verifies all predicates. If a transaction's predicate is invalid, then it is considered invalid to include in the block and dropped. + +This leads to the following advantages: + +1. The EVM execution does not need to verify the Warp Message at runtime (no signature verification or external calls to the P-Chain) +2. The EVM can deterministically re-execute and re-verify blocks assuming the predicate was verified by the network (eg., in bootstrapping) + +This pre-verification is performed using the ProposerVM Block header during [block verification](../../../plugin/evm/block.go#L220) and [block building](../../../miner/worker.go#L200). + +#### getBlockchainID + +`getBlockchainID` returns the blockchainID of the blockchain that the VM is running on. + +This is different from the conventional Ethereum ChainID registered to [ChainList](https://chainlist.org/). + +The `blockchainID` in Avalanche refers to the txID that created the blockchain on the Avalanche P-Chain ([docs](https://docs.avax.network/specs/platform-transaction-serialization#unsigned-create-chain-tx)). + +### Predicate Encoding + +Avalanche Warp Messages are encoded as a signed Avalanche [Warp Message](https://github.com/ava-labs/avalanchego/blob/v1.10.4/vms/platformvm/warp/message.go#L7) where the [UnsignedMessage](https://github.com/ava-labs/avalanchego/blob/v1.10.4/vms/platformvm/warp/unsigned_message.go#L14)'s payload includes an [AddressedPayload](../../../warp/payload/payload.go). + +Since the predicate is encoded into the [Transaction Access List](https://eips.ethereum.org/EIPS/eip-2930), it is packed into 32 byte hashes intended to declare storage slots that should be pre-warmed into the cache prior to transaction execution. + +Therefore, we use the [Predicate Utils](../../../utils/predicate/README.md) package to encode the actual byte slice of size N into the access list. + +### Performance Optimization: C-Chain to Subnet + +To support C-Chain to Subnet communication, or more generally Primary Network to Subnet communication, we special case the C-Chain for two reasons: + +1. Every Subnet validator validates the C-Chain +2. The Primary Network has the largest possible number of validators + +Since the Primary Network has the largest possible number of validators for any Subnet on Avalanche, it would also be the most expensive Subnet to receive and verify Avalanche Warp Messages from as it reaching a threshold of stake on the primary network would require many signatures. Luckily, we can do something much smarter. + +When a Subnet receives a message from a blockchain on the Primary Network, we use the validator set of the receiving Subnet instead of the entire network when validating the message. This means that the C-Chain sending a message can be the exact same as Subnet to Subnet communication. + +However, when Subnet B receives a message from the C-Chain, it changes the semantics to the following: + +1. Read the SourceChainID of the signed message (C-Chain) +2. Look up the SubnetID that validates C-Chain: Primary Network +3. Look up the validator set of Subnet B (instead of the Primary Network) and the registered BLS Public Keys of Subnet B at the P-Chain height specified by the ProposerVM header +4. Continue Warp Message verification using the validator set of Subnet B instead of the Primary Network + +This means that C-Chain to Subnet communication only requires a threshold of stake on the receiving subnet to sign the message instead of a threshold of stake for the entire Primary Network. + +This assumes that the security of Subnet B already depends on the validators of Subnet B to behave virtuously. Therefore, requiring a threshold of stake from the receiving Subnet's validator set instead of the whole Primary Network does not meaningfully change security of the receiving Subnet. + +Note: this special case is ONLY applied during Warp Message verification. The message sent by the Primary Network will still contain the Avalanche C-Chain's blockchainID as the sourceChainID and signatures will be served by querying the C-Chain directly. + +## Design Considerations + +### Re-Processing Historical Blocks + +Avalanche Warp Messaging depends on the Avalanche P-Chain state at the P-Chain height specified by the ProposerVM block header. + +Verifying a message requires looking up the validator set of the source subnet on the P-Chain. To support this, Avalanche Warp Messaging uses the ProposerVM header, which includes the P-Chain height it was issued at as the canonical point to lookup the source subnet's validator set. + +This means verifying the Warp Message and therefore the state transition on a block depends on state that is external to the blockchain itself: the P-Chain. + +The Avalanche P-Chain tracks only its current state and reverse diff layers (reversing the changes from past blocks) in order to re-calculate the validator set at a historical height. This means calculating a very old validator set that is used to verify a Warp Message in an old block may become prohibitively expensive. + +Therefore, we need a heuristic to ensure that the network can correctly re-process old blocks (note: re-processing old blocks is a requirement to perform bootstrapping and is used in some VMs to serve or verify historical data). + +As a result, we require that the block itself provides a deterministic hint which determines which Avalanche Warp Messages were considered valid/invalid during the block's execution. This ensures that we can always re-process blocks and use the hint to decide whether an Avalanche Warp Message should be treated as valid/invalid even after the P-Chain state that was used at the original execution time may no longer support fast lookups. + +To provide that hint, we've explored two designs: + +1. Include a predicate in the transaction to ensure any referenced message is valid +2. Append the results of checking whether a Warp Message is valid/invalid to the block data itself + +The current implementation uses option (1). + +The original reason for this was that the notion of predicates for precompiles was designed with Shared Memory in mind. In the case of shared memory, there is no canonical "P-Chain height" in the block which determines whether or not Avalanche Warp Messages are valid. + +Instead, the VM interprets a shared memory import operation as valid as soon as the UTXO is available in shared memory. This means that if it were up to the block producer to staple the valid/invalid results of whether or not an attempted atomic operation should be treated as valid, a byzantine block producer could arbitrarily report that such atomic operations were invalid and cause a griefing attack to burn the gas of users that attempted to perform an import. + +Therefore, a transaction specified predicate is required to implement the shared memory precompile to prevent such a griefing attack. + +In contrast, Avalanche Warp Messages are validated within the context of an exact P-Chain height. Therefore, if a block producer attempted to lie about the validity of such a message, the network would interpret that block as invalid. + +### Guarantees Offered by Warp Precompile vs. Built on Top + +#### Guarantees Offered by Warp Precompile + +The Warp Precompile was designed with the intention of minimizing the trusted computing base for the VM as much as possible. Therefore, it makes several tradeoffs which encourage users to use protocols built ON TOP of the Warp Precompile itself as opposed to directly using the Warp Precompile. + +The Warp Precompile itself provides ONLY the following ability: + +- Emit a verifiable message from (Address A, Blockchain A) to (Address B, Blockchain B) that can be verified by the destination chain + +#### Explicitly Not Provided / Built on Top + +The Warp Precompile itself does not provide any guarantees of: + +- Eventual message delivery (may require re-send on blockchain A and additional assumptions about off-chain relayers and chain progress) +- Ordering of messages (requires ordering provided a layer above) +- Replay protection (requires replay protection provided a layer above) diff --git a/precompile/contracts/warp/config.go b/precompile/contracts/warp/config.go new file mode 100644 index 0000000000..9024685218 --- /dev/null +++ b/precompile/contracts/warp/config.go @@ -0,0 +1,227 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/precompile/precompileconfig" + "github.com/ava-labs/coreth/predicate" + warpValidators "github.com/ava-labs/coreth/warp/validators" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/log" +) + +var ( + _ precompileconfig.Config = &Config{} + _ precompileconfig.Predicater = &Config{} + _ precompileconfig.Accepter = &Config{} +) + +var ( + errOverflowSignersGasCost = errors.New("overflow calculating warp signers gas cost") + errInvalidPredicateBytes = errors.New("cannot unpack predicate bytes") + errInvalidWarpMsg = errors.New("cannot unpack warp message") + errInvalidWarpMsgPayload = errors.New("cannot unpack warp message payload") + errInvalidAddressedPayload = errors.New("cannot unpack addressed payload") + errInvalidBlockHashPayload = errors.New("cannot unpack block hash payload") + errCannotGetNumSigners = errors.New("cannot fetch num signers from warp message") + errWarpCannotBeActivated = errors.New("warp cannot be activated before DUpgrade") +) + +// Config implements the precompileconfig.Config interface and +// adds specific configuration for Warp. +type Config struct { + precompileconfig.Upgrade + QuorumNumerator uint64 `json:"quorumNumerator"` +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// Warp with the given quorum numerator. +func NewConfig(blockTimestamp *uint64, quorumNumerator uint64) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + QuorumNumerator: quorumNumerator, + } +} + +// NewDefaultConfig returns a config for a network upgrade at [blockTimestamp] that enables +// Warp with the default quorum numerator (0 denotes using the default). +func NewDefaultConfig(blockTimestamp *uint64) *Config { + return NewConfig(blockTimestamp, 0) +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables Warp. +func NewDisableConfig(blockTimestamp *uint64) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +// Key returns the key for the Warp precompileconfig. +// This should be the same key as used in the precompile module. +func (*Config) Key() string { return ConfigKey } + +// Verify tries to verify Config and returns an error accordingly. +func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error { + if c.Timestamp() != nil { + // If Warp attempts to activate before the DUpgrade, fail verification + timestamp := *c.Timestamp() + if !chainConfig.IsDUpgrade(timestamp) { + return errWarpCannotBeActivated + } + } + + if c.QuorumNumerator > params.WarpQuorumDenominator { + return fmt.Errorf("cannot specify quorum numerator (%d) > quorum denominator (%d)", c.QuorumNumerator, params.WarpQuorumDenominator) + } + // If a non-default quorum numerator is specified and it is less than the minimum, return an error + if c.QuorumNumerator != 0 && c.QuorumNumerator < params.WarpQuorumNumeratorMinimum { + return fmt.Errorf("cannot specify quorum numerator (%d) < min quorum numerator (%d)", c.QuorumNumerator, params.WarpQuorumNumeratorMinimum) + } + return nil +} + +// Equal returns true if [s] is a [*Config] and it has been configured identical to [c]. +func (c *Config) Equal(s precompileconfig.Config) bool { + // typecast before comparison + other, ok := (s).(*Config) + if !ok { + return false + } + equals := c.Upgrade.Equal(&other.Upgrade) + return equals && c.QuorumNumerator == other.QuorumNumerator +} + +func (c *Config) Accept(acceptCtx *precompileconfig.AcceptContext, blockHash common.Hash, blockNumber uint64, txHash common.Hash, logIndex int, topics []common.Hash, logData []byte) error { + unsignedMessage, err := UnpackSendWarpEventDataToMessage(logData) + if err != nil { + return fmt.Errorf("failed to parse warp log data into unsigned message (TxHash: %s, LogIndex: %d): %w", txHash, logIndex, err) + } + log.Info( + "Accepted warp unsigned message", + "blockHash", blockHash, + "blockNumber", blockNumber, + "txHash", txHash, + "logIndex", logIndex, + "logData", common.Bytes2Hex(logData), + "warpMessageID", unsignedMessage.ID(), + ) + if err := acceptCtx.Warp.AddMessage(unsignedMessage); err != nil { + return fmt.Errorf("failed to add warp message during accept (TxHash: %s, LogIndex: %d): %w", txHash, logIndex, err) + } + return nil +} + +// verifyWarpMessage checks that [warpMsg] can be parsed as an addressed payload and verifies the Warp Message Signature +// within [predicateContext]. +func (c *Config) verifyWarpMessage(predicateContext *precompileconfig.PredicateContext, warpMsg *warp.Message) bool { + // Use default quorum numerator unless config specifies a non-default option + quorumNumerator := params.WarpDefaultQuorumNumerator + if c.QuorumNumerator != 0 { + quorumNumerator = c.QuorumNumerator + } + + log.Debug("verifying warp message", "warpMsg", warpMsg, "quorumNum", quorumNumerator, "quorumDenom", params.WarpQuorumDenominator) + if err := warpMsg.Signature.Verify( + context.Background(), + &warpMsg.UnsignedMessage, + predicateContext.SnowCtx.NetworkID, + warpValidators.NewState(predicateContext.SnowCtx), // Wrap validators.State on the chain snow context to special case the Primary Network + predicateContext.ProposerVMBlockCtx.PChainHeight, + quorumNumerator, + params.WarpQuorumDenominator, + ); err != nil { + log.Debug("failed to verify warp signature", "msgID", warpMsg.ID(), "err", err) + return false + } + + return true +} + +// PredicateGas returns the amount of gas necessary to verify the predicate +// PredicateGas charges for: +// 1. Base cost of the message +// 2. Size of the message +// 3. Number of signers +// 4. TODO: Lookup of the validator set +// +// If the payload of the warp message fails parsing, return a non-nil error invalidating the transaction. +func (c *Config) PredicateGas(predicateBytes []byte) (uint64, error) { + totalGas := GasCostPerSignatureVerification + bytesGasCost, overflow := math.SafeMul(GasCostPerWarpMessageBytes, uint64(len(predicateBytes))) + if overflow { + return 0, fmt.Errorf("overflow calculating gas cost for warp message bytes of size %d", len(predicateBytes)) + } + totalGas, overflow = math.SafeAdd(totalGas, bytesGasCost) + if overflow { + return 0, fmt.Errorf("overflow adding bytes gas cost of size %d", len(predicateBytes)) + } + + unpackedPredicateBytes, err := predicate.UnpackPredicate(predicateBytes) + if err != nil { + return 0, fmt.Errorf("%w: %s", errInvalidPredicateBytes, err) + } + warpMessage, err := warp.ParseMessage(unpackedPredicateBytes) + if err != nil { + return 0, fmt.Errorf("%w: %s", errInvalidWarpMsg, err) + } + _, err = payload.Parse(warpMessage.Payload) + if err != nil { + return 0, fmt.Errorf("%w: %s", errInvalidWarpMsgPayload, err) + } + + numSigners, err := warpMessage.Signature.NumSigners() + if err != nil { + return 0, fmt.Errorf("%w: %s", errCannotGetNumSigners, err) + } + signerGas, overflow := math.SafeMul(uint64(numSigners), GasCostPerWarpSigner) + if overflow { + return 0, errOverflowSignersGasCost + } + totalGas, overflow = math.SafeAdd(totalGas, signerGas) + if overflow { + return 0, fmt.Errorf("overflow adding signer gas (PrevTotal: %d, VerificationGas: %d)", totalGas, signerGas) + } + + return totalGas, nil +} + +func (c *Config) verifyPredicate(predicateContext *precompileconfig.PredicateContext, predicateBytes []byte) bool { + unpackedPredicateBytes, err := predicate.UnpackPredicate(predicateBytes) + if err != nil { + return false + } + + // Note: PredicateGas should be called before VerifyPredicate, so we should never reach an error case here. + warpMessage, err := warp.ParseMessage(unpackedPredicateBytes) + if err != nil { + return false + } + return c.verifyWarpMessage(predicateContext, warpMessage) +} + +// VerifyPredicate computes indices of predicates that failed verification as a bitset then returns the result +// as a byte slice. +func (c *Config) VerifyPredicate(predicateContext *precompileconfig.PredicateContext, predicates [][]byte) []byte { + resultBitSet := set.NewBits() + + for predicateIndex, predicateBytes := range predicates { + if !c.verifyPredicate(predicateContext, predicateBytes) { + resultBitSet.Add(predicateIndex) + } + } + return resultBitSet.Bytes() +} diff --git a/precompile/contracts/warp/config_test.go b/precompile/contracts/warp/config_test.go new file mode 100644 index 0000000000..5769aafeb3 --- /dev/null +++ b/precompile/contracts/warp/config_test.go @@ -0,0 +1,88 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "fmt" + "testing" + + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/precompile/precompileconfig" + "github.com/ava-labs/coreth/precompile/testutils" + "github.com/ava-labs/coreth/utils" + "go.uber.org/mock/gomock" +) + +func TestVerify(t *testing.T) { + tests := map[string]testutils.ConfigVerifyTest{ + "quorum numerator less than minimum": { + Config: NewConfig(utils.NewUint64(3), params.WarpQuorumNumeratorMinimum-1), + ExpectedError: fmt.Sprintf("cannot specify quorum numerator (%d) < min quorum numerator (%d)", params.WarpQuorumNumeratorMinimum-1, params.WarpQuorumNumeratorMinimum), + }, + "quorum numerator greater than quorum denominator": { + Config: NewConfig(utils.NewUint64(3), params.WarpQuorumDenominator+1), + ExpectedError: fmt.Sprintf("cannot specify quorum numerator (%d) > quorum denominator (%d)", params.WarpQuorumDenominator+1, params.WarpQuorumDenominator), + }, + "default quorum numerator": { + Config: NewDefaultConfig(utils.NewUint64(3)), + }, + "valid quorum numerator 1 less than denominator": { + Config: NewConfig(utils.NewUint64(3), params.WarpQuorumDenominator-1), + }, + "valid quorum numerator 1 more than minimum": { + Config: NewConfig(utils.NewUint64(3), params.WarpQuorumNumeratorMinimum+1), + }, + "invalid cannot activated before DUpgrade activation": { + Config: NewConfig(utils.NewUint64(3), 0), + ChainConfig: func() precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(gomock.NewController(t)) + config.EXPECT().IsDUpgrade(gomock.Any()).Return(false) + return config + }(), + ExpectedError: errWarpCannotBeActivated.Error(), + }, + } + testutils.RunVerifyTests(t, tests) +} + +func TestEqualWarpConfig(t *testing.T) { + tests := map[string]testutils.ConfigEqualTest{ + "non-nil config and nil other": { + Config: NewDefaultConfig(utils.NewUint64(3)), + Other: nil, + Expected: false, + }, + + "different type": { + Config: NewDefaultConfig(utils.NewUint64(3)), + Other: precompileconfig.NewMockConfig(gomock.NewController(t)), + Expected: false, + }, + + "different timestamp": { + Config: NewDefaultConfig(utils.NewUint64(3)), + Other: NewDefaultConfig(utils.NewUint64(4)), + Expected: false, + }, + + "different quorum numerator": { + Config: NewConfig(utils.NewUint64(3), params.WarpQuorumNumeratorMinimum+1), + Other: NewConfig(utils.NewUint64(3), params.WarpQuorumNumeratorMinimum+2), + Expected: false, + }, + + "same default config": { + Config: NewDefaultConfig(utils.NewUint64(3)), + Other: NewDefaultConfig(utils.NewUint64(3)), + Expected: true, + }, + + "same non-default config": { + Config: NewConfig(utils.NewUint64(3), params.WarpQuorumNumeratorMinimum+5), + Other: NewConfig(utils.NewUint64(3), params.WarpQuorumNumeratorMinimum+5), + Expected: true, + }, + } + testutils.RunEqualTests(t, tests) +} diff --git a/precompile/contracts/warp/contract.abi b/precompile/contracts/warp/contract.abi new file mode 100644 index 0000000000..771103ecbc --- /dev/null +++ b/precompile/contracts/warp/contract.abi @@ -0,0 +1,136 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "messageID", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "SendWarpMessage", + "type": "event" + }, + { + "inputs": [], + "name": "getBlockchainID", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockchainID", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "index", + "type": "uint32" + } + ], + "name": "getVerifiedWarpBlockHash", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "sourceChainID", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "internalType": "struct WarpBlockHash", + "name": "warpBlockHash", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "valid", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "index", + "type": "uint32" + } + ], + "name": "getVerifiedWarpMessage", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "sourceChainID", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "originSenderAddress", + "type": "address" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct WarpMessage", + "name": "message", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "valid", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "name": "sendWarpMessage", + "outputs": [ + { + "internalType": "bytes32", + "name": "messageID", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/precompile/contracts/warp/contract.go b/precompile/contracts/warp/contract.go new file mode 100644 index 0000000000..643d9652fb --- /dev/null +++ b/precompile/contracts/warp/contract.go @@ -0,0 +1,333 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/coreth/accounts/abi" + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/precompile/contract" + "github.com/ava-labs/coreth/vmerrs" + + _ "embed" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +const ( + GetVerifiedWarpMessageBaseCost uint64 = 2 // Base cost of entering getVerifiedWarpMessage + GetBlockchainIDGasCost uint64 = 2 // Based on GasQuickStep used in existing EVM instructions + AddWarpMessageGasCost uint64 = 20_000 // Cost of producing and serving a BLS Signature + // Sum of base log gas cost, cost of producing 4 topics, and producing + serving a BLS Signature (sign + trie write) + // Note: using trie write for the gas cost results in a conservative overestimate since the message is stored in a + // flat database that can be cleaned up after a period of time instead of the EVM trie. + + SendWarpMessageGasCost uint64 = params.LogGas + 4*params.LogTopicGas + AddWarpMessageGasCost + contract.WriteGasCostPerSlot + // SendWarpMessageGasCostPerByte cost accounts for producing a signed message of a given size + SendWarpMessageGasCostPerByte uint64 = params.LogDataGas + + GasCostPerWarpSigner uint64 = 500 + GasCostPerWarpMessageBytes uint64 = 100 + GasCostPerSignatureVerification uint64 = 200_000 +) + +var ( + errInvalidSendInput = errors.New("invalid sendWarpMessage input") + errInvalidIndexInput = errors.New("invalid index to specify warp message") +) + +// Singleton StatefulPrecompiledContract and signatures. +var ( + // WarpRawABI contains the raw ABI of Warp contract. + //go:embed contract.abi + WarpRawABI string + + WarpABI = contract.ParseABI(WarpRawABI) + + WarpPrecompile = createWarpPrecompile() +) + +// WarpBlockHash is an auto generated low-level Go binding around an user-defined struct. +type WarpBlockHash struct { + SourceChainID common.Hash + BlockHash common.Hash +} + +type GetVerifiedWarpBlockHashOutput struct { + WarpBlockHash WarpBlockHash + Valid bool +} + +// WarpMessage is an auto generated low-level Go binding around an user-defined struct. +type WarpMessage struct { + SourceChainID common.Hash + OriginSenderAddress common.Address + Payload []byte +} + +type GetVerifiedWarpMessageOutput struct { + Message WarpMessage + Valid bool +} + +type SendWarpMessageEventData struct { + Message []byte +} + +// PackGetBlockchainID packs the include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackGetBlockchainID() ([]byte, error) { + return WarpABI.Pack("getBlockchainID") +} + +// PackGetBlockchainIDOutput attempts to pack given blockchainID of type common.Hash +// to conform the ABI outputs. +func PackGetBlockchainIDOutput(blockchainID common.Hash) ([]byte, error) { + return WarpABI.PackOutput("getBlockchainID", blockchainID) +} + +// getBlockchainID returns the snow Chain Context ChainID of this blockchain. +func getBlockchainID(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetBlockchainIDGasCost); err != nil { + return nil, 0, err + } + packedOutput, err := PackGetBlockchainIDOutput(common.Hash(accessibleState.GetSnowContext().ChainID)) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackGetVerifiedWarpBlockHashInput attempts to unpack [input] into the uint32 type argument +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackGetVerifiedWarpBlockHashInput(input []byte) (uint32, error) { + res, err := WarpABI.UnpackInput("getVerifiedWarpBlockHash", input) + if err != nil { + return 0, err + } + unpacked := *abi.ConvertType(res[0], new(uint32)).(*uint32) + return unpacked, nil +} + +// PackGetVerifiedWarpBlockHash packs [index] of type uint32 into the appropriate arguments for getVerifiedWarpBlockHash. +// the packed bytes include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackGetVerifiedWarpBlockHash(index uint32) ([]byte, error) { + return WarpABI.Pack("getVerifiedWarpBlockHash", index) +} + +// PackGetVerifiedWarpBlockHashOutput attempts to pack given [outputStruct] of type GetVerifiedWarpBlockHashOutput +// to conform the ABI outputs. +func PackGetVerifiedWarpBlockHashOutput(outputStruct GetVerifiedWarpBlockHashOutput) ([]byte, error) { + return WarpABI.PackOutput("getVerifiedWarpBlockHash", + outputStruct.WarpBlockHash, + outputStruct.Valid, + ) +} + +// UnpackGetVerifiedWarpBlockHashOutput attempts to unpack [output] as GetVerifiedWarpBlockHashOutput +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func UnpackGetVerifiedWarpBlockHashOutput(output []byte) (GetVerifiedWarpBlockHashOutput, error) { + outputStruct := GetVerifiedWarpBlockHashOutput{} + err := WarpABI.UnpackIntoInterface(&outputStruct, "getVerifiedWarpBlockHash", output) + + return outputStruct, err +} + +func getVerifiedWarpBlockHash(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + return handleWarpMessage(accessibleState, input, suppliedGas, blockHashHandler{}) +} + +// UnpackGetVerifiedWarpMessageInput attempts to unpack [input] into the uint32 type argument +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackGetVerifiedWarpMessageInput(input []byte) (uint32, error) { + res, err := WarpABI.UnpackInput("getVerifiedWarpMessage", input) + if err != nil { + return 0, err + } + unpacked := *abi.ConvertType(res[0], new(uint32)).(*uint32) + return unpacked, nil +} + +// PackGetVerifiedWarpMessage packs [index] of type uint32 into the appropriate arguments for getVerifiedWarpMessage. +// the packed bytes include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackGetVerifiedWarpMessage(index uint32) ([]byte, error) { + return WarpABI.Pack("getVerifiedWarpMessage", index) +} + +// PackGetVerifiedWarpMessageOutput attempts to pack given [outputStruct] of type GetVerifiedWarpMessageOutput +// to conform the ABI outputs. +func PackGetVerifiedWarpMessageOutput(outputStruct GetVerifiedWarpMessageOutput) ([]byte, error) { + return WarpABI.PackOutput("getVerifiedWarpMessage", + outputStruct.Message, + outputStruct.Valid, + ) +} + +// UnpackGetVerifiedWarpMessageOutput attempts to unpack [output] as GetVerifiedWarpMessageOutput +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func UnpackGetVerifiedWarpMessageOutput(output []byte) (GetVerifiedWarpMessageOutput, error) { + outputStruct := GetVerifiedWarpMessageOutput{} + err := WarpABI.UnpackIntoInterface(&outputStruct, "getVerifiedWarpMessage", output) + + return outputStruct, err +} + +// getVerifiedWarpMessage retrieves the pre-verified warp message from the predicate storage slots and returns +// the expected ABI encoding of the message to the caller. +func getVerifiedWarpMessage(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + return handleWarpMessage(accessibleState, input, suppliedGas, addressedPayloadHandler{}) +} + +// UnpackSendWarpMessageInput attempts to unpack [input] as []byte +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackSendWarpMessageInput(input []byte) ([]byte, error) { + res, err := WarpABI.UnpackInput("sendWarpMessage", input) + if err != nil { + return []byte{}, err + } + unpacked := *abi.ConvertType(res[0], new([]byte)).(*[]byte) + return unpacked, nil +} + +// PackSendWarpMessage packs [inputStruct] of type []byte into the appropriate arguments for sendWarpMessage. +func PackSendWarpMessage(payloadData []byte) ([]byte, error) { + return WarpABI.Pack("sendWarpMessage", payloadData) +} + +// PackSendWarpMessageOutput attempts to pack given messageID of type common.Hash +// to conform the ABI outputs. +func PackSendWarpMessageOutput(messageID common.Hash) ([]byte, error) { + return WarpABI.PackOutput("sendWarpMessage", messageID) +} + +// UnpackSendWarpMessageOutput attempts to unpack given [output] into the common.Hash type output +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func UnpackSendWarpMessageOutput(output []byte) (common.Hash, error) { + res, err := WarpABI.Unpack("sendWarpMessage", output) + if err != nil { + return common.Hash{}, err + } + unpacked := *abi.ConvertType(res[0], new(common.Hash)).(*common.Hash) + return unpacked, nil +} + +// sendWarpMessage constructs an Avalanche Warp Message containing an AddressedPayload and emits a log to signal validators that they should +// be willing to sign this message. +func sendWarpMessage(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SendWarpMessageGasCost); err != nil { + return nil, 0, err + } + // This gas cost includes buffer room because it is based off of the total size of the input instead of the produced payload. + // This ensures that we charge gas before we unpack the variable sized input. + payloadGas, overflow := math.SafeMul(SendWarpMessageGasCostPerByte, uint64(len(input))) + if overflow { + return nil, 0, vmerrs.ErrOutOfGas + } + if remainingGas, err = contract.DeductGas(remainingGas, payloadGas); err != nil { + return nil, 0, err + } + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + // unpack the arguments + payloadData, err := UnpackSendWarpMessageInput(input) + if err != nil { + return nil, remainingGas, fmt.Errorf("%w: %s", errInvalidSendInput, err) + } + + var ( + sourceChainID = accessibleState.GetSnowContext().ChainID + sourceAddress = caller + ) + + addressedPayload, err := payload.NewAddressedCall( + sourceAddress.Bytes(), + payloadData, + ) + if err != nil { + return nil, remainingGas, err + } + unsignedWarpMessage, err := warp.NewUnsignedMessage( + accessibleState.GetSnowContext().NetworkID, + sourceChainID, + addressedPayload.Bytes(), + ) + if err != nil { + return nil, remainingGas, err + } + + // Add a log to be handled if this action is finalized. + topics, data, err := PackSendWarpMessageEvent( + sourceAddress, + common.Hash(unsignedWarpMessage.ID()), + unsignedWarpMessage.Bytes(), + ) + if err != nil { + return nil, remainingGas, err + } + accessibleState.GetStateDB().AddLog( + ContractAddress, + topics, + data, + accessibleState.GetBlockContext().Number().Uint64(), + ) + + packed, err := PackSendWarpMessageOutput(common.Hash(unsignedWarpMessage.ID())) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed message ID and the remaining gas + return packed, remainingGas, nil +} + +// PackSendWarpMessageEvent packs the given arguments into SendWarpMessage events including topics and data. +func PackSendWarpMessageEvent(sourceAddress common.Address, unsignedMessageID common.Hash, unsignedMessageBytes []byte) ([]common.Hash, []byte, error) { + return WarpABI.PackEvent("SendWarpMessage", sourceAddress, unsignedMessageID, unsignedMessageBytes) +} + +// UnpackSendWarpEventDataToMessage attempts to unpack event [data] as warp.UnsignedMessage. +func UnpackSendWarpEventDataToMessage(data []byte) (*warp.UnsignedMessage, error) { + event := SendWarpMessageEventData{} + err := WarpABI.UnpackIntoInterface(&event, "SendWarpMessage", data) + if err != nil { + return nil, err + } + return warp.ParseUnsignedMessage(event.Message) +} + +// createWarpPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. +func createWarpPrecompile() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction + + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "getBlockchainID": getBlockchainID, + "getVerifiedWarpBlockHash": getVerifiedWarpBlockHash, + "getVerifiedWarpMessage": getVerifiedWarpMessage, + "sendWarpMessage": sendWarpMessage, + } + + for name, function := range abiFunctionMap { + method, ok := WarpABI.Methods[name] + if !ok { + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) + } + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) + } + // Construct the contract with no fallback function. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract +} diff --git a/precompile/contracts/warp/contract_test.go b/precompile/contracts/warp/contract_test.go new file mode 100644 index 0000000000..4b20036157 --- /dev/null +++ b/precompile/contracts/warp/contract_test.go @@ -0,0 +1,761 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "math" + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/coreth/core/state" + "github.com/ava-labs/coreth/precompile/contract" + "github.com/ava-labs/coreth/precompile/testutils" + "github.com/ava-labs/coreth/predicate" + "github.com/ava-labs/coreth/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestGetBlockchainID(t *testing.T) { + callerAddr := common.HexToAddress("0x0123") + + defaultSnowCtx := snow.DefaultContextTest() + blockchainID := defaultSnowCtx.ChainID + + tests := map[string]testutils.PrecompileTest{ + "getBlockchainID success": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetBlockchainID() + require.NoError(t, err) + + return input + }, + SuppliedGas: GetBlockchainIDGasCost, + ReadOnly: false, + ExpectedRes: func() []byte { + expectedOutput, err := PackGetBlockchainIDOutput(common.Hash(blockchainID)) + require.NoError(t, err) + + return expectedOutput + }(), + }, + "getBlockchainID readOnly": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetBlockchainID() + require.NoError(t, err) + + return input + }, + SuppliedGas: GetBlockchainIDGasCost, + ReadOnly: true, + ExpectedRes: func() []byte { + expectedOutput, err := PackGetBlockchainIDOutput(common.Hash(blockchainID)) + require.NoError(t, err) + + return expectedOutput + }(), + }, + "getBlockchainID insufficient gas": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetBlockchainID() + require.NoError(t, err) + + return input + }, + SuppliedGas: GetBlockchainIDGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } + + testutils.RunPrecompileTests(t, Module, state.NewTestStateDB, tests) +} + +func TestSendWarpMessage(t *testing.T) { + callerAddr := common.HexToAddress("0x0123") + + defaultSnowCtx := snow.DefaultContextTest() + blockchainID := defaultSnowCtx.ChainID + sendWarpMessagePayload := utils.RandomBytes(100) + + sendWarpMessageInput, err := PackSendWarpMessage(sendWarpMessagePayload) + require.NoError(t, err) + sendWarpMessageAddressedPayload, err := payload.NewAddressedCall( + callerAddr.Bytes(), + sendWarpMessagePayload, + ) + require.NoError(t, err) + unsignedWarpMessage, err := warp.NewUnsignedMessage( + defaultSnowCtx.NetworkID, + blockchainID, + sendWarpMessageAddressedPayload.Bytes(), + ) + require.NoError(t, err) + + tests := map[string]testutils.PrecompileTest{ + "send warp message readOnly": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return sendWarpMessageInput }, + SuppliedGas: SendWarpMessageGasCost + uint64(len(sendWarpMessageInput[4:])*int(SendWarpMessageGasCostPerByte)), + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "send warp message insufficient gas for first step": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return sendWarpMessageInput }, + SuppliedGas: SendWarpMessageGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "send warp message insufficient gas for payload bytes": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return sendWarpMessageInput }, + SuppliedGas: SendWarpMessageGasCost + uint64(len(sendWarpMessageInput[4:])*int(SendWarpMessageGasCostPerByte)) - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "send warp message invalid input": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + return sendWarpMessageInput[:4] // Include only the function selector, so that the input is invalid + }, + SuppliedGas: SendWarpMessageGasCost, + ReadOnly: false, + ExpectedErr: errInvalidSendInput.Error(), + }, + "send warp message success": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return sendWarpMessageInput }, + SuppliedGas: SendWarpMessageGasCost + uint64(len(sendWarpMessageInput[4:])*int(SendWarpMessageGasCostPerByte)), + ReadOnly: false, + ExpectedRes: func() []byte { + bytes, err := PackSendWarpMessageOutput(common.Hash(unsignedWarpMessage.ID())) + if err != nil { + panic(err) + } + return bytes + }(), + AfterHook: func(t testing.TB, state contract.StateDB) { + logsData := state.GetLogData() + require.Len(t, logsData, 1) + logData := logsData[0] + + unsignedWarpMsg, err := UnpackSendWarpEventDataToMessage(logData) + require.NoError(t, err) + addressedPayload, err := payload.ParseAddressedCall(unsignedWarpMsg.Payload) + require.NoError(t, err) + + require.Equal(t, common.BytesToAddress(addressedPayload.SourceAddress), callerAddr) + require.Equal(t, unsignedWarpMsg.SourceChainID, blockchainID) + require.Equal(t, addressedPayload.Payload, sendWarpMessagePayload) + }, + }, + } + + testutils.RunPrecompileTests(t, Module, state.NewTestStateDB, tests) +} + +func TestGetVerifiedWarpMessage(t *testing.T) { + networkID := uint32(54321) + callerAddr := common.HexToAddress("0x0123") + sourceAddress := common.HexToAddress("0x456789") + sourceChainID := ids.GenerateTestID() + packagedPayloadBytes := []byte("mcsorley") + addressedPayload, err := payload.NewAddressedCall( + sourceAddress.Bytes(), + packagedPayloadBytes, + ) + require.NoError(t, err) + unsignedWarpMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, addressedPayload.Bytes()) + require.NoError(t, err) + warpMessage, err := avalancheWarp.NewMessage(unsignedWarpMsg, &avalancheWarp.BitSetSignature{}) // Create message with empty signature for testing + require.NoError(t, err) + warpMessagePredicateBytes := predicate.PackPredicate(warpMessage.Bytes()) + getVerifiedWarpMsg, err := PackGetVerifiedWarpMessage(0) + require.NoError(t, err) + noFailures := set.NewBits().Bytes() + require.Len(t, noFailures, 0) + + tests := map[string]testutils.PrecompileTest{ + "get message success": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)), + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{ + Message: WarpMessage{ + SourceChainID: common.Hash(sourceChainID), + OriginSenderAddress: sourceAddress, + Payload: packagedPayloadBytes, + }, + Valid: true, + }) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message out of bounds non-zero index": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetVerifiedWarpMessage(1) + require.NoError(t, err) + return input + }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message success non-zero index": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetVerifiedWarpMessage(1) + require.NoError(t, err) + return input + }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{{}, warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(set.NewBits(0).Bytes()) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)), + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{ + Message: WarpMessage{ + SourceChainID: common.Hash(sourceChainID), + OriginSenderAddress: sourceAddress, + Payload: packagedPayloadBytes, + }, + Valid: true, + }) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message failure non-zero index": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetVerifiedWarpMessage(1) + require.NoError(t, err) + return input + }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{{}, warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(set.NewBits(0, 1).Bytes()) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get non-existent message": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message success readOnly": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)), + ReadOnly: true, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{ + Message: WarpMessage{ + SourceChainID: common.Hash(sourceChainID), + OriginSenderAddress: sourceAddress, + Payload: packagedPayloadBytes, + }, + Valid: true, + }) + if err != nil { + panic(err) + } + return res + }(), + }, + "get non-existent message readOnly": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: true, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message out of gas for base cost": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "get message out of gas": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)) - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "get message invalid predicate packing": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessage.Bytes()}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessage.Bytes())), + ReadOnly: false, + ExpectedErr: errInvalidPredicateBytes.Error(), + }, + "get message invalid warp message": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{predicate.PackPredicate([]byte{1, 2, 3})}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(32), + ReadOnly: false, + ExpectedErr: errInvalidWarpMsg.Error(), + }, + "get message invalid addressed payload": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpMsg }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, []byte{1, 2, 3}) // Invalid addressed payload + require.NoError(t, err) + warpMessage, err := avalancheWarp.NewMessage(unsignedMessage, &avalancheWarp.BitSetSignature{}) + require.NoError(t, err) + + state.SetPredicateStorageSlots(ContractAddress, [][]byte{predicate.PackPredicate(warpMessage.Bytes())}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(160), + ReadOnly: false, + ExpectedErr: errInvalidAddressedPayload.Error(), + }, + "get message index invalid uint32": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + return append(WarpABI.Methods["getVerifiedWarpMessage"].ID, new(big.Int).SetInt64(math.MaxInt64).Bytes()...) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedErr: errInvalidIndexInput.Error(), + }, + "get message index invalid int32": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + res, err := PackGetVerifiedWarpMessage(math.MaxInt32 + 1) + require.NoError(t, err) + return res + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedErr: errInvalidIndexInput.Error(), + }, + "get message invalid index input bytes": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + res, err := PackGetVerifiedWarpMessage(1) + require.NoError(t, err) + return res[:len(res)-2] + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedErr: errInvalidIndexInput.Error(), + }, + } + + testutils.RunPrecompileTests(t, Module, state.NewTestStateDB, tests) +} + +func TestGetVerifiedWarpBlockHash(t *testing.T) { + networkID := uint32(54321) + callerAddr := common.HexToAddress("0x0123") + sourceChainID := ids.GenerateTestID() + blockHash := ids.GenerateTestID() + blockHashPayload, err := payload.NewHash(blockHash) + require.NoError(t, err) + unsignedWarpMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, blockHashPayload.Bytes()) + require.NoError(t, err) + warpMessage, err := avalancheWarp.NewMessage(unsignedWarpMsg, &avalancheWarp.BitSetSignature{}) // Create message with empty signature for testing + require.NoError(t, err) + warpMessagePredicateBytes := predicate.PackPredicate(warpMessage.Bytes()) + getVerifiedWarpBlockHash, err := PackGetVerifiedWarpBlockHash(0) + require.NoError(t, err) + noFailures := set.NewBits().Bytes() + require.Len(t, noFailures, 0) + + tests := map[string]testutils.PrecompileTest{ + "get message success": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)), + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{ + WarpBlockHash: WarpBlockHash{ + SourceChainID: common.Hash(sourceChainID), + BlockHash: common.Hash(blockHash), + }, + Valid: true, + }) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message out of bounds non-zero index": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetVerifiedWarpBlockHash(1) + require.NoError(t, err) + return input + }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message success non-zero index": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetVerifiedWarpBlockHash(1) + require.NoError(t, err) + return input + }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{{}, warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(set.NewBits(0).Bytes()) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)), + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{ + WarpBlockHash: WarpBlockHash{ + SourceChainID: common.Hash(sourceChainID), + BlockHash: common.Hash(blockHash), + }, + Valid: true, + }) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message failure non-zero index": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + input, err := PackGetVerifiedWarpBlockHash(1) + require.NoError(t, err) + return input + }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{{}, warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(set.NewBits(0, 1).Bytes()) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get non-existent message": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message success readOnly": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)), + ReadOnly: true, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{ + WarpBlockHash: WarpBlockHash{ + SourceChainID: common.Hash(sourceChainID), + BlockHash: common.Hash(blockHash), + }, + Valid: true, + }) + if err != nil { + panic(err) + } + return res + }(), + }, + "get non-existent message readOnly": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: true, + ExpectedRes: func() []byte { + res, err := PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{Valid: false}) + if err != nil { + panic(err) + } + return res + }(), + }, + "get message out of gas for base cost": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "get message out of gas": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessagePredicateBytes}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessagePredicateBytes)) - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "get message invalid predicate packing": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{warpMessage.Bytes()}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(len(warpMessage.Bytes())), + ReadOnly: false, + ExpectedErr: errInvalidPredicateBytes.Error(), + }, + "get message invalid warp message": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + state.SetPredicateStorageSlots(ContractAddress, [][]byte{predicate.PackPredicate([]byte{1, 2, 3})}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(32), + ReadOnly: false, + ExpectedErr: errInvalidWarpMsg.Error(), + }, + "get message invalid block hash payload": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { return getVerifiedWarpBlockHash }, + BeforeHook: func(t testing.TB, state contract.StateDB) { + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, []byte{1, 2, 3}) // Invalid block hash payload + require.NoError(t, err) + warpMessage, err := avalancheWarp.NewMessage(unsignedMessage, &avalancheWarp.BitSetSignature{}) + require.NoError(t, err) + + state.SetPredicateStorageSlots(ContractAddress, [][]byte{predicate.PackPredicate(warpMessage.Bytes())}) + }, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().GetPredicateResults(common.Hash{}, ContractAddress).Return(noFailures) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost + GasCostPerWarpMessageBytes*uint64(160), + ReadOnly: false, + ExpectedErr: errInvalidBlockHashPayload.Error(), + }, + "get message index invalid uint32": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + return append(WarpABI.Methods["getVerifiedWarpBlockHash"].ID, new(big.Int).SetInt64(math.MaxInt64).Bytes()...) + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedErr: errInvalidIndexInput.Error(), + }, + "get message index invalid int32": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + res, err := PackGetVerifiedWarpBlockHash(math.MaxInt32 + 1) + require.NoError(t, err) + return res + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedErr: errInvalidIndexInput.Error(), + }, + "get message invalid index input bytes": { + Caller: callerAddr, + InputFn: func(t testing.TB) []byte { + res, err := PackGetVerifiedWarpBlockHash(1) + require.NoError(t, err) + return res[:len(res)-2] + }, + SuppliedGas: GetVerifiedWarpMessageBaseCost, + ReadOnly: false, + ExpectedErr: errInvalidIndexInput.Error(), + }, + } + + testutils.RunPrecompileTests(t, Module, state.NewTestStateDB, tests) +} + +func TestPackEvents(t *testing.T) { + sourceChainID := ids.GenerateTestID() + sourceAddress := common.HexToAddress("0x0123") + payloadData := []byte("mcsorley") + networkID := uint32(54321) + + addressedPayload, err := payload.NewAddressedCall( + sourceAddress.Bytes(), + payloadData, + ) + require.NoError(t, err) + + unsignedWarpMessage, err := warp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayload.Bytes(), + ) + require.NoError(t, err) + + _, data, err := PackSendWarpMessageEvent( + sourceAddress, + common.Hash(unsignedMsg.ID()), + unsignedWarpMessage.Bytes(), + ) + require.NoError(t, err) + + unpacked, err := UnpackSendWarpEventDataToMessage(data) + require.NoError(t, err) + require.Equal(t, unsignedWarpMessage.Bytes(), unpacked.Bytes()) +} diff --git a/precompile/contracts/warp/contract_warp_handler.go b/precompile/contracts/warp/contract_warp_handler.go new file mode 100644 index 0000000000..71142ed084 --- /dev/null +++ b/precompile/contracts/warp/contract_warp_handler.go @@ -0,0 +1,135 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/coreth/precompile/contract" + "github.com/ava-labs/coreth/predicate" + "github.com/ava-labs/coreth/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +var ( + _ messageHandler = addressedPayloadHandler{} + _ messageHandler = blockHashHandler{} +) + +var ( + getVerifiedWarpMessageInvalidOutput []byte + getVerifiedWarpBlockHashInvalidOutput []byte +) + +func init() { + res, err := PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{Valid: false}) + if err != nil { + panic(err) + } + getVerifiedWarpMessageInvalidOutput = res + + res, err = PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{Valid: false}) + if err != nil { + panic(err) + } + getVerifiedWarpBlockHashInvalidOutput = res +} + +type messageHandler interface { + packFailed() []byte + handleMessage(msg *warp.Message) ([]byte, error) +} + +func handleWarpMessage(accessibleState contract.AccessibleState, input []byte, suppliedGas uint64, handler messageHandler) ([]byte, uint64, error) { + remainingGas, err := contract.DeductGas(suppliedGas, GetVerifiedWarpMessageBaseCost) + if err != nil { + return nil, remainingGas, err + } + + warpIndexInput, err := UnpackGetVerifiedWarpMessageInput(input) + if err != nil { + return nil, remainingGas, fmt.Errorf("%w: %s", errInvalidIndexInput, err) + } + if warpIndexInput > math.MaxInt32 { + return nil, remainingGas, fmt.Errorf("%w: larger than MaxInt32", errInvalidIndexInput) + } + warpIndex := int(warpIndexInput) // This conversion is safe even if int is 32 bits because we checked above. + state := accessibleState.GetStateDB() + predicateBytes, exists := state.GetPredicateStorageSlots(ContractAddress, warpIndex) + predicateResults := accessibleState.GetBlockContext().GetPredicateResults(state.GetTxHash(), ContractAddress) + valid := exists && !set.BitsFromBytes(predicateResults).Contains(warpIndex) + if !valid { + return handler.packFailed(), remainingGas, nil + } + + // Note: we charge for the size of the message during both predicate verification and each time the message is read during + // EVM execution because each execution incurs an additional read cost. + msgBytesGas, overflow := math.SafeMul(GasCostPerWarpMessageBytes, uint64(len(predicateBytes))) + if overflow { + return nil, 0, vmerrs.ErrOutOfGas + } + if remainingGas, err = contract.DeductGas(remainingGas, msgBytesGas); err != nil { + return nil, 0, err + } + // Note: since the predicate is verified in advance of execution, the precompile should not + // hit an error during execution. + unpackedPredicateBytes, err := predicate.UnpackPredicate(predicateBytes) + if err != nil { + return nil, remainingGas, fmt.Errorf("%w: %s", errInvalidPredicateBytes, err) + } + warpMessage, err := warp.ParseMessage(unpackedPredicateBytes) + if err != nil { + return nil, remainingGas, fmt.Errorf("%w: %s", errInvalidWarpMsg, err) + } + res, err := handler.handleMessage(warpMessage) + if err != nil { + return nil, remainingGas, err + } + return res, remainingGas, nil +} + +type addressedPayloadHandler struct{} + +func (addressedPayloadHandler) packFailed() []byte { + return getVerifiedWarpMessageInvalidOutput +} + +func (addressedPayloadHandler) handleMessage(warpMessage *warp.Message) ([]byte, error) { + addressedPayload, err := payload.ParseAddressedCall(warpMessage.UnsignedMessage.Payload) + if err != nil { + return nil, fmt.Errorf("%w: %s", errInvalidAddressedPayload, err) + } + return PackGetVerifiedWarpMessageOutput(GetVerifiedWarpMessageOutput{ + Message: WarpMessage{ + SourceChainID: common.Hash(warpMessage.SourceChainID), + OriginSenderAddress: common.BytesToAddress(addressedPayload.SourceAddress), + Payload: addressedPayload.Payload, + }, + Valid: true, + }) +} + +type blockHashHandler struct{} + +func (blockHashHandler) packFailed() []byte { + return getVerifiedWarpBlockHashInvalidOutput +} + +func (blockHashHandler) handleMessage(warpMessage *warp.Message) ([]byte, error) { + blockHashPayload, err := payload.ParseHash(warpMessage.UnsignedMessage.Payload) + if err != nil { + return nil, fmt.Errorf("%w: %s", errInvalidBlockHashPayload, err) + } + return PackGetVerifiedWarpBlockHashOutput(GetVerifiedWarpBlockHashOutput{ + WarpBlockHash: WarpBlockHash{ + SourceChainID: common.Hash(warpMessage.SourceChainID), + BlockHash: common.BytesToHash(blockHashPayload.Hash[:]), + }, + Valid: true, + }) +} diff --git a/precompile/contracts/warp/module.go b/precompile/contracts/warp/module.go new file mode 100644 index 0000000000..9b688f7017 --- /dev/null +++ b/precompile/contracts/warp/module.go @@ -0,0 +1,55 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "fmt" + + "github.com/ava-labs/coreth/precompile/contract" + "github.com/ava-labs/coreth/precompile/modules" + "github.com/ava-labs/coreth/precompile/precompileconfig" + + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile precompileconfig. +// must be unique across all precompiles. +const ConfigKey = "warpConfig" + +// ContractAddress is the address of the warp precompile contract +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000005") + +// Module is the precompile module. It is used to register the precompile contract. +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: WarpPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + // Register the precompile module. + // Each precompile contract registers itself through [RegisterModule] function. + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +// MakeConfig returns a new precompile config instance. +// This is required for Marshal/Unmarshal the precompile config. +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure is a no-op for warp since it does not need to store any information in the state +func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.ConfigurationBlockContext) error { + if _, ok := cfg.(*Config); !ok { + return fmt.Errorf("expected config type %T, got %T: %v", &Config{}, cfg, cfg) + } + return nil +} diff --git a/precompile/contracts/warp/predicate_test.go b/precompile/contracts/warp/predicate_test.go new file mode 100644 index 0000000000..7ed6c2c167 --- /dev/null +++ b/precompile/contracts/warp/predicate_test.go @@ -0,0 +1,689 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/set" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/precompile/precompileconfig" + "github.com/ava-labs/coreth/precompile/testutils" + "github.com/ava-labs/coreth/predicate" + corethUtils "github.com/ava-labs/coreth/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +const pChainHeight uint64 = 1337 + +var ( + _ utils.Sortable[*testValidator] = (*testValidator)(nil) + + errTest = errors.New("non-nil error") + networkID = uint32(54321) + sourceChainID = ids.GenerateTestID() + sourceSubnetID = ids.GenerateTestID() + + // valid unsigned warp message used throughout testing + unsignedMsg *avalancheWarp.UnsignedMessage + // valid addressed payload + addressedPayload *payload.AddressedCall + addressedPayloadBytes []byte + // blsSignatures of [unsignedMsg] from each of [testVdrs] + blsSignatures []*bls.Signature + + numTestVdrs = 10_000 + testVdrs []*testValidator + vdrs map[ids.NodeID]*validators.GetValidatorOutput + tests []signatureTest + + predicateTests = make(map[string]testutils.PredicateTest) +) + +func init() { + testVdrs = make([]*testValidator, 0, numTestVdrs) + for i := 0; i < numTestVdrs; i++ { + testVdrs = append(testVdrs, newTestValidator()) + } + utils.Sort(testVdrs) + + vdrs = map[ids.NodeID]*validators.GetValidatorOutput{ + testVdrs[0].nodeID: { + NodeID: testVdrs[0].nodeID, + PublicKey: testVdrs[0].vdr.PublicKey, + Weight: testVdrs[0].vdr.Weight, + }, + testVdrs[1].nodeID: { + NodeID: testVdrs[1].nodeID, + PublicKey: testVdrs[1].vdr.PublicKey, + Weight: testVdrs[1].vdr.Weight, + }, + testVdrs[2].nodeID: { + NodeID: testVdrs[2].nodeID, + PublicKey: testVdrs[2].vdr.PublicKey, + Weight: testVdrs[2].vdr.Weight, + }, + } + + var err error + addr := ids.GenerateTestShortID() + addressedPayload, err = payload.NewAddressedCall( + addr[:], + []byte{1, 2, 3}, + ) + if err != nil { + panic(err) + } + addressedPayloadBytes = addressedPayload.Bytes() + unsignedMsg, err = avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, addressedPayload.Bytes()) + if err != nil { + panic(err) + } + + for _, testVdr := range testVdrs { + blsSignature := bls.Sign(testVdr.sk, unsignedMsg.Bytes()) + blsSignatures = append(blsSignatures, blsSignature) + } + + initWarpPredicateTests() +} + +type testValidator struct { + nodeID ids.NodeID + sk *bls.SecretKey + vdr *avalancheWarp.Validator +} + +func (v *testValidator) Less(o *testValidator) bool { + return v.vdr.Less(o.vdr) +} + +func newTestValidator() *testValidator { + sk, err := bls.NewSecretKey() + if err != nil { + panic(err) + } + + nodeID := ids.GenerateTestNodeID() + pk := bls.PublicFromSecretKey(sk) + return &testValidator{ + nodeID: nodeID, + sk: sk, + vdr: &avalancheWarp.Validator{ + PublicKey: pk, + PublicKeyBytes: pk.Serialize(), + Weight: 3, + NodeIDs: []ids.NodeID{nodeID}, + }, + } +} + +type signatureTest struct { + name string + stateF func(*gomock.Controller) validators.State + quorumNum uint64 + quorumDen uint64 + msgF func(*require.Assertions) *avalancheWarp.Message + err error +} + +// createWarpMessage constructs a signed warp message using the global variable [unsignedMsg] +// and the first [numKeys] signatures from [blsSignatures] +func createWarpMessage(numKeys int) *avalancheWarp.Message { + aggregateSignature, err := bls.AggregateSignatures(blsSignatures[0:numKeys]) + if err != nil { + panic(err) + } + bitSet := set.NewBits() + for i := 0; i < numKeys; i++ { + bitSet.Add(i) + } + warpSignature := &avalancheWarp.BitSetSignature{ + Signers: bitSet.Bytes(), + } + copy(warpSignature.Signature[:], bls.SignatureToBytes(aggregateSignature)) + warpMsg, err := avalancheWarp.NewMessage(unsignedMsg, warpSignature) + if err != nil { + panic(err) + } + return warpMsg +} + +// createPredicate constructs a warp message using createWarpMessage with numKeys signers +// and packs it into predicate encoding. +func createPredicate(numKeys int) []byte { + warpMsg := createWarpMessage(numKeys) + predicateBytes := predicate.PackPredicate(warpMsg.Bytes()) + return predicateBytes +} + +// validatorRange specifies a range of validators to include from [start, end), a staking weight +// to specify for each validator in that range, and whether or not to include the public key. +type validatorRange struct { + start int + end int + weight uint64 + publicKey bool +} + +// createSnowCtx creates a snow.Context instance with a validator state specified by the given validatorRanges +func createSnowCtx(validatorRanges []validatorRange) *snow.Context { + getValidatorsOutput := make(map[ids.NodeID]*validators.GetValidatorOutput) + + for _, validatorRange := range validatorRanges { + for i := validatorRange.start; i < validatorRange.end; i++ { + validatorOutput := &validators.GetValidatorOutput{ + NodeID: testVdrs[i].nodeID, + Weight: validatorRange.weight, + } + if validatorRange.publicKey { + validatorOutput.PublicKey = testVdrs[i].vdr.PublicKey + } + getValidatorsOutput[testVdrs[i].nodeID] = validatorOutput + } + } + + snowCtx := snow.DefaultContextTest() + state := &validators.TestState{ + GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) { + return sourceSubnetID, nil + }, + GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + return getValidatorsOutput, nil + }, + } + snowCtx.ValidatorState = state + snowCtx.NetworkID = networkID + return snowCtx +} + +func createValidPredicateTest(snowCtx *snow.Context, numKeys uint64, predicateBytes []byte) testutils.PredicateTest { + return testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + numKeys*GasCostPerWarpSigner, + GasErr: nil, + PredicateRes: set.NewBits().Bytes(), + } +} + +func TestWarpMessageFromPrimaryNetwork(t *testing.T) { + require := require.New(t) + numKeys := 10 + cChainID := ids.GenerateTestID() + addressedCall, err := payload.NewAddressedCall(utils.RandomBytes(20), utils.RandomBytes(100)) + require.NoError(err) + unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, cChainID, addressedCall.Bytes()) + require.NoError(err) + + getValidatorsOutput := make(map[ids.NodeID]*validators.GetValidatorOutput) + blsSignatures := make([]*bls.Signature, 0, numKeys) + for i := 0; i < numKeys; i++ { + validatorOutput := &validators.GetValidatorOutput{ + NodeID: testVdrs[i].nodeID, + Weight: 20, + PublicKey: testVdrs[i].vdr.PublicKey, + } + getValidatorsOutput[testVdrs[i].nodeID] = validatorOutput + blsSignatures = append(blsSignatures, bls.Sign(testVdrs[i].sk, unsignedMsg.Bytes())) + } + aggregateSignature, err := bls.AggregateSignatures(blsSignatures) + require.NoError(err) + bitSet := set.NewBits() + for i := 0; i < numKeys; i++ { + bitSet.Add(i) + } + warpSignature := &avalancheWarp.BitSetSignature{ + Signers: bitSet.Bytes(), + } + copy(warpSignature.Signature[:], bls.SignatureToBytes(aggregateSignature)) + warpMsg, err := avalancheWarp.NewMessage(unsignedMsg, warpSignature) + require.NoError(err) + + predicateBytes := predicate.PackPredicate(warpMsg.Bytes()) + + snowCtx := snow.DefaultContextTest() + snowCtx.SubnetID = ids.GenerateTestID() + snowCtx.ChainID = ids.GenerateTestID() + snowCtx.CChainID = cChainID + snowCtx.NetworkID = networkID + snowCtx.ValidatorState = &validators.TestState{ + GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) { + require.Equal(chainID, cChainID) + return constants.PrimaryNetworkID, nil // Return Primary Network SubnetID + }, + GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + require.Equal(snowCtx.SubnetID, subnetID) + return getValidatorsOutput, nil + }, + } + + test := testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner, + GasErr: nil, + PredicateRes: set.NewBits().Bytes(), + } + + test.Run(t) +} + +func TestInvalidPredicatePacking(t *testing.T) { + numKeys := 1 + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: numKeys, + weight: 20, + publicKey: true, + }, + }) + predicateBytes := createPredicate(numKeys) + predicateBytes = append(predicateBytes, byte(0x01)) // Invalidate the predicate byte packing + + test := testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner, + GasErr: errInvalidPredicateBytes, + } + + test.Run(t) +} + +func TestInvalidWarpMessage(t *testing.T) { + numKeys := 1 + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: numKeys, + weight: 20, + publicKey: true, + }, + }) + warpMsg := createWarpMessage(1) + warpMsgBytes := warpMsg.Bytes() + warpMsgBytes = append(warpMsgBytes, byte(0x01)) // Invalidate warp message packing + predicateBytes := predicate.PackPredicate(warpMsgBytes) + + test := testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner, + GasErr: errInvalidWarpMsg, + PredicateRes: set.NewBits(0).Bytes(), // Won't be reached + } + + test.Run(t) +} + +func TestInvalidAddressedPayload(t *testing.T) { + numKeys := 1 + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: numKeys, + weight: 20, + publicKey: true, + }, + }) + aggregateSignature, err := bls.AggregateSignatures(blsSignatures[0:numKeys]) + require.NoError(t, err) + bitSet := set.NewBits() + for i := 0; i < numKeys; i++ { + bitSet.Add(i) + } + warpSignature := &avalancheWarp.BitSetSignature{ + Signers: bitSet.Bytes(), + } + copy(warpSignature.Signature[:], bls.SignatureToBytes(aggregateSignature)) + // Create an unsigned message with an invalid addressed payload + unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, []byte{1, 2, 3}) + require.NoError(t, err) + warpMsg, err := avalancheWarp.NewMessage(unsignedMsg, warpSignature) + require.NoError(t, err) + warpMsgBytes := warpMsg.Bytes() + predicateBytes := predicate.PackPredicate(warpMsgBytes) + + test := testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner, + GasErr: errInvalidWarpMsgPayload, + } + + test.Run(t) +} + +func TestInvalidBitSet(t *testing.T) { + addressedCall, err := payload.NewAddressedCall(utils.RandomBytes(20), utils.RandomBytes(100)) + require.NoError(t, err) + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedCall.Bytes(), + ) + require.NoError(t, err) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: make([]byte, 1), + Signature: [bls.SignatureLen]byte{}, + }, + ) + require.NoError(t, err) + + numKeys := 1 + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: numKeys, + weight: 20, + publicKey: true, + }, + }) + predicateBytes := predicate.PackPredicate(msg.Bytes()) + test := testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner, + GasErr: errCannotGetNumSigners, + PredicateRes: set.NewBits(0).Bytes(), // Won't be reached + } + + test.Run(t) +} + +func TestWarpSignatureWeightsDefaultQuorumNumerator(t *testing.T) { + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: 100, + weight: 20, + publicKey: true, + }, + }) + + tests := make(map[string]testutils.PredicateTest) + for _, numSigners := range []int{ + 1, + int(params.WarpDefaultQuorumNumerator) - 1, + int(params.WarpDefaultQuorumNumerator), + int(params.WarpDefaultQuorumNumerator) + 1, + int(params.WarpQuorumDenominator) - 1, + int(params.WarpQuorumDenominator), + int(params.WarpQuorumDenominator) + 1, + } { + var ( + predicateBytes = createPredicate(numSigners) + expectedPredicateResults = set.NewBits() + ) + // If the number of signers is less than the required numerator or exceeds the denominator, then + // mark the expected result as invalid. + if numSigners < int(params.WarpDefaultQuorumNumerator) || numSigners > int(params.WarpQuorumDenominator) { + expectedPredicateResults.Add(0) + } + tests[fmt.Sprintf("default quorum %d signature(s)", numSigners)] = testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numSigners)*GasCostPerWarpSigner, + GasErr: nil, + PredicateRes: expectedPredicateResults.Bytes(), + } + } + testutils.RunPredicateTests(t, tests) +} + +// multiple messages all correct, multiple messages all incorrect, mixed bag +func TestWarpMultiplePredicates(t *testing.T) { + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: 100, + weight: 20, + publicKey: true, + }, + }) + + tests := make(map[string]testutils.PredicateTest) + for _, validMessageIndices := range [][]bool{ + {}, + {true, false}, + {false, true}, + {false, false}, + {true, true}, + } { + var ( + numSigners = int(params.WarpQuorumDenominator) + invalidPredicateBytes = createPredicate(1) + validPredicateBytes = createPredicate(numSigners) + expectedPredicateResults = set.NewBits() + ) + predicates := make([][]byte, len(validMessageIndices)) + expectedGas := uint64(0) + for index, valid := range validMessageIndices { + if valid { + predicates[index] = common.CopyBytes(validPredicateBytes) + expectedGas += GasCostPerSignatureVerification + uint64(len(validPredicateBytes))*GasCostPerWarpMessageBytes + uint64(numSigners)*GasCostPerWarpSigner + } else { + expectedPredicateResults.Add(index) + expectedGas += GasCostPerSignatureVerification + uint64(len(invalidPredicateBytes))*GasCostPerWarpMessageBytes + uint64(1)*GasCostPerWarpSigner + predicates[index] = invalidPredicateBytes + } + } + + tests[fmt.Sprintf("multiple predicates %v", validMessageIndices)] = testutils.PredicateTest{ + Config: NewDefaultConfig(corethUtils.NewUint64(0)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: predicates, + Gas: expectedGas, + GasErr: nil, + PredicateRes: expectedPredicateResults.Bytes(), + } + } + testutils.RunPredicateTests(t, tests) +} + +func TestWarpSignatureWeightsNonDefaultQuorumNumerator(t *testing.T) { + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: 100, + weight: 20, + publicKey: true, + }, + }) + + tests := make(map[string]testutils.PredicateTest) + nonDefaultQuorumNumerator := 50 + // Ensure this test fails if the DefaultQuroumNumerator is changed to an unexpected value during development + require.NotEqual(t, nonDefaultQuorumNumerator, int(params.WarpDefaultQuorumNumerator)) + // Add cases with default quorum + for _, numSigners := range []int{nonDefaultQuorumNumerator, nonDefaultQuorumNumerator + 1, 99, 100, 101} { + var ( + predicateBytes = createPredicate(numSigners) + expectedPredicateResults = set.NewBits() + ) + // If the number of signers is less than the required numerator or exceeds the denominator, then + // mark the expected result as invalid. + if numSigners < nonDefaultQuorumNumerator || numSigners > int(params.WarpQuorumDenominator) { + expectedPredicateResults.Add(0) + } + name := fmt.Sprintf("non-default quorum %d signature(s)", numSigners) + tests[name] = testutils.PredicateTest{ + Config: NewConfig(corethUtils.NewUint64(0), uint64(nonDefaultQuorumNumerator)), + PredicateContext: &precompileconfig.PredicateContext{ + SnowCtx: snowCtx, + ProposerVMBlockCtx: &block.Context{ + PChainHeight: 1, + }, + }, + StorageSlots: [][]byte{predicateBytes}, + Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numSigners)*GasCostPerWarpSigner, + GasErr: nil, + PredicateRes: expectedPredicateResults.Bytes(), + } + } + + testutils.RunPredicateTests(t, tests) +} + +func initWarpPredicateTests() { + for _, totalNodes := range []int{10, 100, 1_000, 10_000} { + testName := fmt.Sprintf("%d signers/%d validators", totalNodes, totalNodes) + + predicateBytes := createPredicate(totalNodes) + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: totalNodes, + weight: 20, + publicKey: true, + }, + }) + predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(totalNodes), predicateBytes) + } + + numSigners := 10 + for _, totalNodes := range []int{100, 1_000, 10_000} { + testName := fmt.Sprintf("%d signers (heavily weighted)/%d validators", numSigners, totalNodes) + + predicateBytes := createPredicate(numSigners) + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: numSigners, + weight: 10_000_000, + publicKey: true, + }, + { + start: numSigners, + end: totalNodes, + weight: 20, + publicKey: true, + }, + }) + predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(numSigners), predicateBytes) + } + + for _, totalNodes := range []int{100, 1_000, 10_000} { + testName := fmt.Sprintf("%d signers (heavily weighted)/%d validators (non-signers without registered PublicKey)", numSigners, totalNodes) + + predicateBytes := createPredicate(numSigners) + snowCtx := createSnowCtx([]validatorRange{ + { + start: 0, + end: numSigners, + weight: 10_000_000, + publicKey: true, + }, + { + start: numSigners, + end: totalNodes, + weight: 20, + publicKey: false, + }, + }) + predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(numSigners), predicateBytes) + } + + for _, totalNodes := range []int{100, 1_000, 10_000} { + testName := fmt.Sprintf("%d validators w/ %d signers/repeated PublicKeys", totalNodes, numSigners) + + predicateBytes := createPredicate(numSigners) + getValidatorsOutput := make(map[ids.NodeID]*validators.GetValidatorOutput, totalNodes) + for i := 0; i < totalNodes; i++ { + getValidatorsOutput[testVdrs[i].nodeID] = &validators.GetValidatorOutput{ + NodeID: testVdrs[i].nodeID, + Weight: 20, + PublicKey: testVdrs[i%numSigners].vdr.PublicKey, + } + } + + snowCtx := snow.DefaultContextTest() + snowCtx.NetworkID = networkID + state := &validators.TestState{ + GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) { + return sourceSubnetID, nil + }, + GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + return getValidatorsOutput, nil + }, + } + snowCtx.ValidatorState = state + + predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(numSigners), predicateBytes) + } +} + +func TestWarpPredicate(t *testing.T) { + testutils.RunPredicateTests(t, predicateTests) +} + +func BenchmarkWarpPredicate(b *testing.B) { + testutils.RunPredicateBenchmarks(b, predicateTests) +} diff --git a/precompile/contracts/warp/signature_verification_test.go b/precompile/contracts/warp/signature_verification_test.go new file mode 100644 index 0000000000..dadefeb4d6 --- /dev/null +++ b/precompile/contracts/warp/signature_verification_test.go @@ -0,0 +1,653 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "math" + "testing" + + "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" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +// This test copies the test coverage from https://github.com/ava-labs/avalanchego/blob/v1.10.0/vms/platformvm/warp/signature_test.go#L137. +// These tests are only expected to fail if there is a breaking change in AvalancheGo that unexpectedly changes behavior. +func TestSignatureVerification(t *testing.T) { + tests = []signatureTest{ + { + name: "can't get subnetID", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, errTest) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{}, + ) + require.NoError(err) + return msg + }, + err: errTest, + }, + { + name: "can't get validator set", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(nil, errTest) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{}, + ) + require.NoError(err) + return msg + }, + err: errTest, + }, + { + name: "weight overflow", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(map[ids.NodeID]*validators.GetValidatorOutput{ + testVdrs[0].nodeID: { + NodeID: testVdrs[0].nodeID, + PublicKey: testVdrs[0].vdr.PublicKey, + Weight: math.MaxUint64, + }, + testVdrs[1].nodeID: { + NodeID: testVdrs[1].nodeID, + PublicKey: testVdrs[1].vdr.PublicKey, + Weight: math.MaxUint64, + }, + }, nil) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: make([]byte, 8), + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrWeightOverflow, + }, + { + name: "invalid bit set index", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: make([]byte, 1), + Signature: [bls.SignatureLen]byte{}, + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrInvalidBitSet, + }, + { + name: "unknown index", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + signers := set.NewBits() + signers.Add(3) // vdr oob + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: [bls.SignatureLen]byte{}, + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrUnknownValidator, + }, + { + name: "insufficient weight", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 1, + quorumDen: 1, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + // [signers] has weight from [vdr[0], vdr[1]], + // which is 6, which is less than 9 + signers := set.NewBits() + signers.Add(0) + signers.Add(1) + + unsignedBytes := unsignedMsg.Bytes() + vdr0Sig := bls.Sign(testVdrs[0].sk, unsignedBytes) + vdr1Sig := bls.Sign(testVdrs[1].sk, unsignedBytes) + aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr0Sig, vdr1Sig}) + require.NoError(err) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(aggSig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrInsufficientWeight, + }, + { + name: "can't parse sig", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + signers := set.NewBits() + signers.Add(0) + signers.Add(1) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: [bls.SignatureLen]byte{}, + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrParseSignature, + }, + { + name: "no validators", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(nil, nil) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + unsignedBytes := unsignedMsg.Bytes() + vdr0Sig := bls.Sign(testVdrs[0].sk, unsignedBytes) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(vdr0Sig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: nil, + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: bls.ErrNoPublicKeys, + }, + { + name: "invalid signature (substitute)", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 3, + quorumDen: 5, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + signers := set.NewBits() + signers.Add(0) + signers.Add(1) + + unsignedBytes := unsignedMsg.Bytes() + vdr0Sig := bls.Sign(testVdrs[0].sk, unsignedBytes) + // Give sig from vdr[2] even though the bit vector says it + // should be from vdr[1] + vdr2Sig := bls.Sign(testVdrs[2].sk, unsignedBytes) + aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr0Sig, vdr2Sig}) + require.NoError(err) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(aggSig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrInvalidSignature, + }, + { + name: "invalid signature (missing one)", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 3, + quorumDen: 5, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + signers := set.NewBits() + signers.Add(0) + signers.Add(1) + + unsignedBytes := unsignedMsg.Bytes() + vdr0Sig := bls.Sign(testVdrs[0].sk, unsignedBytes) + // Don't give the sig from vdr[1] + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(vdr0Sig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrInvalidSignature, + }, + { + name: "invalid signature (extra one)", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 3, + quorumDen: 5, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + signers := set.NewBits() + signers.Add(0) + signers.Add(1) + + unsignedBytes := unsignedMsg.Bytes() + vdr0Sig := bls.Sign(testVdrs[0].sk, unsignedBytes) + vdr1Sig := bls.Sign(testVdrs[1].sk, unsignedBytes) + // Give sig from vdr[2] even though the bit vector doesn't have + // it + vdr2Sig := bls.Sign(testVdrs[2].sk, unsignedBytes) + aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr0Sig, vdr1Sig, vdr2Sig}) + require.NoError(err) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(aggSig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: avalancheWarp.ErrInvalidSignature, + }, + { + name: "valid signature", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 1, + quorumDen: 2, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + // [signers] has weight from [vdr[1], vdr[2]], + // which is 6, which is greater than 4.5 + signers := set.NewBits() + signers.Add(1) + signers.Add(2) + + unsignedBytes := unsignedMsg.Bytes() + vdr1Sig := bls.Sign(testVdrs[1].sk, unsignedBytes) + vdr2Sig := bls.Sign(testVdrs[2].sk, unsignedBytes) + aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr1Sig, vdr2Sig}) + require.NoError(err) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(aggSig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: nil, + }, + { + name: "valid signature (boundary)", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil) + return state + }, + quorumNum: 2, + quorumDen: 3, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + // [signers] has weight from [vdr[1], vdr[2]], + // which is 6, which meets the minimum 6 + signers := set.NewBits() + signers.Add(1) + signers.Add(2) + + unsignedBytes := unsignedMsg.Bytes() + vdr1Sig := bls.Sign(testVdrs[1].sk, unsignedBytes) + vdr2Sig := bls.Sign(testVdrs[2].sk, unsignedBytes) + aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr1Sig, vdr2Sig}) + require.NoError(err) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(aggSig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: nil, + }, + { + name: "valid signature (missing key)", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(map[ids.NodeID]*validators.GetValidatorOutput{ + testVdrs[0].nodeID: { + NodeID: testVdrs[0].nodeID, + PublicKey: nil, + Weight: testVdrs[0].vdr.Weight, + }, + testVdrs[1].nodeID: { + NodeID: testVdrs[1].nodeID, + PublicKey: testVdrs[1].vdr.PublicKey, + Weight: testVdrs[1].vdr.Weight, + }, + testVdrs[2].nodeID: { + NodeID: testVdrs[2].nodeID, + PublicKey: testVdrs[2].vdr.PublicKey, + Weight: testVdrs[2].vdr.Weight, + }, + }, nil) + return state + }, + quorumNum: 1, + quorumDen: 3, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + // [signers] has weight from [vdr2, vdr3], + // which is 6, which is greater than 3 + signers := set.NewBits() + // Note: the bits are shifted because vdr[0]'s key was zeroed + signers.Add(0) // vdr[1] + signers.Add(1) // vdr[2] + + unsignedBytes := unsignedMsg.Bytes() + vdr1Sig := bls.Sign(testVdrs[1].sk, unsignedBytes) + vdr2Sig := bls.Sign(testVdrs[2].sk, unsignedBytes) + aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr1Sig, vdr2Sig}) + require.NoError(err) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(aggSig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: nil, + }, + { + name: "valid signature (duplicate key)", + stateF: func(ctrl *gomock.Controller) validators.State { + state := validators.NewMockState(ctrl) + state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil) + state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(map[ids.NodeID]*validators.GetValidatorOutput{ + testVdrs[0].nodeID: { + NodeID: testVdrs[0].nodeID, + PublicKey: nil, + Weight: testVdrs[0].vdr.Weight, + }, + testVdrs[1].nodeID: { + NodeID: testVdrs[1].nodeID, + PublicKey: testVdrs[2].vdr.PublicKey, + Weight: testVdrs[1].vdr.Weight, + }, + testVdrs[2].nodeID: { + NodeID: testVdrs[2].nodeID, + PublicKey: testVdrs[2].vdr.PublicKey, + Weight: testVdrs[2].vdr.Weight, + }, + }, nil) + return state + }, + quorumNum: 2, + quorumDen: 3, + msgF: func(require *require.Assertions) *avalancheWarp.Message { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage( + networkID, + sourceChainID, + addressedPayloadBytes, + ) + require.NoError(err) + + // [signers] has weight from [vdr2, vdr3], + // which is 6, which meets the minimum 6 + signers := set.NewBits() + // Note: the bits are shifted because vdr[0]'s key was zeroed + // Note: vdr[1] and vdr[2] were combined because of a shared pk + signers.Add(0) // vdr[1] + vdr[2] + + unsignedBytes := unsignedMsg.Bytes() + // Because vdr[1] and vdr[2] share a key, only one of them sign. + vdr2Sig := bls.Sign(testVdrs[2].sk, unsignedBytes) + aggSigBytes := [bls.SignatureLen]byte{} + copy(aggSigBytes[:], bls.SignatureToBytes(vdr2Sig)) + + msg, err := avalancheWarp.NewMessage( + unsignedMsg, + &avalancheWarp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: aggSigBytes, + }, + ) + require.NoError(err) + return msg + }, + err: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + msg := tt.msgF(require) + pChainState := tt.stateF(ctrl) + + err := msg.Signature.Verify( + context.Background(), + &msg.UnsignedMessage, + networkID, + pChainState, + pChainHeight, + tt.quorumNum, + tt.quorumDen, + ) + require.ErrorIs(err, tt.err) + }) + } +} diff --git a/precompile/registry/registry.go b/precompile/registry/registry.go index f08ec83950..a0798ebd1f 100644 --- a/precompile/registry/registry.go +++ b/precompile/registry/registry.go @@ -6,5 +6,6 @@ package registry // Force imports of each precompile to ensure each precompile's init function runs and registers itself // with the registry. - -// import _ "github.com/ava-labs/coreth/x/warp" +import ( + _ "github.com/ava-labs/coreth/precompile/contracts/warp" +) diff --git a/warp/client.go b/warp/client.go index 50a8ef0094..5aa16c2203 100644 --- a/warp/client.go +++ b/warp/client.go @@ -16,9 +16,9 @@ var _ Client = (*client)(nil) type Client interface { GetMessageSignature(ctx context.Context, messageID ids.ID) ([]byte, error) - GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) ([]byte, error) + GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) - GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64) ([]byte, error) + GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) } // client implementation for interacting with EVM [chain] @@ -45,9 +45,9 @@ func (c *client) GetMessageSignature(ctx context.Context, messageID ids.ID) ([]b return res, nil } -func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64) ([]byte, error) { +func (c *client) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum); err != nil { + if err := c.client.CallContext(ctx, &res, "warp_getMessageAggregateSignature", messageID, quorumNum, subnetIDStr); err != nil { return nil, fmt.Errorf("call to warp_getMessageAggregateSignature failed. err: %w", err) } return res, nil @@ -61,9 +61,9 @@ func (c *client) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, return res, nil } -func (c *client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64) ([]byte, error) { +func (c *client) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) ([]byte, error) { var res hexutil.Bytes - if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum); err != nil { + if err := c.client.CallContext(ctx, &res, "warp_getBlockAggregateSignature", blockID, quorumNum, subnetIDStr); err != nil { return nil, fmt.Errorf("call to warp_getBlockAggregateSignature failed. err: %w", err) } return res, nil diff --git a/warp/service.go b/warp/service.go index 8a8cd7333f..fdb892e99b 100644 --- a/warp/service.go +++ b/warp/service.go @@ -59,16 +59,16 @@ func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.By } // 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) { +func (a *API) GetMessageAggregateSignature(ctx context.Context, messageID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { unsignedMessage, err := a.backend.GetMessage(messageID) if err != nil { return nil, err } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum) + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) } // 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) { +func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, quorumNum uint64, subnetIDStr string) (signedMessageBytes hexutil.Bytes, err error) { blockHashPayload, err := payload.NewHash(blockID) if err != nil { return nil, err @@ -78,27 +78,38 @@ func (a *API) GetBlockAggregateSignature(ctx context.Context, blockID ids.ID, qu return nil, err } - return a.aggregateSignatures(ctx, unsignedMessage, quorumNum) + return a.aggregateSignatures(ctx, unsignedMessage, quorumNum, subnetIDStr) } -func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64) (hexutil.Bytes, error) { +func (a *API) aggregateSignatures(ctx context.Context, unsignedMessage *warp.UnsignedMessage, quorumNum uint64, subnetIDStr string) (hexutil.Bytes, error) { + subnetID := a.sourceSubnetID + if len(subnetIDStr) > 0 { + sid, err := ids.FromString(subnetIDStr) + if err != nil { + return nil, fmt.Errorf("failed to parse subnetID: %q", subnetIDStr) + } + subnetID = sid + } 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) + validators, totalWeight, err := warp.GetCanonicalValidatorSet(ctx, a.state, pChainHeight, 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.sourceSubnetID, pChainHeight) + return nil, fmt.Errorf("%w (SubnetID: %s, Height: %d)", errNoValidators, subnetID, pChainHeight) } + log.Debug("Fetching signature", + "sourceSubnetID", subnetID, + "height", pChainHeight, + "numValidators", len(validators), + "totalWeight", totalWeight, + ) + agg := aggregator.New(aggregator.NewSignatureGetter(a.client), validators, totalWeight) signatureResult, err := agg.AggregateSignatures(ctx, unsignedMessage, quorumNum) if err != nil {