diff --git a/.gitignore b/.gitignore index 22c1168b29..51765b7253 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,3 @@ cmd/simulator/.simulator/* # goreleaser dist/ - -# generator rpc file for e2e tests -contract-examples/dynamic_rpc.json \ No newline at end of file diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index ba8561eac1..6dd0e7e76f 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -95,6 +95,7 @@ func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error) { // 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) { diff --git a/core/predicate_check.go b/core/predicate_check.go new file mode 100644 index 0000000000..1fbf3614d1 --- /dev/null +++ b/core/predicate_check.go @@ -0,0 +1,83 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package core + +import ( + "errors" + "fmt" + + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" +) + +var errNilProposerVMBlockCtxWithProposerPredicate = errors.New("engine cannot specify nil ProposerVM block context with non-empty proposer predicates") + +// CheckPredicates checks that all precompile predicates are satisfied within the current [predicateContext] for [tx] +func CheckPredicates(rules params.Rules, predicateContext *precompileconfig.ProposerPredicateContext, tx *types.Transaction) error { + if err := checkPrecompilePredicates(rules, &predicateContext.PrecompilePredicateContext, tx); err != nil { + return err + } + return checkProposerPrecompilePredicates(rules, predicateContext, tx) +} + +func checkPrecompilePredicates(rules params.Rules, predicateContext *precompileconfig.PrecompilePredicateContext, tx *types.Transaction) error { + // Short circuit early if there are no precompile predicates to verify + if len(rules.PredicatePrecompiles) == 0 { + return nil + } + precompilePredicates := rules.PredicatePrecompiles + // Track addresses that we've performed a predicate check for + precompileAddressChecks := make(map[common.Address]struct{}) + for _, accessTuple := range tx.AccessList() { + address := accessTuple.Address + predicater, ok := precompilePredicates[address] + if !ok { + continue + } + // Return an error if we've already checked a predicate for this address + if _, ok := precompileAddressChecks[address]; ok { + return fmt.Errorf("predicate %s failed verification for tx %s: specified %s in access list multiple times", address, tx.Hash(), address) + } + precompileAddressChecks[address] = struct{}{} + if err := predicater.VerifyPredicate(predicateContext, utils.HashSliceToBytes(accessTuple.StorageKeys)); err != nil { + return fmt.Errorf("predicate %s failed verification for tx %s: %w", address, tx.Hash(), err) + } + } + + return nil +} + +func checkProposerPrecompilePredicates(rules params.Rules, predicateContext *precompileconfig.ProposerPredicateContext, tx *types.Transaction) error { + // Short circuit early if there are no precompile predicates to verify + if len(rules.ProposerPredicates) == 0 { + return nil + } + // If a proposer predicate is specified, reuqire that the ProposerVMBlockCtx is non-nil. + if predicateContext.ProposerVMBlockCtx == nil { + return errNilProposerVMBlockCtxWithProposerPredicate + } + precompilePredicates := rules.ProposerPredicates + // Track addresses that we've performed a predicate check for + precompileAddressChecks := make(map[common.Address]struct{}) + for _, accessTuple := range tx.AccessList() { + address := accessTuple.Address + predicater, ok := precompilePredicates[address] + if !ok { + continue + } + // Return an error if we've already checked a predicate for this address + if _, ok := precompileAddressChecks[address]; ok { + return fmt.Errorf("predicate %s failed verification for tx %s: specified %s in access list multiple times", address, tx.Hash(), address) + } + precompileAddressChecks[address] = struct{}{} + if err := predicater.VerifyPredicate(predicateContext, utils.HashSliceToBytes(accessTuple.StorageKeys)); err != nil { + return fmt.Errorf("predicate %s failed verification for tx %s: %w", address, tx.Hash(), err) + } + } + + return nil +} diff --git a/core/predicate_check_test.go b/core/predicate_check_test.go new file mode 100644 index 0000000000..76bc9ce1cf --- /dev/null +++ b/core/predicate_check_test.go @@ -0,0 +1,185 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package core + +import ( + "bytes" + "fmt" + "testing" + + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +var ( + _ precompileconfig.PrecompilePredicater = (*mockPredicater)(nil) + _ precompileconfig.ProposerPredicater = (*mockProposerPredicater)(nil) +) + +type mockPredicater struct { + predicateFunc func(*precompileconfig.PrecompilePredicateContext, []byte) error +} + +func (m *mockPredicater) VerifyPredicate(predicateContext *precompileconfig.PrecompilePredicateContext, b []byte) error { + return m.predicateFunc(predicateContext, b) +} + +type mockProposerPredicater struct { + predicateFunc func(*precompileconfig.ProposerPredicateContext, []byte) error +} + +func (m *mockProposerPredicater) VerifyPredicate(predicateContext *precompileconfig.ProposerPredicateContext, b []byte) error { + return m.predicateFunc(predicateContext, b) +} + +type predicateCheckTest struct { + address common.Address + predicater precompileconfig.PrecompilePredicater + proposerPredicater precompileconfig.ProposerPredicater + accessList types.AccessList + emptyProposerBlockCtx bool + expectedErr error +} + +func TestCheckPredicate(t *testing.T) { + for name, test := range map[string]predicateCheckTest{ + "no predicates, no access list passes": { + expectedErr: nil, + }, + "no predicates, with access list passes": { + accessList: types.AccessList([]types.AccessTuple{ + { + Address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + StorageKeys: []common.Hash{ + {1}, + }, + }, + }), + expectedErr: nil, + }, + "proposer predicate, no access list passes": { + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + proposerPredicater: &mockProposerPredicater{predicateFunc: func(*precompileconfig.ProposerPredicateContext, []byte) error { return nil }}, + expectedErr: nil, + }, + "predicate, no access list passes": { + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + predicater: &mockPredicater{predicateFunc: func(*precompileconfig.PrecompilePredicateContext, []byte) error { return nil }}, + expectedErr: nil, + }, + "predicate with valid access list passes": { + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + predicater: &mockPredicater{predicateFunc: func(_ *precompileconfig.PrecompilePredicateContext, b []byte) error { + if bytes.Equal(b, common.Hash{1}.Bytes()) { + return nil + } else { + return fmt.Errorf("unexpected bytes: 0x%x", b) + } + }}, + accessList: types.AccessList([]types.AccessTuple{ + { + Address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + StorageKeys: []common.Hash{ + {1}, + }, + }, + }), + expectedErr: nil, + }, + "proposer predicate with valid access list passes": { + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + proposerPredicater: &mockProposerPredicater{predicateFunc: func(_ *precompileconfig.ProposerPredicateContext, b []byte) error { + if bytes.Equal(b, common.Hash{1}.Bytes()) { + return nil + } else { + return fmt.Errorf("unexpected bytes: 0x%x", b) + } + }}, + accessList: types.AccessList([]types.AccessTuple{ + { + Address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + StorageKeys: []common.Hash{ + {1}, + }, + }, + }), + expectedErr: nil, + }, + "predicate with invalid access list errors": { + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + predicater: &mockPredicater{predicateFunc: func(_ *precompileconfig.PrecompilePredicateContext, b []byte) error { + if bytes.Equal(b, common.Hash{1}.Bytes()) { + return nil + } else { + return fmt.Errorf("unexpected bytes: 0x%x", b) + } + }}, + accessList: types.AccessList([]types.AccessTuple{ + { + Address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + StorageKeys: []common.Hash{ + {2}, + }, + }, + }), + expectedErr: fmt.Errorf("unexpected bytes: 0x%x", common.Hash{2}.Bytes()), + }, + "proposer predicate with invalid access list errors": { + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + proposerPredicater: &mockProposerPredicater{predicateFunc: func(_ *precompileconfig.ProposerPredicateContext, b []byte) error { + if bytes.Equal(b, common.Hash{1}.Bytes()) { + return nil + } else { + return fmt.Errorf("unexpected bytes: 0x%x", b) + } + }}, + accessList: types.AccessList([]types.AccessTuple{ + { + Address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + StorageKeys: []common.Hash{ + {2}, + }, + }, + }), + expectedErr: fmt.Errorf("unexpected bytes: 0x%x", common.Hash{2}.Bytes()), + }, + "proposer predicate with empty proposer block ctx": { + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + proposerPredicater: &mockProposerPredicater{predicateFunc: func(_ *precompileconfig.ProposerPredicateContext, b []byte) error { return nil }}, + emptyProposerBlockCtx: true, + expectedErr: errNilProposerVMBlockCtxWithProposerPredicate, + }, + } { + test := test + t.Run(name, func(t *testing.T) { + // Create the rules from TestChainConfig and update the predicates based on the test params + rules := params.TestChainConfig.AvalancheRules(common.Big0, common.Big0) + if test.proposerPredicater != nil { + rules.ProposerPredicates[test.address] = test.proposerPredicater + } + if test.predicater != nil { + rules.PredicatePrecompiles[test.address] = test.predicater + } + + // Specify only the access list, since this test should not depend on any other values + tx := types.NewTx(&types.DynamicFeeTx{ + AccessList: test.accessList, + }) + predicateContext := &precompileconfig.ProposerPredicateContext{} + if !test.emptyProposerBlockCtx { + predicateContext.ProposerVMBlockCtx = &block.Context{} + } + err := CheckPredicates(rules, predicateContext, tx) + if test.expectedErr == nil { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, test.expectedErr.Error()) + } + }) + } +} diff --git a/core/state/statedb.go b/core/state/statedb.go index e82ec9ffb8..bfbc29dd10 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -38,7 +38,9 @@ import ( "github.com/ava-labs/subnet-evm/core/state/snapshot" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/metrics" + "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/trie" + "github.com/ava-labs/subnet-evm/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -110,6 +112,9 @@ type StateDB struct { // Per-transaction access list accessList *accessList + // Ordered storage slots to be used in predicate verification as set in the tx access list. + // Only set in PrepareAccessList, and un-modified through execution. + predicateStorageSlots map[common.Address][]byte // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. @@ -156,17 +161,18 @@ func NewWithSnapshot(root common.Hash, db Database, snap snapshot.Snapshot) (*St return nil, err } sdb := &StateDB{ - db: db, - trie: tr, - originalRoot: root, - stateObjects: make(map[common.Address]*stateObject), - stateObjectsPending: make(map[common.Address]struct{}), - stateObjectsDirty: make(map[common.Address]struct{}), - logs: make(map[common.Hash][]*types.Log), - preimages: make(map[common.Hash][]byte), - journal: newJournal(), - accessList: newAccessList(), - hasher: crypto.NewKeccakState(), + db: db, + trie: tr, + originalRoot: root, + stateObjects: make(map[common.Address]*stateObject), + stateObjectsPending: make(map[common.Address]struct{}), + stateObjectsDirty: make(map[common.Address]struct{}), + logs: make(map[common.Hash][]*types.Log), + preimages: make(map[common.Hash][]byte), + journal: newJournal(), + predicateStorageSlots: make(map[common.Address][]byte), + accessList: newAccessList(), + hasher: crypto.NewKeccakState(), } if snap != nil { if snap.Root() != root { @@ -674,6 +680,15 @@ func (db *StateDB) ForEachStorage(addr common.Address, cb func(key, value common return nil } +// copyPredicateStorageSlots creates a deep copy of the provided predicateStorageSlots map. +func copyPredicateStorageSlots(predicateStorageSlots map[common.Address][]byte) map[common.Address][]byte { + res := make(map[common.Address][]byte, len(predicateStorageSlots)) + for address, slots := range predicateStorageSlots { + res[address] = common.CopyBytes(slots) + } + return res +} + // Copy creates a deep, independent copy of the state. // Snapshots of the copied state cannot be applied to the copy. func (s *StateDB) Copy() *StateDB { @@ -740,6 +755,7 @@ func (s *StateDB) Copy() *StateDB { // However, it doesn't cost us much to copy an empty list, so we do it anyway // to not blow up if we ever decide copy it in the middle of a transaction state.accessList = s.accessList.Copy() + state.predicateStorageSlots = copyPredicateStorageSlots(s.predicateStorageSlots) // If there's a prefetcher running, make an inactive copy of it that can // only access data but does not actively preload (since the user will not @@ -1048,7 +1064,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool, snaps *snapshot.Tree, blockHas // - Add the contents of the optional tx access list (2930) // // This method should only be called if Berlin/SubnetEVM/2929+2930 is applicable at the current number. -func (s *StateDB) PrepareAccessList(sender common.Address, dst *common.Address, precompiles []common.Address, list types.AccessList) { +func (s *StateDB) PrepareAccessList(sender common.Address, dst *common.Address, rules params.Rules, precompiles []common.Address, list types.AccessList) { // Clear out any leftover from previous executions s.accessList = newAccessList() @@ -1060,12 +1076,29 @@ func (s *StateDB) PrepareAccessList(sender common.Address, dst *common.Address, for _, addr := range precompiles { s.AddAddressToAccessList(addr) } + for _, el := range list { s.AddAddressToAccessList(el.Address) for _, key := range el.StorageKeys { s.AddSlotToAccessList(el.Address, key) } } + s.preparePredicateStorageSlots(rules, list) +} + +// preparePredicateStorageSlots populates the predicateStorageSlots field from the transaction's access list +// Note: if an address is specified multiple times in the access list, only the last storage slots provided +// for it are used in predicates. +// During predicate verification, we require that a precompile address is only specififed in the access list +// once to avoid a situation where we verify multiple predicate and only expose data from the last one. +func (s *StateDB) preparePredicateStorageSlots(rules params.Rules, list types.AccessList) { + s.predicateStorageSlots = make(map[common.Address][]byte) + for _, el := range list { + if !rules.PredicateExists(el.Address) { + continue + } + s.predicateStorageSlots[el.Address] = utils.HashSliceToBytes(el.StorageKeys) + } } // AddAddressToAccessList adds the given address to the access list @@ -1102,3 +1135,13 @@ func (s *StateDB) AddressInAccessList(addr common.Address) bool { func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { return s.accessList.Contains(addr, slot) } + +// GetPredicateStorageSlots returns the storage slots associated with a given address, and whether or not +// that address was included in the optional access list of the transaction. +// The storage slots are returned in the same order as they appeared in the transaction. +// These are the same storage slots that are used to verify any transaction +// predicates for transactions with access list addresses that match a precompile address. +func (s *StateDB) GetPredicateStorageSlots(address common.Address) ([]byte, bool) { + storageSlots, exists := s.predicateStorageSlots[address] + return storageSlots, exists +} diff --git a/core/state_transition.go b/core/state_transition.go index 5056fc7d7a..e40411f67d 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -345,7 +345,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { // Set up the initial access list. if rules.IsSubnetEVM { - st.state.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) + st.state.PrepareAccessList(msg.From(), msg.To(), rules, vm.ActivePrecompiles(rules), msg.AccessList()) } var ( ret []byte diff --git a/core/tx_pool.go b/core/tx_pool.go index dd3e4a4f39..4f4e8e9fc7 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -1118,6 +1118,8 @@ func (pool *TxPool) HasLocal(hash common.Hash) bool { return pool.all.GetLocal(hash) != nil } +// RemoveTx removes a single transaction from the queue, moving all subsequent +// transactions back to the future queue. func (pool *TxPool) RemoveTx(hash common.Hash) { pool.mu.Lock() defer pool.mu.Unlock() diff --git a/core/vm/interface.go b/core/vm/interface.go index c9135fdb24..00dd124465 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -30,6 +30,7 @@ import ( "math/big" "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/params" "github.com/ethereum/go-ethereum/common" ) @@ -68,7 +69,7 @@ type StateDB interface { // is defined according to EIP161 (balance = nonce = code = 0). Empty(common.Address) bool - PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) + PrepareAccessList(sender common.Address, dest *common.Address, rules params.Rules, precompiles []common.Address, txAccesses types.AccessList) AddressInAccessList(addr common.Address) bool SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform @@ -82,6 +83,7 @@ type StateDB interface { Snapshot() int AddLog(addr common.Address, topics []common.Hash, data []byte, blockNumber uint64) + GetPredicateStorageSlots(address common.Address) ([]byte, bool) AddPreimage(common.Hash, []byte) ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) error diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index aa2b41ce52..57f6fe421f 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -128,7 +128,7 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) { sender = vm.AccountRef(cfg.Origin) ) if rules := cfg.ChainConfig.AvalancheRules(vmenv.Context.BlockNumber, vmenv.Context.Time); rules.IsSubnetEVM { - cfg.State.PrepareAccessList(cfg.Origin, &address, vm.ActivePrecompiles(rules), nil) + cfg.State.PrepareAccessList(cfg.Origin, &address, rules, vm.ActivePrecompiles(rules), nil) } cfg.State.CreateAccount(address) // set the receiver's (the executing contract) code for execution. @@ -160,7 +160,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { sender = vm.AccountRef(cfg.Origin) ) if rules := cfg.ChainConfig.AvalancheRules(vmenv.Context.BlockNumber, vmenv.Context.Time); rules.IsSubnetEVM { - cfg.State.PrepareAccessList(cfg.Origin, nil, vm.ActivePrecompiles(rules), nil) + cfg.State.PrepareAccessList(cfg.Origin, nil, rules, vm.ActivePrecompiles(rules), nil) } // Call the code with the given configuration. code, address, leftOverGas, err := vmenv.Create( @@ -186,7 +186,7 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er statedb := cfg.State if rules := cfg.ChainConfig.AvalancheRules(vmenv.Context.BlockNumber, vmenv.Context.Time); rules.IsSubnetEVM { - statedb.PrepareAccessList(cfg.Origin, &address, vm.ActivePrecompiles(rules), nil) + statedb.PrepareAccessList(cfg.Origin, &address, rules, vm.ActivePrecompiles(rules), nil) } // Call the code with the given configuration. ret, leftOverGas, err := vmenv.Call( diff --git a/miner/miner.go b/miner/miner.go index a2786724e4..4b4f3fffc7 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -33,6 +33,7 @@ import ( "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/event" ) @@ -62,8 +63,8 @@ func (miner *Miner) SetEtherbase(addr common.Address) { miner.worker.setEtherbase(addr) } -func (miner *Miner) GenerateBlock() (*types.Block, error) { - return miner.worker.commitNewWork() +func (miner *Miner) GenerateBlock(predicateContext *precompileconfig.ProposerPredicateContext) (*types.Block, error) { + return miner.worker.commitNewWork(predicateContext) } // SubscribePendingLogs starts delivering logs from pending transactions diff --git a/miner/worker.go b/miner/worker.go index 041ca12a90..3d4b978eb4 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -44,6 +44,7 @@ import ( "github.com/ava-labs/subnet-evm/core/state" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" @@ -112,7 +113,7 @@ func (w *worker) setEtherbase(addr common.Address) { } // commitNewWork generates several new sealing tasks based on the parent block. -func (w *worker) commitNewWork() (*types.Block, error) { +func (w *worker) commitNewWork(predicateContext *precompileconfig.ProposerPredicateContext) (*types.Block, error) { w.mu.RLock() defer w.mu.RUnlock() @@ -192,8 +193,11 @@ func (w *worker) commitNewWork() (*types.Block, error) { return nil, err } - // Fill the block with all available pending transactions. + // Get the pending txs from TxPool pending := w.eth.TxPool().Pending(true) + // Filter out transactions that don't satisfy predicateContext and remove them from TxPool + rules := w.chainConfig.AvalancheRules(header.Number, new(big.Int).SetUint64(header.Time)) + pending = w.enforcePredicates(rules, predicateContext, pending) // Split the pending transactions into locals and remotes localTxs := make(map[common.Address]types.Transactions) @@ -391,3 +395,39 @@ func totalFees(block *types.Block, receipts []*types.Receipt) *big.Float { } return new(big.Float).Quo(new(big.Float).SetInt(feesWei), new(big.Float).SetInt(big.NewInt(params.Ether))) } + +// enforcePredicates takes a set of pending transactions (grouped by sender, and ordered by nonce) +// and returns the subset of those transactions (following the same grouping) that satisfy predicateContext. +// Any transaction that fails predicate verification will be removed from the tx pool and excluded +// from the return value. +// Transactions with a nonce that follows a removed transaction will be added back to the future +// queue of the tx pool. +func (w *worker) enforcePredicates( + rules params.Rules, + predicateContext *precompileconfig.ProposerPredicateContext, + pending map[common.Address]types.Transactions, +) map[common.Address]types.Transactions { + // Short circuit early if there are no precompile predicates to verify and return the + // unmodified pending transactions. + if len(rules.PredicatePrecompiles) == 0 { + return pending + } + result := make(map[common.Address]types.Transactions, len(pending)) + for addr, txs := range pending { + for i, tx := range txs { + if err := core.CheckPredicates(rules, predicateContext, tx); err != nil { + log.Debug("Transaction predicate failed verification in miner", "sender", addr, "err", err) + // If the transaction fails the predicate check, we remove the transaction from the mempool + // and move all transactions from the same address with a subsequent nonce back to the + // future queue of the transaction pool. + w.eth.TxPool().RemoveTx(tx.Hash()) + txs = txs[:i] // Cut off any transactions past the failed predicate in the return value + break + } + } + if len(txs) > 0 { + result[addr] = txs + } + } + return result +} diff --git a/params/config.go b/params/config.go index b583ec00dd..0dc08c4e25 100644 --- a/params/config.go +++ b/params/config.go @@ -326,6 +326,19 @@ func (c *ChainConfig) IsSubnetEVM(blockTimestamp *big.Int) bool { return utils.IsForked(c.getNetworkUpgrades().SubnetEVMTimestamp, blockTimestamp) } +func (r *Rules) PredicatesExist() bool { + return len(r.PredicatePrecompiles) > 0 || len(r.ProposerPredicates) > 0 +} + +func (r *Rules) PredicateExists(addr common.Address) bool { + _, predicateExists := r.PredicatePrecompiles[addr] + if predicateExists { + return true + } + _, proposerPredicateExists := r.ProposerPredicates[addr] + return proposerPredicateExists +} + // IsPrecompileEnabled returns whether precompile with [address] is enabled at [blockTimestamp]. func (c *ChainConfig) IsPrecompileEnabled(address common.Address, blockTimestamp *big.Int) bool { config := c.getActivePrecompileConfig(address, blockTimestamp) @@ -574,6 +587,15 @@ type Rules struct { // Note: none of these addresses should conflict with the address space used by // any existing precompiles. ActivePrecompiles map[common.Address]precompileconfig.Config + // PrecompilePredicates maps addresses to stateful precompile predicate functions + // that are enabled for this rule set. + PredicatePrecompiles map[common.Address]precompileconfig.PrecompilePredicater + // ProposerPredicates maps addresses to stateful precompile predicate functions + // that are enabled for this rule set and require access to the ProposerVM wrapper. + ProposerPredicates map[common.Address]precompileconfig.ProposerPredicater + // AccepterPrecompiles map addresses to stateful precompile accepter functions + // that are enabled for this rule set. + AccepterPrecompiles map[common.Address]precompileconfig.Accepter } // IsPrecompileEnabled returns true if the precompile at [addr] is enabled for this rule set. @@ -610,9 +632,21 @@ func (c *ChainConfig) AvalancheRules(blockNum, blockTimestamp *big.Int) Rules { // Initialize the stateful precompiles that should be enabled at [blockTimestamp]. rules.ActivePrecompiles = make(map[common.Address]precompileconfig.Config) + rules.PredicatePrecompiles = make(map[common.Address]precompileconfig.PrecompilePredicater) + rules.ProposerPredicates = make(map[common.Address]precompileconfig.ProposerPredicater) + rules.AccepterPrecompiles = make(map[common.Address]precompileconfig.Accepter) for _, module := range modules.RegisteredModules() { if config := c.getActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { rules.ActivePrecompiles[module.Address] = config + if precompilePredicate, ok := config.(precompileconfig.PrecompilePredicater); ok { + rules.PredicatePrecompiles[module.Address] = precompilePredicate + } + if proposerPredicate, ok := config.(precompileconfig.ProposerPredicater); ok { + rules.ProposerPredicates[module.Address] = proposerPredicate + } + if precompileAccepter, ok := config.(precompileconfig.Accepter); ok { + rules.AccepterPrecompiles[module.Address] = precompileAccepter + } } } diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 3afcccc228..6384c7934b 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -12,10 +12,20 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" + "github.com/ava-labs/subnet-evm/core" + "github.com/ava-labs/subnet-evm/core/rawdb" "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +var ( + _ snowman.Block = (*Block)(nil) + _ block.WithVerifyContext = (*Block)(nil) ) // Block implements the snowman.Block interface @@ -51,11 +61,66 @@ func (b *Block) Accept(context.Context) error { if err := vm.blockChain.Accept(b.ethBlock); err != nil { return fmt.Errorf("chain could not accept %s: %w", b.ID(), err) } + + // Call Accept for relevant precompile logs. This should apply DB operations to the VM's versionDB + // to be committed atomically with marking this block as accepted. + sharedMemoryWriter := NewSharedMemoryWriter() + if err := b.handlePrecompileAccept(sharedMemoryWriter); err != nil { + return err + } if err := vm.acceptedBlockDB.Put(lastAcceptedKey, b.id[:]); err != nil { return fmt.Errorf("failed to put %s as the last accepted block: %w", b.ID(), err) } - return vm.db.Commit() + // Get pending operations on the vm's versionDB so we can apply them atomically + // with the shared memory requests. + vdbBatch, err := vm.db.CommitBatch() + if err != nil { + return fmt.Errorf("failed to get commit batch: %w", err) + } + + // Apply any shared memory requests that accumulated from processing the logs + // of the accepted block (generated by precompiles) atomically with other pending + // changes to the vm's versionDB. + return vm.ctx.SharedMemory.Apply(sharedMemoryWriter.requests, vdbBatch) +} + +// 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. +// Passes in sharedMemoryWriter to accumulate any requests from shared memory to commit on block accept. +func (b *Block) handlePrecompileAccept(sharedMemoryWriter *sharedMemoryWriter) error { + rules := b.vm.chainConfig.AvalancheRules(b.ethBlock.Number(), b.ethBlock.Timestamp()) + // Short circuit early if there are no precompile accepters to execute + if len(rules.AccepterPrecompiles) == 0 { + return nil + } + + // Read the receipts + receipts := rawdb.ReadReceipts(b.vm.chaindb, b.ethBlock.Hash(), b.ethBlock.NumberU64(), b.vm.chainConfig) + if receipts == nil { + return fmt.Errorf("failed to read receipts for accepted block %s, height %d", b.ethBlock.Hash(), b.ethBlock.NumberU64()) + } + + for txIndex, receipt := range receipts { + for _, log := range receipt.Logs { + accepter, ok := rules.AccepterPrecompiles[log.Address] + if !ok { + continue + } + + acceptCtx := &precompileconfig.AcceptContext{ + SnowCtx: b.vm.ctx, + SharedMemory: sharedMemoryWriter, + } + if err := accepter.Accept(acceptCtx, log.TxHash, txIndex, log.Topics, log.Data); err != nil { + return err + } + } + } + + return nil } // Reject implements the snowman.Block interface @@ -102,17 +167,95 @@ func (b *Block) syntacticVerify() error { // Verify implements the snowman.Block interface func (b *Block) Verify(context.Context) error { - return b.verify(true) + return b.verify(&precompileconfig.ProposerPredicateContext{ + PrecompilePredicateContext: precompileconfig.PrecompilePredicateContext{ + SnowCtx: b.vm.ctx, + }, + ProposerVMBlockCtx: nil, + }, true) +} + +// ShouldVerifyWithContext implements the block.WithVerifyContext interface +func (b *Block) ShouldVerifyWithContext(context.Context) (bool, error) { + proposerPredicates := b.vm.chainConfig.AvalancheRules(b.ethBlock.Number(), b.ethBlock.Timestamp()).ProposerPredicates + // Short circuit early if there are no proposer predicates to verify + if len(proposerPredicates) == 0 { + return false, nil + } + + // Check if any of the transactions in the block specify a precompile that enforces a predicate, which requires + // the ProposerVMBlockCtx. + for _, tx := range b.ethBlock.Transactions() { + for _, accessTuple := range tx.AccessList() { + if _, ok := proposerPredicates[accessTuple.Address]; ok { + log.Debug("Block verification requires proposerVM context", "block", b.ID(), "height", b.Height()) + return true, nil + } + } + } + + log.Debug("Block verification does not require proposerVM context", "block", b.ID(), "height", b.Height()) + return false, nil +} + +// VerifyWithContext implements the block.WithVerifyContext interface +func (b *Block) VerifyWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) error { + if proposerVMBlockCtx != nil { + log.Debug("Verifying block with context", "block", b.ID(), "height", b.Height()) + } else { + log.Debug("Verifying block without context", "block", b.ID(), "height", b.Height()) + } + + return b.verify(&precompileconfig.ProposerPredicateContext{ + PrecompilePredicateContext: precompileconfig.PrecompilePredicateContext{ + SnowCtx: b.vm.ctx, + }, + ProposerVMBlockCtx: proposerVMBlockCtx, + }, true) } -func (b *Block) verify(writes bool) error { +// Verify the block is valid. +// Enforces that the predicates are valid within [predicateContext]. +// Writes the block details to disk and the state to the trie manager iff writes=true. +func (b *Block) verify(predicateContext *precompileconfig.ProposerPredicateContext, writes bool) error { if err := b.syntacticVerify(); err != nil { return fmt.Errorf("syntactic block verification failed: %w", err) } + // Only enforce predicates if the chain has already bootstrapped. + // If the chain is still bootstrapping, we can assume that all blocks we are verifying have + // been accepted by the network (so the predicate was validated by the network when the + // block was originally verified). + if b.vm.bootstrapped { + if err := b.verifyPredicates(predicateContext); err != nil { + return fmt.Errorf("failed to verify predicates: %w", err) + } + } + + // The engine may call VerifyWithContext multiple times on the same block with different contexts. + // Since the engine will only call Accept/Reject once, we should only call InsertBlockManual once. + // Additionally, if a block is already in processing, then it has already passed verification and + // at this point we have checked the predicates are still valid in the different context so we + // can return nil. + if b.vm.State.IsProcessing(b.id) { + return nil + } + return b.vm.blockChain.InsertBlockManual(b.ethBlock, writes) } +// verifyPredicates verifies the predicates in the block are valid according to predicateContext. +func (b *Block) verifyPredicates(predicateContext *precompileconfig.ProposerPredicateContext) error { + rules := b.vm.chainConfig.AvalancheRules(b.ethBlock.Number(), b.ethBlock.Timestamp()) + + for _, tx := range b.ethBlock.Transactions() { + if err := core.CheckPredicates(rules, predicateContext, tx); err != nil { + return err + } + } + return nil +} + // Bytes implements the snowman.Block interface func (b *Block) Bytes() []byte { res, err := rlp.EncodeToBytes(b.ethBlock) diff --git a/plugin/evm/shared_memory_writer.go b/plugin/evm/shared_memory_writer.go new file mode 100644 index 0000000000..88589720ee --- /dev/null +++ b/plugin/evm/shared_memory_writer.go @@ -0,0 +1,37 @@ +// (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/subnet-evm/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) +} + +// mergeAtomicOps merges atomic ops for [chainID] represented by [requests] +// to the [output] map provided. +func mergeAtomicOpsToMap(output map[ids.ID]*atomic.Requests, chainID ids.ID, requests *atomic.Requests) { + if request, exists := output[chainID]; exists { + request.PutRequests = append(request.PutRequests, requests.PutRequests...) + request.RemoveRequests = append(request.RemoveRequests, requests.RemoveRequests...) + } else { + output[chainID] = requests + } +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index de7d624a90..92426d3477 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -45,6 +45,7 @@ import ( // inside of cmd/geth. _ "github.com/ava-labs/subnet-evm/eth/tracers/native" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" // Force-load precompiles to trigger registration _ "github.com/ava-labs/subnet-evm/precompile/registry" @@ -539,14 +540,15 @@ func (vm *VM) initChainState(lastAcceptedBlock *types.Block) error { block.status = choices.Accepted config := &chain.Config{ - DecidedCacheSize: decidedCacheSize, - MissingCacheSize: missingCacheSize, - UnverifiedCacheSize: unverifiedCacheSize, - GetBlockIDAtHeight: vm.GetBlockIDAtHeight, - GetBlock: vm.getBlock, - UnmarshalBlock: vm.parseBlock, - BuildBlock: vm.buildBlock, - LastAcceptedBlock: block, + DecidedCacheSize: decidedCacheSize, + MissingCacheSize: missingCacheSize, + UnverifiedCacheSize: unverifiedCacheSize, + GetBlockIDAtHeight: vm.GetBlockIDAtHeight, + GetBlock: vm.getBlock, + UnmarshalBlock: vm.parseBlock, + BuildBlock: vm.buildBlock, + BuildBlockWithContext: vm.buildBlockWithContext, + LastAcceptedBlock: block, } // Register chain state metrics @@ -630,9 +632,24 @@ func (vm *VM) Shutdown(context.Context) error { return nil } -// buildBlock builds a block to be wrapped by ChainState -func (vm *VM) buildBlock(context.Context) (snowman.Block, error) { - block, err := vm.miner.GenerateBlock() +func (vm *VM) buildBlock(ctx context.Context) (snowman.Block, error) { + return vm.buildBlockWithContext(ctx, nil) +} + +func (vm *VM) buildBlockWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) (snowman.Block, error) { + if proposerVMBlockCtx != nil { + log.Debug("Building block with context", "pChainBlockHeight", proposerVMBlockCtx.PChainHeight) + } else { + log.Debug("Building block without context") + } + predicateCtx := &precompileconfig.ProposerPredicateContext{ + PrecompilePredicateContext: precompileconfig.PrecompilePredicateContext{ + SnowCtx: vm.ctx, + }, + ProposerVMBlockCtx: proposerVMBlockCtx, + } + + block, err := vm.miner.GenerateBlock(predicateCtx) vm.builder.handleGenerateBlock() if err != nil { return nil, err @@ -653,7 +670,7 @@ func (vm *VM) buildBlock(context.Context) (snowman.Block, error) { // We call verify without writes here to avoid generating a reference // to the blk state root in the triedb when we are going to call verify // again from the consensus engine with writes enabled. - if err := blk.verify(false /*=writes*/); err != nil { + if err := blk.verify(predicateCtx, false /*=writes*/); err != nil { return nil, fmt.Errorf("block failed verification due to: %w", err) } diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 36fcb2157d..031ce98dd4 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -25,7 +25,9 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/api/keystore" + "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/database/manager" + "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/choices" @@ -143,12 +145,14 @@ func NewContext() *snow.Context { } // If [genesisJSON] is empty, defaults to using [genesisJSONLatest] -func setupGenesis(t *testing.T, +func setupGenesis( + t *testing.T, genesisJSON string, ) (*snow.Context, manager.Manager, []byte, chan commonEng.Message, + *atomic.Memory, ) { if len(genesisJSON) == 0 { genesisJSON = genesisJSONLatest @@ -162,6 +166,10 @@ func setupGenesis(t *testing.T, Patch: 5, }) + // initialize the atomic memory + atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, baseDBManager.Current().Database)) + ctx.SharedMemory = atomicMemory.NewSharedMemory(ctx.ChainID) + // NB: this lock is intentionally left locked when this function returns. // The caller of this function is responsible for unlocking. ctx.Lock.Lock() @@ -178,7 +186,7 @@ func setupGenesis(t *testing.T, issuer := make(chan commonEng.Message, 1) prefixedDBManager := baseDBManager.NewPrefixDBManager([]byte{1}) - return ctx, prefixedDBManager, genesisBytes, issuer + return ctx, prefixedDBManager, genesisBytes, issuer, atomicMemory } // GenesisVM creates a VM instance with the genesis test bytes and returns @@ -195,7 +203,7 @@ func GenesisVM(t *testing.T, *commonEng.SenderTest, ) { vm := &VM{} - ctx, dbManager, genesisBytes, issuer := setupGenesis(t, genesisJSON) + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, genesisJSON) appSender := &commonEng.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte) error { return nil } @@ -403,7 +411,7 @@ func TestSubnetEVMUpgradeRequiredAtGenesis(t *testing.T) { } for _, test := range genesisTests { - ctx, dbManager, genesisBytes, issuer := setupGenesis(t, test.genesisJSON) + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, test.genesisJSON) vm := &VM{} err := vm.Initialize( context.Background(), @@ -2027,7 +2035,7 @@ func TestConfigureLogLevel(t *testing.T) { for _, test := range configTests { t.Run(test.name, func(t *testing.T) { vm := &VM{} - ctx, dbManager, genesisBytes, issuer := setupGenesis(t, test.genesisJSON) + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, test.genesisJSON) appSender := &commonEng.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte) error { return nil } diff --git a/plugin/evm/vm_upgrade_bytes_test.go b/plugin/evm/vm_upgrade_bytes_test.go index 9124945487..c5957a0dda 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -87,9 +87,14 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { } // restart the vm - ctx := NewContext() + // Hack: registering metrics uses global variables, so we need to disable metrics here so that we + // can initialize the VM twice. + metrics.Enabled = false + defer func() { + metrics.Enabled = true + }() if err := vm.Initialize( - context.Background(), ctx, dbManager, []byte(genesisJSONSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*commonEng.Fx{}, appSender, + context.Background(), vm.ctx, dbManager, []byte(genesisJSONSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*commonEng.Fx{}, appSender, ); err != nil { t.Fatal(err) } diff --git a/precompile/contract/interfaces.go b/precompile/contract/interfaces.go index b9c431272d..e36df7aefa 100644 --- a/precompile/contract/interfaces.go +++ b/precompile/contract/interfaces.go @@ -44,6 +44,7 @@ type StateDB interface { Exist(common.Address) bool AddLog(addr common.Address, topics []common.Hash, data []byte, blockNumber uint64) + GetPredicateStorageSlots(address common.Address) ([]byte, bool) Suicide(common.Address) bool Finalise(deleteEmptyObjects bool) diff --git a/precompile/precompileconfig/config.go b/precompile/precompileconfig/config.go index fcee72fc95..41576c4588 100644 --- a/precompile/precompileconfig/config.go +++ b/precompile/precompileconfig/config.go @@ -6,6 +6,12 @@ package precompileconfig import ( "math/big" + + "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ethereum/go-ethereum/common" ) // StatefulPrecompileConfig defines the interface for a stateful precompile to @@ -25,3 +31,62 @@ type Config interface { // Verify is called on startup and an error is treated as fatal. Configure can assume the Config has passed verification. Verify() error } + +// PrecompilePredicateContext is the context passed in to the PrecompilePredicater interface. +type PrecompilePredicateContext struct { + SnowCtx *snow.Context +} + +// PrecompilePredicater is an optional interface for StatefulPrecompileContracts to implement. +// If implemented, the predicate will be enforced on every transaction in a block, prior to +// the block's execution. +// If VerifyPredicate returns an error, the block will fail verification with no further processing. +// WARNING: If you are implementing a custom precompile, beware that subnet-evm +// will not maintain backwards compatibility of this interface and your code should not +// rely on this. Designed for use only by precompiles that ship with subnet-evm. +type PrecompilePredicater interface { + VerifyPredicate(predicateContext *PrecompilePredicateContext, storageSlots []byte) error +} + +// ProposerPredicateContext is the context passed in to the ProposerPredicater interface to verify +// a precompile predicate within a specific ProposerVM wrapper. +type ProposerPredicateContext struct { + PrecompilePredicateContext + // ProposerVMBlockCtx defines the ProposerVM context the predicate is verified within + ProposerVMBlockCtx *block.Context +} + +// ProposerPredicater is an optional interface for StatefulPrecompiledContracts to implement. +// If implemented, the predicate will be enforced on every transaction in a block, prior to +// the block's execution. +// If VerifyPredicate returns an error, the block will fail verification with no further processing. +// Note: ProposerVMBlockCtx is guaranteed to be non-nil. +// Precompiles should use ProposerPredicater instead of PrecompilePredicater iff their execution +// depends on the ProposerVM Block Context. +// WARNING: If you are implementing a custom precompile, beware that subnet-evm +// will not maintain backwards compatibility of this interface and your code should not +// rely on this. Designed for use only by precompiles that ship with subnet-evm. +type ProposerPredicater interface { + VerifyPredicate(proposerPredicateContext *ProposerPredicateContext, storageSlots []byte) error +} + +// SharedMemoryWriter defines an interface to allow a precompile's Accepter to write operations +// into shared memory to be committed atomically on block accept. +type SharedMemoryWriter interface { + AddSharedMemoryRequests(chainID ids.ID, requests *atomic.Requests) +} + +// AcceptContext defines the context passed in to a precompileconfig's Accepter +type AcceptContext struct { + SnowCtx *snow.Context + SharedMemory SharedMemoryWriter +} + +// Accepter is an optional interface for StatefulPrecompiledContracts to implement. +// If implemented, Accept will be called for every log with the address of the precompile when the block is accepted. +// WARNING: If you are implementing a custom precompile, beware that subnet-evm +// will not maintain backwards compatibility of this interface and your code should not +// rely on this. Designed for use only by precompiles that ship with subnet-evm. +type Accepter interface { + Accept(acceptCtx *AcceptContext, txHash common.Hash, logIndex int, topics []common.Hash, logData []byte) error +} diff --git a/utils/bytes.go b/utils/bytes.go index 186e3c41ef..ec7032f711 100644 --- a/utils/bytes.go +++ b/utils/bytes.go @@ -3,6 +3,8 @@ package utils +import "github.com/ethereum/go-ethereum/common" + // IncrOne increments bytes value by one func IncrOne(bytes []byte) { index := len(bytes) - 1 @@ -16,3 +18,12 @@ func IncrOne(bytes []byte) { } } } + +// HashSliceToBytes serializes a []common.Hash into a []byte +func HashSliceToBytes(hashes []common.Hash) []byte { + bytes := make([]byte, common.HashLength*len(hashes)) + for i, hash := range hashes { + copy(bytes[i*common.HashLength:], hash[:]) + } + return bytes +} diff --git a/utils/bytes_test.go b/utils/bytes_test.go new file mode 100644 index 0000000000..86f9299ccc --- /dev/null +++ b/utils/bytes_test.go @@ -0,0 +1,69 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package utils + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestIncrOne(t *testing.T) { + type test struct { + input []byte + expected []byte + } + for name, test := range map[string]test{ + "increment no overflow no carry": { + input: []byte{0, 0}, + expected: []byte{0, 1}, + }, + "increment overflow": { + input: []byte{255, 255}, + expected: []byte{0, 0}, + }, + "increment carry": { + input: []byte{0, 255}, + expected: []byte{1, 0}, + }, + } { + t.Run(name, func(t *testing.T) { + output := common.CopyBytes(test.input) + IncrOne(output) + assert.Equal(t, output, test.expected) + }) + } +} + +func TestHashSliceToBytes(t *testing.T) { + type test struct { + input []common.Hash + expected []byte + } + for name, test := range map[string]test{ + "empty slice": { + input: []common.Hash{}, + expected: []byte{}, + }, + "convert single hash": { + input: []common.Hash{ + common.BytesToHash([]byte{1, 2, 3}), + }, + expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3}, + }, + "convert hash slice": { + input: []common.Hash{ + common.BytesToHash([]byte{1, 2, 3}), + common.BytesToHash([]byte{4, 5, 6}), + }, + expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 6}, + }, + } { + t.Run(name, func(t *testing.T) { + output := HashSliceToBytes(test.input) + assert.Equal(t, output, test.expected) + }) + } +}