diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 2465b52ad1..8f62a9c31c 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -155,6 +155,7 @@ var ( utils.BeaconGenesisTimeFlag, utils.BeaconCheckpointFlag, utils.BeaconCheckpointFileFlag, + utils.ExperimentalBALFlag, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c9da08578c..6a59879e9d 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -997,6 +997,14 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Value: metrics.DefaultConfig.InfluxDBOrganization, Category: flags.MetricsCategory, } + + // Block Access List flags + + ExperimentalBALFlag = &cli.BoolFlag{ + Name: "experimental.bal", + Usage: "Enable block-access-list building when importing post-Cancun blocks, and validation that access lists contained in post-Cancun blocks correctly correspond to the state changes in those blocks. This is used for development purposes only. Do not enable it otherwise.", + Category: flags.MiscCategory, + } ) var ( @@ -1899,6 +1907,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.VMTraceJsonConfig = ctx.String(VMTraceJsonConfigFlag.Name) } } + + cfg.ExperimentalBAL = ctx.Bool(ExperimentalBALFlag.Name) } // MakeBeaconLightConfig constructs a beacon light client config based on the @@ -2301,6 +2311,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh } options.VmConfig = vmcfg + options.EnableBALForTesting = ctx.Bool(ExperimentalBALFlag.Name) chain, err := core.NewBlockChain(chainDb, gspec, engine, options) if err != nil { Fatalf("Can't create BlockChain: %v", err) diff --git a/core/block_access_list_creation.go b/core/block_access_list_creation.go new file mode 100644 index 0000000000..a0367bd6d5 --- /dev/null +++ b/core/block_access_list_creation.go @@ -0,0 +1,109 @@ +package core + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/holiman/uint256" + "math/big" +) + +// BlockAccessListTracer constructs an EIP-7928 block access list from the +// execution of a block +type BlockAccessListTracer struct { + // this is a set of access lists for each call scope. the overall block access lists + // is accrued at index 0, while the access lists of various nested execution + // scopes are in the proceeding indices. + // When an execution scope terminates in a non-reverting fashion, the changes are + // merged into the access list of the parent scope. + callAccessLists []*bal.ConstructionBlockAccessList + txIdx uint16 + + // if non-nil, it's the address of the account which just self-destructed. + // reset at the end of the call-scope which self-destructed. + selfdestructedAccount *common.Address +} + +// NewBlockAccessListTracer returns an BlockAccessListTracer and a set of hooks +func NewBlockAccessListTracer() (*BlockAccessListTracer, *tracing.Hooks) { + balTracer := &BlockAccessListTracer{ + callAccessLists: []*bal.ConstructionBlockAccessList{bal.NewConstructionBlockAccessList()}, + txIdx: 0, + } + hooks := &tracing.Hooks{ + OnTxEnd: balTracer.TxEndHook, + OnTxStart: balTracer.TxStartHook, + OnEnter: balTracer.OnEnter, + OnExit: balTracer.OnExit, + OnCodeChangeV2: balTracer.OnCodeChange, + OnBalanceChange: balTracer.OnBalanceChange, + OnNonceChange: balTracer.OnNonceChange, + OnStorageChange: balTracer.OnStorageChange, + OnColdAccountRead: balTracer.OnColdAccountRead, + OnColdStorageRead: balTracer.OnColdStorageRead, + } + return balTracer, hooks +} + +// AccessList returns the constructed access list +func (a *BlockAccessListTracer) AccessList() *bal.ConstructionBlockAccessList { + return a.callAccessLists[0] +} + +func (a *BlockAccessListTracer) TxEndHook(receipt *types.Receipt, err error) { + a.txIdx++ +} + +func (a *BlockAccessListTracer) TxStartHook(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { + if a.txIdx == 0 { + a.txIdx++ + } +} + +func (a *BlockAccessListTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + a.callAccessLists = append(a.callAccessLists, bal.NewConstructionBlockAccessList()) +} + +func (a *BlockAccessListTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + // any self-destructed accounts must have been created in the same transaction + // so there is no difference between the pre/post tx state of those accounts + if a.selfdestructedAccount != nil { + delete(a.callAccessLists[len(a.callAccessLists)-1].Accounts, *a.selfdestructedAccount) + } + if !reverted { + parentAccessList := a.callAccessLists[len(a.callAccessLists)-2] + scopeAccessList := a.callAccessLists[len(a.callAccessLists)-1] + parentAccessList.Merge(scopeAccessList) + } + + a.callAccessLists = a.callAccessLists[:len(a.callAccessLists)-1] +} + +func (a *BlockAccessListTracer) OnCodeChange(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { + if reason == tracing.CodeChangeSelfDestruct { + a.selfdestructedAccount = &addr + return + } + a.callAccessLists[len(a.callAccessLists)-1].CodeChange(addr, uint16(a.txIdx), code) +} + +func (a *BlockAccessListTracer) OnBalanceChange(addr common.Address, prevBalance, newBalance *big.Int, _ tracing.BalanceChangeReason) { + a.callAccessLists[len(a.callAccessLists)-1].BalanceChange(a.txIdx, addr, new(uint256.Int).SetBytes(newBalance.Bytes())) +} + +func (a *BlockAccessListTracer) OnNonceChange(addr common.Address, prev uint64, new uint64) { + a.callAccessLists[len(a.callAccessLists)-1].NonceChange(addr, a.txIdx, new) +} + +func (a *BlockAccessListTracer) OnColdStorageRead(addr common.Address, key common.Hash) { + a.callAccessLists[len(a.callAccessLists)-1].StorageRead(addr, key) +} + +func (a *BlockAccessListTracer) OnColdAccountRead(addr common.Address) { + a.callAccessLists[len(a.callAccessLists)-1].AccountRead(addr) +} + +func (a *BlockAccessListTracer) OnStorageChange(addr common.Address, slot common.Hash, prev common.Hash, new common.Hash) { + a.callAccessLists[len(a.callAccessLists)-1].StorageWrite(a.txIdx, addr, slot, new) +} diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..455c893a75 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -111,6 +111,31 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } } + // block access lists must be present after the Amsterdam hard fork + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if block.Body().AccessList == nil { + return fmt.Errorf("access list not present in block body") + } else if block.Header().BlockAccessListHash == nil { + return fmt.Errorf("access list hash not present in block header") + } else if *block.Header().BlockAccessListHash != block.Body().AccessList.Hash() { + return fmt.Errorf("access list hash mismatch. local: %x. remote: %x\n", block.Body().AccessList.Hash(), *block.Header().BlockAccessListHash) + } + } else if !v.bc.cfg.EnableBALForTesting { + // if --experimental.bal is not enabled, block headers cannot have access list hash and bodies cannot have access lists. + if block.Body().AccessList != nil { + return fmt.Errorf("access list not allowed in block body if not in amsterdam or --experimental.bal is set") + } else if block.Header().BlockAccessListHash != nil { + return fmt.Errorf("access list hash in block header not allowed when --experimental.bal is set") + } + } else { + // if --experimental.bal is enabled, the BAL hash is not allowed in the header. + // this is in order that Geth can import pre-existing chains augmented with BALs + // and not have a hash mismatch. + if block.Header().BlockAccessListHash != nil { + return fmt.Errorf("access list hash in block header not allowed pre-amsterdam") + } + } + // Ancestor block must be known. if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { diff --git a/core/blockchain.go b/core/blockchain.go index 30f3da3004..3bde32a65c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -196,6 +196,11 @@ type BlockChainConfig struct { // If the value is -1, indexing is disabled. TxLookupLimit int64 + // If EnableBALForTesting is enabled, block access lists will be created as part of + // block processing and embedded in the block body. The block access list hash will + // not be set in the header. + EnableBALForTesting bool + // StateSizeTracking indicates whether the state size tracking is enabled. StateSizeTracking bool } @@ -1904,9 +1909,17 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness if parent == nil { parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1) } + + // construct or verify block access lists if BALs are enabled and + // we are post-selfdestruct removal fork. + enableBAL := (bc.cfg.EnableBALForTesting && bc.chainConfig.IsCancun(block.Number(), block.Time())) || bc.chainConfig.IsAmsterdam(block.Number(), block.Time()) + blockHasAccessList := block.Body().AccessList != nil + makeBAL := enableBAL && !blockHasAccessList + validateBAL := enableBAL && blockHasAccessList + // The traced section of block import. start := time.Now() - res, err := bc.ProcessBlock(parent.Root, block, setHead, makeWitness && len(chain) == 1) + res, err := bc.ProcessBlock(parent.Root, block, setHead, makeWitness && len(chain) == 1, makeBAL, validateBAL) if err != nil { return nil, it.index, err } @@ -1978,7 +1991,7 @@ func (bpr *blockProcessingResult) Witness() *stateless.Witness { // ProcessBlock executes and validates the given block. If there was no error // it writes the block and associated state to database. -func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, setHead bool, makeWitness bool) (_ *blockProcessingResult, blockEndErr error) { +func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, setHead bool, makeWitness bool, constructBALForTesting bool, validateBAL bool) (_ *blockProcessingResult, blockEndErr error) { var ( err error startTime = time.Now() @@ -2074,6 +2087,12 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s }() } + var balTracer *BlockAccessListTracer + // Process block using the parent state as reference point + if constructBALForTesting { + balTracer, bc.cfg.VmConfig.Tracer = NewBlockAccessListTracer() + } + // Process block using the parent state as reference point pstart := time.Now() res, err := bc.processor.Process(block, statedb, bc.cfg.VmConfig) @@ -2090,6 +2109,16 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s } vtime := time.Since(vstart) + if constructBALForTesting { + // very ugly... deep-copy the block body before setting the block access + // list on it to prevent mutating the block instance passed by the caller. + existingBody := block.Body() + block = block.WithBody(*existingBody) + existingBody = block.Body() + existingBody.AccessList = balTracer.AccessList().ToEncodingObj() + block = block.WithBody(*existingBody) + } + // If witnesses was generated and stateless self-validation requested, do // that now. Self validation should *never* run in production, it's more of // a tight integration to enable running *all* consensus tests through the diff --git a/core/genesis.go b/core/genesis.go index 2fd044c70a..932410cdfd 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -67,12 +67,13 @@ type Genesis struct { // These fields are used for consensus tests. Please don't use them // in actual genesis blocks. - Number uint64 `json:"number"` - GasUsed uint64 `json:"gasUsed"` - ParentHash common.Hash `json:"parentHash"` - BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559 - ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 - BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 + Number uint64 `json:"number"` + GasUsed uint64 `json:"gasUsed"` + ParentHash common.Hash `json:"parentHash"` + BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559 + ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 + BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` // EIP-7928 } // copy copies the genesis. @@ -122,6 +123,7 @@ func ReadGenesis(db ethdb.Database) (*Genesis, error) { genesis.BaseFee = genesisHeader.BaseFee genesis.ExcessBlobGas = genesisHeader.ExcessBlobGas genesis.BlobGasUsed = genesisHeader.BlobGasUsed + genesis.BlockAccessListHash = genesisHeader.BlockAccessListHash return &genesis, nil } @@ -469,18 +471,19 @@ func (g *Genesis) ToBlock() *types.Block { // toBlockWithRoot constructs the genesis block with the given genesis state root. func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { head := &types.Header{ - Number: new(big.Int).SetUint64(g.Number), - Nonce: types.EncodeNonce(g.Nonce), - Time: g.Timestamp, - ParentHash: g.ParentHash, - Extra: g.ExtraData, - GasLimit: g.GasLimit, - GasUsed: g.GasUsed, - BaseFee: g.BaseFee, - Difficulty: g.Difficulty, - MixDigest: g.Mixhash, - Coinbase: g.Coinbase, - Root: root, + Number: new(big.Int).SetUint64(g.Number), + Nonce: types.EncodeNonce(g.Nonce), + Time: g.Timestamp, + ParentHash: g.ParentHash, + Extra: g.ExtraData, + GasLimit: g.GasLimit, + GasUsed: g.GasUsed, + BaseFee: g.BaseFee, + Difficulty: g.Difficulty, + MixDigest: g.Mixhash, + Coinbase: g.Coinbase, + BlockAccessListHash: g.BlockAccessListHash, + Root: root, } if g.GasLimit == 0 { head.GasLimit = params.GenesisGasLimit diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 8e50dc3d8f..04be780a77 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -183,6 +183,12 @@ type ( // StorageChangeHook is called when the storage of an account changes. StorageChangeHook = func(addr common.Address, slot common.Hash, prev, new common.Hash) + // ColdStorageReadHook is called before a previously-unread storage slot is read. + ColdStorageReadHook = func(common.Address, common.Hash) + + // ColdAccountReadHook is called before an previously-unread account is read. + ColdAccountReadHook = func(address common.Address) + // LogHook is called when a log is emitted. LogHook = func(log *types.Log) @@ -209,6 +215,7 @@ type Hooks struct { OnSystemCallStart OnSystemCallStartHook OnSystemCallStartV2 OnSystemCallStartHookV2 OnSystemCallEnd OnSystemCallEndHook + // State events OnBalanceChange BalanceChangeHook OnNonceChange NonceChangeHook @@ -217,6 +224,9 @@ type Hooks struct { OnCodeChangeV2 CodeChangeHookV2 OnStorageChange StorageChangeHook OnLog LogHook + //State read events + OnColdStorageRead ColdStorageReadHook + OnColdAccountRead ColdAccountReadHook // Block hash read OnBlockHashRead BlockHashReadHook } diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index fca54f7681..eb85bdaa5b 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -18,6 +18,7 @@ package bal import ( "bytes" + "encoding/json" "maps" "github.com/ethereum/go-ethereum/common" @@ -27,8 +28,8 @@ import ( // CodeChange contains the runtime bytecode deployed at an address and the // transaction index where the deployment took place. type CodeChange struct { - TxIndex uint16 - Code []byte `json:"code,omitempty"` + TxIdx uint16 + Code []byte `json:"code,omitempty"` } // ConstructionAccountAccess contains post-block account state for mutations as well as @@ -38,26 +39,24 @@ type ConstructionAccountAccess struct { // StorageWrites is the post-state values of an account's storage slots // that were modified in a block, keyed by the slot key and the tx index // where the modification occurred. - StorageWrites map[common.Hash]map[uint16]common.Hash `json:"storageWrites,omitempty"` + StorageWrites map[common.Hash]map[uint16]common.Hash // StorageReads is the set of slot keys that were accessed during block // execution. // // Storage slots which are both read and written (with changed values) // appear only in StorageWrites. - StorageReads map[common.Hash]struct{} `json:"storageReads,omitempty"` + StorageReads map[common.Hash]struct{} // BalanceChanges contains the post-transaction balances of an account, // keyed by transaction indices where it was changed. - BalanceChanges map[uint16]*uint256.Int `json:"balanceChanges,omitempty"` + BalanceChanges map[uint16]*uint256.Int // NonceChanges contains the post-state nonce values of an account keyed // by tx index. - NonceChanges map[uint16]uint64 `json:"nonceChanges,omitempty"` + NonceChanges map[uint16]uint64 - // CodeChange is only set for contract accounts which were deployed in - // the block. - CodeChange *CodeChange `json:"codeChange,omitempty"` + CodeChanges map[uint16]CodeChange } // NewConstructionAccountAccess initializes the account access object. @@ -67,6 +66,7 @@ func NewConstructionAccountAccess() *ConstructionAccountAccess { StorageReads: make(map[common.Hash]struct{}), BalanceChanges: make(map[uint16]*uint256.Int), NonceChanges: make(map[uint16]uint64), + CodeChanges: make(map[uint16]CodeChange), } } @@ -77,83 +77,122 @@ type ConstructionBlockAccessList struct { } // NewConstructionBlockAccessList instantiates an empty access list. -func NewConstructionBlockAccessList() ConstructionBlockAccessList { - return ConstructionBlockAccessList{ +func NewConstructionBlockAccessList() *ConstructionBlockAccessList { + return &ConstructionBlockAccessList{ Accounts: make(map[common.Address]*ConstructionAccountAccess), } } // AccountRead records the address of an account that has been read during execution. -func (b *ConstructionBlockAccessList) AccountRead(addr common.Address) { - if _, ok := b.Accounts[addr]; !ok { - b.Accounts[addr] = NewConstructionAccountAccess() +func (c *ConstructionBlockAccessList) AccountRead(addr common.Address) { + if _, ok := c.Accounts[addr]; !ok { + c.Accounts[addr] = NewConstructionAccountAccess() } } // StorageRead records a storage key read during execution. -func (b *ConstructionBlockAccessList) StorageRead(address common.Address, key common.Hash) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() +func (c *ConstructionBlockAccessList) StorageRead(address common.Address, key common.Hash) { + if _, ok := c.Accounts[address]; !ok { + c.Accounts[address] = NewConstructionAccountAccess() } - if _, ok := b.Accounts[address].StorageWrites[key]; ok { + if _, ok := c.Accounts[address].StorageWrites[key]; ok { return } - b.Accounts[address].StorageReads[key] = struct{}{} + c.Accounts[address].StorageReads[key] = struct{}{} } // StorageWrite records the post-transaction value of a mutated storage slot. // The storage slot is removed from the list of read slots. -func (b *ConstructionBlockAccessList) StorageWrite(txIdx uint16, address common.Address, key, value common.Hash) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() +func (c *ConstructionBlockAccessList) StorageWrite(txIdx uint16, address common.Address, key, value common.Hash) { + if _, ok := c.Accounts[address]; !ok { + c.Accounts[address] = NewConstructionAccountAccess() } - if _, ok := b.Accounts[address].StorageWrites[key]; !ok { - b.Accounts[address].StorageWrites[key] = make(map[uint16]common.Hash) + if _, ok := c.Accounts[address].StorageWrites[key]; !ok { + c.Accounts[address].StorageWrites[key] = make(map[uint16]common.Hash) } - b.Accounts[address].StorageWrites[key][txIdx] = value + c.Accounts[address].StorageWrites[key][txIdx] = value - delete(b.Accounts[address].StorageReads, key) + delete(c.Accounts[address].StorageReads, key) } // CodeChange records the code of a newly-created contract. -func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex uint16, code []byte) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() +func (c *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex uint16, code []byte) { + if _, ok := c.Accounts[address]; !ok { + c.Accounts[address] = NewConstructionAccountAccess() } - b.Accounts[address].CodeChange = &CodeChange{ - TxIndex: txIndex, - Code: bytes.Clone(code), + c.Accounts[address].CodeChanges[txIndex] = CodeChange{ + TxIdx: txIndex, + Code: bytes.Clone(code), } } // NonceChange records tx post-state nonce of any contract-like accounts whose // nonce was incremented. -func (b *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx uint16, postNonce uint64) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() +func (c *ConstructionBlockAccessList) NonceChange(address common.Address, txIdx uint16, postNonce uint64) { + if _, ok := c.Accounts[address]; !ok { + c.Accounts[address] = NewConstructionAccountAccess() } - b.Accounts[address].NonceChanges[txIdx] = postNonce + c.Accounts[address].NonceChanges[txIdx] = postNonce } // BalanceChange records the post-transaction balance of an account whose // balance changed. -func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint16, address common.Address, balance *uint256.Int) { - if _, ok := b.Accounts[address]; !ok { - b.Accounts[address] = NewConstructionAccountAccess() +func (c *ConstructionBlockAccessList) BalanceChange(txIdx uint16, address common.Address, balance *uint256.Int) { + if _, ok := c.Accounts[address]; !ok { + c.Accounts[address] = NewConstructionAccountAccess() } - b.Accounts[address].BalanceChanges[txIdx] = balance.Clone() + c.Accounts[address].BalanceChanges[txIdx] = balance.Clone() } -// PrettyPrint returns a human-readable representation of the access list -func (b *ConstructionBlockAccessList) PrettyPrint() string { - enc := b.toEncodingObj() - return enc.PrettyPrint() +func (c *ConstructionBlockAccessList) Delete(address common.Address) { + delete(c.Accounts, address) +} + +func mergeStorageWrites(cur, next map[common.Hash]map[uint16]common.Hash) map[common.Hash]map[uint16]common.Hash { + for slot, _ := range next { + if _, ok := cur[slot]; !ok { + cur[slot] = next[slot] + continue + } + + for idx, val := range next[slot] { + cur[slot][idx] = val + } + } + + return cur +} + +func (c *ConstructionBlockAccessList) Merge(next *ConstructionBlockAccessList) { + for addr, accountAccess := range next.Accounts { + if _, ok := c.Accounts[addr]; !ok { + c.Accounts[addr] = accountAccess + continue + } + + // copy the entries from 'next' into 'c' overwriting 'c' entries with + // 'next entries when the bal index matches. + next.Accounts[addr].StorageWrites = mergeStorageWrites(next.Accounts[addr].StorageWrites, c.Accounts[addr].StorageWrites) + + for storageRead, _ := range c.Accounts[addr].StorageReads { + next.Accounts[addr].StorageReads[storageRead] = struct{}{} + } + for idx, nonce := range next.Accounts[addr].NonceChanges { + c.Accounts[addr].NonceChanges[idx] = nonce + } + for idx, code := range next.Accounts[addr].CodeChanges { + c.Accounts[addr].CodeChanges[idx] = code + } + for idx, balance := range next.Accounts[addr].BalanceChanges { + c.Accounts[addr].BalanceChanges[idx] = balance + } + } } // Copy returns a deep copy of the access list. -func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { +func (c *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { res := NewConstructionBlockAccessList() - for addr, aa := range b.Accounts { + for addr, aa := range c.Accounts { var aaCopy ConstructionAccountAccess slotWrites := make(map[common.Hash]map[uint16]common.Hash, len(aa.StorageWrites)) @@ -170,13 +209,221 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { aaCopy.BalanceChanges = balances aaCopy.NonceChanges = maps.Clone(aa.NonceChanges) - if aa.CodeChange != nil { - aaCopy.CodeChange = &CodeChange{ - TxIndex: aa.CodeChange.TxIndex, - Code: bytes.Clone(aa.CodeChange.Code), + codeChangesCopy := make(map[uint16]CodeChange) + for idx, codeChange := range aa.CodeChanges { + codeChangesCopy[idx] = CodeChange{ + TxIdx: idx, + Code: bytes.Clone(codeChange.Code), } } res.Accounts[addr] = &aaCopy } - return &res + return res +} + +// ApplyDiff includes the given changes in the block access list at the given index. +func (c *ConstructionBlockAccessList) ApplyDiff(i uint, diff *StateDiff) { + idx := uint16(i) + for addr, acctDiff := range diff.Mutations { + if _, ok := c.Accounts[addr]; !ok { + c.Accounts[addr] = &ConstructionAccountAccess{} + } + if acctDiff.Balance != nil { + if c.Accounts[addr].BalanceChanges == nil { + c.Accounts[addr].BalanceChanges = make(map[uint16]*uint256.Int) + } + c.Accounts[addr].BalanceChanges[idx] = acctDiff.Balance + } + if acctDiff.Nonce != nil { + if c.Accounts[addr].NonceChanges == nil { + c.Accounts[addr].NonceChanges = make(map[uint16]uint64) + } + c.Accounts[addr].NonceChanges[idx] = *acctDiff.Nonce + } + if acctDiff.Code != nil { + if c.Accounts[addr].CodeChanges == nil { + c.Accounts[addr].CodeChanges = make(map[uint16]CodeChange) + } + // TODO: make the CodeChanges value just be []byte + c.Accounts[addr].CodeChanges[idx] = CodeChange{idx, acctDiff.Code} + } + if acctDiff.StorageWrites != nil { + if c.Accounts[addr].StorageWrites == nil { + // TODO: can we instantiate all these maps in the constructor? + c.Accounts[addr].StorageWrites = make(map[common.Hash]map[uint16]common.Hash) + } + + for slot, val := range acctDiff.StorageWrites { + if c.Accounts[addr].StorageWrites[slot] == nil { + c.Accounts[addr].StorageWrites[slot] = make(map[uint16]common.Hash) + } + + c.Accounts[addr].StorageWrites[slot][idx] = val + + delete(c.Accounts[addr].StorageReads, slot) + } + } + } +} + +type StateDiff struct { + Mutations map[common.Address]*AccountState `json:"Mutations,omitempty"` +} + +type StateAccesses map[common.Address]map[common.Hash]struct{} + +type AccountState struct { + Balance *uint256.Int `json:"Balance,omitempty"` + Nonce *uint64 `json:"Nonce,omitempty"` + Code ContractCode `json:"Code,omitempty"` + StorageWrites map[common.Hash]common.Hash `json:"StorageWrites,omitempty"` +} + +func (a *AccountState) String() string { + var res bytes.Buffer + enc := json.NewEncoder(&res) + enc.SetIndent("", " ") + enc.Encode(a) + return res.String() +} + +// Merge the changes of a future AccountState into the caller, resulting in the +// combined state changes through next. +func (a *AccountState) Merge(next *AccountState) { + if next.Balance != nil { + a.Balance = next.Balance + } + if next.Nonce != nil { + a.Nonce = next.Nonce + } + if next.Code != nil { + a.Code = next.Code + } + if next.StorageWrites != nil { + if a.StorageWrites == nil { + a.StorageWrites = maps.Clone(next.StorageWrites) + } else { + for key, val := range next.StorageWrites { + a.StorageWrites[key] = val + } + } + } +} + +func NewEmptyAccountState() *AccountState { + return &AccountState{ + nil, + nil, + nil, + nil, + } +} + +func (a *AccountState) Eq(other *AccountState) bool { + if a.Balance != nil || other.Balance != nil { + if a.Balance == nil || other.Balance == nil { + return false + } + + if !a.Balance.Eq(other.Balance) { + return false + } + } + + if (len(a.Code) != 0 || len(other.Code) != 0) && !bytes.Equal(a.Code, other.Code) { + return false + } + + if a.Nonce != nil || other.Nonce != nil { + if a.Nonce == nil || other.Nonce == nil { + return false + } + + if *a.Nonce != *other.Nonce { + return false + } + } + + if a.StorageWrites != nil || other.StorageWrites != nil { + if a.StorageWrites == nil || other.StorageWrites == nil { + return false + } + + if !maps.Equal(a.StorageWrites, other.StorageWrites) { + return false + } + } + return true +} + +func (a *AccountState) Copy() *AccountState { + res := NewEmptyAccountState() + if a.Nonce != nil { + res.Nonce = new(uint64) + *res.Nonce = *a.Nonce + } + if a.Code != nil { + res.Code = bytes.Clone(a.Code) + } + if a.Balance != nil { + res.Balance = new(uint256.Int).Set(a.Balance) + } + if a.StorageWrites != nil { + res.StorageWrites = maps.Clone(a.StorageWrites) + } + return res +} + +func (s *StateDiff) String() string { + var res bytes.Buffer + enc := json.NewEncoder(&res) + enc.SetIndent("", " ") + enc.Encode(s) + return res.String() +} + +// Merge merges the state changes present in next into the caller. After, +// the state of the caller is the aggregate diff through next. +func (s *StateDiff) Merge(next *StateDiff) { + for account, diff := range next.Mutations { + if mut, ok := s.Mutations[account]; ok { + if diff.Balance != nil { + mut.Balance = diff.Balance + } + if diff.Code != nil { + mut.Code = diff.Code + } + if diff.Nonce != nil { + mut.Nonce = diff.Nonce + } + if len(diff.StorageWrites) > 0 { + if mut.StorageWrites == nil { + mut.StorageWrites = maps.Clone(diff.StorageWrites) + } else { + for key, val := range diff.StorageWrites { + mut.StorageWrites[key] = val + } + } + } + } else { + s.Mutations[account] = diff.Copy() + } + } +} + +func (s *StateDiff) Copy() *StateDiff { + res := &StateDiff{make(map[common.Address]*AccountState)} + for addr, accountDiff := range s.Mutations { + cpy := accountDiff.Copy() + res.Mutations[addr] = cpy + } + return res +} + +// Copy returns a deep copy of the access list +func (e BlockAccessList) Copy() (res BlockAccessList) { + for _, accountAccess := range e { + res = append(res, accountAccess.Copy()) + } + return } diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 24dfafa083..3b8fe1585f 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -19,12 +19,12 @@ package bal import ( "bytes" "cmp" + "encoding/json" "errors" "fmt" "io" "maps" "slices" - "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -33,26 +33,57 @@ import ( "github.com/holiman/uint256" ) -//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type BlockAccessList -decoder +//go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen -out bal_encoding_rlp_generated.go -type AccountAccess -decoder // These are objects used as input for the access list encoding. They mirror // the spec format. // BlockAccessList is the encoding format of ConstructionBlockAccessList. -type BlockAccessList struct { - Accesses []AccountAccess `ssz-max:"300000"` +type BlockAccessList []AccountAccess + +func (e BlockAccessList) EncodeRLP(_w io.Writer) error { + w := rlp.NewEncoderBuffer(_w) + l := w.List() + for _, access := range e { + access.EncodeRLP(w) + } + w.ListEnd(l) + return w.Flush() +} + +func (e *BlockAccessList) DecodeRLP(dec *rlp.Stream) error { + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var access AccountAccess + if err := access.DecodeRLP(dec); err != nil { + return err + } + *e = append(*e, access) + } + return nil +} + +func (e *BlockAccessList) String() string { + var res bytes.Buffer + enc := json.NewEncoder(&res) + enc.SetIndent("", " ") + // TODO: check error + enc.Encode(e) + return res.String() } // Validate returns an error if the contents of the access list are not ordered // according to the spec or any code changes are contained which exceed protocol // max code size. -func (e *BlockAccessList) Validate() error { - if !slices.IsSortedFunc(e.Accesses, func(a, b AccountAccess) int { +func (e BlockAccessList) Validate() error { + if !slices.IsSortedFunc(e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") } - for _, entry := range e.Accesses { + for _, entry := range e { if err := entry.validate(); err != nil { return err } @@ -70,42 +101,32 @@ func (e *BlockAccessList) Hash() common.Hash { // under reasonable conditions. panic(err) } + fmt.Printf("bal hash %x\n", enc.Bytes()) return crypto.Keccak256Hash(enc.Bytes()) } -// encodeBalance encodes the provided balance into 16-bytes. -func encodeBalance(val *uint256.Int) [16]byte { - valBytes := val.Bytes() - if len(valBytes) > 16 { - panic("can't encode value that is greater than 16 bytes in size") - } - var enc [16]byte - copy(enc[16-len(valBytes):], valBytes[:]) - return enc -} - // encodingBalanceChange is the encoding format of BalanceChange. type encodingBalanceChange struct { - TxIdx uint16 `ssz-size:"2"` - Balance [16]byte `ssz-size:"16"` + TxIdx uint16 `json:"txIndex"` + Balance *uint256.Int `json:"balance"` } // encodingAccountNonce is the encoding format of NonceChange. type encodingAccountNonce struct { - TxIdx uint16 `ssz-size:"2"` - Nonce uint64 `ssz-size:"8"` + TxIdx uint16 `json:"txIndex"` + Nonce uint64 `json:"nonce"` } // encodingStorageWrite is the encoding format of StorageWrites. type encodingStorageWrite struct { - TxIdx uint16 - ValueAfter [32]byte `ssz-size:"32"` + TxIdx uint16 `json:"txIndex"` + ValueAfter common.Hash `json:"valueAfter"` } // encodingStorageWrite is the encoding format of SlotWrites. type encodingSlotWrites struct { - Slot [32]byte `ssz-size:"32"` - Accesses []encodingStorageWrite `ssz-max:"300000"` + Slot common.Hash `json:"slot"` + Accesses []encodingStorageWrite `json:"accesses"` } // validate returns an instance of the encoding-representation slot writes in @@ -121,12 +142,12 @@ func (e *encodingSlotWrites) validate() error { // AccountAccess is the encoding format of ConstructionAccountAccess. type AccountAccess struct { - Address [20]byte `ssz-size:"20"` // 20-byte Ethereum address - StorageWrites []encodingSlotWrites `ssz-max:"300000"` // Storage changes (slot -> [tx_index -> new_value]) - StorageReads [][32]byte `ssz-max:"300000"` // Read-only storage keys - BalanceChanges []encodingBalanceChange `ssz-max:"300000"` // Balance changes ([tx_index -> post_balance]) - NonceChanges []encodingAccountNonce `ssz-max:"300000"` // Nonce changes ([tx_index -> new_nonce]) - Code []CodeChange `ssz-max:"1"` // Code changes ([tx_index -> new_code]) + Address common.Address `json:"address,omitempty"` // 20-byte Ethereum address + StorageChanges []encodingSlotWrites `json:"storageChanges,omitempty"` // Storage changes (slot -> [tx_index -> new_value]) + StorageReads []common.Hash `json:"storageReads,omitempty"` // Read-only storage keys + BalanceChanges []encodingBalanceChange `json:"balanceChanges,omitempty"` // Balance changes ([tx_index -> post_balance]) + NonceChanges []encodingAccountNonce `json:"nonceChanges,omitempty"` // Nonce changes ([tx_index -> new_nonce]) + CodeChanges []CodeChange `json:"code,omitempty"` // CodeChanges changes ([tx_index -> new_code]) } // validate converts the account accesses out of encoding format. @@ -134,19 +155,19 @@ type AccountAccess struct { // spec, an error is returned. func (e *AccountAccess) validate() error { // Check the storage write slots are sorted in order - if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { + if !slices.IsSortedFunc(e.StorageChanges, func(a, b encodingSlotWrites) int { return bytes.Compare(a.Slot[:], b.Slot[:]) }) { return errors.New("storage writes slots not in lexicographic order") } - for _, write := range e.StorageWrites { + for _, write := range e.StorageChanges { if err := write.validate(); err != nil { return err } } // Check the storage read slots are sorted in order - if !slices.IsSortedFunc(e.StorageReads, func(a, b [32]byte) int { + if !slices.IsSortedFunc(e.StorageReads, func(a, b common.Hash) int { return bytes.Compare(a[:], b[:]) }) { return errors.New("storage read slots not in lexicographic order") @@ -167,9 +188,9 @@ func (e *AccountAccess) validate() error { } // Convert code change - if len(e.Code) == 1 { - if len(e.Code[0].Code) > params.MaxCodeSize { - return errors.New("code change contained oversized code") + for _, codeChange := range e.CodeChanges { + if len(codeChange.Code) > params.MaxCodeSize { + return fmt.Errorf("code change contained oversized code") } } return nil @@ -183,26 +204,25 @@ func (e *AccountAccess) Copy() AccountAccess { BalanceChanges: slices.Clone(e.BalanceChanges), NonceChanges: slices.Clone(e.NonceChanges), } - for _, storageWrite := range e.StorageWrites { - res.StorageWrites = append(res.StorageWrites, encodingSlotWrites{ + for _, storageWrite := range e.StorageChanges { + res.StorageChanges = append(res.StorageChanges, encodingSlotWrites{ Slot: storageWrite.Slot, Accesses: slices.Clone(storageWrite.Accesses), }) } - if len(e.Code) == 1 { - res.Code = []CodeChange{ - { - e.Code[0].TxIndex, - bytes.Clone(e.Code[0].Code), - }, - } + for _, codeChange := range e.CodeChanges { + res.CodeChanges = append(res.CodeChanges, + CodeChange{ + codeChange.TxIdx, + bytes.Clone(codeChange.Code), + }) } return res } // EncodeRLP returns the RLP-encoded access list -func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { - return b.toEncodingObj().EncodeRLP(wr) +func (c *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { + return c.ToEncodingObj().EncodeRLP(wr) } var _ rlp.Encoder = &ConstructionBlockAccessList{} @@ -212,11 +232,11 @@ var _ rlp.Encoder = &ConstructionBlockAccessList{} func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAccess { res := AccountAccess{ Address: addr, - StorageWrites: make([]encodingSlotWrites, 0), - StorageReads: make([][32]byte, 0), + StorageChanges: make([]encodingSlotWrites, 0), + StorageReads: make([]common.Hash, 0), BalanceChanges: make([]encodingBalanceChange, 0), NonceChanges: make([]encodingAccountNonce, 0), - Code: nil, + CodeChanges: make([]CodeChange, 0), } // Convert write slots @@ -237,7 +257,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc ValueAfter: slotWrites[index], }) } - res.StorageWrites = append(res.StorageWrites, obj) + res.StorageChanges = append(res.StorageChanges, obj) } // Convert read slots @@ -253,7 +273,7 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc for _, idx := range balanceIndices { res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{ TxIdx: idx, - Balance: encodeBalance(a.BalanceChanges[idx]), + Balance: new(uint256.Int).Set(a.BalanceChanges[idx]), }) } @@ -268,77 +288,36 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc } // Convert code change - if a.CodeChange != nil { - res.Code = []CodeChange{ - { - a.CodeChange.TxIndex, - bytes.Clone(a.CodeChange.Code), - }, - } + codeChangeIdxs := slices.Collect(maps.Keys(a.CodeChanges)) + slices.SortFunc(codeChangeIdxs, cmp.Compare[uint16]) + for _, idx := range codeChangeIdxs { + res.CodeChanges = append(res.CodeChanges, CodeChange{ + idx, + bytes.Clone(a.CodeChanges[idx].Code), + }) } return res } -// toEncodingObj returns an instance of the access list expressed as the type +// ToEncodingObj returns an instance of the access list expressed as the type // which is used as input for the encoding/decoding. -func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList { +func (c *ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList { var addresses []common.Address - for addr := range b.Accounts { + for addr := range c.Accounts { addresses = append(addresses, addr) } slices.SortFunc(addresses, common.Address.Cmp) var res BlockAccessList for _, addr := range addresses { - res.Accesses = append(res.Accesses, b.Accounts[addr].toEncodingObj(addr)) + res = append(res, c.Accounts[addr].toEncodingObj(addr)) } return &res } -func (e *BlockAccessList) PrettyPrint() string { - var res bytes.Buffer - printWithIndent := func(indent int, text string) { - fmt.Fprintf(&res, "%s%s\n", strings.Repeat(" ", indent), text) - } - for _, accountDiff := range e.Accesses { - printWithIndent(0, fmt.Sprintf("%x:", accountDiff.Address)) +type ContractCode []byte - printWithIndent(1, "storage writes:") - for _, sWrite := range accountDiff.StorageWrites { - printWithIndent(2, fmt.Sprintf("%x:", sWrite.Slot)) - for _, access := range sWrite.Accesses { - printWithIndent(3, fmt.Sprintf("%d: %x", access.TxIdx, access.ValueAfter)) - } - } - - printWithIndent(1, "storage reads:") - for _, slot := range accountDiff.StorageReads { - printWithIndent(2, fmt.Sprintf("%x", slot)) - } - - printWithIndent(1, "balance changes:") - for _, change := range accountDiff.BalanceChanges { - balance := new(uint256.Int).SetBytes(change.Balance[:]).String() - printWithIndent(2, fmt.Sprintf("%d: %s", change.TxIdx, balance)) - } - - printWithIndent(1, "nonce changes:") - for _, change := range accountDiff.NonceChanges { - printWithIndent(2, fmt.Sprintf("%d: %d", change.TxIdx, change.Nonce)) - } - - if len(accountDiff.Code) > 0 { - printWithIndent(1, "code:") - printWithIndent(2, fmt.Sprintf("%d: %x", accountDiff.Code[0].TxIndex, accountDiff.Code[0].Code)) - } - } - return res.String() -} - -// Copy returns a deep copy of the access list -func (e *BlockAccessList) Copy() (res BlockAccessList) { - for _, accountAccess := range e.Accesses { - res.Accesses = append(res.Accesses, accountAccess.Copy()) - } - return +func (c *ContractCode) MarshalJSON() ([]byte, error) { + hexStr := fmt.Sprintf("%x", *c) + return json.Marshal(hexStr) } diff --git a/core/types/bal/bal_encoding_rlp_generated.go b/core/types/bal/bal_encoding_rlp_generated.go index 0d52395329..5a99e1b800 100644 --- a/core/types/bal/bal_encoding_rlp_generated.go +++ b/core/types/bal/bal_encoding_rlp_generated.go @@ -2,275 +2,254 @@ package bal +import "github.com/ethereum/go-ethereum/common" import "github.com/ethereum/go-ethereum/rlp" +import "github.com/holiman/uint256" import "io" -func (obj *BlockAccessList) EncodeRLP(_w io.Writer) error { +func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) _tmp0 := w.List() + w.WriteBytes(obj.Address[:]) _tmp1 := w.List() - for _, _tmp2 := range obj.Accesses { + for _, _tmp2 := range obj.StorageChanges { _tmp3 := w.List() - w.WriteBytes(_tmp2.Address[:]) + w.WriteBytes(_tmp2.Slot[:]) _tmp4 := w.List() - for _, _tmp5 := range _tmp2.StorageWrites { + for _, _tmp5 := range _tmp2.Accesses { _tmp6 := w.List() - w.WriteBytes(_tmp5.Slot[:]) - _tmp7 := w.List() - for _, _tmp8 := range _tmp5.Accesses { - _tmp9 := w.List() - w.WriteUint64(uint64(_tmp8.TxIdx)) - w.WriteBytes(_tmp8.ValueAfter[:]) - w.ListEnd(_tmp9) - } - w.ListEnd(_tmp7) + w.WriteUint64(uint64(_tmp5.TxIdx)) + w.WriteBytes(_tmp5.ValueAfter[:]) w.ListEnd(_tmp6) } w.ListEnd(_tmp4) - _tmp10 := w.List() - for _, _tmp11 := range _tmp2.StorageReads { - w.WriteBytes(_tmp11[:]) - } - w.ListEnd(_tmp10) - _tmp12 := w.List() - for _, _tmp13 := range _tmp2.BalanceChanges { - _tmp14 := w.List() - w.WriteUint64(uint64(_tmp13.TxIdx)) - w.WriteBytes(_tmp13.Balance[:]) - w.ListEnd(_tmp14) - } - w.ListEnd(_tmp12) - _tmp15 := w.List() - for _, _tmp16 := range _tmp2.NonceChanges { - _tmp17 := w.List() - w.WriteUint64(uint64(_tmp16.TxIdx)) - w.WriteUint64(_tmp16.Nonce) - w.ListEnd(_tmp17) - } - w.ListEnd(_tmp15) - _tmp18 := w.List() - for _, _tmp19 := range _tmp2.Code { - _tmp20 := w.List() - w.WriteUint64(uint64(_tmp19.TxIndex)) - w.WriteBytes(_tmp19.Code) - w.ListEnd(_tmp20) - } - w.ListEnd(_tmp18) w.ListEnd(_tmp3) } w.ListEnd(_tmp1) + _tmp7 := w.List() + for _, _tmp8 := range obj.StorageReads { + w.WriteBytes(_tmp8[:]) + } + w.ListEnd(_tmp7) + _tmp9 := w.List() + for _, _tmp10 := range obj.BalanceChanges { + _tmp11 := w.List() + w.WriteUint64(uint64(_tmp10.TxIdx)) + if _tmp10.Balance == nil { + w.Write(rlp.EmptyString) + } else { + w.WriteUint256(_tmp10.Balance) + } + w.ListEnd(_tmp11) + } + w.ListEnd(_tmp9) + _tmp12 := w.List() + for _, _tmp13 := range obj.NonceChanges { + _tmp14 := w.List() + w.WriteUint64(uint64(_tmp13.TxIdx)) + w.WriteUint64(_tmp13.Nonce) + w.ListEnd(_tmp14) + } + w.ListEnd(_tmp12) + _tmp15 := w.List() + for _, _tmp16 := range obj.CodeChanges { + _tmp17 := w.List() + w.WriteUint64(uint64(_tmp16.TxIdx)) + w.WriteBytes(_tmp16.Code) + w.ListEnd(_tmp17) + } + w.ListEnd(_tmp15) w.ListEnd(_tmp0) return w.Flush() } -func (obj *BlockAccessList) DecodeRLP(dec *rlp.Stream) error { - var _tmp0 BlockAccessList +func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { + var _tmp0 AccountAccess { if _, err := dec.List(); err != nil { return err } - // Accesses: - var _tmp1 []AccountAccess + // Address: + var _tmp1 common.Address + if err := dec.ReadBytes(_tmp1[:]); err != nil { + return err + } + _tmp0.Address = _tmp1 + // StorageChanges: + var _tmp2 []encodingSlotWrites if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp2 AccountAccess + var _tmp3 encodingSlotWrites { if _, err := dec.List(); err != nil { return err } - // Address: - var _tmp3 [20]byte - if err := dec.ReadBytes(_tmp3[:]); err != nil { + // Slot: + var _tmp4 common.Hash + if err := dec.ReadBytes(_tmp4[:]); err != nil { return err } - _tmp2.Address = _tmp3 - // StorageWrites: - var _tmp4 []encodingSlotWrites + _tmp3.Slot = _tmp4 + // Accesses: + var _tmp5 []encodingStorageWrite if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp5 encodingSlotWrites - { - if _, err := dec.List(); err != nil { - return err - } - // Slot: - var _tmp6 [32]byte - if err := dec.ReadBytes(_tmp6[:]); err != nil { - return err - } - _tmp5.Slot = _tmp6 - // Accesses: - var _tmp7 []encodingStorageWrite - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp8 encodingStorageWrite - { - if _, err := dec.List(); err != nil { - return err - } - // TxIdx: - _tmp9, err := dec.Uint16() - if err != nil { - return err - } - _tmp8.TxIdx = _tmp9 - // ValueAfter: - var _tmp10 [32]byte - if err := dec.ReadBytes(_tmp10[:]); err != nil { - return err - } - _tmp8.ValueAfter = _tmp10 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp7 = append(_tmp7, _tmp8) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp5.Accesses = _tmp7 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp4 = append(_tmp4, _tmp5) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.StorageWrites = _tmp4 - // StorageReads: - var _tmp11 [][32]byte - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp12 [32]byte - if err := dec.ReadBytes(_tmp12[:]); err != nil { - return err - } - _tmp11 = append(_tmp11, _tmp12) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.StorageReads = _tmp11 - // BalanceChanges: - var _tmp13 []encodingBalanceChange - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp14 encodingBalanceChange + var _tmp6 encodingStorageWrite { if _, err := dec.List(); err != nil { return err } // TxIdx: - _tmp15, err := dec.Uint16() + _tmp7, err := dec.Uint16() if err != nil { return err } - _tmp14.TxIdx = _tmp15 - // Balance: - var _tmp16 [16]byte - if err := dec.ReadBytes(_tmp16[:]); err != nil { + _tmp6.TxIdx = _tmp7 + // ValueAfter: + var _tmp8 common.Hash + if err := dec.ReadBytes(_tmp8[:]); err != nil { return err } - _tmp14.Balance = _tmp16 + _tmp6.ValueAfter = _tmp8 if err := dec.ListEnd(); err != nil { return err } } - _tmp13 = append(_tmp13, _tmp14) + _tmp5 = append(_tmp5, _tmp6) } if err := dec.ListEnd(); err != nil { return err } - _tmp2.BalanceChanges = _tmp13 - // NonceChanges: - var _tmp17 []encodingAccountNonce - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp18 encodingAccountNonce - { - if _, err := dec.List(); err != nil { - return err - } - // TxIdx: - _tmp19, err := dec.Uint16() - if err != nil { - return err - } - _tmp18.TxIdx = _tmp19 - // Nonce: - _tmp20, err := dec.Uint64() - if err != nil { - return err - } - _tmp18.Nonce = _tmp20 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp17 = append(_tmp17, _tmp18) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.NonceChanges = _tmp17 - // Code: - var _tmp21 []CodeChange - if _, err := dec.List(); err != nil { - return err - } - for dec.MoreDataInList() { - var _tmp22 CodeChange - { - if _, err := dec.List(); err != nil { - return err - } - // TxIndex: - _tmp23, err := dec.Uint16() - if err != nil { - return err - } - _tmp22.TxIndex = _tmp23 - // Code: - _tmp24, err := dec.Bytes() - if err != nil { - return err - } - _tmp22.Code = _tmp24 - if err := dec.ListEnd(); err != nil { - return err - } - } - _tmp21 = append(_tmp21, _tmp22) - } - if err := dec.ListEnd(); err != nil { - return err - } - _tmp2.Code = _tmp21 + _tmp3.Accesses = _tmp5 if err := dec.ListEnd(); err != nil { return err } } - _tmp1 = append(_tmp1, _tmp2) + _tmp2 = append(_tmp2, _tmp3) } if err := dec.ListEnd(); err != nil { return err } - _tmp0.Accesses = _tmp1 + _tmp0.StorageChanges = _tmp2 + // StorageReads: + var _tmp9 []common.Hash + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp10 common.Hash + if err := dec.ReadBytes(_tmp10[:]); err != nil { + return err + } + _tmp9 = append(_tmp9, _tmp10) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.StorageReads = _tmp9 + // BalanceChanges: + var _tmp11 []encodingBalanceChange + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp12 encodingBalanceChange + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp13, err := dec.Uint16() + if err != nil { + return err + } + _tmp12.TxIdx = _tmp13 + // Balance: + var _tmp14 uint256.Int + if err := dec.ReadUint256(&_tmp14); err != nil { + return err + } + _tmp12.Balance = &_tmp14 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp11 = append(_tmp11, _tmp12) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.BalanceChanges = _tmp11 + // NonceChanges: + var _tmp15 []encodingAccountNonce + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp16 encodingAccountNonce + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp17, err := dec.Uint16() + if err != nil { + return err + } + _tmp16.TxIdx = _tmp17 + // Nonce: + _tmp18, err := dec.Uint64() + if err != nil { + return err + } + _tmp16.Nonce = _tmp18 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp15 = append(_tmp15, _tmp16) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.NonceChanges = _tmp15 + // CodeChanges: + var _tmp19 []CodeChange + if _, err := dec.List(); err != nil { + return err + } + for dec.MoreDataInList() { + var _tmp20 CodeChange + { + if _, err := dec.List(); err != nil { + return err + } + // TxIdx: + _tmp21, err := dec.Uint16() + if err != nil { + return err + } + _tmp20.TxIdx = _tmp21 + // Code: + _tmp22, err := dec.Bytes() + if err != nil { + return err + } + _tmp20.Code = _tmp22 + if err := dec.ListEnd(); err != nil { + return err + } + } + _tmp19 = append(_tmp19, _tmp20) + } + if err := dec.ListEnd(); err != nil { + return err + } + _tmp0.CodeChanges = _tmp19 if err := dec.ListEnd(); err != nil { return err } diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 29414e414e..062eb38c8c 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -60,10 +60,10 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { 1: 2, 2: 6, }, - CodeChange: &CodeChange{ - TxIndex: 0, - Code: common.Hex2Bytes("deadbeef"), - }, + CodeChanges: map[uint16]CodeChange{0: { + TxIdx: 0, + Code: common.Hex2Bytes("deadbeef"), + }}, }, common.BytesToAddress([]byte{0xff, 0xff, 0xff}): { StorageWrites: map[common.Hash]map[uint16]common.Hash{ @@ -102,10 +102,10 @@ func TestBALEncoding(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 10000000)); err != nil { t.Fatalf("decoding failed: %v\n", err) } - if dec.Hash() != bal.toEncodingObj().Hash() { + if dec.Hash() != bal.ToEncodingObj().Hash() { t.Fatalf("encoded block hash doesn't match decoded") } - if !equalBALs(bal.toEncodingObj(), &dec) { + if !equalBALs(bal.ToEncodingObj(), &dec) { t.Fatal("decoded BAL doesn't match") } } @@ -113,7 +113,7 @@ func TestBALEncoding(t *testing.T) { func makeTestAccountAccess(sort bool) AccountAccess { var ( storageWrites []encodingSlotWrites - storageReads [][32]byte + storageReads []common.Hash balances []encodingBalanceChange nonces []encodingAccountNonce ) @@ -144,7 +144,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { storageReads = append(storageReads, testrand.Hash()) } if sort { - slices.SortFunc(storageReads, func(a, b [32]byte) int { + slices.SortFunc(storageReads, func(a, b common.Hash) int { return bytes.Compare(a[:], b[:]) }) } @@ -152,7 +152,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { for i := 0; i < 5; i++ { balances = append(balances, encodingBalanceChange{ TxIdx: uint16(2 * i), - Balance: [16]byte(testrand.Bytes(16)), + Balance: new(uint256.Int).SetBytes(testrand.Bytes(32)), }) } if sort { @@ -175,14 +175,14 @@ func makeTestAccountAccess(sort bool) AccountAccess { return AccountAccess{ Address: [20]byte(testrand.Bytes(20)), - StorageWrites: storageWrites, + StorageChanges: storageWrites, StorageReads: storageReads, BalanceChanges: balances, NonceChanges: nonces, - Code: []CodeChange{ + CodeChanges: []CodeChange{ { - TxIndex: 100, - Code: testrand.Bytes(256), + TxIdx: 100, + Code: testrand.Bytes(256), }, }, } @@ -191,10 +191,10 @@ func makeTestAccountAccess(sort bool) AccountAccess { func makeTestBAL(sort bool) BlockAccessList { list := BlockAccessList{} for i := 0; i < 5; i++ { - list.Accesses = append(list.Accesses, makeTestAccountAccess(sort)) + list = append(list, makeTestAccountAccess(sort)) } if sort { - slices.SortFunc(list.Accesses, func(a, b AccountAccess) int { + slices.SortFunc(list, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) } @@ -214,7 +214,7 @@ func TestBlockAccessListCopy(t *testing.T) { } // Make sure the mutations on copy won't affect the origin - for _, aa := range cpyCpy.Accesses { + for _, aa := range cpyCpy { for i := 0; i < len(aa.StorageReads); i++ { aa.StorageReads[i] = [32]byte(testrand.Bytes(32)) } @@ -245,8 +245,11 @@ func TestBlockAccessListValidation(t *testing.T) { // Validate the derived block access list cBAL := makeTestConstructionBAL() - listB := cBAL.toEncodingObj() + listB := cBAL.ToEncodingObj() if err := listB.Validate(); err != nil { t.Fatalf("Unexpected validation error: %v", err) } } + +// BALReader test ideas +// * BAL which doesn't have any pre-tx system contracts should return an empty state diff at idx 0 diff --git a/core/types/block.go b/core/types/block.go index da9614793a..f115f14245 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -28,6 +28,8 @@ import ( "sync/atomic" "time" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rlp" @@ -106,6 +108,9 @@ type Header struct { // RequestsHash was added by EIP-7685 and is ignored in legacy headers. RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + + // BlockAccessListHash was added by EIP-7928 and is ignored in legacy headers. + BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` } // field type overrides for gencodec @@ -183,7 +188,8 @@ func (h *Header) EmptyReceipts() bool { type Body struct { Transactions []*Transaction Uncles []*Header - Withdrawals []*Withdrawal `rlp:"optional"` + Withdrawals []*Withdrawal `rlp:"optional"` + AccessList *bal.BlockAccessList `rlp:"optional,nil"` } // Block represents an Ethereum block. @@ -214,6 +220,8 @@ type Block struct { // that process it. witness *ExecutionWitness + accessList *bal.BlockAccessList + // caches hash atomic.Pointer[common.Hash] size atomic.Uint64 @@ -229,7 +237,8 @@ type extblock struct { Header *Header Txs []*Transaction Uncles []*Header - Withdrawals []*Withdrawal `rlp:"optional"` + Withdrawals []*Withdrawal `rlp:"optional"` + AccessList *bal.BlockAccessList `rlp:"optional"` } // NewBlock creates a new block. The input data is copied, changes to header and to the @@ -290,6 +299,12 @@ func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher b.withdrawals = slices.Clone(withdrawals) } + if body.AccessList != nil { + balHash := body.AccessList.Hash() + b.header.BlockAccessListHash = &balHash + b.accessList = body.AccessList + } + return b } @@ -339,7 +354,7 @@ func (b *Block) DecodeRLP(s *rlp.Stream) error { if err := s.Decode(&eb); err != nil { return err } - b.header, b.uncles, b.transactions, b.withdrawals = eb.Header, eb.Uncles, eb.Txs, eb.Withdrawals + b.header, b.uncles, b.transactions, b.withdrawals, b.accessList = eb.Header, eb.Uncles, eb.Txs, eb.Withdrawals, eb.AccessList b.size.Store(rlp.ListSize(size)) return nil } @@ -357,7 +372,7 @@ func (b *Block) EncodeRLP(w io.Writer) error { // Body returns the non-header content of the block. // Note the returned data is not an independent copy. func (b *Block) Body() *Body { - return &Body{b.transactions, b.uncles, b.withdrawals} + return &Body{b.transactions, b.uncles, b.withdrawals, b.accessList} } // Accessors for body data. These do not return a copy because the content @@ -508,6 +523,10 @@ func (b *Block) WithBody(body Body) *Block { withdrawals: slices.Clone(body.Withdrawals), witness: b.witness, } + if body.AccessList != nil { + balCopy := body.AccessList.Copy() + block.accessList = &balCopy + } for i := range body.Uncles { block.uncles[i] = CopyHeader(body.Uncles[i]) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 085b018e4c..43f01ba5cd 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -44,6 +44,12 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { cost = params.ColdSloadCostEIP2929 // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + if evm.Config.Tracer != nil && evm.Config.Tracer.OnColdStorageRead != nil { + // TODO: should these only be called if the cold storage read didn't go OOG? + // it's harder to implement, but I lean towards "yes". + // need to clarify this in the spec. + evm.Config.Tracer.OnColdStorageRead(contract.Address(), slot) + } } value := common.Hash(y.Bytes32()) @@ -123,6 +129,12 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { evm.StateDB.AddAddressToAccessList(addr) + + // TODO: same issue as OnColdSStorageRead. See the TODO above near OnColdStorageRead + if evm.Config.Tracer != nil && evm.Config.Tracer.OnColdAccountRead != nil { + evm.Config.Tracer.OnColdAccountRead(contract.Address()) + } + var overflow bool // We charge (cold-warm), since 'warm' is already charged as constantGas if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow { diff --git a/eth/api_debug.go b/eth/api_debug.go index 892e103213..2d1adb20e2 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -17,9 +17,11 @@ package eth import ( + "bytes" "context" "errors" "fmt" + "github.com/ethereum/go-ethereum/core/types/bal" "time" "github.com/ethereum/go-ethereum/common" @@ -505,7 +507,7 @@ func (api *DebugAPI) ExecutionWitness(bn rpc.BlockNumber) (*stateless.ExtWitness return &stateless.ExtWitness{}, fmt.Errorf("block number %v found, but parent missing", bn) } - result, err := bc.ProcessBlock(parent.Root, block, false, true) + result, err := bc.ProcessBlock(parent.Root, block, false, true, false, false) if err != nil { return nil, err } @@ -525,10 +527,40 @@ func (api *DebugAPI) ExecutionWitnessByHash(hash common.Hash) (*stateless.ExtWit return &stateless.ExtWitness{}, fmt.Errorf("block number %x found, but parent missing", hash) } - result, err := bc.ProcessBlock(parent.Root, block, false, true) + result, err := bc.ProcessBlock(parent.Root, block, false, true, false, false) if err != nil { return nil, err } return result.Witness().ToExtWitness(), nil } + +// GetBlockAccessList returns a block access list for the given number/hash +// or nil if one does not exist. +func (api *DebugAPI) GetBlockAccessList(number rpc.BlockNumberOrHash) (*bal.BlockAccessList, error) { + var block *types.Block + if num := number.BlockNumber; num != nil { + block = api.eth.blockchain.GetBlockByNumber(uint64(num.Int64())) + } else if hash := number.BlockHash; hash != nil { + block = api.eth.blockchain.GetBlockByHash(*hash) + } + + if block == nil { + return nil, fmt.Errorf("block not found") + } + return block.Body().AccessList, nil +} + +// GetEncodedBlockAccessList returns a block access list corresponding to a +// block number/hash in RLP-encoded form. It returns nil if one does not exist. +func (api *DebugAPI) GetEncodedBlockAccessList(number rpc.BlockNumberOrHash) ([]byte, error) { + bal, err := api.GetBlockAccessList(number) + if err != nil { + return nil, err + } + var enc bytes.Buffer + if err = bal.EncodeRLP(&enc); err != nil { + return nil, err + } + return enc.Bytes(), nil +} diff --git a/eth/backend.go b/eth/backend.go index 8509561822..a280dfdeeb 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -244,6 +244,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // - DATADIR/triedb/verkle.journal TrieJournalDirectory: stack.ResolvePath("triedb"), StateSizeTracking: config.EnableStateSizeTracking, + EnableBALForTesting: config.ExperimentalBAL, } ) if config.VMTrace != "" { diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 6020387bcd..afb4a762d1 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -183,6 +183,10 @@ type Config struct { // OverrideVerkle (TODO: remove after the fork) OverrideVerkle *uint64 `toml:",omitempty"` + + // ExperimentalBAL enables EIP-7928 block access list creation during execution + // of post Cancun blocks, and persistence via embedding the BAL in the block body. + ExperimentalBAL bool `toml:",omitempty"` } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 0aedffe230..2810d1e3a4 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -474,6 +474,16 @@ web3._extend({ params: 1, inputFormatter: [null], }), + new web3._extend.Method({ + name: 'getBlockAccessList', + call: 'debug_getBlockAccessList', + params: 1 + }), + new web3._extend.Method({ + name: 'getEncodedBlockAccessList', + call: 'debug_getEncodedBlockAccessList', + params: 1 + }), ], properties: [] }); diff --git a/miner/worker.go b/miner/worker.go index c0574eac23..059d3e8ddf 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -60,7 +60,8 @@ type environment struct { sidecars []*types.BlobTxSidecar blobs int - witness *stateless.Witness + witness *stateless.Witness + alTracer *core.BlockAccessListTracer } // txFits reports whether the transaction fits into the block size limit. @@ -134,6 +135,9 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay } } body := types.Body{Transactions: work.txs, Withdrawals: genParam.withdrawals} + if work.alTracer != nil { + body.AccessList = work.alTracer.AccessList().ToEncodingObj() + } allLogs := make([]*types.Log, 0) for _, r := range work.receipts { @@ -273,6 +277,11 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase } state.StartPrefetcher("miner", bundle, nil) } + vmConf := vm.Config{} + var alTracer *core.BlockAccessListTracer + if miner.chainConfig.IsAmsterdam(header.Number, header.Time) { + alTracer, vmConf.Tracer = core.NewBlockAccessListTracer() + } // Note the passed coinbase may be different with header.Coinbase. return &environment{ signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time), @@ -281,7 +290,8 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase coinbase: coinbase, header: header, witness: state.Witness(), - evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}), + evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vmConf), + alTracer: alTracer, }, nil } diff --git a/params/config.go b/params/config.go index ff27b69259..632cf32f85 100644 --- a/params/config.go +++ b/params/config.go @@ -1067,6 +1067,9 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork { // BlobConfig returns the blob config associated with the provided fork. func (c *ChainConfig) BlobConfig(fork forks.Fork) *BlobConfig { switch fork { + case forks.Amsterdam: + // TODO: (????) + return c.BlobScheduleConfig.BPO2 case forks.BPO5: return c.BlobScheduleConfig.BPO5 case forks.BPO4: @@ -1112,6 +1115,8 @@ func (c *ChainConfig) ActiveSystemContracts(time uint64) map[string]common.Addre // the fork isn't defined or isn't a time-based fork. func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { switch { + case fork == forks.Amsterdam: + return c.AmsterdamTime case fork == forks.BPO5: return c.BPO5Time case fork == forks.BPO4: