From d0c3069a032b9aa9467f78ef9ade152e8231700a Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Wed, 6 May 2026 12:45:16 +0530 Subject: [PATCH 01/19] chore: initial commit --- consensus/bor/contract/client.go | 38 ++- consensus/bor/contract/reserved_registry.go | 320 ++++++++++++++++++ .../bor/contract/reserved_registry_test.go | 46 +++ consensus/misc/eip1559/eip1559_test.go | 1 + core/genesis.go | 46 ++- core/genesis_test.go | 39 +++ internal/ethapi/bor_api.go | 5 +- params/config.go | 27 +- params/reserved_blockspace.go | 186 ++++++++++ 9 files changed, 686 insertions(+), 22 deletions(-) create mode 100644 consensus/bor/contract/reserved_registry.go create mode 100644 consensus/bor/contract/reserved_registry_test.go create mode 100644 params/reserved_blockspace.go diff --git a/consensus/bor/contract/client.go b/consensus/bor/contract/client.go index 1965c3b157..3e5748163f 100644 --- a/consensus/bor/contract/client.go +++ b/consensus/bor/contract/client.go @@ -29,6 +29,7 @@ var SystemTxGas = (hexutil.Uint64)(math.MaxUint64 / 2) var ( vABI, _ = abi.JSON(strings.NewReader(validatorsetABI)) sABI, _ = abi.JSON(strings.NewReader(stateReceiverABI)) + rABI, _ = abi.JSON(strings.NewReader(reservedRegistryABI)) ) func ValidatorSet() abi.ABI { @@ -39,18 +40,25 @@ func StateReceiver() abi.ABI { return sABI } +func ReservedBlockspaceRegistry() abi.ABI { + return rABI +} + type GenesisContractsClient struct { - validatorSetABI abi.ABI - stateReceiverABI abi.ABI - ValidatorContract string - StateReceiverContract string - chainConfig *params.ChainConfig - ethAPI api.Caller + validatorSetABI abi.ABI + stateReceiverABI abi.ABI + reservedRegistryABI abi.ABI + ValidatorContract string + StateReceiverContract string + ReservedRegistryContract string + chainConfig *params.ChainConfig + ethAPI api.Caller } const ( - validatorsetABI = `[{"constant":true,"inputs":[],"name":"SPRINT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SYSTEM_ADDRESS","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"CHAIN","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"FIRST_END_BLOCK","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"producers","outputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ROUND_TYPE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"BOR_ID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"spanNumbers","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"VOTE_TYPE","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"validators","outputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"spans","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"startBlock","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"endBlock","type":"uint256"}],"name":"NewSpan","type":"event"},{"constant":true,"inputs":[],"name":"currentSprint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNextSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"number","type":"uint256"}],"name":"getSpanByBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"currentSpanNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getValidatorsTotalStakeBySpan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getProducersTotalStakeBySpan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"getValidatorBySigner","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"internalType":"struct BorValidatorSet.Validator","name":"result","type":"tuple"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"isValidator","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"isProducer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"signer","type":"address"}],"name":"isCurrentValidator","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"signer","type":"address"}],"name":"isCurrentProducer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"number","type":"uint256"}],"name":"getBorValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitialValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"newSpan","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"},{"internalType":"bytes","name":"validatorBytes","type":"bytes"},{"internalType":"bytes","name":"producerBytes","type":"bytes"}],"name":"commitSpan","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bytes","name":"sigs","type":"bytes"}],"name":"getStakePowerBySigs","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"rootHash","type":"bytes32"},{"internalType":"bytes32","name":"leaf","type":"bytes32"},{"internalType":"bytes","name":"proof","type":"bytes"}],"name":"checkMembership","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"d","type":"bytes32"}],"name":"leafNode","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"left","type":"bytes32"},{"internalType":"bytes32","name":"right","type":"bytes32"}],"name":"innerNode","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"}]` - stateReceiverABI = `[{"constant":true,"inputs":[],"name":"SYSTEM_ADDRESS","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"lastStateId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"syncTime","type":"uint256"},{"internalType":"bytes","name":"recordBytes","type":"bytes"}],"name":"commitState","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]` + validatorsetABI = `[{"constant":true,"inputs":[],"name":"SPRINT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SYSTEM_ADDRESS","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"CHAIN","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"FIRST_END_BLOCK","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"producers","outputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ROUND_TYPE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"BOR_ID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"spanNumbers","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"VOTE_TYPE","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"validators","outputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"spans","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"startBlock","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"endBlock","type":"uint256"}],"name":"NewSpan","type":"event"},{"constant":true,"inputs":[],"name":"currentSprint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNextSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"number","type":"uint256"}],"name":"getSpanByBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"currentSpanNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getValidatorsTotalStakeBySpan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getProducersTotalStakeBySpan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"getValidatorBySigner","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"internalType":"struct BorValidatorSet.Validator","name":"result","type":"tuple"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"isValidator","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"isProducer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"signer","type":"address"}],"name":"isCurrentValidator","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"signer","type":"address"}],"name":"isCurrentProducer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"number","type":"uint256"}],"name":"getBorValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitialValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"newSpan","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"},{"internalType":"bytes","name":"validatorBytes","type":"bytes"},{"internalType":"bytes","name":"producerBytes","type":"bytes"}],"name":"commitSpan","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bytes","name":"sigs","type":"bytes"}],"name":"getStakePowerBySigs","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"rootHash","type":"bytes32"},{"internalType":"bytes32","name":"leaf","type":"bytes32"},{"internalType":"bytes","name":"proof","type":"bytes"}],"name":"checkMembership","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"d","type":"bytes32"}],"name":"leafNode","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"left","type":"bytes32"},{"internalType":"bytes32","name":"right","type":"bytes32"}],"name":"innerNode","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"}]` + stateReceiverABI = `[{"constant":true,"inputs":[],"name":"SYSTEM_ADDRESS","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"lastStateId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"syncTime","type":"uint256"},{"internalType":"bytes","name":"recordBytes","type":"bytes"}],"name":"commitState","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]` + reservedRegistryABI = `[{"inputs":[{"name":"account","type":"address"}],"name":"isReservedAddress","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"account","type":"address"}],"name":"getClientForAddress","outputs":[{"name":"clientId","type":"uint256"},{"name":"gasQuota","type":"uint64"},{"name":"admin","type":"address"},{"name":"active","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"clientId","type":"uint256"}],"name":"getClient","outputs":[{"name":"admin","type":"address"},{"name":"gasQuota","type":"uint64"},{"name":"active","type":"bool"},{"name":"metadata","type":"string"},{"name":"addressCount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"clientId","type":"uint256"}],"name":"getClientAddresses","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getWhitelistedAddresses","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getReservedClients","outputs":[{"name":"clientIds","type":"uint256[]"},{"name":"admins","type":"address[]"},{"name":"gasQuotas","type":"uint64[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalReservedGas","outputs":[{"name":"","type":"uint64"}],"stateMutability":"view","type":"function"}]` ) func NewGenesisContractsClient( @@ -60,12 +68,14 @@ func NewGenesisContractsClient( ethAPI api.Caller, ) *GenesisContractsClient { return &GenesisContractsClient{ - validatorSetABI: ValidatorSet(), - stateReceiverABI: StateReceiver(), - ValidatorContract: validatorContract, - StateReceiverContract: stateReceiverContract, - chainConfig: chainConfig, - ethAPI: ethAPI, + validatorSetABI: ValidatorSet(), + stateReceiverABI: StateReceiver(), + reservedRegistryABI: ReservedBlockspaceRegistry(), + ValidatorContract: validatorContract, + StateReceiverContract: stateReceiverContract, + ReservedRegistryContract: chainConfig.Bor.ReservedRegistryContract, + chainConfig: chainConfig, + ethAPI: ethAPI, } } diff --git a/consensus/bor/contract/reserved_registry.go b/consensus/bor/contract/reserved_registry.go new file mode 100644 index 0000000000..5518a9872f --- /dev/null +++ b/consensus/bor/contract/reserved_registry.go @@ -0,0 +1,320 @@ +package contract + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/rpc" +) + +var errReservedRegistryNotConfigured = errors.New("reserved blockspace registry contract is not configured") + +type ReservedClientLookup struct { + ClientID *big.Int + GasQuota uint64 + Admin common.Address + Active bool +} + +type ReservedClient struct { + ClientID *big.Int + Admin common.Address + GasQuota uint64 + Active bool + Metadata string + Addresses []common.Address +} + +func (gc *GenesisContractsClient) HasReservedRegistry() bool { + if gc == nil || gc.ReservedRegistryContract == "" { + return false + } + return common.HexToAddress(gc.ReservedRegistryContract) != (common.Address{}) +} + +func (gc *GenesisContractsClient) IsReservedAddress( + state *state.StateDB, + number uint64, + hash common.Hash, + account common.Address, +) (bool, error) { + if !gc.HasReservedRegistry() { + return false, nil + } + + values, err := gc.callReservedRegistry(state, number, hash, "isReservedAddress", account) + if err != nil { + return false, err + } + if len(values) != 1 { + return false, fmt.Errorf("reserved registry isReservedAddress returned %d values", len(values)) + } + + reserved, ok := values[0].(bool) + if !ok { + return false, fmt.Errorf("reserved registry isReservedAddress returned %T", values[0]) + } + + return reserved, nil +} + +func (gc *GenesisContractsClient) ReservedClientForAddress( + state *state.StateDB, + number uint64, + hash common.Hash, + account common.Address, +) (ReservedClientLookup, error) { + if !gc.HasReservedRegistry() { + return ReservedClientLookup{ClientID: new(big.Int)}, nil + } + + values, err := gc.callReservedRegistry(state, number, hash, "getClientForAddress", account) + if err != nil { + return ReservedClientLookup{}, err + } + if len(values) != 4 { + return ReservedClientLookup{}, fmt.Errorf("reserved registry getClientForAddress returned %d values", len(values)) + } + + clientID, ok := values[0].(*big.Int) + if !ok { + return ReservedClientLookup{}, fmt.Errorf("reserved registry client id has type %T", values[0]) + } + gasQuota, ok := values[1].(uint64) + if !ok { + return ReservedClientLookup{}, fmt.Errorf("reserved registry gas quota has type %T", values[1]) + } + admin, ok := values[2].(common.Address) + if !ok { + return ReservedClientLookup{}, fmt.Errorf("reserved registry admin has type %T", values[2]) + } + active, ok := values[3].(bool) + if !ok { + return ReservedClientLookup{}, fmt.Errorf("reserved registry active flag has type %T", values[3]) + } + + return ReservedClientLookup{ + ClientID: new(big.Int).Set(clientID), + GasQuota: gasQuota, + Admin: admin, + Active: active, + }, nil +} + +func (gc *GenesisContractsClient) ReservedClientByID( + state *state.StateDB, + number uint64, + hash common.Hash, + clientID *big.Int, +) (ReservedClient, error) { + if !gc.HasReservedRegistry() { + return ReservedClient{}, errReservedRegistryNotConfigured + } + if clientID == nil { + return ReservedClient{}, errors.New("reserved registry client id is nil") + } + + values, err := gc.callReservedRegistry(state, number, hash, "getClient", clientID) + if err != nil { + return ReservedClient{}, err + } + if len(values) != 5 { + return ReservedClient{}, fmt.Errorf("reserved registry getClient returned %d values", len(values)) + } + + admin, ok := values[0].(common.Address) + if !ok { + return ReservedClient{}, fmt.Errorf("reserved registry admin has type %T", values[0]) + } + gasQuota, ok := values[1].(uint64) + if !ok { + return ReservedClient{}, fmt.Errorf("reserved registry gas quota has type %T", values[1]) + } + active, ok := values[2].(bool) + if !ok { + return ReservedClient{}, fmt.Errorf("reserved registry active flag has type %T", values[2]) + } + metadata, ok := values[3].(string) + if !ok { + return ReservedClient{}, fmt.Errorf("reserved registry metadata has type %T", values[3]) + } + + addresses, err := gc.ClientAddresses(state, number, hash, clientID) + if err != nil { + return ReservedClient{}, err + } + + return ReservedClient{ + ClientID: new(big.Int).Set(clientID), + Admin: admin, + GasQuota: gasQuota, + Active: active, + Metadata: metadata, + Addresses: addresses, + }, nil +} + +func (gc *GenesisContractsClient) ClientAddresses( + state *state.StateDB, + number uint64, + hash common.Hash, + clientID *big.Int, +) ([]common.Address, error) { + if !gc.HasReservedRegistry() { + return nil, errReservedRegistryNotConfigured + } + if clientID == nil { + return nil, errors.New("reserved registry client id is nil") + } + + values, err := gc.callReservedRegistry(state, number, hash, "getClientAddresses", clientID) + if err != nil { + return nil, err + } + if len(values) != 1 { + return nil, fmt.Errorf("reserved registry getClientAddresses returned %d values", len(values)) + } + + addresses, ok := values[0].([]common.Address) + if !ok { + return nil, fmt.Errorf("reserved registry addresses have type %T", values[0]) + } + + return addresses, nil +} + +func (gc *GenesisContractsClient) WhitelistedAddresses( + state *state.StateDB, + number uint64, + hash common.Hash, +) ([]common.Address, error) { + if !gc.HasReservedRegistry() { + return nil, nil + } + + values, err := gc.callReservedRegistry(state, number, hash, "getWhitelistedAddresses") + if err != nil { + return nil, err + } + if len(values) != 1 { + return nil, fmt.Errorf("reserved registry getWhitelistedAddresses returned %d values", len(values)) + } + + addresses, ok := values[0].([]common.Address) + if !ok { + return nil, fmt.Errorf("reserved registry whitelist has type %T", values[0]) + } + + return addresses, nil +} + +func (gc *GenesisContractsClient) ReservedClients( + state *state.StateDB, + number uint64, + hash common.Hash, +) ([]ReservedClientLookup, error) { + if !gc.HasReservedRegistry() { + return nil, nil + } + + values, err := gc.callReservedRegistry(state, number, hash, "getReservedClients") + if err != nil { + return nil, err + } + if len(values) != 3 { + return nil, fmt.Errorf("reserved registry getReservedClients returned %d values", len(values)) + } + + clientIDs, ok := values[0].([]*big.Int) + if !ok { + return nil, fmt.Errorf("reserved registry client ids have type %T", values[0]) + } + admins, ok := values[1].([]common.Address) + if !ok { + return nil, fmt.Errorf("reserved registry admins have type %T", values[1]) + } + gasQuotas, ok := values[2].([]uint64) + if !ok { + return nil, fmt.Errorf("reserved registry quotas have type %T", values[2]) + } + if len(clientIDs) != len(admins) || len(clientIDs) != len(gasQuotas) { + return nil, fmt.Errorf("reserved registry returned mismatched client slices: ids=%d admins=%d quotas=%d", len(clientIDs), len(admins), len(gasQuotas)) + } + + clients := make([]ReservedClientLookup, len(clientIDs)) + for i := range clientIDs { + clients[i] = ReservedClientLookup{ + ClientID: new(big.Int).Set(clientIDs[i]), + GasQuota: gasQuotas[i], + Admin: admins[i], + Active: true, + } + } + + return clients, nil +} + +func (gc *GenesisContractsClient) TotalReservedGas( + state *state.StateDB, + number uint64, + hash common.Hash, +) (uint64, error) { + if !gc.HasReservedRegistry() { + return 0, nil + } + + values, err := gc.callReservedRegistry(state, number, hash, "totalReservedGas") + if err != nil { + return 0, err + } + if len(values) != 1 { + return 0, fmt.Errorf("reserved registry totalReservedGas returned %d values", len(values)) + } + + total, ok := values[0].(uint64) + if !ok { + return 0, fmt.Errorf("reserved registry totalReservedGas returned %T", values[0]) + } + + return total, nil +} + +func (gc *GenesisContractsClient) callReservedRegistry( + state *state.StateDB, + number uint64, + hash common.Hash, + method string, + args ...interface{}, +) ([]interface{}, error) { + if !gc.HasReservedRegistry() { + return nil, errReservedRegistryNotConfigured + } + if gc.ethAPI == nil { + return nil, errors.New("reserved registry eth api is nil") + } + + data, err := gc.reservedRegistryABI.Pack(method, args...) + if err != nil { + return nil, err + } + + msgData := (hexutil.Bytes)(data) + toAddress := common.HexToAddress(gc.ReservedRegistryContract) + blockNr := rpc.BlockNumber(number) + result, err := gc.ethAPI.CallWithState(ethapi.WithBorInternalCall(context.Background()), ethapi.TransactionArgs{ + Gas: &SystemTxGas, + To: &toAddress, + Data: &msgData, + }, &rpc.BlockNumberOrHash{BlockNumber: &blockNr, BlockHash: &hash}, state, nil, nil) + if err != nil { + return nil, err + } + + return gc.reservedRegistryABI.Unpack(method, result) +} diff --git a/consensus/bor/contract/reserved_registry_test.go b/consensus/bor/contract/reserved_registry_test.go new file mode 100644 index 0000000000..75707ebf21 --- /dev/null +++ b/consensus/bor/contract/reserved_registry_test.go @@ -0,0 +1,46 @@ +package contract + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestReservedRegistryABIUnpacksBorFacingViews(t *testing.T) { + registryABI := ReservedBlockspaceRegistry() + + clientOutput, err := registryABI.Methods["getClient"].Outputs.Pack( + common.HexToAddress("0x1"), + uint64(20_000_000), + true, + "Polymarket", + big.NewInt(3), + ) + if err != nil { + t.Fatal(err) + } + clientValues, err := registryABI.Unpack("getClient", clientOutput) + if err != nil { + t.Fatal(err) + } + if _, ok := clientValues[1].(uint64); !ok { + t.Fatalf("expected uint64 gas quota, got %T", clientValues[1]) + } + + clientsOutput, err := registryABI.Methods["getReservedClients"].Outputs.Pack( + []*big.Int{big.NewInt(1)}, + []common.Address{common.HexToAddress("0x2")}, + []uint64{20_000_000}, + ) + if err != nil { + t.Fatal(err) + } + clientsValues, err := registryABI.Unpack("getReservedClients", clientsOutput) + if err != nil { + t.Fatal(err) + } + if _, ok := clientsValues[2].([]uint64); !ok { + t.Fatalf("expected []uint64 gas quotas, got %T", clientsValues[2]) + } +} diff --git a/consensus/misc/eip1559/eip1559_test.go b/consensus/misc/eip1559/eip1559_test.go index a8af74efe8..40db856e02 100644 --- a/consensus/misc/eip1559/eip1559_test.go +++ b/consensus/misc/eip1559/eip1559_test.go @@ -56,6 +56,7 @@ func copyConfig(original *params.ChainConfig) *params.ChainConfig { BackupMultiplier: original.Bor.BackupMultiplier, ValidatorContract: original.Bor.ValidatorContract, StateReceiverContract: original.Bor.StateReceiverContract, + ReservedRegistryContract: original.Bor.ReservedRegistryContract, OverrideStateSyncRecords: original.Bor.OverrideStateSyncRecords, OverrideStateSyncRecordsInRange: original.Bor.OverrideStateSyncRecordsInRange, BlockAlloc: original.Bor.BlockAlloc, diff --git a/core/genesis.go b/core/genesis.go index 20e3d8e8a2..283c34c70a 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -480,13 +480,49 @@ func (g *Genesis) IsVerkle() bool { // ToBlock returns the genesis block according to genesis specification. func (g *Genesis) ToBlock() *types.Block { - root, err := hashAlloc(&g.Alloc, g.IsVerkle()) + alloc, err := g.allocWithBlockZeroAlloc() + if err != nil { + panic(err) + } + root, err := hashAlloc(&alloc, g.IsVerkle()) if err != nil { panic(err) } return g.toBlockWithRoot(root) } +func (g *Genesis) allocWithBlockZeroAlloc() (types.GenesisAlloc, error) { + if g == nil || g.Config == nil || g.Config.Bor == nil || g.Config.Bor.BlockAlloc == nil { + return g.Alloc, nil + } + + blockZeroAlloc, ok := g.Config.Bor.BlockAlloc["0"] + if !ok { + return g.Alloc, nil + } + + merged := make(types.GenesisAlloc, len(g.Alloc)) + for address, account := range g.Alloc { + merged[address] = account + } + + blob, err := json.Marshal(blockZeroAlloc) + if err != nil { + return nil, err + } + + var alloc types.GenesisAlloc + if err := json.Unmarshal(blob, &alloc); err != nil { + return nil, err + } + + for address, account := range alloc { + merged[address] = account + } + + return merged, nil +} + // toBlockWithRoot constructs the genesis block with the given genesis state root. func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { head := &types.Header{ @@ -575,15 +611,19 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database) (*types.Blo if config.Clique != nil && len(g.ExtraData) < 32+crypto.SignatureLength { return nil, errors.New("can't start clique chain without signers") } + alloc, err := g.allocWithBlockZeroAlloc() + if err != nil { + return nil, err + } // flush the data to disk and compute the state root - root, err := flushAlloc(&g.Alloc, triedb) + root, err := flushAlloc(&alloc, triedb) if err != nil { return nil, err } block := g.toBlockWithRoot(root) // Marshal the genesis state specification and persist. - blob, err := json.Marshal(g.Alloc) + blob, err := json.Marshal(alloc) if err != nil { return nil, err } diff --git a/core/genesis_test.go b/core/genesis_test.go index 0502e252c7..c1bc13649c 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -40,6 +40,45 @@ func TestSetupGenesis(t *testing.T) { testSetupGenesis(t, rawdb.PathScheme) } +func TestGenesisBlockZeroBlockAlloc(t *testing.T) { + db := rawdb.NewMemoryDatabase() + tdb := triedb.NewDatabase(db, newDbConfig(rawdb.HashScheme)) + addr := common.HexToAddress(params.DefaultReservedRegistryContract) + + genesis := &Genesis{ + Config: ¶ms.ChainConfig{ + ChainID: big.NewInt(1337), + Bor: ¶ms.BorConfig{ + BlockAlloc: map[string]interface{}{ + "0": map[string]interface{}{ + addr.Hex(): map[string]interface{}{ + "balance": "0x0", + "code": "0x6000", + }, + }, + }, + }, + }, + } + + block := genesis.MustCommit(db, tdb) + if block.Hash() != genesis.ToBlock().Hash() { + t.Fatalf("genesis hash mismatch after applying block zero alloc") + } + + stored, err := ReadGenesis(db) + if err != nil { + t.Fatal(err) + } + account, ok := stored.Alloc[addr] + if !ok { + t.Fatalf("expected block zero alloc account %s in stored genesis", addr) + } + if !bytes.Equal(account.Code, common.FromHex("0x6000")) { + t.Fatalf("unexpected block zero alloc code: %x", account.Code) + } +} + func testSetupGenesis(t *testing.T, scheme string) { var ( customghash = common.HexToHash("0x89c99d90b79719238d2645c7642f2c9295246e80775b38cfd162b696817fbd50") diff --git a/internal/ethapi/bor_api.go b/internal/ethapi/bor_api.go index cb3377bdbd..ac6a538e26 100644 --- a/internal/ethapi/bor_api.go +++ b/internal/ethapi/bor_api.go @@ -76,7 +76,10 @@ func isBorSystemTx(borCfg *params.BorConfig, to *common.Address) bool { validatorContract := common.HexToAddress(borCfg.ValidatorContract) stateReceiverContract := common.HexToAddress(borCfg.StateReceiverContract) - if to.Cmp(validatorContract) == 0 || to.Cmp(stateReceiverContract) == 0 { + reservedRegistryContract := common.HexToAddress(borCfg.ReservedRegistryContract) + if to.Cmp(validatorContract) == 0 || + to.Cmp(stateReceiverContract) == 0 || + (borCfg.ReservedRegistryContract != "" && to.Cmp(reservedRegistryContract) == 0) { return true } diff --git a/params/config.go b/params/config.go index b03fdbd325..12cf84d0f4 100644 --- a/params/config.go +++ b/params/config.go @@ -191,11 +191,20 @@ var ( BackupMultiplier: map[string]uint64{ "0": 2, }, - ValidatorContract: "0x0000000000000000000000000000000000001000", - StateReceiverContract: "0x0000000000000000000000000000000000001001", + ValidatorContract: "0x0000000000000000000000000000000000001000", + StateReceiverContract: "0x0000000000000000000000000000000000001001", + ReservedRegistryContract: DefaultReservedRegistryContract, BurntContract: map[string]string{ "0": "0x00000000000000000000000000000000000000000", }, + BlockAlloc: map[string]interface{}{ + "0": map[string]interface{}{ + DefaultReservedRegistryContract: map[string]interface{}{ + "balance": "0x0", + "code": ReservedBlockspaceRegistryCode, + }, + }, + }, }, ShanghaiBlock: big.NewInt(0), CancunBlock: big.NewInt(0), @@ -231,11 +240,20 @@ var ( Coinbase: map[string]string{ "0": "0x000000000000000000000000000000000000ba5e", }, - ValidatorContract: "0x0000000000000000000000000000000000001000", - StateReceiverContract: "0x0000000000000000000000000000000000001001", + ValidatorContract: "0x0000000000000000000000000000000000001000", + StateReceiverContract: "0x0000000000000000000000000000000000001001", + ReservedRegistryContract: DefaultReservedRegistryContract, BurntContract: map[string]string{ "0": "0x00000000000000000000000000000000000000000", }, + BlockAlloc: map[string]interface{}{ + "0": map[string]interface{}{ + DefaultReservedRegistryContract: map[string]interface{}{ + "balance": "0x0", + "code": ReservedBlockspaceRegistryCode, + }, + }, + }, }, } @@ -919,6 +937,7 @@ type BorConfig struct { BackupMultiplier map[string]uint64 `json:"backupMultiplier"` // Backup multiplier to determine the wiggle time ValidatorContract string `json:"validatorContract"` // Validator set contract StateReceiverContract string `json:"stateReceiverContract"` // State receiver contract + ReservedRegistryContract string `json:"reservedRegistryContract"` // Reserved blockspace registry contract OverrideStateSyncRecords map[string]int `json:"overrideStateSyncRecords"` // override state records count OverrideStateSyncRecordsInRange []BlockRangeOverride `json:"overrideStateSyncRecordsInRange"` // override state records count in a given block range OverrideValidatorSetInRange []BlockRangeOverrideValidatorSet `json:"overrideValidatorSetInRange"` // override validator set in a given block range diff --git a/params/reserved_blockspace.go b/params/reserved_blockspace.go new file mode 100644 index 0000000000..a0d3f606a4 --- /dev/null +++ b/params/reserved_blockspace.go @@ -0,0 +1,186 @@ +package params + +const ( + DefaultReservedRegistryContract = "0x0000000000000000000000000000000000001002" + + // ReservedBlockspaceRegistryCode is the deployed runtime bytecode of + // registry-contract/src/ReservedBlockspaceRegistry.sol. + ReservedBlockspaceRegistryCode = "0x" + + "608060405234801561000f575f5ffd5b5060043610610153575f3560e01c806393dc1052116100bf578063d0068f8011" + + "610079578063d0068f8014610340578063e3ab7a2914610364578063e541c71e14610387578063ea38cd531461038f57" + + "8063f2fde38b146103a2578063feafde83146103b5575f5ffd5b806393dc1052146102ca578063989a8366146102dd57" + + "8063b98330e6146102f0578063bbae06db14610307578063c242867b1461031a578063cc2763261461032d575f5ffd5b" + + "806362ae1e941161011057806362ae1e94146102265780636d0280271461025c5780636f78bb36146102715780637816" + + "e2ed1461028457806388b45de51461028d5780638da5cb5b146102a0575f5ffd5b80630315818f146101575780630ee6" + + "7c8b1461018d578063151d6b0c146101a057806318d93a34146101eb5780633f34c51414610200578063469d1e381461" + + "0213575b5f5ffd5b5f5461017090600160a01b90046001600160401b031681565b6040516001600160401b0390911681" + + "526020015b60405180910390f35b600154610170906001600160401b031681565b6101b36101ae366004611a3e565b61" + + "03c8565b604080519485526001600160401b0390931660208501526001600160a01b0390911691830191909152151560" + + "60820152608001610184565b6101fe6101f9366004611a9b565b610439565b005b6101fe61020e366004611af8565b61" + + "050e565b6101fe610221366004611b69565b610670565b61024e610234366004611a3e565b6001600160a01b03165f90" + + "81526005602052604090205490565b604051908152602001610184565b610264610732565b6040516101849190611be6" + + "565b61026461027f366004611bf8565b6108ad565b61024e60025481565b6101fe61029b366004611c0f565b61091956" + + "5b5f546102b2906001600160a01b031681565b6040516001600160a01b039091168152602001610184565b6003546101" + + "70906001600160401b031681565b6101fe6102eb366004611c41565b610aa8565b6102f8610b89565b60405161018493" + + "929190611c81565b6101fe610315366004611d1d565b610d8c565b6101fe610328366004611d3e565b610e63565b6102" + + "4e61033b366004611d5f565b610fb8565b61035361034e366004611bf8565b611169565b604051610184959493929190" + + "611ded565b610377610372366004611a3e565b61124e565b6040519015158152602001610184565b61024e611290565b" + + "6101fe61039d366004611d1d565b6112b3565b6101fe6103b0366004611a3e565b6113d6565b6101fe6103c336600461" + + "1d1d565b6114a8565b6001600160a01b0381165f908152600560205260408120549080808381036103fa57505f925082" + + "915081905080610432565b5050505f81815260046020526040902054600160a01b81046001600160401b031690600160" + + "0160a01b03811690600160e01b900460ff165b9193509193565b5f546001600160a01b0316610461576040516321c4e3" + + "5760e21b815260040160405180910390fd5b825f61046c82611530565b5f549091506001600160a01b03163314801590" + + "610493575080546001600160a01b03163314155b156104b1576040516301940db560e51b815260040160405180910390" + + "fd5b5f8581526004602052604090206001016104cc848683611ef0565b50847f3cf32b7851a39f3e6ced7e2df88eee32" + + "24b27dbf667c3c92c9cd43a6a91ca39085856040516104ff929190611fd2565b60405180910390a25050505050565b5f" + + "546001600160a01b0316610536576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b" + + "03163314610560576040516330cd747160e01b815260040160405180910390fd5b61056a828261156a565b6003546001" + + "600160401b03808416911611156105995760405163ad7e403f60e01b815260040160405180910390fd5b60015b600254" + + "8110156105ef575f818152600460205260409020546001600160401b03808416600160a01b9092041611156105e75760" + + "405163dbf8c09d60e01b815260040160405180910390fd5b60010161059c565b505f805467ffffffffffffffff60a01b" + + "1916600160a01b6001600160401b03858116918202929092179092556001805467ffffffffffffffff19169184169182" + + "1790556040805192835260208301919091527f24d48e187a24f49e3551ce9d77a89e2a5959da2a1932f86ebab7b31684" + + "83ac1b910160405180910390a15050565b5f546001600160a01b0316610698576040516321c4e35760e21b8152600401" + + "60405180910390fd5b825f6106a382611530565b5f549091506001600160a01b031633148015906106ca575080546001" + + "600160a01b03163314155b156106e8576040516301940db560e51b815260040160405180910390fd5b5f5b8381101561" + + "072a576107228686868481811061070857610708611fed565b905060200201602081019061071d9190611a3e565b6115" + + "c1565b6001016106ea565b505050505050565b60605f805b6006548110156107b55760045f60055f6006858154811061" + + "075a5761075a611fed565b5f9182526020808320909101546001600160a01b0316835282810193909352604091820181" + + "2054845291830193909352910190205460ff600160e01b90910416156107ad57816107a981612015565b9250505b6001" + + "01610737565b505f816001600160401b038111156107cf576107cf611e51565b60405190808252806020026020018201" + + "60405280156107f8578160200160208202803683370190505b5090505f805b6006548110156108a4575f600682815481" + + "1061081c5761081c611fed565b5f9182526020808320909101546001600160a01b031680835260058252604080842054" + + "8452600490925291205490915060ff600160e01b909104161561089b5780848461086881612015565b95508151811061" + + "087a5761087a611fed565b60200260200101906001600160a01b031690816001600160a01b0316815250505b50600101" + + "6107fe565b50909392505050565b60606108b882611530565b6002018054806020026020016040519081016040528092" + + "9190818152602001828054801561090d57602002820191905f5260205f20905b81546001600160a01b03168152600190" + + "9101906020018083116108ef575b50505050509050919050565b5f546001600160a01b0316610941576040516321c4e3" + + "5760e21b815260040160405180910390fd5b5f546001600160a01b0316331461096b576040516330cd747160e01b8152" + + "60040160405180910390fd5b5f61097583611530565b8054909150821515600160e01b90910460ff1615150361099457" + + "505050565b8115610a065780546109b690600160a01b90046001600160401b03165f611706565b805460038054600160" + + "0160401b03600160a01b9093048316925f916109dd9185911661202d565b92506101000a8154816001600160401b0302" + + "191690836001600160401b03160217905550610a52565b8054600380546001600160401b03600160a01b909304831692" + + "5f91610a2d91859116612052565b92506101000a8154816001600160401b0302191690836001600160401b0316021790" + + "55505b805460ff60e01b1916600160e01b83151590810291909117825560405190815283907f81cdb3516fac5fa15a5e" + + "6738774f6eab2657ef4b6f6486451655eb882dc202cd906020015b60405180910390a2505b5050565b5f546001600160" + + "a01b031615610ad05760405162dc149f60e41b815260040160405180910390fd5b6001600160a01b038316610af75760" + + "405163d92e233d60e01b815260040160405180910390fd5b610b01828261156a565b5f80546001600160a01b03851660" + + "01600160e01b03199091168117600160a01b6001600160401b03868116918202929092179093556001805467ffffffff" + + "ffffffff1916918516918217815560025560408051938452602084019190915290917f6fe9ef307ea9c9e4c7e9dd8df7" + + "afa1a49139e88d2273cbb5b2c3a939bc19eb029101610a9a565b606080805f60015b600254811015610bce575f818152" + + "60046020526040902054600160e01b900460ff1615610bc65781610bc281612015565b9250505b600101610b91565b50" + + "806001600160401b03811115610be757610be7611e51565b604051908082528060200260200182016040528015610c10" + + "578160200160208202803683370190505b509350806001600160401b03811115610c2b57610c2b611e51565b60405190" + + "8082528060200260200182016040528015610c54578160200160208202803683370190505b509250806001600160401b" + + "03811115610c6f57610c6f611e51565b604051908082528060200260200182016040528015610c985781602001602082" + + "02803683370190505b5091505f60015b600254811015610d84575f8181526004602052604090208054600160e01b9004" + + "60ff16610ccc5750610d7c565b81878481518110610cdf57610cdf611fed565b60209081029190910101528054865160" + + "01600160a01b0390911690879085908110610d0c57610d0c611fed565b60200260200101906001600160a01b03169081" + + "6001600160a01b031681525050805f0160149054906101000a90046001600160401b0316858481518110610d5557610d" + + "55611fed565b6001600160401b039092166020928302919091019091015282610d7781612015565b935050505b600101" + + "610c9f565b505050909192565b5f546001600160a01b0316610db4576040516321c4e35760e21b815260040160405180" + + "910390fd5b5f546001600160a01b03163314610dde576040516330cd747160e01b815260040160405180910390fd5b60" + + "01600160a01b038116610e055760405163d92e233d60e01b815260040160405180910390fd5b5f610e0f83611530565b" + + "80546001600160a01b038481166001600160a01b0319831681178455604051939450911691829086907fc5be841a0df0" + + "5495e1b0c1417dd3e47b706482e380cf1878d41adbb2ec27d43c905f90a450505050565b5f546001600160a01b031661" + + "0e8b576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b03163314610eb557604051" + + "6330cd747160e01b815260040160405180910390fd5b5f610ebf83611530565b8054909150600160a01b810460016001" + + "60401b031690610ef5908490600160e01b900460ff16610eef575f611706565b82611706565b8154600160e01b900460" + + "ff1615610f4b576003548390610f1f9083906001600160401b0316612052565b610f29919061202d565b6003805467ff" + + "ffffffffffffff19166001600160401b03929092169190911790555b815467ffffffffffffffff60a01b1916600160a0" + + "1b6001600160401b03858116918202929092178455604080519284168352602083019190915285917f27e19ef4fd95b0" + + "e09b35f3a0160fbc4c0adeb8a03d5353f1e0fcee66e9ed41e2910160405180910390a250505050565b5f805460016001" + + "60a01b0316610fe1576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b0316331461" + + "100b576040516330cd747160e01b815260040160405180910390fd5b6001600160a01b0387166110325760405163d92e" + + "233d60e01b815260040160405180910390fd5b61103c865f611706565b60028054905f61104b83612015565b90915550" + + "5f818152600460205260409020805460ff60e01b196001600160401b038a16600160a01b026001600160e01b03199092" + + "166001600160a01b038c16179190911716600160e01b178155909150600181016110aa868883611ef0565b5060038054" + + "8891905f906110c89084906001600160401b031661202d565b92506101000a8154816001600160401b03021916908360" + + "01600160401b031602179055505f5f90505b838110156111175761110f8386868481811061070857610708611fed565b" + + "6001016110f1565b50876001600160a01b0316827f09a706e40d9ad723a2810180344b3c848de397087ce3da3fead0d3" + + "9b2c809d4c89898960405161115693929190612071565b60405180910390a3509695505050505050565b5f5f5f60605f" + + "5f61117987611530565b805460028201546001830180549394506001600160a01b03831693600160a01b840460016001" + + "60401b031693600160e01b900460ff16929082906111bc90611e65565b80601f01602080910402602001604051908101" + + "604052809291908181526020018280546111e890611e65565b80156112335780601f1061120a57610100808354040283" + + "529160200191611233565b820191905f5260205f20905b81548152906001019060200180831161121657829003601f16" + + "8201915b50505050509150955095509550955095505091939590929450565b6001600160a01b0381165f908152600560" + + "20526040812054801580159061128957505f81815260046020526040902054600160e01b900460ff165b939250505056" + + "5b5f6002545f0361129f57505f90565b60016002546112ae919061209c565b905090565b5f546001600160a01b031661" + + "12db576040516321c4e35760e21b815260040160405180910390fd5b815f6112e682611530565b5f5490915060016001" + + "60a01b0316331480159061130d575080546001600160a01b03163314155b1561132b576040516301940db560e51b8152" + + "60040160405180910390fd5b5f61133585611530565b6001600160a01b0385165f908152600560205260409020549091" + + "50851461136f576040516343876cef60e11b815260040160405180910390fd5b6001600160a01b0384165f9081526005" + + "602052604081205561139181856117b7565b61139a846118e2565b6040516001600160a01b0385169086907f1dba5136" + + "2ce159be9df822b10a7257119c8852ec78713df99e4072ab10842e1e905f90a35050505050565b5f546001600160a01b" + + "03166113fe576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b0316331461142857" + + "6040516330cd747160e01b815260040160405180910390fd5b6001600160a01b03811661144f5760405163d92e233d60" + + "e01b815260040160405180910390fd5b5f80546040516001600160a01b03808516939216917f8be0079c531659141344" + + "cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a35f80546001600160a01b0319166001600160a01b03929092" + + "16919091179055565b5f546001600160a01b03166114d0576040516321c4e35760e21b815260040160405180910390fd" + + "5b815f6114db82611530565b5f549091506001600160a01b03163314801590611502575080546001600160a01b031633" + + "14155b15611520576040516301940db560e51b815260040160405180910390fd5b61152a84846115c1565b5050505056" + + "5b5f81815260046020526040902080546001600160a01b0316611565576040516366f1f3b160e01b8152600401604051" + + "80910390fd5b919050565b6001600160401b038216158061158757506001600160401b038116155b806115a357508160" + + "01600160401b0316816001600160401b0316115b15610aa457604051634377d4dd60e11b815260040160405180910390" + + "fd5b6001600160a01b0381166115e85760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b" + + "0381165f908152600560205260409020541561161e576040516316a163b960e11b815260040160405180910390fd5b5f" + + "61162883611530565b600281018054600180820183555f928352602080842090920180546001600160a01b0319166001" + + "600160a01b03881690811790915583526005909152604090912085905560065491925061167c91906120af565b600160" + + "0160a01b0383165f8181526007602052604080822093909355600680546001810182559082527ff652222313e2845952" + + "8d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f0180546001600160a01b031916831790559151909185917f80" + + "e74249b1abc4956ba12c76ad9d1ffdfebe41c1df74308c8d5f9f6a437bc9599190a3505050565b816001600160401b03" + + "165f0361172f57604051634377d4dd60e11b815260040160405180910390fd5b6001546001600160401b039081169083" + + "16111561175f5760405163dbf8c09d60e01b815260040160405180910390fd5b5f546003546001600160401b03600160" + + "a01b909204821691849161178591859116612052565b61178f919061202d565b6001600160401b03161115610aa45760" + + "405163ad7e403f60e01b815260040160405180910390fd5b60028201545f5b818110156118c857826001600160a01b03" + + "168460020182815481106117e5576117e5611fed565b5f918252602090912001546001600160a01b0316036118c05761" + + "180960018361209c565b8114611887576002840161181e60018461209c565b8154811061182e5761182e611fed565b5f" + + "918252602090912001546002850180546001600160a01b03909216918390811061185b5761185b611fed565b905f5260" + + "205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505b8360020180548061" + + "189a5761189a6120c2565b5f8281526020902081015f1990810180546001600160a01b03191690550190555050505056" + + "5b6001016117be565b506040516343876cef60e11b815260040160405180910390fd5b6001600160a01b0381165f9081" + + "52600760205260408120549081900361191b576040516343876cef60e11b815260040160405180910390fd5b5f611927" + + "60018361209c565b6006549091505f9061193b9060019061209c565b90508082146119d3575f60068281548110611958" + + "57611958611fed565b5f91825260209091200154600680546001600160a01b0390921692508291859081106119865761" + + "1986611fed565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905561" + + "19b98360016120af565b6001600160a01b039091165f908152600760205260409020555b60068054806119e4576119e4" + + "6120c2565b5f828152602080822083015f1990810180546001600160a01b03191690559092019092556001600160a01b" + + "0395909516815260079094525050604082209190915550565b80356001600160a01b0381168114611565575f5ffd5b5f" + + "60208284031215611a4e575f5ffd5b61128982611a28565b5f5f83601f840112611a67575f5ffd5b5081356001600160" + + "401b03811115611a7d575f5ffd5b602083019150836020828501011115611a94575f5ffd5b9250929050565b5f5f5f60" + + "408486031215611aad575f5ffd5b8335925060208401356001600160401b03811115611ac9575f5ffd5b611ad5868287" + + "01611a57565b9497909650939450505050565b80356001600160401b0381168114611565575f5ffd5b5f5f6040838503" + + "1215611b09575f5ffd5b611b1283611ae2565b9150611b2060208401611ae2565b90509250929050565b5f5f83601f84" + + "0112611b39575f5ffd5b5081356001600160401b03811115611b4f575f5ffd5b6020830191508360208260051b850101" + + "1115611a94575f5ffd5b5f5f5f60408486031215611b7b575f5ffd5b8335925060208401356001600160401b03811115" + + "611b97575f5ffd5b611ad586828701611b29565b5f8151808452602084019350602083015f5b82811015611bdc578151" + + "6001600160a01b0316865260209586019590910190600101611bb5565b5093949350505050565b602081525f61128960" + + "20830184611ba3565b5f60208284031215611c08575f5ffd5b5035919050565b5f5f60408385031215611c20575f5ffd" + + "5b8235915060208301358015158114611c36575f5ffd5b809150509250929050565b5f5f5f60608486031215611c5357" + + "5f5ffd5b611c5c84611a28565b9250611c6a60208501611ae2565b9150611c7860408501611ae2565b90509250925092" + + "565b606080825284519082018190525f9060208601906080840190835b81811015611cba578351835260209384019390" + + "920191600101611c9c565b50508381036020850152611cce8187611ba3565b8481036040860152855180825260208088" + + "01945090910191505f5b81811015611d105783516001600160401b0316835260209384019390920191600101611ce956" + + "5b5090979650505050505050565b5f5f60408385031215611d2e575f5ffd5b82359150611b2060208401611a28565b5f" + + "5f60408385031215611d4f575f5ffd5b82359150611b2060208401611ae2565b5f5f5f5f5f5f60808789031215611d74" + + "575f5ffd5b611d7d87611a28565b9550611d8b60208801611ae2565b945060408701356001600160401b03811115611d" + + "a5575f5ffd5b611db189828a01611a57565b90955093505060608701356001600160401b03811115611dcf575f5ffd5b" + + "611ddb89828a01611b29565b979a9699509497509295939492505050565b60018060a01b03861681526001600160401b" + + "0385166020820152831515604082015260a060608201525f83518060a0840152806020860160c085015e5f60c0828501" + + "015260c0601f19601f8301168401019150508260808301529695505050505050565b634e487b7160e01b5f5260416004" + + "5260245ffd5b600181811c90821680611e7957607f821691505b602082108103611e9757634e487b7160e01b5f526022" + + "60045260245ffd5b50919050565b601f821115611eeb5782821115611eeb57805f5260205f20601f840160051c602085" + + "1015611ec857505f5b90810190601f840160051c035f5b8181101561072a575f83820155600101611ed6565b50505056" + + "5b6001600160401b03831115611f0757611f07611e51565b611f1b83611f158354611e65565b83611e9d565b5f601f84" + + "1160018114611f4c575f8515611f355750838201355b5f19600387901b1c1916600186901b178355611fa3565b5f8381" + + "5260208120601f198716915b82811015611f7b5786850135825560209485019460019092019101611f5b565b50868210" + + "15611f97575f1960f88860031b161c19848701351681555b505060018560011b0183555b5050505050565b8183528181" + + "6020850137505f828201602090810191909152601f909101601f19169091010190565b602081525f611fe56020830184" + + "86611faa565b949350505050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f5260116004" + + "5260245ffd5b5f6001820161202657612026612001565b5060010190565b6001600160401b0381811683821601908111" + + "1561204c5761204c612001565b92915050565b6001600160401b03828116828216039081111561204c5761204c612001" + + "565b6001600160401b0384168152604060208201525f612093604083018486611faa565b95945050505050565b818103" + + "8181111561204c5761204c612001565b8082018082111561204c5761204c612001565b634e487b7160e01b5f52603160" + + "045260245ffdfea2646970667358221220b2bc134fc8386dd09cd1e37136d732ae698d2c5fe5afb4f67fef1f1d189a4d" + + "a664736f6c63430008210033" +) From c25899e8377b44ca74ca829397e16a7671e8b523 Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Tue, 26 May 2026 18:18:39 +0530 Subject: [PATCH 02/19] chore: expose reserved registry reader across filtering modules --- consensus/bor/bor.go | 12 + .../bor/contract/registrytest/harness.go | 208 +++++++++++ .../bor/contract/registrytest/harness_test.go | 29 ++ consensus/bor/contract/reserved_registry.go | 14 +- consensus/bor/registryreader/reader.go | 33 ++ core/blockchain.go | 6 + core/blockchain_reader.go | 11 + core/reserved_registry_test.go | 60 ++++ core/txpool/reserved_registry_test.go | 42 +++ core/txpool/txpool.go | 26 ++ eth/backend.go | 11 + miner/miner.go | 28 ++ miner/reserved_registry_test.go | 43 +++ miner/worker.go | 5 + registry-contract/.gitignore | 16 + registry-contract/.gitmodules | 3 + registry-contract/foundry.lock | 8 + registry-contract/foundry.toml | 8 + .../src/ReservedBlockspaceRegistry.sol | 340 ++++++++++++++++++ .../test/ReservedBlockspaceRegistry.t.sol | 160 +++++++++ 20 files changed, 1057 insertions(+), 6 deletions(-) create mode 100644 consensus/bor/contract/registrytest/harness.go create mode 100644 consensus/bor/contract/registrytest/harness_test.go create mode 100644 consensus/bor/registryreader/reader.go create mode 100644 core/reserved_registry_test.go create mode 100644 core/txpool/reserved_registry_test.go create mode 100644 miner/reserved_registry_test.go create mode 100644 registry-contract/.gitignore create mode 100644 registry-contract/.gitmodules create mode 100644 registry-contract/foundry.lock create mode 100644 registry-contract/foundry.toml create mode 100644 registry-contract/src/ReservedBlockspaceRegistry.sol create mode 100644 registry-contract/test/ReservedBlockspaceRegistry.t.sol diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index ec2dfcd79f..62dd3b4f86 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/bor/api" "github.com/ethereum/go-ethereum/consensus/bor/clerk" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" borSpan "github.com/ethereum/go-ethereum/consensus/bor/heimdall/span" "github.com/ethereum/go-ethereum/consensus/bor/statefull" "github.com/ethereum/go-ethereum/consensus/bor/valset" @@ -393,6 +394,17 @@ func (c *Bor) GetSpanner() Spanner { return c.spanner } +// ReservedRegistry returns the read-only handle to the reserved blockspace +// registry, or nil when the GenesisContractsClient does not implement the +// reader (e.g. test mocks). Callers must tolerate a nil return. +func (c *Bor) ReservedRegistry() registryreader.Reader { + if c == nil { + return nil + } + reader, _ := c.GenesisContractsClient.(registryreader.Reader) + return reader +} + func (c *Bor) SetSpanner(spanner Spanner) { c.spanner = spanner } diff --git a/consensus/bor/contract/registrytest/harness.go b/consensus/bor/contract/registrytest/harness.go new file mode 100644 index 0000000000..cf8dc73ae0 --- /dev/null +++ b/consensus/bor/contract/registrytest/harness.go @@ -0,0 +1,208 @@ +// Package registrytest builds a working reserved blockspace registry inside an +// in-memory state and returns a registryreader.Reader that talks to it. It is +// shared across module-level tests (core, core/txpool, miner) so the deploy + +// initialize + createClient sequence isn't duplicated three times. +package registrytest + +import ( + "fmt" + "math/big" + "strings" + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + borabi "github.com/ethereum/go-ethereum/consensus/bor/abi" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +// writeABI exposes the contract write methods that tests need to drive +// (initialize, createClient). The shared bor/abi package only ships the read +// side because production code never invokes the write side from Go — admins +// mutate the registry via normal user transactions. +const writeABI = `[ + {"inputs":[{"name":"initialOwner","type":"address"},{"name":"maxTotalGas","type":"uint64"},{"name":"maxClientGas","type":"uint64"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"admin","type":"address"},{"name":"gasQuota","type":"uint64"},{"name":"metadata","type":"string"},{"name":"addresses","type":"address[]"}],"name":"createClient","outputs":[{"name":"clientId","type":"uint256"}],"stateMutability":"nonpayable","type":"function"} +]` + +// Harness exposes a Reader bound to an in-memory state with the registry +// deployed and one client registered, plus the addresses tests assert on. +type Harness struct { + Reader registryreader.Reader + ReservedAddr common.Address + UnreservedAddr common.Address +} + +// NewHarness deploys the registry, initializes it under a fresh owner, and +// registers one client with one whitelisted address. +func NewHarness(t *testing.T) *Harness { + t.Helper() + + statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + require.NoError(t, err) + + contractAddr := common.HexToAddress(params.DefaultReservedRegistryContract) + owner := common.HexToAddress("0x00000000000000000000000000000000000000aa") + clientAdmin := common.HexToAddress("0x00000000000000000000000000000000000000bb") + reserved := common.HexToAddress("0x00000000000000000000000000000000000000cc") + unreserved := common.HexToAddress("0x00000000000000000000000000000000000000dd") + + // Install runtime bytecode directly — the Solidity contract has no + // constructor (see ReservedBlockspaceRegistry.sol). + statedb.SetCode(contractAddr, common.FromHex(params.ReservedBlockspaceRegistryCode), tracing.CodeChangeGenesis) + statedb.AddBalance(owner, uint256.NewInt(params.Ether), tracing.BalanceChangeUnspecified) + + writeAbi, err := abi.JSON(strings.NewReader(writeABI)) + require.NoError(t, err) + + initData, err := writeAbi.Pack("initialize", owner, uint64(200_000_000), uint64(100_000_000)) + require.NoError(t, err) + callContract(t, statedb, owner, contractAddr, initData) + + createData, err := writeAbi.Pack("createClient", clientAdmin, uint64(10_000_000), "test", []common.Address{reserved}) + require.NoError(t, err) + callContract(t, statedb, owner, contractAddr, createData) + + return &Harness{ + Reader: &evmReader{ + state: statedb, + contract: contractAddr, + readerAB: borabi.ReservedBlockspaceRegistry(), + }, + ReservedAddr: reserved, + UnreservedAddr: unreserved, + } +} + +func callContract(t *testing.T, statedb *state.StateDB, from, to common.Address, data []byte) { + t.Helper() + evm := newEVM(statedb, from) + ret, _, err := evm.Call(from, to, data, 30_000_000, uint256.NewInt(0)) + if err != nil { + t.Fatalf("registry call reverted: err=%v ret=0x%x", err, ret) + } +} + +func newEVM(statedb *state.StateDB, author common.Address) *vm.EVM { + // Built without importing core/ — the harness is shared across module + // tests including core's own tests, so we can't import core here without + // creating an import cycle. + blockCtx := vm.BlockContext{ + CanTransfer: canTransfer, + Transfer: transfer, + GetHash: func(uint64) common.Hash { return common.Hash{} }, + Coinbase: author, + BlockNumber: big.NewInt(1), + Time: 1700000000, + Difficulty: big.NewInt(1), + GasLimit: 30_000_000, + } + return vm.NewEVM(blockCtx, statedb, testChainConfig(), vm.Config{}) +} + +func canTransfer(db vm.StateDB, addr common.Address, amount *uint256.Int) bool { + return db.GetBalance(addr).Cmp(amount) >= 0 +} + +func transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int) { + db.SubBalance(sender, amount, tracing.BalanceChangeTransfer) + db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer) +} + +// testChainConfig enables every Ethereum fork the registry bytecode could +// depend on. The contract uses PUSH0 (Shanghai) so ShanghaiBlock must be 0. +func testChainConfig() *params.ChainConfig { + return ¶ms.ChainConfig{ + ChainID: big.NewInt(137), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ShanghaiBlock: big.NewInt(0), + } +} + +// evmReader satisfies registryreader.Reader by running the registry's view +// methods against a fixed in-memory state. Uses a snapshot/revert pair per call +// so view executions never leak state mutations into the harness DB. +type evmReader struct { + state *state.StateDB + contract common.Address + readerAB abi.ABI +} + +func (r *evmReader) HasReservedRegistry() bool { + return r != nil && r.contract != (common.Address{}) +} + +func (r *evmReader) IsReservedAddress(_ *state.StateDB, _ uint64, _ common.Hash, account common.Address) (bool, error) { + values, err := r.call("isReservedAddress", account) + if err != nil { + return false, err + } + if len(values) != 1 { + return false, fmt.Errorf("isReservedAddress returned %d values", len(values)) + } + reserved, ok := values[0].(bool) + if !ok { + return false, fmt.Errorf("isReservedAddress returned %T", values[0]) + } + return reserved, nil +} + +func (r *evmReader) ReservedClientForAddress(_ *state.StateDB, _ uint64, _ common.Hash, account common.Address) (registryreader.ClientLookup, error) { + values, err := r.call("getClientForAddress", account) + if err != nil { + return registryreader.ClientLookup{}, err + } + if len(values) != 4 { + return registryreader.ClientLookup{}, fmt.Errorf("getClientForAddress returned %d values", len(values)) + } + clientID, _ := values[0].(*big.Int) + gasQuota, _ := values[1].(uint64) + admin, _ := values[2].(common.Address) + active, _ := values[3].(bool) + if clientID == nil { + return registryreader.ClientLookup{}, fmt.Errorf("getClientForAddress returned nil clientID") + } + return registryreader.ClientLookup{ + ClientID: new(big.Int).Set(clientID), + GasQuota: gasQuota, + Admin: admin, + Active: active, + }, nil +} + +func (r *evmReader) call(method string, args ...interface{}) ([]interface{}, error) { + data, err := r.readerAB.Pack(method, args...) + if err != nil { + return nil, err + } + // View methods don't mutate state, but the EVM still consumes + // finalisation cycles; snapshot/revert keeps the harness DB pristine. + snap := r.state.Snapshot() + defer r.state.RevertToSnapshot(snap) + + caller := common.HexToAddress("0x00000000000000000000000000000000deadbeef") + evm := newEVM(r.state, caller) + ret, _, err := evm.Call(caller, r.contract, data, 30_000_000, uint256.NewInt(0)) + if err != nil { + return nil, err + } + return r.readerAB.Unpack(method, ret) +} diff --git a/consensus/bor/contract/registrytest/harness_test.go b/consensus/bor/contract/registrytest/harness_test.go new file mode 100644 index 0000000000..4ec54c8bf1 --- /dev/null +++ b/consensus/bor/contract/registrytest/harness_test.go @@ -0,0 +1,29 @@ +package registrytest + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestHarness_DeploysAndAnswersQueries(t *testing.T) { + h := NewHarness(t) + + require.True(t, h.Reader.HasReservedRegistry(), "deployed registry must report HasReservedRegistry") + + got, err := h.Reader.IsReservedAddress(nil, 0, common.Hash{}, h.ReservedAddr) + require.NoError(t, err) + require.True(t, got, "address registered via createClient must be reserved") + + got, err = h.Reader.IsReservedAddress(nil, 0, common.Hash{}, h.UnreservedAddr) + require.NoError(t, err) + require.False(t, got, "unregistered address must not be reserved") + + lookup, err := h.Reader.ReservedClientForAddress(nil, 0, common.Hash{}, h.ReservedAddr) + require.NoError(t, err) + require.True(t, lookup.Active) + require.Equal(t, uint64(10_000_000), lookup.GasQuota) + require.NotNil(t, lookup.ClientID) + require.Equal(t, int64(1), lookup.ClientID.Int64(), "createClient assigns id starting at 1") +} diff --git a/consensus/bor/contract/reserved_registry.go b/consensus/bor/contract/reserved_registry.go index 27bcc367e0..9cbde46911 100644 --- a/consensus/bor/contract/reserved_registry.go +++ b/consensus/bor/contract/reserved_registry.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" borabi "github.com/ethereum/go-ethereum/consensus/bor/abi" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/rpc" @@ -16,12 +17,13 @@ import ( var errReservedRegistryNotConfigured = errors.New("reserved blockspace registry contract is not configured") -type ReservedClientLookup struct { - ClientID *big.Int - GasQuota uint64 - Admin common.Address - Active bool -} +// ReservedClientLookup is the slim "client for address" view returned by the +// registry contract. Aliased to registryreader.ClientLookup so the type lives +// in a leaf package that core/, miner/, and core/txpool/ can import without +// triggering an import cycle through consensus/bor/statefull. +type ReservedClientLookup = registryreader.ClientLookup + +var _ registryreader.Reader = (*GenesisContractsClient)(nil) type ReservedClient struct { ClientID *big.Int diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go new file mode 100644 index 0000000000..c85b49b78f --- /dev/null +++ b/consensus/bor/registryreader/reader.go @@ -0,0 +1,33 @@ +// Package registryreader exposes the minimal read-only surface of the reserved +// blockspace registry that filtering modules (txpool, miner, block validator) +// need. It lives in a leaf package so core/, miner/, and core/txpool/ can +// import it without pulling in consensus/bor/contract → consensus/bor/statefull +// → core/, which would form an import cycle. +package registryreader + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" +) + +// ClientLookup mirrors the slim "client for address" view returned by the +// registry contract. Defined here (not in consensus/bor/contract) so the +// interface is self-contained in this leaf package. +type ClientLookup struct { + ClientID *big.Int + GasQuota uint64 + Admin common.Address + Active bool +} + +// Reader is the read-only view of the reserved blockspace registry consumed by +// transaction filtering paths. Callers must nil-check the interface before +// invoking — chain/txpool/miner expose a nil Reader when the chain has no +// registry configured (non-bor engines, devnets without the contract). +type Reader interface { + HasReservedRegistry() bool + IsReservedAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (bool, error) + ReservedClientForAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (ClientLookup, error) +} diff --git a/core/blockchain.go b/core/blockchain.go index 2def72bba2..8a1bed7d90 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/history" "github.com/ethereum/go-ethereum/core/rawdb" @@ -429,6 +430,11 @@ type BlockChain struct { chain2HeadFeed event.Feed // Reorg/NewHead/Fork data feed chainSideFeed event.Feed // Side chain data feed (removed from geth but needed in bor) milestoneFetcher func(ctx context.Context) (uint64, error) // Function to fetch the latest milestone end block from Heimdall. + + // reservedRegistry is the read-only handle to the reserved blockspace + // registry used by block validation / filtering paths. Nil when the chain + // has no registry configured or runs under a non-bor consensus engine. + reservedRegistry registryreader.Reader } // NewBlockChain returns a fully initialised block chain using information diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 58a8a49e13..cce149cfa7 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" @@ -548,6 +549,16 @@ func (bc *BlockChain) Config() *params.ChainConfig { return bc.chainConfig } // Engine retrieves the blockchain's consensus engine. func (bc *BlockChain) Engine() consensus.Engine { return bc.engine } +// ReservedRegistry returns the reserved blockspace registry reader configured +// for this chain, or nil when none has been wired (non-bor engines, tests, or +// chains without the registry contract). +func (bc *BlockChain) ReservedRegistry() registryreader.Reader { return bc.reservedRegistry } + +// SetReservedRegistry wires the reserved blockspace registry reader into the +// chain. Called once at startup from the backend after the consensus engine is +// ready. Passing nil is valid and disables reserved-tx awareness. +func (bc *BlockChain) SetReservedRegistry(r registryreader.Reader) { bc.reservedRegistry = r } + // Snapshots returns the blockchain snapshot tree. func (bc *BlockChain) Snapshots() *snapshot.Tree { return bc.snaps diff --git a/core/reserved_registry_test.go b/core/reserved_registry_test.go new file mode 100644 index 0000000000..9c3112f2df --- /dev/null +++ b/core/reserved_registry_test.go @@ -0,0 +1,60 @@ +package core + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/contract/registrytest" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/params" +) + +// TestBlockChain_ReservedRegistry_NilByDefault asserts that a chain built +// without wiring a registry returns nil from the accessor — block validation +// paths must tolerate this rather than dereferencing. +func TestBlockChain_ReservedRegistry_NilByDefault(t *testing.T) { + chain := newBareBlockChain(t) + defer chain.Stop() + + require.Nil(t, chain.ReservedRegistry(), "fresh BlockChain must report no registry until SetReservedRegistry is called") +} + +// TestBlockChain_ReservedRegistry_SeesContractState wires a registry-backed +// reader into the chain and confirms the chain's accessor surfaces the same +// view as the harness reader. +func TestBlockChain_ReservedRegistry_SeesContractState(t *testing.T) { + h := registrytest.NewHarness(t) + chain := newBareBlockChain(t) + defer chain.Stop() + + chain.SetReservedRegistry(h.Reader) + + reader := chain.ReservedRegistry() + require.NotNil(t, reader) + require.True(t, reader.HasReservedRegistry()) + + reserved, err := reader.IsReservedAddress(nil, 0, common.Hash{}, h.ReservedAddr) + require.NoError(t, err) + require.True(t, reserved) + + reserved, err = reader.IsReservedAddress(nil, 0, common.Hash{}, h.UnreservedAddr) + require.NoError(t, err) + require.False(t, reserved) +} + +func newBareBlockChain(t *testing.T) *BlockChain { + t.Helper() + db := rawdb.NewMemoryDatabase() + gspec := &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + engine := ethash.NewFullFaker() + chain, err := NewBlockChain(db, gspec, engine, DefaultConfig().WithStateScheme(rawdb.HashScheme)) + require.NoError(t, err) + return chain +} diff --git a/core/txpool/reserved_registry_test.go b/core/txpool/reserved_registry_test.go new file mode 100644 index 0000000000..7988a812fb --- /dev/null +++ b/core/txpool/reserved_registry_test.go @@ -0,0 +1,42 @@ +package txpool + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/contract/registrytest" +) + +// TestTxPool_ReservedRegistry_NilByDefault asserts the accessor on a freshly +// zeroed TxPool returns nil so nil-handle consumers don't panic. +func TestTxPool_ReservedRegistry_NilByDefault(t *testing.T) { + var pool *TxPool + require.Nil(t, pool.ReservedRegistry()) + + pool = &TxPool{} + require.Nil(t, pool.ReservedRegistry()) +} + +// TestTxPool_ReservedRegistry_SeesContractState wires a real registry-backed +// reader into the pool and verifies the pool reports the same view as the +// underlying contract for both registered and unregistered addresses. +func TestTxPool_ReservedRegistry_SeesContractState(t *testing.T) { + h := registrytest.NewHarness(t) + + pool := &TxPool{} + pool.SetReservedRegistry(h.Reader) + + reader := pool.ReservedRegistry() + require.NotNil(t, reader) + require.True(t, reader.HasReservedRegistry()) + + reserved, err := reader.IsReservedAddress(nil, 0, common.Hash{}, h.ReservedAddr) + require.NoError(t, err) + require.True(t, reserved, "txpool must see registered address as reserved") + + reserved, err = reader.IsReservedAddress(nil, 0, common.Hash{}, h.UnreservedAddr) + require.NoError(t, err) + require.False(t, reserved) +} diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 33ffb7e550..37e1dfdfb9 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -24,6 +24,7 @@ import ( "sync/atomic" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -70,6 +71,11 @@ type TxPool struct { stateLock sync.RWMutex // The lock for protecting state instance state *state.StateDB // Current state at the blockchain head + // reservedRegistry is the read-only handle to the reserved blockspace + // registry. Nil when no registry is configured; subpools may pull it via + // ReservedRegistry() to classify reserved transactions. + reservedRegistry registryreader.Reader + subs event.SubscriptionScope // Subscription scope to unsubscribe all on shutdown quit chan chan error // Quit channel to tear down the head updater term chan struct{} // Termination channel to detect a closed pool @@ -262,6 +268,26 @@ func (p *TxPool) loop(head *types.Header) { errc <- nil } +// ReservedRegistry returns the reserved blockspace registry reader wired into +// the pool. Subpools that need to classify reserved txs should read it through +// this accessor — it tolerates a nil pool by returning nil. +func (p *TxPool) ReservedRegistry() registryreader.Reader { + if p == nil { + return nil + } + return p.reservedRegistry +} + +// SetReservedRegistry installs the reserved blockspace registry reader. +// Called once at startup from the backend, after the consensus engine is +// available. Passing nil is valid. +func (p *TxPool) SetReservedRegistry(r registryreader.Reader) { + if p == nil { + return + } + p.reservedRegistry = r +} + // SetGasTip updates the minimum gas tip required by the transaction pool for a // new transaction, and drops all transactions below this threshold. func (p *TxPool) SetGasTip(tip *big.Int) { diff --git a/eth/backend.go b/eth/backend.go index d6414dd0b1..01c8fb90a0 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -474,6 +474,17 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth.miner.SetPrioAddresses(config.TxPool.Locals) } + // Wire the reserved blockspace registry into chain/txpool/miner. Only Bor + // exposes a registry; non-Bor engines silently leave nil handles in place. + if borEngine, ok := eth.engine.(*bor.Bor); ok { + reg := borEngine.ReservedRegistry() + eth.blockchain.SetReservedRegistry(reg) + eth.txPool.SetReservedRegistry(reg) + if eth.miner != nil { + eth.miner.SetReservedRegistry(reg) + } + } + // 1.14.8: NewOracle function definition was changed to accept (startPrice *big.Int) param. eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, config.GPO, config.Miner.GasPrice) eth.APIBackend.gpo.ProcessCache() diff --git a/miner/miner.go b/miner/miner.go index dbd4607542..13487dffd2 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" @@ -109,6 +110,11 @@ type Miner struct { worker *worker prio []common.Address // A list of senders to prioritize + // reservedRegistry is the read-only handle to the reserved blockspace + // registry. Nil when no registry is configured. Block builders that need + // to filter reserved txs should pull it via ReservedRegistry(). + reservedRegistry registryreader.Reader + wg sync.WaitGroup } @@ -133,6 +139,28 @@ func (miner *Miner) GetWorker() *worker { return miner.worker } +// ReservedRegistry returns the reserved blockspace registry reader wired into +// the miner, or nil when none is configured. +func (miner *Miner) ReservedRegistry() registryreader.Reader { + if miner == nil { + return nil + } + return miner.reservedRegistry +} + +// SetReservedRegistry installs the reserved blockspace registry reader. +// Called once at startup from the backend, after the consensus engine is +// available. Propagates the handle to the worker. Passing nil is valid. +func (miner *Miner) SetReservedRegistry(r registryreader.Reader) { + if miner == nil { + return + } + miner.reservedRegistry = r + if miner.worker != nil { + miner.worker.reservedRegistry = r + } +} + // update keeps track of the downloader events. Please be aware that this is a one shot type of update loop. // It's entered once and as soon as `Done` or `Failed` has been broadcasted the events are unregistered and // the loop is exited. This to prevent a major security vuln where external parties can DOS you with blocks diff --git a/miner/reserved_registry_test.go b/miner/reserved_registry_test.go new file mode 100644 index 0000000000..0266b14bde --- /dev/null +++ b/miner/reserved_registry_test.go @@ -0,0 +1,43 @@ +package miner + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/contract/registrytest" +) + +// TestMiner_ReservedRegistry_NilByDefault verifies the accessor on a Miner +// (and a typed-nil receiver) returns nil so consumers don't panic. +func TestMiner_ReservedRegistry_NilByDefault(t *testing.T) { + var miner *Miner + require.Nil(t, miner.ReservedRegistry()) + + miner = &Miner{worker: &worker{}} + require.Nil(t, miner.ReservedRegistry()) +} + +// TestMiner_ReservedRegistry_PropagatesToWorker confirms SetReservedRegistry +// installs the handle on both the Miner and its worker, and that the handle +// answers EVM-backed queries identically to the harness reader. +func TestMiner_ReservedRegistry_PropagatesToWorker(t *testing.T) { + h := registrytest.NewHarness(t) + + w := &worker{} + miner := &Miner{worker: w} + miner.SetReservedRegistry(h.Reader) + + require.NotNil(t, miner.ReservedRegistry()) + require.Same(t, h.Reader, miner.ReservedRegistry()) + require.Same(t, h.Reader, w.reservedRegistry, "worker must mirror the miner's registry handle") + + reserved, err := miner.ReservedRegistry().IsReservedAddress(nil, 0, common.Hash{}, h.ReservedAddr) + require.NoError(t, err) + require.True(t, reserved) + + reserved, err = miner.ReservedRegistry().IsReservedAddress(nil, 0, common.Hash{}, h.UnreservedAddr) + require.NoError(t, err) + require.False(t, reserved) +} diff --git a/miner/worker.go b/miner/worker.go index 6a7eb86f02..f266f7086d 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/common/tracing" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/bor" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" @@ -382,6 +383,10 @@ type worker struct { eth Backend chain *core.BlockChain + // reservedRegistry mirrors the Miner field. Populated via + // Miner.SetReservedRegistry — see miner.go. + reservedRegistry registryreader.Reader + prio []common.Address // A list of senders to prioritize // Feeds diff --git a/registry-contract/.gitignore b/registry-contract/.gitignore new file mode 100644 index 0000000000..85b77a6019 --- /dev/null +++ b/registry-contract/.gitignore @@ -0,0 +1,16 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +lib/ diff --git a/registry-contract/.gitmodules b/registry-contract/.gitmodules new file mode 100644 index 0000000000..888d42dcd9 --- /dev/null +++ b/registry-contract/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/registry-contract/foundry.lock b/registry-contract/foundry.lock new file mode 100644 index 0000000000..7521bfd030 --- /dev/null +++ b/registry-contract/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.16.1", + "rev": "620536fa5277db4e3fd46772d5cbc1ea0696fb43" + } + } +} \ No newline at end of file diff --git a/registry-contract/foundry.toml b/registry-contract/foundry.toml new file mode 100644 index 0000000000..885852a041 --- /dev/null +++ b/registry-contract/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +optimizer = true +optimizer_runs = 200 + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/registry-contract/src/ReservedBlockspaceRegistry.sol b/registry-contract/src/ReservedBlockspaceRegistry.sol new file mode 100644 index 0000000000..65d8e48042 --- /dev/null +++ b/registry-contract/src/ReservedBlockspaceRegistry.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title ReservedBlockspaceRegistry +/// @notice Registry for reserved blockspace clients and their whitelisted sender addresses. +/// @dev This contract has no constructor so its deployed runtime bytecode can be embedded directly in genesis. +contract ReservedBlockspaceRegistry { + error AlreadyInitialized(); + error NotInitialized(); + error NotOwner(); + error NotClientAdmin(); + error ZeroAddress(); + error UnknownClient(); + error AddressAlreadyRegistered(); + error AddressNotRegistered(); + error InvalidQuota(); + error MaxTotalReservedGasExceeded(); + error MaxClientReservedGasExceeded(); + + struct Client { + address admin; + uint64 gasQuota; + bool active; + string metadata; + address[] addresses; + } + + address public owner; + uint64 public maxTotalReservedGas; + uint64 public maxClientReservedGas; + uint256 public nextClientId; + uint64 public totalReservedGas; + + mapping(uint256 clientId => Client client) private clients; + mapping(address account => uint256 clientId) private clientIdByAddress; + address[] private allClientAddresses; + mapping(address account => uint256 indexPlusOne) private allAddressIndex; + + event Initialized(address indexed owner, uint64 maxTotalReservedGas, uint64 maxClientReservedGas); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event LimitsUpdated(uint64 maxTotalReservedGas, uint64 maxClientReservedGas); + event ClientCreated(uint256 indexed clientId, address indexed admin, uint64 gasQuota, string metadata); + event ClientAdminUpdated(uint256 indexed clientId, address indexed previousAdmin, address indexed newAdmin); + event ClientQuotaUpdated(uint256 indexed clientId, uint64 previousQuota, uint64 newQuota); + event ClientActiveUpdated(uint256 indexed clientId, bool active); + event ClientMetadataUpdated(uint256 indexed clientId, string metadata); + event ClientAddressAdded(uint256 indexed clientId, address indexed account); + event ClientAddressRemoved(uint256 indexed clientId, address indexed account); + + modifier onlyInitialized() { + if (owner == address(0)) revert NotInitialized(); + _; + } + + modifier onlyOwner() { + if (msg.sender != owner) revert NotOwner(); + _; + } + + modifier onlyClientAdminOrOwner(uint256 clientId) { + Client storage client = _client(clientId); + if (msg.sender != owner && msg.sender != client.admin) revert NotClientAdmin(); + _; + } + + function initialize(address initialOwner, uint64 maxTotalGas, uint64 maxClientGas) external { + if (owner != address(0)) revert AlreadyInitialized(); + if (initialOwner == address(0)) revert ZeroAddress(); + _validateLimits(maxTotalGas, maxClientGas); + + owner = initialOwner; + maxTotalReservedGas = maxTotalGas; + maxClientReservedGas = maxClientGas; + nextClientId = 1; + + emit Initialized(initialOwner, maxTotalGas, maxClientGas); + } + + function transferOwnership(address newOwner) external onlyInitialized onlyOwner { + if (newOwner == address(0)) revert ZeroAddress(); + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } + + function setLimits(uint64 maxTotalGas, uint64 maxClientGas) external onlyInitialized onlyOwner { + _validateLimits(maxTotalGas, maxClientGas); + if (totalReservedGas > maxTotalGas) revert MaxTotalReservedGasExceeded(); + for (uint256 clientId = 1; clientId < nextClientId; clientId++) { + if (clients[clientId].gasQuota > maxClientGas) revert MaxClientReservedGasExceeded(); + } + + maxTotalReservedGas = maxTotalGas; + maxClientReservedGas = maxClientGas; + + emit LimitsUpdated(maxTotalGas, maxClientGas); + } + + function createClient(address admin, uint64 gasQuota, string calldata metadata, address[] calldata addresses) + external + onlyInitialized + onlyOwner + returns (uint256 clientId) + { + if (admin == address(0)) revert ZeroAddress(); + _validateQuota(gasQuota, 0); + + clientId = nextClientId++; + Client storage client = clients[clientId]; + client.admin = admin; + client.gasQuota = gasQuota; + client.active = true; + client.metadata = metadata; + totalReservedGas += gasQuota; + + for (uint256 i = 0; i < addresses.length; i++) { + _addAddress(clientId, addresses[i]); + } + + emit ClientCreated(clientId, admin, gasQuota, metadata); + } + + function setClientAdmin(uint256 clientId, address newAdmin) external onlyInitialized onlyOwner { + if (newAdmin == address(0)) revert ZeroAddress(); + Client storage client = _client(clientId); + address previousAdmin = client.admin; + client.admin = newAdmin; + + emit ClientAdminUpdated(clientId, previousAdmin, newAdmin); + } + + function setClientQuota(uint256 clientId, uint64 newQuota) external onlyInitialized onlyOwner { + Client storage client = _client(clientId); + uint64 previousQuota = client.gasQuota; + _validateQuota(newQuota, client.active ? previousQuota : 0); + + if (client.active) { + totalReservedGas = totalReservedGas - previousQuota + newQuota; + } + client.gasQuota = newQuota; + + emit ClientQuotaUpdated(clientId, previousQuota, newQuota); + } + + function setClientActive(uint256 clientId, bool active) external onlyInitialized onlyOwner { + Client storage client = _client(clientId); + if (client.active == active) return; + + if (active) { + _validateQuota(client.gasQuota, 0); + totalReservedGas += client.gasQuota; + } else { + totalReservedGas -= client.gasQuota; + } + client.active = active; + + emit ClientActiveUpdated(clientId, active); + } + + function setClientMetadata(uint256 clientId, string calldata metadata) + external + onlyInitialized + onlyClientAdminOrOwner(clientId) + { + clients[clientId].metadata = metadata; + emit ClientMetadataUpdated(clientId, metadata); + } + + function addClientAddress(uint256 clientId, address account) + external + onlyInitialized + onlyClientAdminOrOwner(clientId) + { + _addAddress(clientId, account); + } + + function addClientAddresses(uint256 clientId, address[] calldata accounts) + external + onlyInitialized + onlyClientAdminOrOwner(clientId) + { + for (uint256 i = 0; i < accounts.length; i++) { + _addAddress(clientId, accounts[i]); + } + } + + function removeClientAddress(uint256 clientId, address account) + external + onlyInitialized + onlyClientAdminOrOwner(clientId) + { + Client storage client = _client(clientId); + if (clientIdByAddress[account] != clientId) revert AddressNotRegistered(); + + delete clientIdByAddress[account]; + _removeFromClient(client, account); + _removeFromGlobal(account); + + emit ClientAddressRemoved(clientId, account); + } + + function clientCount() external view returns (uint256) { + if (nextClientId == 0) return 0; + return nextClientId - 1; + } + + function getClient(uint256 clientId) + external + view + returns (address admin, uint64 gasQuota, bool active, string memory metadata, uint256 addressCount) + { + Client storage client = _client(clientId); + return (client.admin, client.gasQuota, client.active, client.metadata, client.addresses.length); + } + + function getClientAddresses(uint256 clientId) external view returns (address[] memory) { + return _client(clientId).addresses; + } + + function getClientId(address account) external view returns (uint256) { + return clientIdByAddress[account]; + } + + function getClientForAddress(address account) + external + view + returns (uint256 clientId, uint64 gasQuota, address admin, bool active) + { + clientId = clientIdByAddress[account]; + if (clientId == 0) return (0, 0, address(0), false); + + Client storage client = clients[clientId]; + return (clientId, client.gasQuota, client.admin, client.active); + } + + function isReservedAddress(address account) external view returns (bool) { + uint256 clientId = clientIdByAddress[account]; + return clientId != 0 && clients[clientId].active; + } + + function getWhitelistedAddresses() external view returns (address[] memory) { + uint256 count; + for (uint256 i = 0; i < allClientAddresses.length; i++) { + if (clients[clientIdByAddress[allClientAddresses[i]]].active) count++; + } + + address[] memory activeAddresses = new address[](count); + uint256 out; + for (uint256 i = 0; i < allClientAddresses.length; i++) { + address account = allClientAddresses[i]; + if (clients[clientIdByAddress[account]].active) { + activeAddresses[out++] = account; + } + } + return activeAddresses; + } + + function getReservedClients() + external + view + returns (uint256[] memory clientIds, address[] memory admins, uint64[] memory gasQuotas) + { + uint256 count; + for (uint256 clientId = 1; clientId < nextClientId; clientId++) { + if (clients[clientId].active) count++; + } + + clientIds = new uint256[](count); + admins = new address[](count); + gasQuotas = new uint64[](count); + + uint256 out; + for (uint256 clientId = 1; clientId < nextClientId; clientId++) { + Client storage client = clients[clientId]; + if (!client.active) continue; + clientIds[out] = clientId; + admins[out] = client.admin; + gasQuotas[out] = client.gasQuota; + out++; + } + } + + function _client(uint256 clientId) private view returns (Client storage client) { + client = clients[clientId]; + if (client.admin == address(0)) revert UnknownClient(); + } + + function _addAddress(uint256 clientId, address account) private { + if (account == address(0)) revert ZeroAddress(); + if (clientIdByAddress[account] != 0) revert AddressAlreadyRegistered(); + + Client storage client = _client(clientId); + client.addresses.push(account); + clientIdByAddress[account] = clientId; + allAddressIndex[account] = allClientAddresses.length + 1; + allClientAddresses.push(account); + + emit ClientAddressAdded(clientId, account); + } + + function _removeFromClient(Client storage client, address account) private { + uint256 length = client.addresses.length; + for (uint256 i = 0; i < length; i++) { + if (client.addresses[i] == account) { + if (i != length - 1) { + client.addresses[i] = client.addresses[length - 1]; + } + client.addresses.pop(); + return; + } + } + revert AddressNotRegistered(); + } + + function _removeFromGlobal(address account) private { + uint256 indexPlusOne = allAddressIndex[account]; + if (indexPlusOne == 0) revert AddressNotRegistered(); + + uint256 index = indexPlusOne - 1; + uint256 lastIndex = allClientAddresses.length - 1; + if (index != lastIndex) { + address moved = allClientAddresses[lastIndex]; + allClientAddresses[index] = moved; + allAddressIndex[moved] = index + 1; + } + allClientAddresses.pop(); + delete allAddressIndex[account]; + } + + function _validateLimits(uint64 maxTotalGas, uint64 maxClientGas) private pure { + if (maxTotalGas == 0 || maxClientGas == 0 || maxClientGas > maxTotalGas) revert InvalidQuota(); + } + + function _validateQuota(uint64 newQuota, uint64 previousActiveQuota) private view { + if (newQuota == 0) revert InvalidQuota(); + if (newQuota > maxClientReservedGas) revert MaxClientReservedGasExceeded(); + if (totalReservedGas - previousActiveQuota + newQuota > maxTotalReservedGas) { + revert MaxTotalReservedGasExceeded(); + } + } +} diff --git a/registry-contract/test/ReservedBlockspaceRegistry.t.sol b/registry-contract/test/ReservedBlockspaceRegistry.t.sol new file mode 100644 index 0000000000..c744302006 --- /dev/null +++ b/registry-contract/test/ReservedBlockspaceRegistry.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ReservedBlockspaceRegistry} from "../src/ReservedBlockspaceRegistry.sol"; + +contract ReservedBlockspaceRegistryTest is Test { + ReservedBlockspaceRegistry internal registry; + + address internal owner = address(0xA11CE); + address internal admin = address(0xB0B); + address internal other = address(0xCAFE); + address internal senderA = address(0x100); + address internal senderB = address(0x200); + address internal senderC = address(0x300); + + function setUp() public { + registry = new ReservedBlockspaceRegistry(); + registry.initialize(owner, 80_000_000, 30_000_000); + } + + function testInitializeCanOnlyRunOnce() public { + vm.expectRevert(ReservedBlockspaceRegistry.AlreadyInitialized.selector); + registry.initialize(owner, 80_000_000, 30_000_000); + } + + function testOwnerCreatesClientAndBorFacingReadsWork() public { + address[] memory addresses = new address[](2); + addresses[0] = senderA; + addresses[1] = senderB; + + vm.prank(owner); + uint256 clientId = registry.createClient(admin, 20_000_000, "Polymarket", addresses); + + assertEq(clientId, 1); + assertEq(registry.totalReservedGas(), 20_000_000); + assertTrue(registry.isReservedAddress(senderA)); + assertTrue(registry.isReservedAddress(senderB)); + assertFalse(registry.isReservedAddress(senderC)); + + (uint256 resolvedClientId, uint64 gasQuota, address resolvedAdmin, bool active) = + registry.getClientForAddress(senderA); + assertEq(resolvedClientId, clientId); + assertEq(gasQuota, 20_000_000); + assertEq(resolvedAdmin, admin); + assertTrue(active); + + (address returnedAdmin, uint64 returnedQuota, bool returnedActive, string memory metadata, uint256 count) = + registry.getClient(clientId); + assertEq(returnedAdmin, admin); + assertEq(returnedQuota, 20_000_000); + assertTrue(returnedActive); + assertEq(metadata, "Polymarket"); + assertEq(count, 2); + } + + function testClientAdminCanManageAddresses() public { + uint256 clientId = _createSingleAddressClient(senderA); + + vm.prank(admin); + registry.addClientAddress(clientId, senderB); + assertTrue(registry.isReservedAddress(senderB)); + + vm.prank(admin); + registry.removeClientAddress(clientId, senderA); + assertFalse(registry.isReservedAddress(senderA)); + assertTrue(registry.isReservedAddress(senderB)); + + address[] memory addresses = registry.getClientAddresses(clientId); + assertEq(addresses.length, 1); + assertEq(addresses[0], senderB); + } + + function testNonAdminCannotManageAddresses() public { + uint256 clientId = _createSingleAddressClient(senderA); + + vm.prank(other); + vm.expectRevert(ReservedBlockspaceRegistry.NotClientAdmin.selector); + registry.addClientAddress(clientId, senderB); + } + + function testQuotaLimitsAreEnforced() public { + _createSingleAddressClient(senderA); + + vm.prank(owner); + vm.expectRevert(ReservedBlockspaceRegistry.MaxClientReservedGasExceeded.selector); + registry.setLimits(60_000_000, 19_999_999); + + vm.prank(owner); + registry.setLimits(60_000_000, 30_000_000); + + address[] memory addresses = new address[](1); + addresses[0] = senderB; + + vm.prank(owner); + vm.expectRevert(ReservedBlockspaceRegistry.MaxClientReservedGasExceeded.selector); + registry.createClient(admin, 30_000_001, "too-large-client", addresses); + + vm.prank(owner); + registry.createClient(admin, 30_000_000, "Courtyard", addresses); + + address[] memory moreAddresses = new address[](1); + moreAddresses[0] = senderC; + + vm.prank(owner); + vm.expectRevert(ReservedBlockspaceRegistry.MaxTotalReservedGasExceeded.selector); + registry.createClient(admin, 20_000_000, "would-exceed-total", moreAddresses); + } + + function testInactiveClientIsNotReservedButKeepsAddressMapping() public { + uint256 clientId = _createSingleAddressClient(senderA); + + vm.prank(owner); + registry.setClientActive(clientId, false); + + assertFalse(registry.isReservedAddress(senderA)); + assertEq(registry.getClientId(senderA), clientId); + assertEq(registry.totalReservedGas(), 0); + + (uint256 resolvedClientId, uint64 gasQuota, address resolvedAdmin, bool active) = + registry.getClientForAddress(senderA); + assertEq(resolvedClientId, clientId); + assertEq(gasQuota, 20_000_000); + assertEq(resolvedAdmin, admin); + assertFalse(active); + } + + function testReservedClientListOnlyReturnsActiveClients() public { + uint256 firstClientId = _createSingleAddressClient(senderA); + + address[] memory addresses = new address[](1); + addresses[0] = senderB; + + vm.prank(owner); + uint256 secondClientId = registry.createClient(admin, 10_000_000, "Courtyard", addresses); + + vm.prank(owner); + registry.setClientActive(firstClientId, false); + + (uint256[] memory clientIds, address[] memory admins, uint64[] memory quotas) = registry.getReservedClients(); + assertEq(clientIds.length, 1); + assertEq(admins.length, 1); + assertEq(quotas.length, 1); + assertEq(clientIds[0], secondClientId); + assertEq(admins[0], admin); + assertEq(quotas[0], 10_000_000); + + address[] memory whitelisted = registry.getWhitelistedAddresses(); + assertEq(whitelisted.length, 1); + assertEq(whitelisted[0], senderB); + } + + function _createSingleAddressClient(address account) internal returns (uint256 clientId) { + address[] memory addresses = new address[](1); + addresses[0] = account; + + vm.prank(owner); + clientId = registry.createClient(admin, 20_000_000, "Polymarket", addresses); + } +} From 3bcb710ab16285d56d7ac3b43ce6caf8d620cb7a Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 11:07:04 +0200 Subject: [PATCH 03/19] core/types, params, consensus/bor: reserved-blockspace header fields + hardfork gate (POS-3637) --- consensus/bor/bor.go | 30 +++++++++ consensus/bor/bor_test.go | 65 +++++++++++++++++++ core/forkid/forkid.go | 1 + core/types/block.go | 31 +++++++++ core/types/block_test.go | 123 +++++++++++++++++++++++++++++++++++ params/config.go | 132 +++++++++++++++++++++++++++----------- params/reserved_test.go | 94 +++++++++++++++++++++++++++ 7 files changed, 440 insertions(+), 36 deletions(-) create mode 100644 params/reserved_test.go diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index ec2dfcd79f..34be51de87 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -106,6 +106,10 @@ var ( // the gas target or base fee change denominator in its extra data. errMissingGiuglianoFields = errors.New("missing gas target or base fee change denominator in extra data") + // errMissingReservedBlockspaceFields is returned if a post-ReservedBlockspace + // block is missing the reserved tx count or reserved gas used in its extra data. + errMissingReservedBlockspaceFields = errors.New("missing reserved tx count or reserved gas used in extra data") + // errInvalidMixDigest is returned if a block's mix digest is non-zero. errInvalidMixDigest = errors.New("non-zero mix digest") @@ -504,6 +508,16 @@ func (c *Bor) verifyHeader(chain consensus.ChainHeaderReader, header *types.Head } } + // Post-ReservedBlockspace: verify the reserved tx count and reserved gas used + // are present. Presence only — value correctness (count matches the reserved + // region, gas within per-client quota) is enforced during block validation. + if c.config.IsReservedBlockspace(header.Number) { + reservedTxCount, reservedGasUsed := header.GetReservedInfo(c.chainConfig) + if reservedTxCount == nil || reservedGasUsed == nil { + return errMissingReservedBlockspaceFields + } + } + // Ensure that the mix digest is zero as we don't have fork protection currently if header.MixDigest != (common.Hash{}) { return errInvalidMixDigest @@ -1019,6 +1033,20 @@ func (c *Bor) setGiuglianoExtraFields(header *types.Header, parent *types.Header } } +// setReservedBlockspaceExtraFields initializes the reserved-region header fields +// for post-ReservedBlockspace blocks. The producer fills the real values during +// block building (the reserved pass); here we ensure the fields are present +// (non-nil) so every post-fork block is structurally valid and passes the +// verifyHeader presence check, even when there are no reserved transactions. +func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraData *types.BlockExtraData) { + if c.config.IsReservedBlockspace(header.Number) { + var zeroCount uint32 + var zeroGas uint64 + blockExtraData.ReservedTxCount = &zeroCount + blockExtraData.ReservedGasUsed = &zeroGas + } +} + // Prepare implements consensus.Engine, preparing all the consensus fields of the // header for running the transactions on top. func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, waitOnPrepare bool) error { @@ -1074,6 +1102,7 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, w } c.setGiuglianoExtraFields(header, parent, blockExtraData) + c.setReservedBlockspaceExtraFields(header, blockExtraData) blockExtraDataBytes, err := rlp.EncodeToBytes(blockExtraData) if err != nil { @@ -1094,6 +1123,7 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, w } c.setGiuglianoExtraFields(header, parent, blockExtraData) + c.setReservedBlockspaceExtraFields(header, blockExtraData) blockExtraDataBytes, err := rlp.EncodeToBytes(blockExtraData) if err != nil { diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 92fed881e6..f559364e93 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -5705,6 +5705,71 @@ func TestVerifyHeader_PreGiugliano_NoCheck(t *testing.T) { } } +func TestSetReservedBlockspaceExtraFields(t *testing.T) { + t.Parallel() + + // Pre-fork: the reserved fields are left nil. + bPre := &Bor{config: ¶ms.BorConfig{ReservedBlockspaceBlock: big.NewInt(100)}} + bedPre := &types.BlockExtraData{} + bPre.setReservedBlockspaceExtraFields(&types.Header{Number: big.NewInt(99)}, bedPre) + require.Nil(t, bedPre.ReservedTxCount) + require.Nil(t, bedPre.ReservedGasUsed) + + // Post-fork: the fields are initialized to non-nil zero so the block is valid + // even before the reserved pass fills the real values. + bPost := &Bor{config: ¶ms.BorConfig{ReservedBlockspaceBlock: big.NewInt(100)}} + bedPost := &types.BlockExtraData{} + bPost.setReservedBlockspaceExtraFields(&types.Header{Number: big.NewInt(100)}, bedPost) + require.NotNil(t, bedPost.ReservedTxCount) + require.Equal(t, uint32(0), *bedPost.ReservedTxCount) + require.NotNil(t, bedPost.ReservedGasUsed) + require.Equal(t, uint64(0), *bedPost.ReservedGasUsed) +} + +func TestVerifyHeader_ReservedBlockspaceMissingFields(t *testing.T) { + t.Parallel() + s := newGiuglianoVerifySetup(t, true) + s.b.config.ReservedBlockspaceBlock = big.NewInt(0) // reserved-blockspace active from genesis + + // Giugliano fields present (passes that check) but reserved fields absent. + gasTarget := uint64(15_000_000) + bfcd := uint64(64) + extra := buildBlockExtraBytes(&types.BlockExtraData{ + GasTarget: &gasTarget, + BaseFeeChangeDenominator: &bfcd, + }) + h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee)) + + chain := newRawDBChain(s.db, s.cfg, h, nil, nil) + err := s.b.verifyHeader(chain, h, nil) + require.ErrorIs(t, err, errMissingReservedBlockspaceFields) +} + +func TestVerifyHeader_ReservedBlockspaceFieldsPresent(t *testing.T) { + t.Parallel() + s := newGiuglianoVerifySetup(t, true) + s.b.config.ReservedBlockspaceBlock = big.NewInt(0) + + gasTarget := uint64(15_000_000) + bfcd := uint64(64) + count := uint32(0) + gasUsed := uint64(0) + extra := buildBlockExtraBytes(&types.BlockExtraData{ + GasTarget: &gasTarget, + BaseFeeChangeDenominator: &bfcd, + ReservedTxCount: &count, + ReservedGasUsed: &gasUsed, + }) + h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee)) + + chain := newRawDBChain(s.db, s.cfg, h, nil, nil) + err := s.b.verifyHeader(chain, h, nil) + if err != nil { + require.NotErrorIs(t, err, errMissingReservedBlockspaceFields) + require.NotErrorIs(t, err, errMissingGiuglianoFields) + } +} + // TestApplyMessage_StateSyncTxContext validates if TxContext is correctly // set for state-sync transactions. func TestApplyMessage_StateSyncTxContext(t *testing.T) { diff --git a/core/forkid/forkid.go b/core/forkid/forkid.go index 2b4623ffab..eb88fbf67b 100644 --- a/core/forkid/forkid.go +++ b/core/forkid/forkid.go @@ -336,6 +336,7 @@ func GatherForks(config *params.ChainConfig, genesisTime uint64) (heightForks [] config.Bor.LisovoProBlock, config.Bor.GiuglianoBlock, config.Bor.ChicagoBlock, + config.Bor.ReservedBlockspaceBlock, } { if fork != nil { heightForks = append(heightForks, fork.Uint64()) diff --git a/core/types/block.go b/core/types/block.go index c47c564dd0..c713d68451 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -134,6 +134,16 @@ type BlockExtraData struct { GasTarget *uint64 `rlp:"optional"` // BaseFeeChangeDenominator is the EIP-1559 base fee change denominator used by the block producer (post-Giugliano) BaseFeeChangeDenominator *uint64 `rlp:"optional"` + + // ReservedTxCount is the number of reserved-blockspace transactions placed at + // the top of the block (post-ReservedBlockspace fork). nil before the fork. + // These optional fields must stay last: RLP omits a trailing nil optional, and + // ReservedBlockspace activates after Giugliano so the fields above are always + // set when these are. + ReservedTxCount *uint32 `rlp:"optional"` + // ReservedGasUsed is the cumulative gas used by the reserved-region + // transactions (post-ReservedBlockspace fork). nil before the fork. + ReservedGasUsed *uint64 `rlp:"optional"` } // field type overrides for gencodec @@ -544,6 +554,27 @@ func (h *Header) GetBaseFeeParams(chainConfig *params.ChainConfig) (gasTarget *u return blockExtraData.GasTarget, blockExtraData.BaseFeeChangeDenominator } +// GetReservedInfo extracts the reserved-blockspace tx count and cumulative gas +// used from the block header's extra field. Returns nil, nil for pre-Cancun +// blocks, on decode error, or for blocks that predate the ReservedBlockspace +// fork (the optional fields are absent). +func (h *Header) GetReservedInfo(chainConfig *params.ChainConfig) (reservedTxCount *uint32, reservedGasUsed *uint64) { + if !chainConfig.IsCancun(h.Number) { + return nil, nil + } + + if len(h.Extra) < ExtraVanityLength+ExtraSealLength { + return nil, nil + } + + var blockExtraData BlockExtraData + if err := rlp.DecodeBytes(h.Extra[ExtraVanityLength:len(h.Extra)-ExtraSealLength], &blockExtraData); err != nil { + return nil, nil + } + + return blockExtraData.ReservedTxCount, blockExtraData.ReservedGasUsed +} + // DecodeBlockExtraData decodes the full BlockExtraData struct from the header's // Extra field in a single RLP decode. Returns nil for pre-Cancun blocks or on error. func (h *Header) DecodeBlockExtraData(chainConfig *params.ChainConfig) *BlockExtraData { diff --git a/core/types/block_test.go b/core/types/block_test.go index a9100a3358..874aa2db41 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -789,6 +789,129 @@ func TestBlockExtraDataRLPBackwardCompatibility(t *testing.T) { } } +func TestReservedBlockExtraDataRLP(t *testing.T) { + t.Parallel() + + gasTarget := uint64(15000000) + bfcd := uint64(64) + + // Pre-ReservedBlockspace: reserved fields nil -> omitted from RLP, decode as nil. + preReserved := &BlockExtraData{ + ValidatorBytes: []byte{0x01}, + TxDependency: [][]uint64{{1}}, + GasTarget: &gasTarget, + BaseFeeChangeDenominator: &bfcd, + } + encoded, err := rlp.EncodeToBytes(preReserved) + if err != nil { + t.Fatalf("encode pre-reserved: %v", err) + } + var dec BlockExtraData + if err := rlp.DecodeBytes(encoded, &dec); err != nil { + t.Fatalf("decode pre-reserved: %v", err) + } + if dec.ReservedTxCount != nil || dec.ReservedGasUsed != nil { + t.Fatalf("reserved fields should be nil pre-fork, got %v %v", dec.ReservedTxCount, dec.ReservedGasUsed) + } + + // Post-ReservedBlockspace: reserved fields set round-trip as non-nil. + count := uint32(3) + gasUsed := uint64(21000) + postReserved := &BlockExtraData{ + ValidatorBytes: []byte{0x02}, + TxDependency: [][]uint64{{2}}, + GasTarget: &gasTarget, + BaseFeeChangeDenominator: &bfcd, + ReservedTxCount: &count, + ReservedGasUsed: &gasUsed, + } + encoded, err = rlp.EncodeToBytes(postReserved) + if err != nil { + t.Fatalf("encode post-reserved: %v", err) + } + var dec2 BlockExtraData + if err := rlp.DecodeBytes(encoded, &dec2); err != nil { + t.Fatalf("decode post-reserved: %v", err) + } + if dec2.ReservedTxCount == nil || *dec2.ReservedTxCount != count { + t.Errorf("ReservedTxCount mismatch: got %v want %d", dec2.ReservedTxCount, count) + } + if dec2.ReservedGasUsed == nil || *dec2.ReservedGasUsed != gasUsed { + t.Errorf("ReservedGasUsed mismatch: got %v want %d", dec2.ReservedGasUsed, gasUsed) + } + + // Zero values must round-trip as non-nil (post-fork block with no reserved txs). + zeroCount := uint32(0) + zeroGas := uint64(0) + zero := &BlockExtraData{ + GasTarget: &gasTarget, + BaseFeeChangeDenominator: &bfcd, + ReservedTxCount: &zeroCount, + ReservedGasUsed: &zeroGas, + } + encoded, err = rlp.EncodeToBytes(zero) + if err != nil { + t.Fatalf("encode zero-reserved: %v", err) + } + var dec3 BlockExtraData + if err := rlp.DecodeBytes(encoded, &dec3); err != nil { + t.Fatalf("decode zero-reserved: %v", err) + } + if dec3.ReservedTxCount == nil || *dec3.ReservedTxCount != 0 { + t.Errorf("zero ReservedTxCount should decode non-nil 0, got %v", dec3.ReservedTxCount) + } + if dec3.ReservedGasUsed == nil || *dec3.ReservedGasUsed != 0 { + t.Errorf("zero ReservedGasUsed should decode non-nil 0, got %v", dec3.ReservedGasUsed) + } +} + +func TestGetReservedInfo(t *testing.T) { + t.Parallel() + + chainConfig := ¶ms.ChainConfig{ + ChainID: big.NewInt(137), + CancunBlock: big.NewInt(100), + } + + buildExtra := func(bed *BlockExtraData) []byte { + vanity := make([]byte, ExtraVanityLength) + seal := make([]byte, ExtraSealLength) + encoded, _ := rlp.EncodeToBytes(bed) + extra := append(vanity, encoded...) + extra = append(extra, seal...) + return extra + } + + count := uint32(2) + gasUsed := uint64(42000) + gasTarget := uint64(15000000) + bfcd := uint64(64) + bed := &BlockExtraData{ + GasTarget: &gasTarget, + BaseFeeChangeDenominator: &bfcd, + ReservedTxCount: &count, + ReservedGasUsed: &gasUsed, + } + + // Post-Cancun header with reserved fields present. + postHeader := &Header{Number: big.NewInt(200), Extra: buildExtra(bed)} + if rc, rg := postHeader.GetReservedInfo(chainConfig); rc == nil || *rc != count || rg == nil || *rg != gasUsed { + t.Errorf("post-Cancun: got count=%v gas=%v want %d %d", rc, rg, count, gasUsed) + } + + // Pre-Cancun header: getter returns nil, nil regardless of extra contents. + preHeader := &Header{Number: big.NewInt(50), Extra: buildExtra(bed)} + if rc, rg := preHeader.GetReservedInfo(chainConfig); rc != nil || rg != nil { + t.Errorf("pre-Cancun should return nil,nil; got %v %v", rc, rg) + } + + // Short extra (vanity only): getter returns nil, nil. + short := &Header{Number: big.NewInt(200), Extra: make([]byte, ExtraVanityLength)} + if rc, rg := short.GetReservedInfo(chainConfig); rc != nil || rg != nil { + t.Errorf("short extra should return nil,nil; got %v %v", rc, rg) + } +} + func TestGetBaseFeeParams(t *testing.T) { t.Parallel() diff --git a/params/config.go b/params/config.go index 2b35b885f1..acfe95e839 100644 --- a/params/config.go +++ b/params/config.go @@ -729,18 +729,19 @@ var ( BurntContract: map[string]string{"0": "0x000000000000000000000000000000000000dead"}, BlockAlloc: map[string]interface{}{}, // Bor hard forks - JaipurBlock: big.NewInt(0), - DelhiBlock: big.NewInt(0), - IndoreBlock: big.NewInt(0), - AhmedabadBlock: big.NewInt(0), - BhilaiBlock: big.NewInt(0), - RioBlock: big.NewInt(0), - MadhugiriBlock: big.NewInt(0), - MadhugiriProBlock: big.NewInt(0), - DandeliBlock: big.NewInt(0), - LisovoBlock: big.NewInt(0), - LisovoProBlock: big.NewInt(0), - ChicagoBlock: big.NewInt(0), + JaipurBlock: big.NewInt(0), + DelhiBlock: big.NewInt(0), + IndoreBlock: big.NewInt(0), + AhmedabadBlock: big.NewInt(0), + BhilaiBlock: big.NewInt(0), + RioBlock: big.NewInt(0), + MadhugiriBlock: big.NewInt(0), + MadhugiriProBlock: big.NewInt(0), + DandeliBlock: big.NewInt(0), + LisovoBlock: big.NewInt(0), + LisovoProBlock: big.NewInt(0), + ChicagoBlock: big.NewInt(0), + ReservedBlockspaceBlock: big.NewInt(0), }, } @@ -958,6 +959,19 @@ type BorConfig struct { LisovoProBlock *big.Int `json:"lisovoProBlock"` // LisovoPro switch block (nil = no fork, 0 = already on lisovoPro) GiuglianoBlock *big.Int `json:"giuglianoBlock"` // Giugliano switch block (nil = no fork, 0 = already on giugliano) ChicagoBlock *big.Int `json:"chicagoBlock"` // Chicago switch block (nil = no fork, 0 = already on chicago) + ReservedBlockspaceBlock *big.Int `json:"reservedBlockspaceBlock"` // ReservedBlockspace switch block (nil = no fork, 0 = already on reservedBlockspace) + + // ReservedClients is the config-backed stub source for reserved-blockspace + // classification (allowlist + per-client quota). The registry contract + // (POS-3572) will replace this source without changing the query methods below. + ReservedClients []ReservedClient `json:"reservedClients,omitempty"` +} + +// ReservedClient is one reserved-blockspace client in the config-backed stub +// registry: whitelisted sender addresses sharing a single per-block gas quota. +type ReservedClient struct { + Addresses []common.Address `json:"addresses"` + QuotaGas uint64 `json:"quotaGas"` } // String implements the stringer interface, returning the consensus engine details. @@ -1037,6 +1051,47 @@ func (c *BorConfig) IsChicago(number *big.Int) bool { return isBlockForked(c.ChicagoBlock, number) } +func (c *BorConfig) IsReservedBlockspace(number *big.Int) bool { + return isBlockForked(c.ReservedBlockspaceBlock, number) +} + +// IsReservedSender reports whether addr is a whitelisted sender of any +// reserved-blockspace client. Config-backed stub; the registry contract +// (POS-3572) replaces the source without changing this signature. +func (c *BorConfig) IsReservedSender(addr common.Address) bool { + for i := range c.ReservedClients { + for _, a := range c.ReservedClients[i].Addresses { + if a == addr { + return true + } + } + } + return false +} + +// ReservedQuotaOf returns the per-block reserved gas quota of the client that +// owns addr, or 0 if addr is not a reserved sender. +func (c *BorConfig) ReservedQuotaOf(addr common.Address) uint64 { + for i := range c.ReservedClients { + for _, a := range c.ReservedClients[i].Addresses { + if a == addr { + return c.ReservedClients[i].QuotaGas + } + } + } + return 0 +} + +// ReservedCapacity returns the sum of all reserved clients' per-block gas +// quotas — the capacity removed from the EIP-1559 normal region. +func (c *BorConfig) ReservedCapacity() uint64 { + var total uint64 + for i := range c.ReservedClients { + total += c.ReservedClients[i].QuotaGas + } + return total +} + // GetTargetGasPercentage returns the target gas percentage for gas limit calculation. // After Lisovo hard fork, this value can be configured via CLI flags (stored in BorConfig at runtime). // It validates the configured value and falls back to defaults if invalid or nil. @@ -1248,6 +1303,9 @@ func (c *ChainConfig) Description() string { if c.Bor.ChicagoBlock != nil { banner += fmt.Sprintf(" - Chicago: #%-8v\n", c.Bor.ChicagoBlock) } + if c.Bor.ReservedBlockspaceBlock != nil { + banner += fmt.Sprintf(" - ReservedBlockspace: #%-8v\n", c.Bor.ReservedBlockspaceBlock) + } return banner } @@ -1887,6 +1945,7 @@ type Rules struct { IsLisovo bool IsLisovoPro bool IsChicago bool + IsReservedBlockspace bool } // Rules ensures c's ChainID is not nil. @@ -1899,29 +1958,30 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, _ uint64) Rules { isMerge = isMerge && c.IsLondon(num) isVerkle := isMerge && c.IsVerkle(num) return Rules{ - ChainID: new(big.Int).Set(chainID), - IsHomestead: c.IsHomestead(num), - IsEIP150: c.IsEIP150(num), - IsEIP155: c.IsEIP155(num), - IsEIP158: c.IsEIP158(num), - IsByzantium: c.IsByzantium(num), - IsConstantinople: c.IsConstantinople(num), - IsPetersburg: c.IsPetersburg(num), - IsIstanbul: c.IsIstanbul(num), - IsBerlin: c.IsBerlin(num), - IsEIP2929: c.IsBerlin(num) && !isVerkle, - IsLondon: c.IsLondon(num), - IsMerge: isMerge, - IsShanghai: c.IsShanghai(num), - IsCancun: c.IsCancun(num), - IsPrague: c.IsPrague(num), - IsVerkle: c.IsVerkle(num), - IsOsaka: c.IsOsaka(num), - IsEIP4762: c.IsVerkle(num), - IsMadhugiri: c.Bor != nil && c.Bor.IsMadhugiri(num), - IsMadhugiriPro: c.Bor != nil && c.Bor.IsMadhugiriPro(num), - IsLisovo: c.Bor != nil && c.Bor.IsLisovo(num), - IsLisovoPro: c.Bor != nil && c.Bor.IsLisovoPro(num), - IsChicago: c.Bor != nil && c.Bor.IsChicago(num), + ChainID: new(big.Int).Set(chainID), + IsHomestead: c.IsHomestead(num), + IsEIP150: c.IsEIP150(num), + IsEIP155: c.IsEIP155(num), + IsEIP158: c.IsEIP158(num), + IsByzantium: c.IsByzantium(num), + IsConstantinople: c.IsConstantinople(num), + IsPetersburg: c.IsPetersburg(num), + IsIstanbul: c.IsIstanbul(num), + IsBerlin: c.IsBerlin(num), + IsEIP2929: c.IsBerlin(num) && !isVerkle, + IsLondon: c.IsLondon(num), + IsMerge: isMerge, + IsShanghai: c.IsShanghai(num), + IsCancun: c.IsCancun(num), + IsPrague: c.IsPrague(num), + IsVerkle: c.IsVerkle(num), + IsOsaka: c.IsOsaka(num), + IsEIP4762: c.IsVerkle(num), + IsMadhugiri: c.Bor != nil && c.Bor.IsMadhugiri(num), + IsMadhugiriPro: c.Bor != nil && c.Bor.IsMadhugiriPro(num), + IsLisovo: c.Bor != nil && c.Bor.IsLisovo(num), + IsLisovoPro: c.Bor != nil && c.Bor.IsLisovoPro(num), + IsChicago: c.Bor != nil && c.Bor.IsChicago(num), + IsReservedBlockspace: c.Bor != nil && c.Bor.IsReservedBlockspace(num), } } diff --git a/params/reserved_test.go b/params/reserved_test.go new file mode 100644 index 0000000000..dd3eacb02b --- /dev/null +++ b/params/reserved_test.go @@ -0,0 +1,94 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package params + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestIsReservedBlockspace(t *testing.T) { + t.Parallel() + + cfg := &BorConfig{ReservedBlockspaceBlock: big.NewInt(100)} + if cfg.IsReservedBlockspace(big.NewInt(99)) { + t.Error("should not be active at N-1") + } + if !cfg.IsReservedBlockspace(big.NewInt(100)) { + t.Error("should be active at N") + } + if !cfg.IsReservedBlockspace(big.NewInt(101)) { + t.Error("should be active at N+1") + } + + // nil fork block = never active. + none := &BorConfig{} + if none.IsReservedBlockspace(big.NewInt(1_000_000)) { + t.Error("nil ReservedBlockspaceBlock should never be active") + } +} + +func TestReservedClientClassifier(t *testing.T) { + t.Parallel() + + a := common.HexToAddress("0x00000000000000000000000000000000000000Aa") + b := common.HexToAddress("0x00000000000000000000000000000000000000Bb") + c := common.HexToAddress("0x00000000000000000000000000000000000000Cc") + other := common.HexToAddress("0x00000000000000000000000000000000000000Ff") + + cfg := &BorConfig{ + ReservedClients: []ReservedClient{ + {Addresses: []common.Address{a, b}, QuotaGas: 20_000_000}, + {Addresses: []common.Address{c}, QuotaGas: 10_000_000}, + }, + } + + for _, addr := range []common.Address{a, b, c} { + if !cfg.IsReservedSender(addr) { + t.Errorf("%s should be reserved", addr.Hex()) + } + } + if cfg.IsReservedSender(other) { + t.Error("non-whitelisted address must not be reserved") + } + + // QuotaOf returns the owning client's quota; addresses of the same client share it. + if got := cfg.ReservedQuotaOf(a); got != 20_000_000 { + t.Errorf("quota of a: got %d want 20000000", got) + } + if got := cfg.ReservedQuotaOf(b); got != 20_000_000 { + t.Errorf("quota of b (same client as a): got %d want 20000000", got) + } + if got := cfg.ReservedQuotaOf(c); got != 10_000_000 { + t.Errorf("quota of c: got %d want 10000000", got) + } + if got := cfg.ReservedQuotaOf(other); got != 0 { + t.Errorf("quota of non-reserved: got %d want 0", got) + } + + if got := cfg.ReservedCapacity(); got != 30_000_000 { + t.Errorf("capacity: got %d want 30000000", got) + } + + // Empty config classifies nothing and has zero capacity. + empty := &BorConfig{} + if empty.IsReservedSender(a) || empty.ReservedQuotaOf(a) != 0 || empty.ReservedCapacity() != 0 { + t.Error("empty reserved config should classify nothing") + } +} From 28d9d34dceb91609a059047e2c53034d2401c0a0 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 11:25:30 +0200 Subject: [PATCH 04/19] core: skip in-protocol fees for reserved-blockspace transactions (POS-3573) Reserved-blockspace senders (post-fork, classified via BorConfig) execute fee-free: no gas debit at buyGas, the EIP-1559 fee-cap floor is waived in preCheck, no gas refund, no producer tip, and no base-fee burn. The decision is computed once in newStateTransition so the serial and BlockSTM paths agree on the same state root. Fee fields stay readable for off-chain settlement. --- core/reserved_fee_test.go | 310 ++++++++++++++++++++++++++++++++++++++ core/state_transition.go | 68 +++++++-- 2 files changed, 367 insertions(+), 11 deletions(-) create mode 100644 core/reserved_fee_test.go diff --git a/core/reserved_fee_test.go b/core/reserved_fee_test.go new file mode 100644 index 0000000000..52d9aab48d --- /dev/null +++ b/core/reserved_fee_test.go @@ -0,0 +1,310 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "context" + "math/big" + "testing" + + "github.com/holiman/uint256" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/blockstm" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" +) + +var reservedBurntAddr = common.HexToAddress("0x00000000000000000000000000000000000000dd") + +// reservedTestConfig clones BorUnittestChainConfig (London active at 0) and +// layers the reserved-blockspace fork + an optional reserved client on top, +// without mutating the shared global config. +func reservedTestConfig(forkBlock *big.Int, reservedSenders ...common.Address) *params.ChainConfig { + cc := *params.BorUnittestChainConfig + bor := *cc.Bor + bor.BurntContract = map[string]string{"0": reservedBurntAddr.Hex()} + bor.ReservedBlockspaceBlock = forkBlock + if len(reservedSenders) > 0 { + bor.ReservedClients = []params.ReservedClient{ + {Addresses: reservedSenders, QuotaGas: 30_000_000}, + } + } + cc.Bor = &bor + return &cc +} + +func reservedBlockCtx(coinbase common.Address, blockNumber *big.Int, baseFee *big.Int) vm.BlockContext { + return vm.BlockContext{ + CanTransfer: CanTransfer, + Transfer: Transfer, + GetHash: func(n uint64) common.Hash { return common.Hash{} }, + Coinbase: coinbase, + GasLimit: 30_000_000, + BlockNumber: blockNumber, + Time: 1, + BaseFee: baseFee, + } +} + +// fundedState returns an in-memory StateDB with `sender` funded and nonce 0. +func fundedState(t *testing.T, sender common.Address, balance *uint256.Int) *state.StateDB { + t.Helper() + memdb := rawdb.NewMemoryDatabase() + tdb := triedb.NewDatabase(memdb, triedb.HashDefaults) + sdb, err := state.New(types.EmptyRootHash, state.NewDatabase(tdb, nil)) + if err != nil { + t.Fatal(err) + } + sdb.AddBalance(sender, balance, 0) + sdb.SetNonce(sender, 0, 0) + return sdb +} + +// TestReservedTxSkipsFees pins the core ticket-B behaviour: a reserved-sender +// tx executes fee-free past the fork — no gas debit, no base-fee floor, no +// producer tip, no burn. Only msg.Value moves. +func TestReservedTxSkipsFees(t *testing.T) { + key, _ := crypto.GenerateKey() + sender := crypto.PubkeyToAddress(key.PublicKey) + coinbase := common.HexToAddress("0x000000000000000000000000000000000000c0b0") + recipient := common.HexToAddress("0x1111111111111111111111111111111111111111") + + cc := reservedTestConfig(big.NewInt(0), sender) + baseFee := big.NewInt(1_000_000_000) + blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee) + + initial := uint256.NewInt(1e18) + sdb := fundedState(t, sender, initial) + + value := big.NewInt(5) + signer := types.NewLondonSigner(cc.ChainID) + tx, err := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + ChainID: cc.ChainID, + Nonce: 0, + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(0), // below baseFee — would be ErrFeeCapTooLow if not reserved + Gas: 21000, + To: &recipient, + Value: value, + }), signer, key) + if err != nil { + t.Fatal(err) + } + msg, err := TransactionToMessage(tx, signer, baseFee) + if err != nil { + t.Fatal(err) + } + + evm := vm.NewEVM(blockCtx, sdb, cc, vm.Config{}) + evm.SetTxContext(NewEVMTxContext(msg)) + result, err := ApplyMessage(evm, msg, new(GasPool).AddGas(blockCtx.GasLimit)) + if err != nil { + t.Fatalf("reserved tx failed: %v", err) + } + if result.Failed() { + t.Fatalf("reserved tx reverted: %v", result.Err) + } + + // No fee was applied in-protocol. + if result.FeeBurnt.Sign() != 0 { + t.Errorf("FeeBurnt=%s, want 0", result.FeeBurnt) + } + if result.FeeTipped.Sign() != 0 { + t.Errorf("FeeTipped=%s, want 0", result.FeeTipped) + } + if got := sdb.GetBalance(coinbase); !got.IsZero() { + t.Errorf("coinbase tip=%s, want 0", got) + } + if got := sdb.GetBalance(reservedBurntAddr); !got.IsZero() { + t.Errorf("burnt-contract balance=%s, want 0", got) + } + + // Sender paid only value — no gas was debited. + wantSender := new(uint256.Int).Sub(initial, uint256.MustFromBig(value)) + if got := sdb.GetBalance(sender); got.Cmp(wantSender) != 0 { + t.Errorf("sender balance=%s, want %s (value only, no gas)", got, wantSender) + } + if got := sdb.GetBalance(recipient); got.Cmp(uint256.MustFromBig(value)) != 0 { + t.Errorf("recipient balance=%s, want %s", got, value) + } + if result.UsedGas != 21000 { + t.Errorf("UsedGas=%d, want 21000", result.UsedGas) + } +} + +// TestReservedTxGatedByFork verifies the fork gate: before the activation +// block a reserved sender gets no exemption, so a zero-fee tx is rejected +// with ErrFeeCapTooLow exactly as any other underpriced tx. +func TestReservedTxGatedByFork(t *testing.T) { + key, _ := crypto.GenerateKey() + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x1111111111111111111111111111111111111111") + + cc := reservedTestConfig(big.NewInt(100), sender) // fork at 100 + baseFee := big.NewInt(1_000_000_000) + blockCtx := reservedBlockCtx(common.HexToAddress("0xc0b0"), big.NewInt(1), baseFee) // pre-fork + + sdb := fundedState(t, sender, uint256.NewInt(1e18)) + signer := types.NewLondonSigner(cc.ChainID) + tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + ChainID: cc.ChainID, + Nonce: 0, + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(0), + Gas: 21000, + To: &recipient, + Value: big.NewInt(1), + }), signer, key) + msg, _ := TransactionToMessage(tx, signer, baseFee) + + evm := vm.NewEVM(blockCtx, sdb, cc, vm.Config{}) + evm.SetTxContext(NewEVMTxContext(msg)) + _, err := ApplyMessage(evm, msg, new(GasPool).AddGas(blockCtx.GasLimit)) + if err == nil { + t.Fatal("pre-fork zero-fee tx should be rejected, got nil error") + } +} + +// TestNonReservedSenderPaysNormally verifies the negative case: with the fork +// active but the sender NOT a reserved client, fees apply as usual (tip to +// coinbase, base fee burnt). +func TestNonReservedSenderPaysNormally(t *testing.T) { + key, _ := crypto.GenerateKey() + sender := crypto.PubkeyToAddress(key.PublicKey) + coinbase := common.HexToAddress("0x000000000000000000000000000000000000c0b0") + recipient := common.HexToAddress("0x1111111111111111111111111111111111111111") + + // Fork active, but the reserved client is a DIFFERENT address. + reserved := common.HexToAddress("0x00000000000000000000000000000000000000ee") + cc := reservedTestConfig(big.NewInt(0), reserved) + baseFee := big.NewInt(1_000_000_000) + blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee) + + sdb := fundedState(t, sender, uint256.NewInt(1e18)) + signer := types.NewLondonSigner(cc.ChainID) + tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + ChainID: cc.ChainID, + Nonce: 0, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(2_000_000_000), + Gas: 21000, + To: &recipient, + Value: big.NewInt(1), + }), signer, key) + msg, _ := TransactionToMessage(tx, signer, baseFee) + + evm := vm.NewEVM(blockCtx, sdb, cc, vm.Config{}) + evm.SetTxContext(NewEVMTxContext(msg)) + result, err := ApplyMessage(evm, msg, new(GasPool).AddGas(blockCtx.GasLimit)) + if err != nil { + t.Fatalf("non-reserved tx failed: %v", err) + } + if result.FeeBurnt == nil || result.FeeBurnt.Sign() == 0 { + t.Errorf("expected non-zero burn for non-reserved sender, got %v", result.FeeBurnt) + } + if got := sdb.GetBalance(coinbase); got.IsZero() { + t.Error("expected non-zero coinbase tip for non-reserved sender") + } + if got := sdb.GetBalance(reservedBurntAddr); got.IsZero() { + t.Error("expected non-zero burnt-contract balance for non-reserved sender") + } +} + +// TestReservedTxSerialParallelParity is the consensus-critical check: the +// reserved fee path must produce a byte-identical post-state whether the tx +// runs through the serial executor (ApplyMessage) or BlockSTM +// (ExecuteV2BlockSTM). Any divergence is a chain split. +func TestReservedTxSerialParallelParity(t *testing.T) { + key, _ := crypto.GenerateKey() + sender := crypto.PubkeyToAddress(key.PublicKey) + coinbase := common.HexToAddress("0x000000000000000000000000000000000000c0b0") + recipient := common.HexToAddress("0x1111111111111111111111111111111111111111") + + cc := reservedTestConfig(big.NewInt(0), sender) + baseFee := big.NewInt(1_000_000_000) + blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee) + + // Committed base state shared by both paths. + memdb := rawdb.NewMemoryDatabase() + tdb := triedb.NewDatabase(memdb, triedb.HashDefaults) + sdb, _ := state.New(types.EmptyRootHash, state.NewDatabase(tdb, nil)) + sdb.AddBalance(sender, uint256.NewInt(1e18), 0) + sdb.SetNonce(sender, 0, 0) + root, _ := sdb.Commit(0, false, false) + tdb.Commit(root, false) + + signer := types.NewLondonSigner(cc.ChainID) + tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + ChainID: cc.ChainID, + Nonce: 0, + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(0), + Gas: 21000, + To: &recipient, + Value: big.NewInt(7), + }), signer, key) + msg, _ := TransactionToMessage(tx, signer, baseFee) + + // Serial path. + serialDB, _ := state.New(root, state.NewDatabase(tdb, nil)) + evm := vm.NewEVM(blockCtx, serialDB, cc, vm.Config{}) + evm.SetTxContext(NewEVMTxContext(msg)) + if _, err := ApplyMessage(evm, msg, new(GasPool).AddGas(blockCtx.GasLimit)); err != nil { + t.Fatalf("serial reserved tx failed: %v", err) + } + serialRoot := serialDB.IntermediateRoot(true) + + // Parallel (BlockSTM) path. + base, _ := state.New(root, state.NewDatabase(tdb, nil)) + finalDB := base.Copy() + finalDB.StartPrefetcher("test", nil, nil) + defer finalDB.StopPrefetcher() + tasks := []V2Task{{Index: 0, Tx: tx, Msg: msg}} + result := ExecuteV2BlockSTM(context.Background(), tasks, base, + blockstm.NewMVStore(), blockstm.NewMVBalanceStore(), + blockCtx, common.Hash{}, vm.Config{}, cc, blockCtx.GasLimit, 1, finalDB, nil) + if result.ExecErrIdx >= 0 { + t.Fatalf("parallel reserved tx errored at %d: %v", result.ExecErrIdx, result.ExecErr) + } + if result.PanickedIdx >= 0 { + t.Fatalf("parallel reserved tx panicked at %d", result.PanickedIdx) + } + parallelRoot := finalDB.IntermediateRoot(true) + + if serialRoot != parallelRoot { + t.Fatalf("state root divergence: serial=%s parallel=%s", serialRoot.Hex(), parallelRoot.Hex()) + } + + // And the balances landed where expected on both. + for _, db := range []*state.StateDB{serialDB, finalDB} { + if got := db.GetBalance(coinbase); !got.IsZero() { + t.Errorf("coinbase tip=%s, want 0", got) + } + if got := db.GetBalance(reservedBurntAddr); !got.IsZero() { + t.Errorf("burnt balance=%s, want 0", got) + } + if got := db.GetBalance(recipient); got.Uint64() != 7 { + t.Errorf("recipient=%s, want 7", got) + } + } +} diff --git a/core/state_transition.go b/core/state_transition.go index 0893fef662..7ff9fe02e8 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -282,15 +282,33 @@ type stateTransition struct { // This is useful during parallel state transition, where the common account read/write should be minimized. noFeeBurnAndTip bool noFeeLog bool // If true, skip fee transfer log and coinbase balance read (for parallel execution) + + // reserved is true for a reserved-blockspace transaction (post-fork, from a + // whitelisted client). Such a tx pays zero in-protocol fee: no gas debit, no + // gas refund, no producer tip, no base-fee burn. Computed once in + // newStateTransition so serial and parallel execution agree. + reserved bool } // newStateTransition initialises and returns a new state transition object. func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition { + // Classify the message as reserved-blockspace here — the single point every + // execution path (serial, parallel, and the ApplyMessage* variants) funnels + // through — so the zero-fee decision is identical across executors. The source + // is the config-backed stub (POS-3572 will swap in the registry without + // changing this call). + var reserved bool + if cfg := evm.ChainConfig(); cfg.Bor != nil && + cfg.Bor.IsReservedBlockspace(evm.Context.BlockNumber) && + cfg.Bor.IsReservedSender(msg.From) { + reserved = true + } return &stateTransition{ - gp: gp, - evm: evm, - msg: msg, - state: evm.StateDB, + gp: gp, + evm: evm, + msg: msg, + state: evm.StateDB, + reserved: reserved, } } @@ -325,6 +343,16 @@ func (st *stateTransition) buyGas() error { mgval.Add(mgval, blobFee) } } + + if st.reserved { + // Reserved-blockspace tx pays zero in-protocol fee: don't require or debit + // the gas cost; only the call value must be funded. Block gas is still + // consumed (SubGas below), and gasRemaining is still set, so execution and + // gas accounting are unchanged. + mgval = new(big.Int) + balanceCheck = new(big.Int).Set(st.msg.Value) + } + balanceCheckU256, overflow := uint256.FromBig(balanceCheck) if overflow { return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) @@ -401,7 +429,9 @@ func (st *stateTransition) preCheck() error { } // This will panic if baseFee is nil, but basefee presence is verified // as part of header validation. - if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 { + // Reserved-blockspace txs are exempt from the base-fee floor: they may + // carry maxFeePerGas below baseFee (or zero) and still execute fee-free. + if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 && !st.reserved { return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow, msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee) } @@ -603,6 +633,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee) } + if st.reserved { + // Reserved-blockspace tx pays zero in-protocol fee: no producer tip and no + // base-fee burn (the burn is skipped below). The tx's fee fields stay + // readable for off-chain settlement but are not applied in-protocol. + effectiveTip = new(big.Int) + } + // TODO(raneet10): Double check. We might want to inculcate this fix in a separate condition // if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { // // Skip fee payment when NoBaseFee is set and the fee fields @@ -628,9 +665,14 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // fixtures with Bor == nil panic here. if bor := st.evm.ChainConfig().Bor; bor != nil { burntContractAddress = common.HexToAddress(bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64())) - burnAmount = new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) - if !st.noFeeBurnAndTip { - st.state.AddBalance(burntContractAddress, cmath.BigIntToUint256Int(burnAmount), tracing.BalanceChangeTransfer) + if st.reserved { + // No base-fee burn for reserved-blockspace txs. + burnAmount = new(big.Int) + } else { + burnAmount = new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) + if !st.noFeeBurnAndTip { + st.state.AddBalance(burntContractAddress, cmath.BigIntToUint256Int(burnAmount), tracing.BalanceChangeTransfer) + } } } } @@ -758,9 +800,13 @@ func (st *stateTransition) calcRefund() uint64 { // returnGas returns ETH for remaining gas, // exchanged at the original rate. func (st *stateTransition) returnGas() { - remaining := uint256.NewInt(st.gasRemaining) - remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) - st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) + // Reserved-blockspace txs were never charged for gas (see buyGas), so there is + // nothing to refund. Unused gas is still returned to the block gas pool below. + if !st.reserved { + remaining := uint256.NewInt(st.gasRemaining) + remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) + st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) + } if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining > 0 { st.evm.Config.Tracer.OnGasChange(st.gasRemaining, 0, tracing.GasChangeTxLeftOverReturned) From 2090e680d98cddb8587b12ed06186549b2f0186e Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 11:33:03 +0200 Subject: [PATCH 05/19] eip1559: price the public base fee on the normal region only (POS-3638) Post-ReservedBlockspace, CalcBaseFee holds the reserved capacity out of the gas target and nets the parent's ReservedGasUsed out of gas used, so the public base fee tracks only normal-region demand against normal-region capacity. Anchoring the target on reserved capacity (Sigma quotas) rather than reserved used gas keeps the public market stable and stops a reserved client from moving the public fee with its own usage. Header.GasLimit is untouched (formula-only input). VerifyEIP1559Header inherits the change: its pre-Lisovo path recomputes via CalcBaseFee and its post-Lisovo path is a bounded check. --- consensus/misc/eip1559/eip1559.go | 58 ++++- .../misc/eip1559/eip1559_reserved_test.go | 204 ++++++++++++++++++ 2 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 consensus/misc/eip1559/eip1559_reserved_test.go diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index 69f96865aa..b43189d430 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -116,8 +116,21 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { // Modified for bor to derive gas target by percentage instead of using elasticity multiplier post dandeli HF parentGasTarget := calcParentGasTarget(config, parent) + parentGasUsed := parent.GasUsed + + // Reserved blockspace prices the public fee market on the normal region + // only: hold the reserved capacity out of the target and net the parent's + // reserved gas out of the used figure. Reserved txs pay zero in-protocol + // fee, so counting them would let a reserved client move the public base + // fee with its own usage. Anchoring the target on capacity (not used gas) + // keeps the public market stable and isolated from reserved behaviour. + if config.Bor != nil && config.Bor.IsReservedBlockspace(parent.Number) { + parentGasTarget = reservedAwareGasTarget(config, parent) + parentGasUsed = publicGasUsed(config, parent) + } + // If the parent gasUsed is the same as the target, the baseFee remains unchanged. - if parent.GasUsed == parentGasTarget { + if parentGasUsed == parentGasTarget { return new(big.Int).Set(parent.BaseFee) } @@ -142,10 +155,10 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { } } - if parent.GasUsed > parentGasTarget { + if parentGasUsed > parentGasTarget { // If the parent block used more gas than its target, the baseFee should increase. // max(1, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) - num.SetUint64(parent.GasUsed - parentGasTarget) + num.SetUint64(parentGasUsed - parentGasTarget) num.Mul(num, parent.BaseFee) num.Div(num, denom.SetUint64(parentGasTarget)) num.Div(num, denom.SetUint64(baseFeeChangeDenominatorUint64)) @@ -162,7 +175,7 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { } else { // Otherwise if the parent block used less gas than its target, the baseFee should decrease. // max(0, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) - num.SetUint64(parentGasTarget - parent.GasUsed) + num.SetUint64(parentGasTarget - parentGasUsed) num.Mul(num, parent.BaseFee) num.Div(num, denom.SetUint64(parentGasTarget)) num.Div(num, denom.SetUint64(baseFeeChangeDenominatorUint64)) @@ -185,12 +198,45 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { // Dandeli HF, a percentage value is used to calculate the gas target (validated with fallback to default). // Post-Lisovo, if EnableDynamicTargetGas is configured, the percentage adjusts dynamically based on parent base fee. func calcParentGasTarget(config *params.ChainConfig, parent *types.Header) uint64 { + return gasTargetForLimit(config, parent, parent.GasLimit) +} + +// gasTargetForLimit applies the EIP-1559 gas-target curve to an arbitrary gas +// limit. calcParentGasTarget passes the full parent.GasLimit; the reserved +// path passes the public capacity (limit minus reserved quotas). The target +// percentage is independent of the limit, so the same curve composes cleanly. +func gasTargetForLimit(config *params.ChainConfig, parent *types.Header, gasLimit uint64) uint64 { if config.Bor != nil && config.Bor.IsDandeli(parent.Number) { // Use dynamic helper which falls back to static GetTargetGasPercentage when feature is disabled targetPercentage := config.Bor.GetDynamicTargetGasPercentage(parent.BaseFee, parent.Number) - return parent.GasLimit * targetPercentage / 100 + return gasLimit * targetPercentage / 100 + } + return gasLimit / config.ElasticityMultiplier() +} + +// reservedAwareGasTarget returns the EIP-1559 target for the public region: +// the standard curve applied to capacity that excludes the reserved quotas +// (Σ active client quotas). Header.GasLimit is left untouched — this is a +// formula-only input. +func reservedAwareGasTarget(config *params.ChainConfig, parent *types.Header) uint64 { + reservedCapacity := config.Bor.ReservedCapacity() + if reservedCapacity >= parent.GasLimit { + // Misconfiguration guard: reserved capacity is capped well below the + // block limit by design. Fall back to the full target rather than + // producing a zero or negative public target. + return calcParentGasTarget(config, parent) + } + return gasTargetForLimit(config, parent, parent.GasLimit-reservedCapacity) +} + +// publicGasUsed nets the parent's reserved gas out of its total gas used, so +// the base-fee controller tracks only normal-region demand. +func publicGasUsed(config *params.ChainConfig, parent *types.Header) uint64 { + _, reservedGasUsed := parent.GetReservedInfo(config) + if reservedGasUsed == nil || *reservedGasUsed > parent.GasUsed { + return parent.GasUsed } - return parent.GasLimit / config.ElasticityMultiplier() + return parent.GasUsed - *reservedGasUsed } // CalcGasTarget exports calcParentGasTarget for use by consensus code (e.g. Prepare). diff --git a/consensus/misc/eip1559/eip1559_reserved_test.go b/consensus/misc/eip1559/eip1559_reserved_test.go new file mode 100644 index 0000000000..2b0099dbac --- /dev/null +++ b/consensus/misc/eip1559/eip1559_reserved_test.go @@ -0,0 +1,204 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package eip1559 + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" +) + +// reservedFeeConfig builds a London+Cancun bor config with the reserved fork +// gated at reservedBlock (nil = never) and a single reserved client holding +// `capacity` gas. Pre-Dandeli, so the gas target is gasLimit/elasticity (÷2), +// which keeps the arithmetic in the tests exact and readable. +func reservedFeeConfig(reservedBlock *big.Int, capacity uint64) *params.ChainConfig { + cc := ¶ms.ChainConfig{ + ChainID: big.NewInt(80003), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ShanghaiBlock: big.NewInt(0), + CancunBlock: big.NewInt(0), + Bor: ¶ms.BorConfig{ + Period: map[string]uint64{"0": 1}, + Sprint: map[string]uint64{"0": 64}, + ReservedBlockspaceBlock: reservedBlock, + }, + } + if capacity > 0 { + cc.Bor.ReservedClients = []params.ReservedClient{ + {Addresses: []common.Address{{0x01}}, QuotaGas: capacity}, + } + } + return cc +} + +// extraWithReserved encodes a header Extra carrying the reserved-region fields. +// The two preceding optional fields (GasTarget, BaseFeeChangeDenominator) must +// be non-nil for RLP to emit the trailing reserved optionals. +func extraWithReserved(t *testing.T, reservedTxCount uint32, reservedGasUsed uint64) []byte { + t.Helper() + zero := uint64(0) + enc, err := rlp.EncodeToBytes(&types.BlockExtraData{ + GasTarget: &zero, + BaseFeeChangeDenominator: &zero, + ReservedTxCount: &reservedTxCount, + ReservedGasUsed: &reservedGasUsed, + }) + require.NoError(t, err) + + extra := make([]byte, types.ExtraVanityLength) + extra = append(extra, enc...) + extra = append(extra, make([]byte, types.ExtraSealLength)...) + return extra +} + +// TestReservedBaseFee_CapacityReducesTarget pins the capacity anchor: the +// public gas target excludes the reserved quotas, so a block whose usage lands +// exactly on the reduced target holds the base fee steady — whereas with the +// fork inactive the same usage sits below the full target and the fee drops. +func TestReservedBaseFee_CapacityReducesTarget(t *testing.T) { + t.Parallel() + + const gasLimit = 60_000_000 + const capacity = 20_000_000 + baseFee := big.NewInt(params.InitialBaseFee) + + // Public target = (gasLimit - capacity) / 2 = 20M. + parent := &types.Header{ + Number: big.NewInt(1), + GasLimit: gasLimit, + GasUsed: (gasLimit - capacity) / 2, + BaseFee: baseFee, + } + + active := reservedFeeConfig(big.NewInt(0), capacity) + if got := CalcBaseFee(active, parent); got.Cmp(baseFee) != 0 { + t.Errorf("reserved-active base fee = %s, want unchanged %s (usage == public target)", got, baseFee) + } + + // Fork inactive: full target = gasLimit/2 = 30M, so 20M usage is below + // target and the base fee must fall. + inactive := reservedFeeConfig(nil, capacity) + if got := CalcBaseFee(inactive, parent); got.Cmp(baseFee) >= 0 { + t.Errorf("reserved-inactive base fee = %s, want < %s (usage below full target)", got, baseFee) + } +} + +// TestReservedBaseFee_NetsReservedGasUsed pins the used-side netting: the +// parent's ReservedGasUsed is subtracted from gas used before the controller +// runs, so reserved consumption doesn't move the public base fee. +func TestReservedBaseFee_NetsReservedGasUsed(t *testing.T) { + t.Parallel() + + const gasLimit = 60_000_000 + const capacity = 20_000_000 + baseFee := big.NewInt(params.InitialBaseFee) + cfg := reservedFeeConfig(big.NewInt(0), capacity) + + // Public target = (60M - 20M)/2 = 20M. Total used 35M, of which 15M is + // reserved → public used = 20M == target → base fee unchanged. + parent := &types.Header{ + Number: big.NewInt(1), + GasLimit: gasLimit, + GasUsed: 35_000_000, + BaseFee: baseFee, + Extra: extraWithReserved(t, 3, 15_000_000), + } + if got := CalcBaseFee(cfg, parent); got.Cmp(baseFee) != 0 { + t.Errorf("base fee = %s, want unchanged %s (public used == target after netting)", got, baseFee) + } + + // Same total usage but zero reserved gas: public used = 35M > 20M target, + // so the base fee must rise. Proves the netting is what held it steady above. + parentNoReserved := &types.Header{ + Number: big.NewInt(1), + GasLimit: gasLimit, + GasUsed: 35_000_000, + BaseFee: baseFee, + Extra: extraWithReserved(t, 0, 0), + } + if got := CalcBaseFee(cfg, parentNoReserved); got.Cmp(baseFee) <= 0 { + t.Errorf("base fee = %s, want > %s (full usage above target without netting)", got, baseFee) + } +} + +// TestReservedBaseFee_CapacityExceedsLimitFallsBack guards the misconfiguration +// path: if reserved capacity is mis-set at or above the block gas limit, the +// target falls back to the full-limit curve instead of going to zero (which +// would divide by zero) or negative. +func TestReservedBaseFee_CapacityExceedsLimitFallsBack(t *testing.T) { + t.Parallel() + + const gasLimit = 30_000_000 + baseFee := big.NewInt(params.InitialBaseFee) + cfg := reservedFeeConfig(big.NewInt(0), gasLimit+1) // capacity > limit + + parent := &types.Header{ + Number: big.NewInt(1), + GasLimit: gasLimit, + GasUsed: gasLimit / 2, // == full target → unchanged + BaseFee: baseFee, + } + + var got *big.Int + require.NotPanics(t, func() { got = CalcBaseFee(cfg, parent) }, + "CalcBaseFee must not panic when reserved capacity >= gas limit") + if got.Cmp(baseFee) != 0 { + t.Errorf("fallback base fee = %s, want unchanged %s (full target)", got, baseFee) + } +} + +// TestReservedBaseFee_VerifyAcceptsProducerValue checks that the reserved-aware +// base fee a producer computes passes strict (pre-Lisovo) header verification, +// since VerifyEIP1559Header recomputes via CalcBaseFee on that path. +func TestReservedBaseFee_VerifyAcceptsProducerValue(t *testing.T) { + t.Parallel() + + cfg := reservedFeeConfig(big.NewInt(0), 20_000_000) // Lisovo nil → strict verify + baseFee := big.NewInt(params.InitialBaseFee) + parent := &types.Header{ + Number: big.NewInt(1), + GasLimit: 60_000_000, + GasUsed: 50_000_000, + BaseFee: baseFee, + } + expected := CalcBaseFee(cfg, parent) + child := &types.Header{ + Number: big.NewInt(2), + GasLimit: 60_000_000, + GasUsed: 0, + BaseFee: expected, + } + require.NoError(t, VerifyEIP1559Header(cfg, parent, child), + "strict verification must accept the reserved-aware base fee") +} From 0be354777761a04694bae6204e6df12dc03a46d6 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 11:43:07 +0200 Subject: [PATCH 06/19] txpool: admit and keep zero-fee reserved-blockspace transactions (POS-3570) Reserved-blockspace senders pay zero in-protocol fee, but the pool rejected and dropped their txs in several places. Add a sender-based, consensus-uniform reserved branch (the set comes from the chain config, matching the EVM and base-fee paths): - validation.go: waive the admission tip floor for reserved senders. - legacypool Pending: don't cap reserved senders' txs by the miner tip filter, so they reach the producer. - list.Add: reserved senders replace by arrival order (skip the price-bump rule) so a stuck zero-fee tx can be cancelled or replaced. The balance check needs no change: a zero-feeCap tx's Cost() is already just its value. Sender classification is gated on the ReservedBlockspace fork height. --- core/txpool/legacypool/legacypool.go | 27 ++++- core/txpool/legacypool/list.go | 8 +- core/txpool/legacypool/list_test.go | 20 +-- core/txpool/legacypool/queue.go | 4 +- core/txpool/legacypool/reserved_test.go | 154 ++++++++++++++++++++++++ core/txpool/validation.go | 23 +++- 6 files changed, 218 insertions(+), 18 deletions(-) create mode 100644 core/txpool/legacypool/reserved_test.go diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index ea90e641a0..038eaac2e0 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -729,10 +729,14 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter, interrupt *atomic.B txs := list.Flatten() + // Reserved-blockspace senders pay zero in-protocol fee; their txs must not + // be capped by the tip floor or they would never reach the miner. + reserved := pool.isReserved(addr) + // If the miner requests tip enforcement, cap the lists now if filter.MinTip != nil || filter.GasLimitCap != 0 { for i, tx := range txs { - if filter.MinTip != nil { + if filter.MinTip != nil && !reserved { if tx.EffectiveGasTipIntCmp(filter.MinTip, filter.BaseFee) < 0 { txs = txs[:i] break @@ -1125,7 +1129,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, async bool) (replaced bool, e // Try to replace an existing transaction in the pending pool if list := pool.pending[from]; list != nil && list.Contains(tx.Nonce()) { // Nonce already pending, check if required price bump is met - inserted, old := list.Add(tx, pool.config.PriceBump) + inserted, old := list.Add(tx, pool.config.PriceBump, pool.isReserved(from)) if !inserted { pendingDiscardMeter.Mark(1) stage2Duration = time.Since(stage2Time) @@ -1229,7 +1233,7 @@ func (pool *LegacyPool) promoteTx(addr common.Address, hash common.Hash, tx *typ } list := pool.pending[addr] - inserted, old := list.Add(tx, pool.config.PriceBump) + inserted, old := list.Add(tx, pool.config.PriceBump, pool.isReserved(addr)) if !inserted { // An older transaction was better, discard this pool.all.Remove(hash) @@ -2278,3 +2282,20 @@ func (pool *LegacyPool) isFiltered(addr common.Address) bool { _, exists := pool.filteredAddrs[addr] return exists } + +// isReserved reports whether addr is a reserved-blockspace client active at the +// current head. Reserved senders' zero-fee transactions bypass the pool's fee +// floors (PIP-35 min tip, base-fee tip floor) so they are admitted, kept, and +// surfaced to the miner. Classification is sender-based and consensus-uniform +// (the reserved set comes from the chain config), matching the EVM fee path. +func (pool *LegacyPool) isReserved(addr common.Address) bool { + cfg := pool.chainconfig + if cfg.Bor == nil { + return false + } + head := pool.currentHead.Load() + if head == nil || !cfg.Bor.IsReservedBlockspace(head.Number) { + return false + } + return cfg.Bor.IsReservedSender(addr) +} diff --git a/core/txpool/legacypool/list.go b/core/txpool/legacypool/list.go index f8d96e967e..1a5c013163 100644 --- a/core/txpool/legacypool/list.go +++ b/core/txpool/legacypool/list.go @@ -331,10 +331,14 @@ func (l *list) Contains(nonce uint64) bool { // // If the new transaction is accepted into the list, the lists' cost and gas // thresholds are also potentially updated. -func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transaction) { +func (l *list) Add(tx *types.Transaction, priceBump uint64, reserved bool) (bool, *types.Transaction) { // If there's an older better transaction, abort old := l.txs.Get(tx.Nonce()) - if old != nil { + // Reserved-blockspace senders replace by arrival order: two zero-fee txs at + // the same nonce can't out-bid each other, so the fee-bump rule is skipped. + // A reserved client must be able to cancel or replace a stuck tx without + // attaching a fee we told them they don't need. + if old != nil && !reserved { if old.GasFeeCapCmp(tx) >= 0 || old.GasTipCapCmp(tx) >= 0 { return false, nil } diff --git a/core/txpool/legacypool/list_test.go b/core/txpool/legacypool/list_test.go index 938ebb8189..2715db1033 100644 --- a/core/txpool/legacypool/list_test.go +++ b/core/txpool/legacypool/list_test.go @@ -50,7 +50,7 @@ func TestStrictListAdd(t *testing.T) { // Insert the transactions in a random order list := newList(true) for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump) + list.Add(txs[v], DefaultConfig.PriceBump, false) } // Verify internal state if len(list.txs.items) != len(txs) { @@ -75,7 +75,7 @@ func TestListAddVeryExpensive(t *testing.T) { gaslimit := uint64(i) tx, _ := types.SignTx(types.NewTransaction(uint64(i), common.Address{}, value, gaslimit, gasprice, nil), types.HomesteadSigner{}, key) t.Logf("cost: %x bitlen: %d\n", tx.Cost(), tx.Cost().BitLen()) - list.Add(tx, DefaultConfig.PriceBump) + list.Add(tx, DefaultConfig.PriceBump, false) } } @@ -93,7 +93,7 @@ func BenchmarkListAdd(b *testing.B) { for i := 0; i < b.N; i++ { list := newList(true) for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump) + list.Add(txs[v], DefaultConfig.PriceBump, false) list.Filter(priceLimit, DefaultConfig.PriceBump) } } @@ -121,7 +121,7 @@ func TestFilterTxConditionalKnownAccounts(t *testing.T) { // Create a transaction with no defined tx options // and add to the list. tx := transaction(0, 1000, key) - list.Add(tx, DefaultConfig.PriceBump) + list.Add(tx, DefaultConfig.PriceBump, false) // There should be no drops at this point. // No state has been modified. @@ -155,7 +155,7 @@ func TestFilterTxConditionalKnownAccounts(t *testing.T) { fmt.Println("after", trie.Hash()) tx2.PutOptions(&options) - list.Add(tx2, DefaultConfig.PriceBump) + list.Add(tx2, DefaultConfig.PriceBump, false) // There should still be no drops as no state has been modified. drops = list.FilterTxConditional(state, header) @@ -202,7 +202,7 @@ func TestFilterTxConditionalBlockNumber(t *testing.T) { // Create a transaction with no defined tx options // and add to the list. tx := transaction(0, 1000, key) - list.Add(tx, DefaultConfig.PriceBump) + list.Add(tx, DefaultConfig.PriceBump, false) // There should be no drops at this point. // No state has been modified. @@ -220,7 +220,7 @@ func TestFilterTxConditionalBlockNumber(t *testing.T) { options.BlockNumberMax = big.NewInt(110) tx2.PutOptions(&options) - list.Add(tx2, DefaultConfig.PriceBump) + list.Add(tx2, DefaultConfig.PriceBump, false) // There should still be no drops as no state has been modified. drops = list.FilterTxConditional(state, header) @@ -263,7 +263,7 @@ func TestFilterTxConditionalTimestamp(t *testing.T) { // Create a transaction with no defined tx options // and add to the list. tx := transaction(0, 1000, key) - list.Add(tx, DefaultConfig.PriceBump) + list.Add(tx, DefaultConfig.PriceBump, false) // There should be no drops at this point. // No state has been modified. @@ -284,7 +284,7 @@ func TestFilterTxConditionalTimestamp(t *testing.T) { options.TimestampMax = &maxTimestamp tx2.PutOptions(&options) - list.Add(tx2, DefaultConfig.PriceBump) + list.Add(tx2, DefaultConfig.PriceBump, false) // There should still be no drops as no state has been modified. drops = list.FilterTxConditional(state, header) @@ -375,7 +375,7 @@ func BenchmarkListCapOneTx(b *testing.B) { list := newList(true) // Insert the transactions in a random order for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump) + list.Add(txs[v], DefaultConfig.PriceBump, false) } b.StartTimer() list.Cap(list.Len() - 1) diff --git a/core/txpool/legacypool/queue.go b/core/txpool/legacypool/queue.go index a889debe37..cbb7cbb25e 100644 --- a/core/txpool/legacypool/queue.go +++ b/core/txpool/legacypool/queue.go @@ -124,7 +124,9 @@ func (q *queue) add(tx *types.Transaction) (*common.Hash, error) { if q.queued[from] == nil { q.queued[from] = newList(false) } - inserted, old := q.queued[from].Add(tx, q.config.PriceBump) + // Reserved replacement-by-arrival applies to the pending pool; the future + // queue keeps the standard fee-bump rule. + inserted, old := q.queued[from].Add(tx, q.config.PriceBump, false) if !inserted { // An older transaction was better, discard this queuedDiscardMeter.Mark(1) diff --git a/core/txpool/legacypool/reserved_test.go b/core/txpool/legacypool/reserved_test.go new file mode 100644 index 0000000000..20241d6137 --- /dev/null +++ b/core/txpool/legacypool/reserved_test.go @@ -0,0 +1,154 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package legacypool + +import ( + "crypto/ecdsa" + "math/big" + "testing" + + "github.com/holiman/uint256" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/params" +) + +func reservedChainConfig(reserved common.Address) *params.ChainConfig { + cfg := *params.BorUnittestChainConfig // London active at 0 + bor := *cfg.Bor + bor.ReservedBlockspaceBlock = big.NewInt(0) + bor.ReservedClients = []params.ReservedClient{ + {Addresses: []common.Address{reserved}, QuotaGas: 30_000_000}, + } + cfg.Bor = &bor + return &cfg +} + +func setupReservedPool(reserved common.Address) *LegacyPool { + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + cfg := reservedChainConfig(reserved) + bc := newTestBlockChain(cfg, 10_000_000, statedb, new(event.Feed)) + pool := New(testTxPoolConfig, bc) + if err := pool.Init(testTxPoolConfig.PriceLimit, bc.CurrentBlock(), newReserver()); err != nil { + panic(err) + } + <-pool.initDoneCh + // A realistic positive tip floor (PIP-35 ~25 gwei). Reserved senders must + // bypass it; everyone else is held to it. + pool.SetGasTip(big.NewInt(30_000_000_000)) + return pool +} + +func zeroFeeTx(t *testing.T, cfg *params.ChainConfig, key *ecdsa.PrivateKey, nonce uint64, to common.Address) *types.Transaction { + t.Helper() + tx, err := types.SignNewTx(key, types.LatestSigner(cfg), &types.DynamicFeeTx{ + ChainID: cfg.ChainID, + Nonce: nonce, + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(0), + Gas: 100_000, + To: &to, + Value: big.NewInt(0), + }) + if err != nil { + t.Fatal(err) + } + return tx +} + +// TestReservedZeroFeeTxAdmittedAndPending verifies the core ticket-D behaviour: +// a zero-fee tx from a reserved sender is admitted past the tip floor, kept in +// the pool, and surfaced by Pending even under a high miner MinTip — whereas the +// same tx from a non-reserved sender is rejected at admission. +func TestReservedZeroFeeTxAdmittedAndPending(t *testing.T) { + t.Parallel() + + reservedKey, _ := crypto.GenerateKey() + reservedAddr := crypto.PubkeyToAddress(reservedKey.PublicKey) + pool := setupReservedPool(reservedAddr) + defer pool.Close() + + otherKey, _ := crypto.GenerateKey() + otherAddr := crypto.PubkeyToAddress(otherKey.PublicKey) + + testAddBalance(pool, reservedAddr, big.NewInt(1_000_000)) + testAddBalance(pool, otherAddr, big.NewInt(1_000_000)) + + cfg := pool.chainconfig + + // Reserved sender: zero-fee tx must be admitted. + rtx := zeroFeeTx(t, cfg, reservedKey, 0, common.Address{0x42}) + if err := pool.Add([]*types.Transaction{rtx}, true)[0]; err != nil { + t.Fatalf("reserved zero-fee tx rejected: %v", err) + } + if pool.Get(rtx.Hash()) == nil { + t.Fatal("reserved zero-fee tx not kept in pool") + } + + // Non-reserved sender: identical zero-fee tx must be rejected at the floor. + otx := zeroFeeTx(t, cfg, otherKey, 0, common.Address{0x42}) + if err := pool.Add([]*types.Transaction{otx}, true)[0]; err == nil { + t.Fatal("non-reserved zero-fee tx should be rejected at the tip floor") + } + + // The reserved tx must survive a high miner tip filter so it reaches the miner. + pending := pool.Pending(txpool.PendingFilter{ + MinTip: uint256.NewInt(30_000_000_000), + BaseFee: uint256.NewInt(1_000_000_000), + }, nil) + if got := pending[reservedAddr]; len(got) != 1 || got[0].Hash != rtx.Hash() { + t.Fatalf("reserved zero-fee tx missing from Pending under high MinTip: %v", got) + } +} + +// TestReservedZeroFeeReplacement verifies replacement-by-arrival: a reserved +// sender can replace a stuck zero-fee tx with another zero-fee tx at the same +// nonce, which the standard price-bump rule would reject. +func TestReservedZeroFeeReplacement(t *testing.T) { + t.Parallel() + + reservedKey, _ := crypto.GenerateKey() + reservedAddr := crypto.PubkeyToAddress(reservedKey.PublicKey) + pool := setupReservedPool(reservedAddr) + defer pool.Close() + + testAddBalance(pool, reservedAddr, big.NewInt(1_000_000)) + cfg := pool.chainconfig + + first := zeroFeeTx(t, cfg, reservedKey, 0, common.Address{0xaa}) + if err := pool.Add([]*types.Transaction{first}, true)[0]; err != nil { + t.Fatalf("first reserved tx rejected: %v", err) + } + + // Same nonce, different recipient, still zero fee — must replace by arrival. + second := zeroFeeTx(t, cfg, reservedKey, 0, common.Address{0xbb}) + if err := pool.Add([]*types.Transaction{second}, true)[0]; err != nil { + t.Fatalf("reserved zero-fee replacement rejected: %v", err) + } + + if pool.Get(second.Hash()) == nil { + t.Fatal("replacement tx not in pool") + } + if pool.Get(first.Hash()) != nil { + t.Fatal("replaced tx should have been evicted") + } +} diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 73ab55bd97..db85a978bc 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -143,8 +143,11 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrFloorDataGas, tx.Gas(), floorDataGas) } } - // Ensure the gasprice is high enough to cover the requirement of the calling pool - if tx.GasTipCapIntCmp(opts.MinTip) < 0 { + // Ensure the gasprice is high enough to cover the requirement of the calling + // pool. Reserved-blockspace senders are exempt: they pay zero in-protocol fee, + // so enforcing the tip floor would reject their transactions at admission and + // they would never reach the miner. + if tx.GasTipCapIntCmp(opts.MinTip) < 0 && !isReservedSender(opts.Config, head, signer, tx) { return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip) } if tx.Type() == types.BlobTxType { @@ -158,6 +161,22 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types return nil } +// isReservedSender reports whether tx's recovered sender is a reserved-blockspace +// client active at head. Reserved senders bypass the pool's fee floors so their +// zero-fee transactions are admitted and kept. Classification is sender-based and +// consensus-uniform (the reserved set comes from the chain config), matching the +// EVM fee path in core/state_transition.go. +func isReservedSender(config *params.ChainConfig, head *types.Header, signer types.Signer, tx *types.Transaction) bool { + if config == nil || config.Bor == nil || head == nil || !config.Bor.IsReservedBlockspace(head.Number) { + return false + } + from, err := types.Sender(signer, tx) + if err != nil { + return false + } + return config.Bor.IsReservedSender(from) +} + // validateBlobTx implements the blob-transaction specific validations. func validateBlobTx(tx *types.Transaction, head *types.Header, opts *ValidationOptions) error { sidecar := tx.BlobTxSidecar() From 2143558a37bf1c88be199bfd1f119dd49175fd9d Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 11:48:43 +0200 Subject: [PATCH 07/19] bor, core: extract reserved-blockspace fee/verify helpers (POS-3570) Factor the reserved branches out of the EVM and consensus hot paths into focused helpers (reservedZeroFeeGas, calcBaseFeeBurn, verifyReservedFields). Behaviour is unchanged; this keeps buyGas, execute, and verifyHeader within the cognitive- complexity budget after the reserved-blockspace additions. --- consensus/bor/bor.go | 25 ++++++++++------ core/state_transition.go | 62 +++++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 34be51de87..a40b622df6 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -508,14 +508,8 @@ func (c *Bor) verifyHeader(chain consensus.ChainHeaderReader, header *types.Head } } - // Post-ReservedBlockspace: verify the reserved tx count and reserved gas used - // are present. Presence only — value correctness (count matches the reserved - // region, gas within per-client quota) is enforced during block validation. - if c.config.IsReservedBlockspace(header.Number) { - reservedTxCount, reservedGasUsed := header.GetReservedInfo(c.chainConfig) - if reservedTxCount == nil || reservedGasUsed == nil { - return errMissingReservedBlockspaceFields - } + if err := c.verifyReservedFields(header); err != nil { + return err } // Ensure that the mix digest is zero as we don't have fork protection currently @@ -1047,6 +1041,21 @@ func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraD } } +// verifyReservedFields checks that post-ReservedBlockspace headers carry the +// reserved-region fields. Presence only — value correctness (count matches the +// reserved region, gas within per-client quota) is enforced during block +// validation. +func (c *Bor) verifyReservedFields(header *types.Header) error { + if !c.config.IsReservedBlockspace(header.Number) { + return nil + } + reservedTxCount, reservedGasUsed := header.GetReservedInfo(c.chainConfig) + if reservedTxCount == nil || reservedGasUsed == nil { + return errMissingReservedBlockspaceFields + } + return nil +} + // Prepare implements consensus.Engine, preparing all the consensus fields of the // header for running the transactions on top. func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, waitOnPrepare bool) error { diff --git a/core/state_transition.go b/core/state_transition.go index 7ff9fe02e8..bda5f9fb2e 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -344,13 +344,8 @@ func (st *stateTransition) buyGas() error { } } - if st.reserved { - // Reserved-blockspace tx pays zero in-protocol fee: don't require or debit - // the gas cost; only the call value must be funded. Block gas is still - // consumed (SubGas below), and gasRemaining is still set, so execution and - // gas accounting are unchanged. - mgval = new(big.Int) - balanceCheck = new(big.Int).Set(st.msg.Value) + if st.reserved { // zero in-protocol fee: don't require or debit gas, fund only the call value + mgval, balanceCheck = reservedZeroFeeGas(st.msg.Value) } balanceCheckU256, overflow := uint256.FromBig(balanceCheck) @@ -376,6 +371,35 @@ func (st *stateTransition) buyGas() error { return nil } +// reservedZeroFeeGas returns the (mgval, balanceCheck) for a reserved-blockspace +// tx: zero gas cost to debit, only the call value to fund. Block gas is still +// consumed by the caller, so gas accounting is unchanged. +func reservedZeroFeeGas(value *big.Int) (mgval, balanceCheck *big.Int) { + return new(big.Int), new(big.Int).Set(value) +} + +// calcBaseFeeBurn returns the base-fee burn amount and the configured burnt- +// contract address. Bor redirects the burned base fee to a "burnt contract" +// (a dead address on mainnet/amoy); the credit happens here unless fees are +// deferred (noFeeBurnAndTip). Reserved-blockspace txs burn nothing. On non-Bor +// configs (Bor == nil) the base fee is implicitly burned the upstream way and no +// credit happens, matching Ethereum mainnet semantics and Ethereum-spec fixtures. +func (st *stateTransition) calcBaseFeeBurn(rules params.Rules) (*big.Int, common.Address) { + bor := st.evm.ChainConfig().Bor + if !rules.IsLondon || bor == nil { + return nil, common.Address{} + } + addr := common.HexToAddress(bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64())) + if st.reserved { + return new(big.Int), addr + } + burn := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) + if !st.noFeeBurnAndTip { + st.state.AddBalance(addr, cmath.BigIntToUint256Int(burn), tracing.BalanceChangeTransfer) + } + return burn, addr +} + func (st *stateTransition) preCheck() error { // Only check transactions that are not fake msg := st.msg @@ -653,29 +677,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { amount := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip) - var burnAmount *big.Int - var burntContractAddress common.Address - - if rules.IsLondon { - // Bor-specific behavior: redirect the burned base fee to a configured - // "burnt contract" (a dead address on bor mainnet/amoy). On non-Bor - // chain configs (Bor == nil), no credit happens — the base fee is - // implicitly burned the upstream go-ethereum way, matching Ethereum - // mainnet semantics. Without the nil-guard, Ethereum-spec test - // fixtures with Bor == nil panic here. - if bor := st.evm.ChainConfig().Bor; bor != nil { - burntContractAddress = common.HexToAddress(bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64())) - if st.reserved { - // No base-fee burn for reserved-blockspace txs. - burnAmount = new(big.Int) - } else { - burnAmount = new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) - if !st.noFeeBurnAndTip { - st.state.AddBalance(burntContractAddress, cmath.BigIntToUint256Int(burnAmount), tracing.BalanceChangeTransfer) - } - } - } - } + burnAmount, burntContractAddress := st.calcBaseFeeBurn(rules) if !st.noFeeBurnAndTip { st.state.AddBalance(st.evm.Context.Coinbase, cmath.BigIntToUint256Int(amount), tracing.BalanceIncreaseRewardTransactionFee) From 89df6ecefdd66f25bf8e886d22e7c5fc67d5c19f Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 11:55:58 +0200 Subject: [PATCH 08/19] params: assert ReservedBlockspace appears in the chain config banner (POS-3637) Pins the hardfork-rollout requirement that a scheduled fork prints its activation height on startup, and that an unscheduled (nil) fork stays silent. --- params/reserved_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/params/reserved_test.go b/params/reserved_test.go index dd3eacb02b..793d813e68 100644 --- a/params/reserved_test.go +++ b/params/reserved_test.go @@ -18,6 +18,7 @@ package params import ( "math/big" + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -92,3 +93,23 @@ func TestReservedClientClassifier(t *testing.T) { t.Error("empty reserved config should classify nothing") } } + +func TestDescriptionReservedBlockspace(t *testing.T) { + t.Parallel() + + // When scheduled, the startup banner must print the fork (hardfork-rollout + // review requires a visible activation height). + scheduled := &ChainConfig{ + ChainID: big.NewInt(137), + Bor: &BorConfig{ReservedBlockspaceBlock: big.NewInt(500)}, + } + if got := scheduled.Description(); !strings.Contains(got, "ReservedBlockspace") || !strings.Contains(got, "500") { + t.Errorf("banner should advertise ReservedBlockspace #500, got:\n%s", got) + } + + // Unscheduled (nil) must not print the line. + unscheduled := &ChainConfig{ChainID: big.NewInt(137), Bor: &BorConfig{}} + if strings.Contains(unscheduled.Description(), "ReservedBlockspace") { + t.Error("banner must omit ReservedBlockspace when unscheduled") + } +} From 948036dbc114f4038f95f80b72253ffecdd62811 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 12:04:50 +0200 Subject: [PATCH 09/19] miner, txpool: include reserved zero-fee transactions in produced blocks (POS-3570) The pool admitted reserved zero-fee txs but the producer dropped them: the price-and-nonce ordering rejects any tx with GasFeeCap below the base fee (ErrGasFeeCapTooLow). Carry the pool's reserved classification on LazyTransaction and exempt reserved senders from the base-fee floor in newTxWithMinerFee (zero miner tip). Caught by a two-node produce/verify integration test (tests/bor, integration tag): a reserved zero-fee tx is now mined and cross-verified, the sender pays only its call value, and a non-reserved zero-fee tx is still rejected at admission. --- core/txpool/legacypool/legacypool.go | 1 + core/txpool/subpool.go | 2 + miner/ordering.go | 19 ++- tests/bor/reserved_blockspace_test.go | 168 ++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 tests/bor/reserved_blockspace_test.go diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 038eaac2e0..61cdc578c4 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -762,6 +762,7 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter, interrupt *atomic.B GasTipCap: uint256.MustFromBig(txs[i].GasTipCap()), Gas: txs[i].Gas(), BlobGas: txs[i].BlobGas(), + Reserved: reserved, } } pending[addr] = lazies diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go index 4b2e258930..d240e2164d 100644 --- a/core/txpool/subpool.go +++ b/core/txpool/subpool.go @@ -43,6 +43,8 @@ type LazyTransaction struct { Gas uint64 // Amount of gas required by the transaction BlobGas uint64 // Amount of blob gas required by the transaction + + Reserved bool // Sender is a reserved-blockspace client (zero in-protocol fee, exempt from the miner's base-fee floor) } // Resolve retrieves the full transaction belonging to a lazy handle if it is still diff --git a/miner/ordering.go b/miner/ordering.go index 5515457a74..922a185ac7 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -41,12 +41,19 @@ type txWithMinerFee struct { func newTxWithMinerFee(tx *txpool.LazyTransaction, from common.Address, baseFee *uint256.Int) (*txWithMinerFee, error) { tip := new(uint256.Int).Set(tx.GasTipCap) if baseFee != nil { - if tx.GasFeeCap.Cmp(baseFee) < 0 { - return nil, types.ErrGasFeeCapTooLow - } - tip = new(uint256.Int).Sub(tx.GasFeeCap, baseFee) - if tip.Gt(tx.GasTipCap) { - tip = tx.GasTipCap + // Reserved-blockspace senders pay zero in-protocol fee, so their txs are + // exempt from the base-fee floor and contribute no miner tip. Without this + // the producer would drop every reserved zero-fee tx before sealing. + if tx.Reserved { + tip = new(uint256.Int) + } else { + if tx.GasFeeCap.Cmp(baseFee) < 0 { + return nil, types.ErrGasFeeCapTooLow + } + tip = new(uint256.Int).Sub(tx.GasFeeCap, baseFee) + if tip.Gt(tx.GasTipCap) { + tip = tx.GasTipCap + } } } return &txWithMinerFee{ diff --git a/tests/bor/reserved_blockspace_test.go b/tests/bor/reserved_blockspace_test.go new file mode 100644 index 0000000000..f0798d3255 --- /dev/null +++ b/tests/bor/reserved_blockspace_test.go @@ -0,0 +1,168 @@ +//go:build integration +// +build integration + +package bor + +import ( + "context" + "crypto/ecdsa" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +// TestReservedBlockspaceZeroFeeProduction is the end-to-end (in-process devnet) +// validation of the reserved-blockspace feature across all four tickets: a real +// two-node bor network produces and cross-verifies blocks; a zero-fee tx from a +// config-whitelisted reserved sender is admitted to the pool (POS-3570), mined +// past the ReservedBlockspace fork with the reserved header fields set by the +// real Prepare path (POS-3637), and executes fee-free so the sender pays only +// its call value with no gas debit (POS-3573). An identical zero-fee tx from a +// non-reserved sender is rejected at admission. +// +// Run with: go test -tags=integration -run TestReservedBlockspaceZeroFeeProduction ./tests/bor/ +func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { + faucets := make([]*ecdsa.PrivateKey, 10) + for i := range faucets { + faucets[i], _ = crypto.GenerateKey() + } + reservedAddr := crypto.PubkeyToAddress(faucets[0].PublicKey) + nonReservedAddr := crypto.PubkeyToAddress(faucets[1].PublicKey) + + genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 8) + // Reserved fork at block 5 (Cancun is active from block 3, so the reserved + // header fields encode in the post-Cancun BlockExtraData format). + reservedFork := uint64(5) + genesis.Config.Bor.ReservedBlockspaceBlock = new(big.Int).SetUint64(reservedFork) + genesis.Config.Bor.ReservedClients = []params.ReservedClient{ + {Addresses: []common.Address{reservedAddr}, QuotaGas: 30_000_000}, + } + startBalance := new(big.Int).SetUint64(1_000_000_000_000_000_000) // 1 ETH + genesis.Alloc[reservedAddr] = types.Account{Balance: new(big.Int).Set(startBalance)} + genesis.Alloc[nonReservedAddr] = types.Account{Balance: new(big.Int).Set(startBalance)} + + stacks, nodes, _ := setupMiner(t, 2, genesis) + defer func() { + for _, stack := range stacks { + stack.Close() + } + }() + for _, node := range nodes { + if err := node.StartMining(); err != nil { + t.Fatal("start mining:", err) + } + } + + waitForBlock := func(target uint64) { + t.Helper() + deadline := time.After(120 * time.Second) + for { + if nodes[0].BlockChain().CurrentBlock().Number.Uint64() >= target { + return + } + select { + case <-deadline: + t.Fatalf("timeout waiting for block %d (at %d)", target, nodes[0].BlockChain().CurrentBlock().Number.Uint64()) + case <-time.After(200 * time.Millisecond): + } + } + } + + // Build past the reserved fork before submitting. + waitForBlock(reservedFork + 2) + + signer := types.LatestSigner(genesis.Config) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000aa") + + zeroFeeTx := func(from *ecdsa.PrivateKey, nonce uint64) *types.Transaction { + tx, err := types.SignNewTx(from, signer, &types.DynamicFeeTx{ + ChainID: genesis.Config.ChainID, + Nonce: nonce, + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(0), + Gas: 21000, + To: &recipient, + Value: big.NewInt(100), + }) + if err != nil { + t.Fatal(err) + } + return tx + } + + // Reserved sender: zero-fee tx must be accepted by the pool. + rNonce, err := nodes[0].APIBackend.GetPoolNonce(context.Background(), reservedAddr) + if err != nil { + t.Fatal(err) + } + rtx := zeroFeeTx(faucets[0], rNonce) + for _, node := range nodes { + if err := node.APIBackend.SendTx(context.Background(), rtx); err != nil { + t.Fatalf("reserved zero-fee tx rejected by pool: %v", err) + } + } + + // Non-reserved sender: identical zero-fee tx must be rejected at admission. + oNonce, err := nodes[0].APIBackend.GetPoolNonce(context.Background(), nonReservedAddr) + if err != nil { + t.Fatal(err) + } + otx := zeroFeeTx(faucets[1], oNonce) + if err := nodes[0].APIBackend.SendTx(context.Background(), otx); err == nil { + t.Fatal("non-reserved zero-fee tx should be rejected at admission, but was accepted") + } + + // Wait for the reserved tx to be mined, then locate its block. + var includedBlock *types.Block + deadline := time.After(60 * time.Second) + for includedBlock == nil { + select { + case <-deadline: + t.Fatal("timeout waiting for reserved tx inclusion") + case <-time.After(200 * time.Millisecond): + } + head := nodes[0].BlockChain().CurrentBlock().Number.Uint64() + for n := reservedFork; n <= head && includedBlock == nil; n++ { + blk := nodes[0].BlockChain().GetBlockByNumber(n) + if blk == nil { + continue + } + for _, tx := range blk.Transactions() { + if tx.Hash() == rtx.Hash() { + includedBlock = blk + break + } + } + } + } + t.Logf("reserved zero-fee tx mined in block %d", includedBlock.NumberU64()) + + // The producing block must carry the reserved header fields (set by Prepare). + rc, rg := includedBlock.Header().GetReservedInfo(genesis.Config) + if rc == nil || rg == nil { + t.Fatalf("block %d missing reserved header fields: count=%v gas=%v", includedBlock.NumberU64(), rc, rg) + } + + // The reserved sender paid only its call value — no gas was debited. + state, err := nodes[0].BlockChain().StateAt(includedBlock.Root()) + if err != nil { + t.Fatal(err) + } + got := state.GetBalance(reservedAddr).ToBig() + want := new(big.Int).Sub(startBalance, big.NewInt(100)) + if got.Cmp(want) != 0 { + t.Fatalf("reserved sender balance = %s, want %s (call value only, no gas debit)", got, want) + } + + // Both nodes agreed on the block (the validator verified the producer's + // header, including the reserved-field presence check). + other := nodes[1].BlockChain().GetBlockByNumber(includedBlock.NumberU64()) + if other == nil || other.Hash() != includedBlock.Hash() { + t.Fatalf("nodes disagree at block %d: %v vs %s", includedBlock.NumberU64(), other, includedBlock.Hash()) + } +} From 67dd1ab0d92c78ebb91393272f00c2b9d61c2912 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Tue, 23 Jun 2026 18:55:36 +0200 Subject: [PATCH 10/19] bor, eip1559, core: mark reserved-blockspace enforcement as deferred (POS-3570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review surfaced that several comments described not-yet-implemented work as if it were live. Correct them so the foundation's deferrals read honestly; no logic change: - verifyReservedFields: quota/count value-correctness is enforced by block validation in POS-3576, not today — a producer can write any present values. - setReservedBlockspaceExtraFields: the zero ReservedTxCount/ReservedGasUsed are placeholders; the producer's reserved pass (POS-3575) sets the real values. - CalcBaseFee: the capacity-side target reduction is live while the ReservedGasUsed used-side netting stays inert until POS-3575 populates the header field, so the public base fee runs slightly hot under reserved load. Deterministic (a fee-accuracy gap, not a consensus split); resolves with POS-3575, no code change needed here. - buyGas: note the zero-fee waiver also drops a reserved blob tx's blob fee; reserved clients are not a blob use case today. --- consensus/bor/bor.go | 13 ++++++++++--- consensus/misc/eip1559/eip1559.go | 9 +++++++++ core/state_transition.go | 7 ++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index a40b622df6..7e984b02ea 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1034,6 +1034,12 @@ func (c *Bor) setGiuglianoExtraFields(header *types.Header, parent *types.Header // verifyHeader presence check, even when there are no reserved transactions. func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraData *types.BlockExtraData) { if c.config.IsReservedBlockspace(header.Number) { + // Placeholder zeros. The producer's reserved pass (POS-3575) will set the + // real ReservedTxCount / ReservedGasUsed during block building; this + // foundation only guarantees the fields are present (non-nil) so the + // header is structurally valid and passes the verifyReservedFields + // presence check. Until POS-3575 lands, every block reports zero reserved + // gas regardless of how many reserved txs it actually contains. var zeroCount uint32 var zeroGas uint64 blockExtraData.ReservedTxCount = &zeroCount @@ -1042,9 +1048,10 @@ func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraD } // verifyReservedFields checks that post-ReservedBlockspace headers carry the -// reserved-region fields. Presence only — value correctness (count matches the -// reserved region, gas within per-client quota) is enforced during block -// validation. +// reserved-region fields. Presence only. Value correctness — that ReservedTxCount +// matches the actual reserved region and ReservedGasUsed stays within each +// client's quota — is NOT enforced yet; it lands with block validation in +// POS-3576. Until then a producer can write any present values here. func (c *Bor) verifyReservedFields(header *types.Header) error { if !c.config.IsReservedBlockspace(header.Number) { return nil diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index b43189d430..0bf3ca33af 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -124,6 +124,15 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { // fee, so counting them would let a reserved client move the public base // fee with its own usage. Anchoring the target on capacity (not used gas) // keeps the public market stable and isolated from reserved behaviour. + // + // Note the two halves are not yet symmetric: the capacity-side reduction + // (reservedAwareGasTarget) is live, but the used-side netting (publicGasUsed) + // reads parent.ReservedGasUsed, which the producer stubs to 0 until its + // reserved pass lands (POS-3575). Until then the netting is inert, so under + // real reserved load the public base fee runs slightly hot. This is + // deterministic — every node reads the same header field — so it is a fee + // accuracy gap, not a consensus split. It resolves once POS-3575 populates + // ReservedGasUsed; no change is needed here. if config.Bor != nil && config.Bor.IsReservedBlockspace(parent.Number) { parentGasTarget = reservedAwareGasTarget(config, parent) parentGasUsed = publicGasUsed(config, parent) diff --git a/core/state_transition.go b/core/state_transition.go index bda5f9fb2e..f21bd8afda 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -344,7 +344,12 @@ func (st *stateTransition) buyGas() error { } } - if st.reserved { // zero in-protocol fee: don't require or debit gas, fund only the call value + if st.reserved { + // Zero in-protocol fee: don't require or debit gas, fund only the call + // value. This overwrites mgval/balanceCheck after the blob branch above, + // so a reserved blob tx also skips its blob-fee debit and blob-fee balance + // requirement. Reserved clients are not a blob-tx use case today; if that + // changes, revisit whether the blob fee (a separate burn) should be waived. mgval, balanceCheck = reservedZeroFeeGas(st.msg.Value) } From 378148e423e3fe44d551586bb82510e8e631ff11 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Sat, 27 Jun 2026 09:21:27 +0200 Subject: [PATCH 11/19] registry: add feeMode, effectiveFrom, and root() to the reserved-blockspace registry (POS-3572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend Krishang's ReservedBlockspaceRegistry per the finalized spec (design.md §4, §7): - feeMode (0=free, 1=routed) per client + setClientFeeMode; surfaced via getClientForAddress. - effectiveFrom block per client + setClientEffectiveFrom; Bor gates on active && effectiveFrom <= number (read at parent state, so deterministic). - configVersion + root(): a change-epoch bumped on every mutation, so Bor can cache its reserved-set snapshot keyed on root() and only rebuild when it moves (spec §4.5), avoiding a per-tx state read. Regenerate the embedded runtime bytecode (forge, solc 0.8.33 pinned), update the read-only ABI (getClientForAddress now 6 returns + root), and sync the Go reader (decode feeMode/effectiveFrom, add Root()) + registryreader.ClientLookup/Reader. 11 foundry tests and the Go registry tests pass. --- consensus/bor/abi/common.go | 2 +- .../bor/contract/registrytest/harness.go | 33 +- consensus/bor/contract/reserved_registry.go | 47 +- consensus/bor/registryreader/reader.go | 10 + params/reserved_blockspace.go | 461 +++++++++++------- registry-contract/foundry.toml | 1 + .../src/ReservedBlockspaceRegistry.sol | 88 +++- .../test/ReservedBlockspaceRegistry.t.sol | 94 +++- 8 files changed, 525 insertions(+), 211 deletions(-) diff --git a/consensus/bor/abi/common.go b/consensus/bor/abi/common.go index 773cc24d65..9a2818ca97 100644 --- a/consensus/bor/abi/common.go +++ b/consensus/bor/abi/common.go @@ -33,7 +33,7 @@ func ReservedBlockspaceRegistry() abi.ABI { const ( validatorsetABI = `[{"constant":true,"inputs":[],"name":"SPRINT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SYSTEM_ADDRESS","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"CHAIN","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"FIRST_END_BLOCK","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"producers","outputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ROUND_TYPE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"BOR_ID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"spanNumbers","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"VOTE_TYPE","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"validators","outputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"spans","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"startBlock","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"endBlock","type":"uint256"}],"name":"NewSpan","type":"event"},{"constant":true,"inputs":[],"name":"currentSprint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNextSpan","outputs":[{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"number","type":"uint256"}],"name":"getSpanByBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"currentSpanNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getValidatorsTotalStakeBySpan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"}],"name":"getProducersTotalStakeBySpan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"getValidatorBySigner","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"power","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"internalType":"struct BorValidatorSet.Validator","name":"result","type":"tuple"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"isValidator","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"address","name":"signer","type":"address"}],"name":"isProducer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"signer","type":"address"}],"name":"isCurrentValidator","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"signer","type":"address"}],"name":"isCurrentProducer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"number","type":"uint256"}],"name":"getBorValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitialValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"newSpan","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"},{"internalType":"bytes","name":"validatorBytes","type":"bytes"},{"internalType":"bytes","name":"producerBytes","type":"bytes"}],"name":"commitSpan","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"span","type":"uint256"},{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bytes","name":"sigs","type":"bytes"}],"name":"getStakePowerBySigs","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"rootHash","type":"bytes32"},{"internalType":"bytes32","name":"leaf","type":"bytes32"},{"internalType":"bytes","name":"proof","type":"bytes"}],"name":"checkMembership","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"d","type":"bytes32"}],"name":"leafNode","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"left","type":"bytes32"},{"internalType":"bytes32","name":"right","type":"bytes32"}],"name":"innerNode","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"}]` stateReceiverABI = `[{"constant":true,"inputs":[],"name":"SYSTEM_ADDRESS","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"lastStateId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"syncTime","type":"uint256"},{"internalType":"bytes","name":"recordBytes","type":"bytes"}],"name":"commitState","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]` - reservedRegistryABI = `[{"inputs":[{"name":"account","type":"address"}],"name":"isReservedAddress","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"account","type":"address"}],"name":"getClientForAddress","outputs":[{"name":"clientId","type":"uint256"},{"name":"gasQuota","type":"uint64"},{"name":"admin","type":"address"},{"name":"active","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"clientId","type":"uint256"}],"name":"getClient","outputs":[{"name":"admin","type":"address"},{"name":"gasQuota","type":"uint64"},{"name":"active","type":"bool"},{"name":"metadata","type":"string"},{"name":"addressCount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"clientId","type":"uint256"}],"name":"getClientAddresses","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getWhitelistedAddresses","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getReservedClients","outputs":[{"name":"clientIds","type":"uint256[]"},{"name":"admins","type":"address[]"},{"name":"gasQuotas","type":"uint64[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalReservedGas","outputs":[{"name":"","type":"uint64"}],"stateMutability":"view","type":"function"}]` + reservedRegistryABI = `[{"inputs":[{"name":"account","type":"address"}],"name":"isReservedAddress","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"account","type":"address"}],"name":"getClientForAddress","outputs":[{"name":"clientId","type":"uint256"},{"name":"gasQuota","type":"uint64"},{"name":"admin","type":"address"},{"name":"active","type":"bool"},{"name":"feeMode","type":"uint8"},{"name":"effectiveFrom","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"clientId","type":"uint256"}],"name":"getClient","outputs":[{"name":"admin","type":"address"},{"name":"gasQuota","type":"uint64"},{"name":"active","type":"bool"},{"name":"metadata","type":"string"},{"name":"addressCount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"clientId","type":"uint256"}],"name":"getClientAddresses","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getWhitelistedAddresses","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getReservedClients","outputs":[{"name":"clientIds","type":"uint256[]"},{"name":"admins","type":"address[]"},{"name":"gasQuotas","type":"uint64[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalReservedGas","outputs":[{"name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"root","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"}]` ) func mustParseABI(json string) abi.ABI { diff --git a/consensus/bor/contract/registrytest/harness.go b/consensus/bor/contract/registrytest/harness.go index cf8dc73ae0..2e8461463f 100644 --- a/consensus/bor/contract/registrytest/harness.go +++ b/consensus/bor/contract/registrytest/harness.go @@ -30,7 +30,7 @@ import ( // mutate the registry via normal user transactions. const writeABI = `[ {"inputs":[{"name":"initialOwner","type":"address"},{"name":"maxTotalGas","type":"uint64"},{"name":"maxClientGas","type":"uint64"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"}, - {"inputs":[{"name":"admin","type":"address"},{"name":"gasQuota","type":"uint64"},{"name":"metadata","type":"string"},{"name":"addresses","type":"address[]"}],"name":"createClient","outputs":[{"name":"clientId","type":"uint256"}],"stateMutability":"nonpayable","type":"function"} + {"inputs":[{"name":"admin","type":"address"},{"name":"gasQuota","type":"uint64"},{"name":"feeMode","type":"uint8"},{"name":"effectiveFrom","type":"uint64"},{"name":"metadata","type":"string"},{"name":"addresses","type":"address[]"}],"name":"createClient","outputs":[{"name":"clientId","type":"uint256"}],"stateMutability":"nonpayable","type":"function"} ]` // Harness exposes a Reader bound to an in-memory state with the registry @@ -67,7 +67,7 @@ func NewHarness(t *testing.T) *Harness { require.NoError(t, err) callContract(t, statedb, owner, contractAddr, initData) - createData, err := writeAbi.Pack("createClient", clientAdmin, uint64(10_000_000), "test", []common.Address{reserved}) + createData, err := writeAbi.Pack("createClient", clientAdmin, uint64(10_000_000), uint8(0), uint64(0), "test", []common.Address{reserved}) require.NoError(t, err) callContract(t, statedb, owner, contractAddr, createData) @@ -170,24 +170,43 @@ func (r *evmReader) ReservedClientForAddress(_ *state.StateDB, _ uint64, _ commo if err != nil { return registryreader.ClientLookup{}, err } - if len(values) != 4 { + if len(values) != 6 { return registryreader.ClientLookup{}, fmt.Errorf("getClientForAddress returned %d values", len(values)) } clientID, _ := values[0].(*big.Int) gasQuota, _ := values[1].(uint64) admin, _ := values[2].(common.Address) active, _ := values[3].(bool) + feeMode, _ := values[4].(uint8) + effectiveFrom, _ := values[5].(uint64) if clientID == nil { return registryreader.ClientLookup{}, fmt.Errorf("getClientForAddress returned nil clientID") } return registryreader.ClientLookup{ - ClientID: new(big.Int).Set(clientID), - GasQuota: gasQuota, - Admin: admin, - Active: active, + ClientID: new(big.Int).Set(clientID), + GasQuota: gasQuota, + Admin: admin, + Active: active, + FeeMode: feeMode, + EffectiveFrom: effectiveFrom, }, nil } +func (r *evmReader) Root(_ *state.StateDB, _ uint64, _ common.Hash) (common.Hash, error) { + values, err := r.call("root") + if err != nil { + return common.Hash{}, err + } + if len(values) != 1 { + return common.Hash{}, fmt.Errorf("root returned %d values", len(values)) + } + root, ok := values[0].([32]byte) + if !ok { + return common.Hash{}, fmt.Errorf("root returned %T", values[0]) + } + return common.Hash(root), nil +} + func (r *evmReader) call(method string, args ...interface{}) ([]interface{}, error) { data, err := r.readerAB.Pack(method, args...) if err != nil { diff --git a/consensus/bor/contract/reserved_registry.go b/consensus/bor/contract/reserved_registry.go index 9cbde46911..d7640fa617 100644 --- a/consensus/bor/contract/reserved_registry.go +++ b/consensus/bor/contract/reserved_registry.go @@ -81,7 +81,7 @@ func (gc *GenesisContractsClient) ReservedClientForAddress( if err != nil { return ReservedClientLookup{}, err } - if len(values) != 4 { + if len(values) != 6 { return ReservedClientLookup{}, fmt.Errorf("reserved registry getClientForAddress returned %d values", len(values)) } @@ -101,15 +101,52 @@ func (gc *GenesisContractsClient) ReservedClientForAddress( if !ok { return ReservedClientLookup{}, fmt.Errorf("reserved registry active flag has type %T", values[3]) } + feeMode, ok := values[4].(uint8) + if !ok { + return ReservedClientLookup{}, fmt.Errorf("reserved registry fee mode has type %T", values[4]) + } + effectiveFrom, ok := values[5].(uint64) + if !ok { + return ReservedClientLookup{}, fmt.Errorf("reserved registry effective-from has type %T", values[5]) + } return ReservedClientLookup{ - ClientID: new(big.Int).Set(clientID), - GasQuota: gasQuota, - Admin: admin, - Active: active, + ClientID: new(big.Int).Set(clientID), + GasQuota: gasQuota, + Admin: admin, + Active: active, + FeeMode: feeMode, + EffectiveFrom: effectiveFrom, }, nil } +// Root reads the registry's configVersion-derived root. The cache (spec §4.5) +// rebuilds its reserved-set snapshot only when this value changes. +func (gc *GenesisContractsClient) Root( + state *state.StateDB, + number uint64, + hash common.Hash, +) (common.Hash, error) { + if !gc.HasReservedRegistry() { + return common.Hash{}, nil + } + + values, err := gc.callReservedRegistry(state, number, hash, "root") + if err != nil { + return common.Hash{}, err + } + if len(values) != 1 { + return common.Hash{}, fmt.Errorf("reserved registry root returned %d values", len(values)) + } + + root, ok := values[0].([32]byte) + if !ok { + return common.Hash{}, fmt.Errorf("reserved registry root has type %T", values[0]) + } + + return common.Hash(root), nil +} + func (gc *GenesisContractsClient) ReservedClientByID( state *state.StateDB, number uint64, diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go index c85b49b78f..c06967d360 100644 --- a/consensus/bor/registryreader/reader.go +++ b/consensus/bor/registryreader/reader.go @@ -20,6 +20,12 @@ type ClientLookup struct { GasQuota uint64 Admin common.Address Active bool + // FeeMode: 0 = free (zero in-protocol fee), 1 = routed (fee credited to the + // producer). See the reserved-blockspace spec §7. + FeeMode uint8 + // EffectiveFrom: block from which the client's reserved status applies. + // Callers gate on Active && EffectiveFrom <= number. + EffectiveFrom uint64 } // Reader is the read-only view of the reserved blockspace registry consumed by @@ -30,4 +36,8 @@ type Reader interface { HasReservedRegistry() bool IsReservedAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (bool, error) ReservedClientForAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (ClientLookup, error) + // Root returns the registry's configVersion-derived root. It changes + // whenever the reserved set or its limits change, so a snapshot keyed on it + // can be reused until it moves (spec §4.5). + Root(state *state.StateDB, number uint64, hash common.Hash) (common.Hash, error) } diff --git a/params/reserved_blockspace.go b/params/reserved_blockspace.go index a0d3f606a4..528fd0c2a9 100644 --- a/params/reserved_blockspace.go +++ b/params/reserved_blockspace.go @@ -4,183 +4,288 @@ const ( DefaultReservedRegistryContract = "0x0000000000000000000000000000000000001002" // ReservedBlockspaceRegistryCode is the deployed runtime bytecode of - // registry-contract/src/ReservedBlockspaceRegistry.sol. + // registry-contract/src/ReservedBlockspaceRegistry.sol (solc 0.8.33, optimizer 200). + // Regenerate with: forge build (in registry-contract/) then re-extract + // .deployedBytecode.object. Do not hand-edit. ReservedBlockspaceRegistryCode = "0x" + - "608060405234801561000f575f5ffd5b5060043610610153575f3560e01c806393dc1052116100bf578063d0068f8011" + - "610079578063d0068f8014610340578063e3ab7a2914610364578063e541c71e14610387578063ea38cd531461038f57" + - "8063f2fde38b146103a2578063feafde83146103b5575f5ffd5b806393dc1052146102ca578063989a8366146102dd57" + - "8063b98330e6146102f0578063bbae06db14610307578063c242867b1461031a578063cc2763261461032d575f5ffd5b" + - "806362ae1e941161011057806362ae1e94146102265780636d0280271461025c5780636f78bb36146102715780637816" + - "e2ed1461028457806388b45de51461028d5780638da5cb5b146102a0575f5ffd5b80630315818f146101575780630ee6" + - "7c8b1461018d578063151d6b0c146101a057806318d93a34146101eb5780633f34c51414610200578063469d1e381461" + - "0213575b5f5ffd5b5f5461017090600160a01b90046001600160401b031681565b6040516001600160401b0390911681" + - "526020015b60405180910390f35b600154610170906001600160401b031681565b6101b36101ae366004611a3e565b61" + - "03c8565b604080519485526001600160401b0390931660208501526001600160a01b0390911691830191909152151560" + - "60820152608001610184565b6101fe6101f9366004611a9b565b610439565b005b6101fe61020e366004611af8565b61" + - "050e565b6101fe610221366004611b69565b610670565b61024e610234366004611a3e565b6001600160a01b03165f90" + - "81526005602052604090205490565b604051908152602001610184565b610264610732565b6040516101849190611be6" + - "565b61026461027f366004611bf8565b6108ad565b61024e60025481565b6101fe61029b366004611c0f565b61091956" + - "5b5f546102b2906001600160a01b031681565b6040516001600160a01b039091168152602001610184565b6003546101" + - "70906001600160401b031681565b6101fe6102eb366004611c41565b610aa8565b6102f8610b89565b60405161018493" + - "929190611c81565b6101fe610315366004611d1d565b610d8c565b6101fe610328366004611d3e565b610e63565b6102" + - "4e61033b366004611d5f565b610fb8565b61035361034e366004611bf8565b611169565b604051610184959493929190" + - "611ded565b610377610372366004611a3e565b61124e565b6040519015158152602001610184565b61024e611290565b" + - "6101fe61039d366004611d1d565b6112b3565b6101fe6103b0366004611a3e565b6113d6565b6101fe6103c336600461" + - "1d1d565b6114a8565b6001600160a01b0381165f908152600560205260408120549080808381036103fa57505f925082" + - "915081905080610432565b5050505f81815260046020526040902054600160a01b81046001600160401b031690600160" + - "0160a01b03811690600160e01b900460ff165b9193509193565b5f546001600160a01b0316610461576040516321c4e3" + - "5760e21b815260040160405180910390fd5b825f61046c82611530565b5f549091506001600160a01b03163314801590" + - "610493575080546001600160a01b03163314155b156104b1576040516301940db560e51b815260040160405180910390" + - "fd5b5f8581526004602052604090206001016104cc848683611ef0565b50847f3cf32b7851a39f3e6ced7e2df88eee32" + - "24b27dbf667c3c92c9cd43a6a91ca39085856040516104ff929190611fd2565b60405180910390a25050505050565b5f" + - "546001600160a01b0316610536576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b" + - "03163314610560576040516330cd747160e01b815260040160405180910390fd5b61056a828261156a565b6003546001" + - "600160401b03808416911611156105995760405163ad7e403f60e01b815260040160405180910390fd5b60015b600254" + - "8110156105ef575f818152600460205260409020546001600160401b03808416600160a01b9092041611156105e75760" + - "405163dbf8c09d60e01b815260040160405180910390fd5b60010161059c565b505f805467ffffffffffffffff60a01b" + - "1916600160a01b6001600160401b03858116918202929092179092556001805467ffffffffffffffff19169184169182" + - "1790556040805192835260208301919091527f24d48e187a24f49e3551ce9d77a89e2a5959da2a1932f86ebab7b31684" + - "83ac1b910160405180910390a15050565b5f546001600160a01b0316610698576040516321c4e35760e21b8152600401" + - "60405180910390fd5b825f6106a382611530565b5f549091506001600160a01b031633148015906106ca575080546001" + - "600160a01b03163314155b156106e8576040516301940db560e51b815260040160405180910390fd5b5f5b8381101561" + - "072a576107228686868481811061070857610708611fed565b905060200201602081019061071d9190611a3e565b6115" + - "c1565b6001016106ea565b505050505050565b60605f805b6006548110156107b55760045f60055f6006858154811061" + - "075a5761075a611fed565b5f9182526020808320909101546001600160a01b0316835282810193909352604091820181" + - "2054845291830193909352910190205460ff600160e01b90910416156107ad57816107a981612015565b9250505b6001" + - "01610737565b505f816001600160401b038111156107cf576107cf611e51565b60405190808252806020026020018201" + - "60405280156107f8578160200160208202803683370190505b5090505f805b6006548110156108a4575f600682815481" + - "1061081c5761081c611fed565b5f9182526020808320909101546001600160a01b031680835260058252604080842054" + - "8452600490925291205490915060ff600160e01b909104161561089b5780848461086881612015565b95508151811061" + - "087a5761087a611fed565b60200260200101906001600160a01b031690816001600160a01b0316815250505b50600101" + - "6107fe565b50909392505050565b60606108b882611530565b6002018054806020026020016040519081016040528092" + - "9190818152602001828054801561090d57602002820191905f5260205f20905b81546001600160a01b03168152600190" + - "9101906020018083116108ef575b50505050509050919050565b5f546001600160a01b0316610941576040516321c4e3" + - "5760e21b815260040160405180910390fd5b5f546001600160a01b0316331461096b576040516330cd747160e01b8152" + - "60040160405180910390fd5b5f61097583611530565b8054909150821515600160e01b90910460ff1615150361099457" + - "505050565b8115610a065780546109b690600160a01b90046001600160401b03165f611706565b805460038054600160" + - "0160401b03600160a01b9093048316925f916109dd9185911661202d565b92506101000a8154816001600160401b0302" + - "191690836001600160401b03160217905550610a52565b8054600380546001600160401b03600160a01b909304831692" + - "5f91610a2d91859116612052565b92506101000a8154816001600160401b0302191690836001600160401b0316021790" + - "55505b805460ff60e01b1916600160e01b83151590810291909117825560405190815283907f81cdb3516fac5fa15a5e" + - "6738774f6eab2657ef4b6f6486451655eb882dc202cd906020015b60405180910390a2505b5050565b5f546001600160" + - "a01b031615610ad05760405162dc149f60e41b815260040160405180910390fd5b6001600160a01b038316610af75760" + - "405163d92e233d60e01b815260040160405180910390fd5b610b01828261156a565b5f80546001600160a01b03851660" + - "01600160e01b03199091168117600160a01b6001600160401b03868116918202929092179093556001805467ffffffff" + - "ffffffff1916918516918217815560025560408051938452602084019190915290917f6fe9ef307ea9c9e4c7e9dd8df7" + - "afa1a49139e88d2273cbb5b2c3a939bc19eb029101610a9a565b606080805f60015b600254811015610bce575f818152" + - "60046020526040902054600160e01b900460ff1615610bc65781610bc281612015565b9250505b600101610b91565b50" + - "806001600160401b03811115610be757610be7611e51565b604051908082528060200260200182016040528015610c10" + - "578160200160208202803683370190505b509350806001600160401b03811115610c2b57610c2b611e51565b60405190" + - "8082528060200260200182016040528015610c54578160200160208202803683370190505b509250806001600160401b" + - "03811115610c6f57610c6f611e51565b604051908082528060200260200182016040528015610c985781602001602082" + - "02803683370190505b5091505f60015b600254811015610d84575f8181526004602052604090208054600160e01b9004" + - "60ff16610ccc5750610d7c565b81878481518110610cdf57610cdf611fed565b60209081029190910101528054865160" + - "01600160a01b0390911690879085908110610d0c57610d0c611fed565b60200260200101906001600160a01b03169081" + - "6001600160a01b031681525050805f0160149054906101000a90046001600160401b0316858481518110610d5557610d" + - "55611fed565b6001600160401b039092166020928302919091019091015282610d7781612015565b935050505b600101" + - "610c9f565b505050909192565b5f546001600160a01b0316610db4576040516321c4e35760e21b815260040160405180" + - "910390fd5b5f546001600160a01b03163314610dde576040516330cd747160e01b815260040160405180910390fd5b60" + - "01600160a01b038116610e055760405163d92e233d60e01b815260040160405180910390fd5b5f610e0f83611530565b" + - "80546001600160a01b038481166001600160a01b0319831681178455604051939450911691829086907fc5be841a0df0" + - "5495e1b0c1417dd3e47b706482e380cf1878d41adbb2ec27d43c905f90a450505050565b5f546001600160a01b031661" + - "0e8b576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b03163314610eb557604051" + - "6330cd747160e01b815260040160405180910390fd5b5f610ebf83611530565b8054909150600160a01b810460016001" + - "60401b031690610ef5908490600160e01b900460ff16610eef575f611706565b82611706565b8154600160e01b900460" + - "ff1615610f4b576003548390610f1f9083906001600160401b0316612052565b610f29919061202d565b6003805467ff" + - "ffffffffffffff19166001600160401b03929092169190911790555b815467ffffffffffffffff60a01b1916600160a0" + - "1b6001600160401b03858116918202929092178455604080519284168352602083019190915285917f27e19ef4fd95b0" + - "e09b35f3a0160fbc4c0adeb8a03d5353f1e0fcee66e9ed41e2910160405180910390a250505050565b5f805460016001" + - "60a01b0316610fe1576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b0316331461" + - "100b576040516330cd747160e01b815260040160405180910390fd5b6001600160a01b0387166110325760405163d92e" + - "233d60e01b815260040160405180910390fd5b61103c865f611706565b60028054905f61104b83612015565b90915550" + - "5f818152600460205260409020805460ff60e01b196001600160401b038a16600160a01b026001600160e01b03199092" + - "166001600160a01b038c16179190911716600160e01b178155909150600181016110aa868883611ef0565b5060038054" + - "8891905f906110c89084906001600160401b031661202d565b92506101000a8154816001600160401b03021916908360" + - "01600160401b031602179055505f5f90505b838110156111175761110f8386868481811061070857610708611fed565b" + - "6001016110f1565b50876001600160a01b0316827f09a706e40d9ad723a2810180344b3c848de397087ce3da3fead0d3" + - "9b2c809d4c89898960405161115693929190612071565b60405180910390a3509695505050505050565b5f5f5f60605f" + - "5f61117987611530565b805460028201546001830180549394506001600160a01b03831693600160a01b840460016001" + - "60401b031693600160e01b900460ff16929082906111bc90611e65565b80601f01602080910402602001604051908101" + - "604052809291908181526020018280546111e890611e65565b80156112335780601f1061120a57610100808354040283" + - "529160200191611233565b820191905f5260205f20905b81548152906001019060200180831161121657829003601f16" + - "8201915b50505050509150955095509550955095505091939590929450565b6001600160a01b0381165f908152600560" + - "20526040812054801580159061128957505f81815260046020526040902054600160e01b900460ff165b939250505056" + - "5b5f6002545f0361129f57505f90565b60016002546112ae919061209c565b905090565b5f546001600160a01b031661" + - "12db576040516321c4e35760e21b815260040160405180910390fd5b815f6112e682611530565b5f5490915060016001" + - "60a01b0316331480159061130d575080546001600160a01b03163314155b1561132b576040516301940db560e51b8152" + - "60040160405180910390fd5b5f61133585611530565b6001600160a01b0385165f908152600560205260409020549091" + - "50851461136f576040516343876cef60e11b815260040160405180910390fd5b6001600160a01b0384165f9081526005" + - "602052604081205561139181856117b7565b61139a846118e2565b6040516001600160a01b0385169086907f1dba5136" + - "2ce159be9df822b10a7257119c8852ec78713df99e4072ab10842e1e905f90a35050505050565b5f546001600160a01b" + - "03166113fe576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b0316331461142857" + - "6040516330cd747160e01b815260040160405180910390fd5b6001600160a01b03811661144f5760405163d92e233d60" + - "e01b815260040160405180910390fd5b5f80546040516001600160a01b03808516939216917f8be0079c531659141344" + - "cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a35f80546001600160a01b0319166001600160a01b03929092" + - "16919091179055565b5f546001600160a01b03166114d0576040516321c4e35760e21b815260040160405180910390fd" + - "5b815f6114db82611530565b5f549091506001600160a01b03163314801590611502575080546001600160a01b031633" + - "14155b15611520576040516301940db560e51b815260040160405180910390fd5b61152a84846115c1565b5050505056" + - "5b5f81815260046020526040902080546001600160a01b0316611565576040516366f1f3b160e01b8152600401604051" + - "80910390fd5b919050565b6001600160401b038216158061158757506001600160401b038116155b806115a357508160" + - "01600160401b0316816001600160401b0316115b15610aa457604051634377d4dd60e11b815260040160405180910390" + - "fd5b6001600160a01b0381166115e85760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b" + - "0381165f908152600560205260409020541561161e576040516316a163b960e11b815260040160405180910390fd5b5f" + - "61162883611530565b600281018054600180820183555f928352602080842090920180546001600160a01b0319166001" + - "600160a01b03881690811790915583526005909152604090912085905560065491925061167c91906120af565b600160" + - "0160a01b0383165f8181526007602052604080822093909355600680546001810182559082527ff652222313e2845952" + - "8d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f0180546001600160a01b031916831790559151909185917f80" + - "e74249b1abc4956ba12c76ad9d1ffdfebe41c1df74308c8d5f9f6a437bc9599190a3505050565b816001600160401b03" + - "165f0361172f57604051634377d4dd60e11b815260040160405180910390fd5b6001546001600160401b039081169083" + - "16111561175f5760405163dbf8c09d60e01b815260040160405180910390fd5b5f546003546001600160401b03600160" + - "a01b909204821691849161178591859116612052565b61178f919061202d565b6001600160401b03161115610aa45760" + - "405163ad7e403f60e01b815260040160405180910390fd5b60028201545f5b818110156118c857826001600160a01b03" + - "168460020182815481106117e5576117e5611fed565b5f918252602090912001546001600160a01b0316036118c05761" + - "180960018361209c565b8114611887576002840161181e60018461209c565b8154811061182e5761182e611fed565b5f" + - "918252602090912001546002850180546001600160a01b03909216918390811061185b5761185b611fed565b905f5260" + - "205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505b8360020180548061" + - "189a5761189a6120c2565b5f8281526020902081015f1990810180546001600160a01b03191690550190555050505056" + - "5b6001016117be565b506040516343876cef60e11b815260040160405180910390fd5b6001600160a01b0381165f9081" + - "52600760205260408120549081900361191b576040516343876cef60e11b815260040160405180910390fd5b5f611927" + - "60018361209c565b6006549091505f9061193b9060019061209c565b90508082146119d3575f60068281548110611958" + - "57611958611fed565b5f91825260209091200154600680546001600160a01b0390921692508291859081106119865761" + - "1986611fed565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905561" + - "19b98360016120af565b6001600160a01b039091165f908152600760205260409020555b60068054806119e4576119e4" + - "6120c2565b5f828152602080822083015f1990810180546001600160a01b03191690559092019092556001600160a01b" + - "0395909516815260079094525050604082209190915550565b80356001600160a01b0381168114611565575f5ffd5b5f" + - "60208284031215611a4e575f5ffd5b61128982611a28565b5f5f83601f840112611a67575f5ffd5b5081356001600160" + - "401b03811115611a7d575f5ffd5b602083019150836020828501011115611a94575f5ffd5b9250929050565b5f5f5f60" + - "408486031215611aad575f5ffd5b8335925060208401356001600160401b03811115611ac9575f5ffd5b611ad5868287" + - "01611a57565b9497909650939450505050565b80356001600160401b0381168114611565575f5ffd5b5f5f6040838503" + - "1215611b09575f5ffd5b611b1283611ae2565b9150611b2060208401611ae2565b90509250929050565b5f5f83601f84" + - "0112611b39575f5ffd5b5081356001600160401b03811115611b4f575f5ffd5b6020830191508360208260051b850101" + - "1115611a94575f5ffd5b5f5f5f60408486031215611b7b575f5ffd5b8335925060208401356001600160401b03811115" + - "611b97575f5ffd5b611ad586828701611b29565b5f8151808452602084019350602083015f5b82811015611bdc578151" + - "6001600160a01b0316865260209586019590910190600101611bb5565b5093949350505050565b602081525f61128960" + - "20830184611ba3565b5f60208284031215611c08575f5ffd5b5035919050565b5f5f60408385031215611c20575f5ffd" + - "5b8235915060208301358015158114611c36575f5ffd5b809150509250929050565b5f5f5f60608486031215611c5357" + - "5f5ffd5b611c5c84611a28565b9250611c6a60208501611ae2565b9150611c7860408501611ae2565b90509250925092" + - "565b606080825284519082018190525f9060208601906080840190835b81811015611cba578351835260209384019390" + - "920191600101611c9c565b50508381036020850152611cce8187611ba3565b8481036040860152855180825260208088" + - "01945090910191505f5b81811015611d105783516001600160401b0316835260209384019390920191600101611ce956" + - "5b5090979650505050505050565b5f5f60408385031215611d2e575f5ffd5b82359150611b2060208401611a28565b5f" + - "5f60408385031215611d4f575f5ffd5b82359150611b2060208401611ae2565b5f5f5f5f5f5f60808789031215611d74" + - "575f5ffd5b611d7d87611a28565b9550611d8b60208801611ae2565b945060408701356001600160401b03811115611d" + - "a5575f5ffd5b611db189828a01611a57565b90955093505060608701356001600160401b03811115611dcf575f5ffd5b" + - "611ddb89828a01611b29565b979a9699509497509295939492505050565b60018060a01b03861681526001600160401b" + - "0385166020820152831515604082015260a060608201525f83518060a0840152806020860160c085015e5f60c0828501" + - "015260c0601f19601f8301168401019150508260808301529695505050505050565b634e487b7160e01b5f5260416004" + - "5260245ffd5b600181811c90821680611e7957607f821691505b602082108103611e9757634e487b7160e01b5f526022" + - "60045260245ffd5b50919050565b601f821115611eeb5782821115611eeb57805f5260205f20601f840160051c602085" + - "1015611ec857505f5b90810190601f840160051c035f5b8181101561072a575f83820155600101611ed6565b50505056" + - "5b6001600160401b03831115611f0757611f07611e51565b611f1b83611f158354611e65565b83611e9d565b5f601f84" + - "1160018114611f4c575f8515611f355750838201355b5f19600387901b1c1916600186901b178355611fa3565b5f8381" + - "5260208120601f198716915b82811015611f7b5786850135825560209485019460019092019101611f5b565b50868210" + - "15611f97575f1960f88860031b161c19848701351681555b505060018560011b0183555b5050505050565b8183528181" + - "6020850137505f828201602090810191909152601f909101601f19169091010190565b602081525f611fe56020830184" + - "86611faa565b949350505050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f5260116004" + - "5260245ffd5b5f6001820161202657612026612001565b5060010190565b6001600160401b0381811683821601908111" + - "1561204c5761204c612001565b92915050565b6001600160401b03828116828216039081111561204c5761204c612001" + - "565b6001600160401b0384168152604060208201525f612093604083018486611faa565b95945050505050565b818103" + - "8181111561204c5761204c612001565b8082018082111561204c5761204c612001565b634e487b7160e01b5f52603160" + - "045260245ffdfea2646970667358221220b2bc134fc8386dd09cd1e37136d732ae698d2c5fe5afb4f67fef1f1d189a4d" + - "a664736f6c63430008210033" + "608060405234801561000f575f5ffd5b50600436106101bb575f3560e01c806393dc10" + + "52116100f3578063e3ab7a2911610093578063ebf0c7171161006e578063ebf0c71714" + + "610435578063f2fde38b1461043d578063f339c03c14610450578063feafde83146104" + + "63575f5ffd5b8063e3ab7a29146103f7578063e541c71e1461041a578063ea38cd5314" + + "610422575f5ffd5b8063bbae06db116100ce578063bbae06db146103a4578063c24286" + + "7b146103b7578063d0068f80146103ca578063dd64d24d146103ee575f5ffd5b806393" + + "dc105214610367578063989a83661461037a578063b98330e61461038d575f5ffd5b80" + + "63469d1e381161015e5780636f78bb36116101395780636f78bb361461030e57806378" + + "16e2ed1461032157806388b45de51461032a5780638da5cb5b1461033d575f5ffd5b80" + + "63469d1e38146102be57806362ae1e94146102d15780636d028027146102f9575f5ffd" + + "5b8063151d6b0c11610199578063151d6b0c1461022957806318d93a34146102835780" + + "63386bd8c0146102985780633f34c514146102ab575f5ffd5b80630315818f146101bf" + + "5780630ee67c8b146101f55780631159d12f14610208575b5f5ffd5b5f546101d89060" + + "0160a01b90046001600160401b031681565b6040516001600160401b03909116815260" + + "20015b60405180910390f35b6001546101d8906001600160401b031681565b61021b61" + + "0216366004611f9a565b610476565b6040519081526020016101ec565b61023c610237" + + "366004612048565b610786565b604080519687526001600160401b0395861660208801" + + "526001600160a01b0390941693860193909352901515606085015260ff166080840152" + + "1660a082015260c0016101ec565b610296610291366004612061565b610817565b005b" + + "6102966102a63660046120a8565b6108ec565b6102966102b93660046120d2565b6109" + + "ce565b6102966102cc3660046120fa565b610b54565b61021b6102df36600461204856" + + "5b6001600160a01b03165f9081526006602052604090205490565b610301610c27565b" + + "6040516101ec9190612177565b61030161031c366004612189565b610da2565b61021b" + + "60025481565b6102966103383660046121a0565b610e0e565b5f5461034f9060016001" + + "60a01b031681565b6040516001600160a01b0390911681526020016101ec565b600354" + + "6101d8906001600160401b031681565b6102966103883660046121d2565b610fc1565b" + + "6103956110aa565b6040516101ec93929190612212565b6102966103b23660046122ae" + + "565b6112ad565b6102966103c53660046120a8565b611384565b6103dd6103d8366004" + + "612189565b6114fd565b6040516101ec9594939291906122cf565b61021b6004548156" + + "5b61040a610405366004612048565b6115e2565b60405190151581526020016101ec56" + + "5b61021b611624565b6102966104303660046122ae565b611647565b60045461021b56" + + "5b61029661044b366004612048565b611796565b61029661045e366004612333565b61" + + "1868565b6102966104713660046122ae565b611934565b5f80546001600160a01b0316" + + "61049f576040516321c4e35760e21b815260040160405180910390fd5b5f5460016001" + + "60a01b031633146104c9576040516330cd747160e01b815260040160405180910390fd" + + "5b6001600160a01b0389166104f05760405163d92e233d60e01b815260040160405180" + + "910390fd5b600160ff8816111561051557604051632e573a3f60e01b81526004016040" + + "5180910390fd5b61051f885f6119de565b60028054905f61052e83612368565b919050" + + "5590505f60055f8381526020019081526020015f20905089815f015f6101000a815481" + + "6001600160a01b0302191690836001600160a01b0316021790555088815f0160146101" + + "000a8154816001600160401b0302191690836001600160401b03160217905550600181" + + "5f01601c6101000a81548160ff02191690831515021790555087815f01601d6101000a" + + "81548160ff021916908360ff16021790555086816001015f6101000a81548160016001" + + "60401b0302191690836001600160401b03160217905550858582600201918261060a92" + + "9190612423565b50600380548a91905f906106289084906001600160401b03166124dd" + + "565b92506101000a8154816001600160401b0302191690836001600160401b03160217" + + "9055505f5f90505b83811015610691576106898386868481811061066f5761066f6125" + + "02565b90506020020160208101906106849190612048565b611a93565b600101610651" + + "565b50896001600160a01b0316827f09a706e40d9ad723a2810180344b3c848de39708" + + "7ce3da3fead0d39b2c809d4c8b89896040516106d09392919061253e565b6040518091" + + "0390a360405160ff8916815282907f314809947a390d762614db0e47ab34380ad55ab7" + + "6465eae94a8121c707148cdc9060200160405180910390a26040516001600160401b03" + + "8816815282907f41a37a9425042b4dfae0283bee3672cc13fce972d860f8ad764bc8a5" + + "50df99b39060200160405180910390a25060048054600101908190556040519081525f" + + "5160206125de5f395f51905f529060200160405180910390a198975050505050505050" + + "565b6001600160a01b0381165f90815260066020526040812054908080808085810361" + + "07c057505f94508493508392508291508190508061080e565b5050505f838152600560" + + "20526040902080546001909101546001600160401b03600160a01b8304811694506001" + + "600160a01b038316935060ff600160e01b8404811693600160e81b90041691165b9193" + + "9550919395565b5f546001600160a01b031661083f576040516321c4e35760e21b8152" + + "60040160405180910390fd5b825f61084a82611bd8565b5f549091506001600160a01b" + + "03163314801590610871575080546001600160a01b03163314155b1561088f57604051" + + "6301940db560e51b815260040160405180910390fd5b5f858152600560205260409020" + + "6002016108aa848683612423565b50847f3cf32b7851a39f3e6ced7e2df88eee3224b2" + + "7dbf667c3c92c9cd43a6a91ca39085856040516108dd929190612569565b6040518091" + + "0390a25050505050565b5f546001600160a01b0316610914576040516321c4e35760e2" + + "1b815260040160405180910390fd5b5f546001600160a01b0316331461093e57604051" + + "6330cd747160e01b815260040160405180910390fd5b8061094883611bd8565b600101" + + "805467ffffffffffffffff19166001600160401b039283161790556040519082168152" + + "82907f41a37a9425042b4dfae0283bee3672cc13fce972d860f8ad764bc8a550df99b3" + + "906020015b60405180910390a260048054600101908190556040519081525f51602061" + + "25de5f395f51905f52906020015b60405180910390a15050565b5f546001600160a01b" + + "03166109f6576040516321c4e35760e21b815260040160405180910390fd5b5f546001" + + "600160a01b03163314610a20576040516330cd747160e01b8152600401604051809103" + + "90fd5b610a2a8282611c12565b6003546001600160401b0380841691161115610a5957" + + "60405163ad7e403f60e01b815260040160405180910390fd5b60015b60025481101561" + + "0aaf575f818152600560205260409020546001600160401b03808416600160a01b9092" + + "04161115610aa75760405163dbf8c09d60e01b815260040160405180910390fd5b6001" + + "01610a5c565b505f805467ffffffffffffffff60a01b1916600160a01b600160016040" + + "1b03858116918202929092179092556001805467ffffffffffffffff19169184169182" + + "1790556040805192835260208301919091527f24d48e187a24f49e3551ce9d77a89e2a" + + "5959da2a1932f86ebab7b3168483ac1b910160405180910390a1600480546001019081" + + "90556040519081525f5160206125de5f395f51905f52906020016109c2565b5f546001" + + "600160a01b0316610b7c576040516321c4e35760e21b815260040160405180910390fd" + + "5b825f610b8782611bd8565b5f549091506001600160a01b03163314801590610bae57" + + "5080546001600160a01b03163314155b15610bcc576040516301940db560e51b815260" + + "040160405180910390fd5b5f5b83811015610bf457610bec8686868481811061066f57" + + "61066f612502565b600101610bce565b5060048054600101908190556040519081525f" + + "5160206125de5f395f51905f529060200160405180910390a15050505050565b60605f" + + "805b600754811015610caa5760055f60065f60078581548110610c4f57610c4f612502" + + "565b5f9182526020808320909101546001600160a01b03168352828101939093526040" + + "918201812054845291830193909352910190205460ff600160e01b9091041615610ca2" + + "5781610c9e81612368565b9250505b600101610c2c565b505f816001600160401b0381" + + "1115610cc457610cc4612380565b604051908082528060200260200182016040528015" + + "610ced578160200160208202803683370190505b5090505f805b600754811015610d99" + + "575f60078281548110610d1157610d11612502565b5f91825260208083209091015460" + + "01600160a01b0316808352600682526040808420548452600590925291205490915060" + + "ff600160e01b9091041615610d9057808484610d5d81612368565b955081518110610d" + + "6f57610d6f612502565b60200260200101906001600160a01b031690816001600160a0" + + "1b0316815250505b50600101610cf3565b50909392505050565b6060610dad82611bd8" + + "565b600301805480602002602001604051908101604052809291908181526020018280" + + "548015610e0257602002820191905f5260205f20905b81546001600160a01b03168152" + + "600190910190602001808311610de4575b50505050509050919050565b5f5460016001" + + "60a01b0316610e36576040516321c4e35760e21b815260040160405180910390fd5b5f" + + "546001600160a01b03163314610e60576040516330cd747160e01b8152600401604051" + + "80910390fd5b5f610e6a83611bd8565b8054909150821515600160e01b90910460ff16" + + "151503610e8a5750610f99565b8115610efc578054610eac90600160a01b9004600160" + + "0160401b03165f6119de565b8054600380546001600160401b03600160a01b90930483" + + "16925f91610ed3918591166124dd565b92506101000a8154816001600160401b030219" + + "1690836001600160401b03160217905550610f48565b8054600380546001600160401b" + + "03600160a01b9093048316925f91610f2391859116612584565b92506101000a815481" + + "6001600160401b0302191690836001600160401b031602179055505b805460ff60e01b" + + "1916600160e01b83151590810291909117825560405190815283907f81cdb3516fac5f" + + "a15a5e6738774f6eab2657ef4b6f6486451655eb882dc202cd90602001604051809103" + + "90a2505b60048054600101908190556040519081525f5160206125de5f395f51905f52" + + "906020016109c2565b5f546001600160a01b031615610fe95760405162dc149f60e41b" + + "815260040160405180910390fd5b6001600160a01b0383166110105760405163d92e23" + + "3d60e01b815260040160405180910390fd5b61101a8282611c12565b5f805460016001" + + "60a01b0385166001600160e01b03199091168117600160a01b6001600160401b038681" + + "16918202929092179093556001805467ffffffffffffffff1916918516918217815560" + + "025560408051938452602084019190915290917f6fe9ef307ea9c9e4c7e9dd8df7afa1" + + "a49139e88d2273cbb5b2c3a939bc19eb02910160405180910390a2505050565b606080" + + "805f60015b6002548110156110ef575f81815260056020526040902054600160e01b90" + + "0460ff16156110e757816110e381612368565b9250505b6001016110b2565b50806001" + + "600160401b0381111561110857611108612380565b6040519080825280602002602001" + + "82016040528015611131578160200160208202803683370190505b5093508060016001" + + "60401b0381111561114c5761114c612380565b60405190808252806020026020018201" + + "6040528015611175578160200160208202803683370190505b50925080600160016040" + + "1b0381111561119057611190612380565b604051908082528060200260200182016040" + + "5280156111b9578160200160208202803683370190505b5091505f60015b6002548110" + + "156112a5575f8181526005602052604090208054600160e01b900460ff166111ed5750" + + "61129d565b8187848151811061120057611200612502565b6020908102919091010152" + + "805486516001600160a01b039091169087908590811061122d5761122d612502565b60" + + "200260200101906001600160a01b031690816001600160a01b031681525050805f0160" + + "149054906101000a90046001600160401b031685848151811061127657611276612502" + + "565b6001600160401b0390921660209283029190910190910152826112988161236856" + + "5b935050505b6001016111c0565b505050909192565b5f546001600160a01b03166112" + + "d5576040516321c4e35760e21b815260040160405180910390fd5b5f546001600160a0" + + "1b031633146112ff576040516330cd747160e01b815260040160405180910390fd5b60" + + "01600160a01b0381166113265760405163d92e233d60e01b8152600401604051809103" + + "90fd5b5f61133083611bd8565b80546001600160a01b038481166001600160a01b0319" + + "831681178455604051939450911691829086907fc5be841a0df05495e1b0c1417dd3e4" + + "7b706482e380cf1878d41adbb2ec27d43c905f90a450505050565b5f546001600160a0" + + "1b03166113ac576040516321c4e35760e21b815260040160405180910390fd5b5f5460" + + "01600160a01b031633146113d6576040516330cd747160e01b81526004016040518091" + + "0390fd5b5f6113e083611bd8565b8054909150600160a01b81046001600160401b0316" + + "90611416908490600160e01b900460ff16611410575f6119de565b826119de565b8154" + + "600160e01b900460ff161561146c5760035483906114409083906001600160401b0316" + + "612584565b61144a91906124dd565b6003805467ffffffffffffffff19166001600160" + + "401b03929092169190911790555b815467ffffffffffffffff60a01b1916600160a01b" + + "6001600160401b03858116918202929092178455604080519284168352602083019190" + + "915285917f27e19ef4fd95b0e09b35f3a0160fbc4c0adeb8a03d5353f1e0fcee66e9ed" + + "41e2910160405180910390a2505060048054600101908190556040519081525f516020" + + "6125de5f395f51905f52906020016109c2565b5f5f5f60605f5f61150d87611bd8565b" + + "805460038201546002830180549394506001600160a01b03831693600160a01b840460" + + "01600160401b031693600160e01b900460ff169290829061155090612394565b80601f" + + "016020809104026020016040519081016040528092919081815260200182805461157c" + + "90612394565b80156115c75780601f1061159e57610100808354040283529160200191" + + "6115c7565b820191905f5260205f20905b8154815290600101906020018083116115aa" + + "57829003601f168201915b505050505091509550955095509550955050919395909294" + + "50565b6001600160a01b0381165f90815260066020526040812054801580159061161d" + + "57505f81815260056020526040902054600160e01b900460ff165b9392505050565b5f" + + "6002545f0361163357505f90565b600160025461164291906125a3565b905090565b5f" + + "546001600160a01b031661166f576040516321c4e35760e21b81526004016040518091" + + "0390fd5b815f61167a82611bd8565b5f549091506001600160a01b0316331480159061" + + "16a1575080546001600160a01b03163314155b156116bf576040516301940db560e51b" + + "815260040160405180910390fd5b5f6116c985611bd8565b6001600160a01b0385165f" + + "908152600660205260409020549091508514611703576040516343876cef60e11b8152" + + "60040160405180910390fd5b6001600160a01b0384165f908152600660205260408120" + + "556117258185611c69565b61172e84611d94565b6040516001600160a01b0385169086" + + "907f1dba51362ce159be9df822b10a7257119c8852ec78713df99e4072ab10842e1e90" + + "5f90a35060048054600101908190556040519081525f5160206125de5f395f51905f52" + + "906020015b60405180910390a150505050565b5f546001600160a01b03166117be5760" + + "40516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b0316" + + "33146117e8576040516330cd747160e01b815260040160405180910390fd5b60016001" + + "60a01b03811661180f5760405163d92e233d60e01b815260040160405180910390fd5b" + + "5f80546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0" + + "a4f28419497f9722a3daafe3b4186f6b6457e091a35f80546001600160a01b03191660" + + "01600160a01b0392909216919091179055565b5f546001600160a01b03166118905760" + + "40516321c4e35760e21b815260040160405180910390fd5b5f546001600160a01b0316" + + "33146118ba576040516330cd747160e01b815260040160405180910390fd5b600160ff" + + "821611156118df57604051632e573a3f60e01b815260040160405180910390fd5b8061" + + "18e983611bd8565b805460ff60e81b1916600160e81b60ff9384160217905560405190" + + "8216815282907f314809947a390d762614db0e47ab34380ad55ab76465eae94a8121c7" + + "07148cdc90602001610996565b5f546001600160a01b031661195c576040516321c4e3" + + "5760e21b815260040160405180910390fd5b815f61196782611bd8565b5f5490915060" + + "01600160a01b0316331480159061198e575080546001600160a01b03163314155b1561" + + "19ac576040516301940db560e51b815260040160405180910390fd5b6119b68484611a" + + "93565b60048054600101908190556040519081525f5160206125de5f395f51905f5290" + + "602001611788565b816001600160401b03165f03611a0757604051634377d4dd60e11b" + + "815260040160405180910390fd5b6001546001600160401b039081169083161115611a" + + "375760405163dbf8c09d60e01b815260040160405180910390fd5b5f54600354600160" + + "0160401b03600160a01b9092048216918491611a5d91859116612584565b611a679190" + + "6124dd565b6001600160401b03161115611a8f5760405163ad7e403f60e01b81526004" + + "0160405180910390fd5b5050565b6001600160a01b038116611aba5760405163d92e23" + + "3d60e01b815260040160405180910390fd5b6001600160a01b0381165f908152600660" + + "2052604090205415611af0576040516316a163b960e11b815260040160405180910390" + + "fd5b5f611afa83611bd8565b600381018054600180820183555f928352602080842090" + + "920180546001600160a01b0319166001600160a01b0388169081179091558352600690" + + "91526040909120859055600754919250611b4e91906125b6565b6001600160a01b0383" + + "165f8181526008602052604080822093909355600780546001810182559082527fa66c" + + "c928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8736c6880180546001" + + "600160a01b031916831790559151909185917f80e74249b1abc4956ba12c76ad9d1ffd" + + "febe41c1df74308c8d5f9f6a437bc9599190a3505050565b5f81815260056020526040" + + "902080546001600160a01b0316611c0d576040516366f1f3b160e01b81526004016040" + + "5180910390fd5b919050565b6001600160401b0382161580611c2f5750600160016040" + + "1b038116155b80611c4b5750816001600160401b0316816001600160401b0316115b15" + + "611a8f57604051634377d4dd60e11b815260040160405180910390fd5b60038201545f" + + "5b81811015611d7a57826001600160a01b0316846003018281548110611c9757611c97" + + "612502565b5f918252602090912001546001600160a01b031603611d7257611cbb6001" + + "836125a3565b8114611d395760038401611cd06001846125a3565b81548110611ce057" + + "611ce0612502565b5f918252602090912001546003850180546001600160a01b039092" + + "169183908110611d0d57611d0d612502565b905f5260205f20015f6101000a81548160" + + "01600160a01b0302191690836001600160a01b031602179055505b8360030180548061" + + "1d4c57611d4c6125c9565b5f8281526020902081015f1990810180546001600160a01b" + + "031916905501905550505050565b600101611c70565b506040516343876cef60e11b81" + + "5260040160405180910390fd5b6001600160a01b0381165f9081526008602052604081" + + "205490819003611dcd576040516343876cef60e11b815260040160405180910390fd5b" + + "5f611dd96001836125a3565b6007549091505f90611ded906001906125a3565b905080" + + "8214611e85575f60078281548110611e0a57611e0a612502565b5f9182526020909120" + + "0154600780546001600160a01b039092169250829185908110611e3857611e38612502" + + "565b5f91825260209091200180546001600160a01b0319166001600160a01b03929092" + + "16919091179055611e6b8360016125b6565b6001600160a01b039091165f9081526008" + + "60205260409020555b6007805480611e9657611e966125c9565b5f8281526020808220" + + "83015f1990810180546001600160a01b03191690559092019092556001600160a01b03" + + "95909516815260089094525050604082209190915550565b80356001600160a01b0381" + + "168114611c0d575f5ffd5b80356001600160401b0381168114611c0d575f5ffd5b8035" + + "60ff81168114611c0d575f5ffd5b5f5f83601f840112611f26575f5ffd5b5081356001" + + "600160401b03811115611f3c575f5ffd5b602083019150836020828501011115611f53" + + "575f5ffd5b9250929050565b5f5f83601f840112611f6a575f5ffd5b50813560016001" + + "60401b03811115611f80575f5ffd5b6020830191508360208260051b8501011115611f" + + "53575f5ffd5b5f5f5f5f5f5f5f5f60c0898b031215611fb1575f5ffd5b611fba89611e" + + "da565b9750611fc860208a01611ef0565b9650611fd660408a01611f06565b9550611f" + + "e460608a01611ef0565b945060808901356001600160401b03811115611ffe575f5ffd" + + "5b61200a8b828c01611f16565b90955093505060a08901356001600160401b03811115" + + "612028575f5ffd5b6120348b828c01611f5a565b999c989b5096995094979396929594" + + "505050565b5f60208284031215612058575f5ffd5b61161d82611eda565b5f5f5f6040" + + "8486031215612073575f5ffd5b8335925060208401356001600160401b038111156120" + + "8f575f5ffd5b61209b86828701611f16565b9497909650939450505050565b5f5f6040" + + "83850312156120b9575f5ffd5b823591506120c960208401611ef0565b905092509290" + + "50565b5f5f604083850312156120e3575f5ffd5b6120ec83611ef0565b91506120c960" + + "208401611ef0565b5f5f5f6040848603121561210c575f5ffd5b833592506020840135" + + "6001600160401b03811115612128575f5ffd5b61209b86828701611f5a565b5f815180" + + "8452602084019350602083015f5b8281101561216d5781516001600160a01b03168652" + + "60209586019590910190600101612146565b5093949350505050565b602081525f6116" + + "1d6020830184612134565b5f60208284031215612199575f5ffd5b5035919050565b5f" + + "5f604083850312156121b1575f5ffd5b82359150602083013580151581146121c7575f" + + "5ffd5b809150509250929050565b5f5f5f606084860312156121e4575f5ffd5b6121ed" + + "84611eda565b92506121fb60208501611ef0565b915061220960408501611ef0565b90" + + "509250925092565b606080825284519082018190525f9060208601906080840190835b" + + "8181101561224b57835183526020938401939092019160010161222d565b5050838103" + + "602085015261225f8187612134565b8481036040860152855180825260208088019450" + + "90910191505f5b818110156122a15783516001600160401b0316835260209384019390" + + "92019160010161227a565b5090979650505050505050565b5f5f604083850312156122" + + "bf575f5ffd5b823591506120c960208401611eda565b60018060a01b03861681526001" + + "600160401b0385166020820152831515604082015260a060608201525f83518060a084" + + "0152806020860160c085015e5f60c0828501015260c0601f19601f8301168401019150" + + "508260808301529695505050505050565b5f5f60408385031215612344575f5ffd5b82" + + "3591506120c960208401611f06565b634e487b7160e01b5f52601160045260245ffd5b" + + "5f6001820161237957612379612354565b5060010190565b634e487b7160e01b5f5260" + + "4160045260245ffd5b600181811c908216806123a857607f821691505b602082108103" + + "6123c657634e487b7160e01b5f52602260045260245ffd5b50919050565b601f821115" + + "61241e578282111561241e57805f5260205f20601f840160051c60208510156123f757" + + "505f5b90810190601f840160051c035f5b8181101561241a575f838201556001016124" + + "05565b5050505b505050565b6001600160401b0383111561243a5761243a612380565b" + + "61244e836124488354612394565b836123cc565b5f601f84116001811461247f575f85" + + "156124685750838201355b5f19600387901b1c1916600186901b1783556124d6565b5f" + + "83815260208120601f198716915b828110156124ae5786850135825560209485019460" + + "01909201910161248e565b50868210156124ca575f1960f88860031b161c1984870135" + + "1681555b505060018560011b0183555b5050505050565b6001600160401b0381811683" + + "821601908111156124fc576124fc612354565b92915050565b634e487b7160e01b5f52" + + "603260045260245ffd5b81835281816020850137505f82820160209081019190915260" + + "1f909101601f19169091010190565b6001600160401b0384168152604060208201525f" + + "612560604083018486612516565b95945050505050565b602081525f61257c60208301" + + "8486612516565b949350505050565b6001600160401b03828116828216039081111561" + + "24fc576124fc612354565b818103818111156124fc576124fc612354565b8082018082" + + "11156124fc576124fc612354565b634e487b7160e01b5f52603160045260245ffdfe61" + + "26ab0a9aadc9f729174668b7965114b5198740cc236df82aa4435b33d3626aa2646970" + + "6673582212207c3ab7fc7e401b4dc67275cd3c9117f3361164f9ff9c5dc761635fd63e" + + "c15fa364736f6c63430008210033" ) diff --git a/registry-contract/foundry.toml b/registry-contract/foundry.toml index 885852a041..0699035cb9 100644 --- a/registry-contract/foundry.toml +++ b/registry-contract/foundry.toml @@ -2,6 +2,7 @@ src = "src" out = "out" libs = ["lib"] +solc = "0.8.33" optimizer = true optimizer_runs = 200 diff --git a/registry-contract/src/ReservedBlockspaceRegistry.sol b/registry-contract/src/ReservedBlockspaceRegistry.sol index 65d8e48042..f94735a228 100644 --- a/registry-contract/src/ReservedBlockspaceRegistry.sol +++ b/registry-contract/src/ReservedBlockspaceRegistry.sol @@ -14,6 +14,7 @@ contract ReservedBlockspaceRegistry { error AddressAlreadyRegistered(); error AddressNotRegistered(); error InvalidQuota(); + error InvalidFeeMode(); error MaxTotalReservedGasExceeded(); error MaxClientReservedGasExceeded(); @@ -21,16 +22,32 @@ contract ReservedBlockspaceRegistry { address admin; uint64 gasQuota; bool active; + // feeMode: 0 = free (zero in-protocol fee), 1 = routed (fee paid but + // credited to the producer). See the reserved-blockspace spec §7. + uint8 feeMode; + // effectiveFrom: block number from which this client's reserved status + // applies. Lets governance schedule/announce a change at a future height + // (a kill-switch via setClientActive(false) is still immediate). + uint64 effectiveFrom; string metadata; address[] addresses; } + uint8 internal constant FEE_MODE_FREE = 0; + uint8 internal constant FEE_MODE_ROUTED = 1; + address public owner; uint64 public maxTotalReservedGas; uint64 public maxClientReservedGas; uint256 public nextClientId; uint64 public totalReservedGas; + // configVersion increments on every change to the reserved set or its + // limits. Bor reads root() once per block and only rebuilds its cached + // snapshot when the value changes (reserved-blockspace spec §4.5), avoiding + // a per-transaction state read. + uint256 public configVersion; + mapping(uint256 clientId => Client client) private clients; mapping(address account => uint256 clientId) private clientIdByAddress; address[] private allClientAddresses; @@ -46,12 +63,26 @@ contract ReservedBlockspaceRegistry { event ClientMetadataUpdated(uint256 indexed clientId, string metadata); event ClientAddressAdded(uint256 indexed clientId, address indexed account); event ClientAddressRemoved(uint256 indexed clientId, address indexed account); + event ClientFeeModeUpdated(uint256 indexed clientId, uint8 feeMode); + event ClientEffectiveFromUpdated(uint256 indexed clientId, uint64 effectiveFrom); + event ConfigVersionUpdated(uint256 version); modifier onlyInitialized() { if (owner == address(0)) revert NotInitialized(); _; } + // bumpsVersion increments configVersion after a mutation so Bor's cached + // snapshot (keyed on root()) is invalidated. Applied to every function that + // changes the reserved set, quotas, fee modes, effective heights, or limits. + modifier bumpsVersion() { + _; + unchecked { + configVersion++; + } + emit ConfigVersionUpdated(configVersion); + } + modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; @@ -82,7 +113,7 @@ contract ReservedBlockspaceRegistry { owner = newOwner; } - function setLimits(uint64 maxTotalGas, uint64 maxClientGas) external onlyInitialized onlyOwner { + function setLimits(uint64 maxTotalGas, uint64 maxClientGas) external onlyInitialized onlyOwner bumpsVersion { _validateLimits(maxTotalGas, maxClientGas); if (totalReservedGas > maxTotalGas) revert MaxTotalReservedGasExceeded(); for (uint256 clientId = 1; clientId < nextClientId; clientId++) { @@ -95,13 +126,16 @@ contract ReservedBlockspaceRegistry { emit LimitsUpdated(maxTotalGas, maxClientGas); } - function createClient(address admin, uint64 gasQuota, string calldata metadata, address[] calldata addresses) - external - onlyInitialized - onlyOwner - returns (uint256 clientId) - { + function createClient( + address admin, + uint64 gasQuota, + uint8 feeMode, + uint64 effectiveFrom, + string calldata metadata, + address[] calldata addresses + ) external onlyInitialized onlyOwner bumpsVersion returns (uint256 clientId) { if (admin == address(0)) revert ZeroAddress(); + if (feeMode > FEE_MODE_ROUTED) revert InvalidFeeMode(); _validateQuota(gasQuota, 0); clientId = nextClientId++; @@ -109,6 +143,8 @@ contract ReservedBlockspaceRegistry { client.admin = admin; client.gasQuota = gasQuota; client.active = true; + client.feeMode = feeMode; + client.effectiveFrom = effectiveFrom; client.metadata = metadata; totalReservedGas += gasQuota; @@ -117,6 +153,8 @@ contract ReservedBlockspaceRegistry { } emit ClientCreated(clientId, admin, gasQuota, metadata); + emit ClientFeeModeUpdated(clientId, feeMode); + emit ClientEffectiveFromUpdated(clientId, effectiveFrom); } function setClientAdmin(uint256 clientId, address newAdmin) external onlyInitialized onlyOwner { @@ -128,7 +166,7 @@ contract ReservedBlockspaceRegistry { emit ClientAdminUpdated(clientId, previousAdmin, newAdmin); } - function setClientQuota(uint256 clientId, uint64 newQuota) external onlyInitialized onlyOwner { + function setClientQuota(uint256 clientId, uint64 newQuota) external onlyInitialized onlyOwner bumpsVersion { Client storage client = _client(clientId); uint64 previousQuota = client.gasQuota; _validateQuota(newQuota, client.active ? previousQuota : 0); @@ -141,7 +179,7 @@ contract ReservedBlockspaceRegistry { emit ClientQuotaUpdated(clientId, previousQuota, newQuota); } - function setClientActive(uint256 clientId, bool active) external onlyInitialized onlyOwner { + function setClientActive(uint256 clientId, bool active) external onlyInitialized onlyOwner bumpsVersion { Client storage client = _client(clientId); if (client.active == active) return; @@ -169,6 +207,7 @@ contract ReservedBlockspaceRegistry { external onlyInitialized onlyClientAdminOrOwner(clientId) + bumpsVersion { _addAddress(clientId, account); } @@ -177,6 +216,7 @@ contract ReservedBlockspaceRegistry { external onlyInitialized onlyClientAdminOrOwner(clientId) + bumpsVersion { for (uint256 i = 0; i < accounts.length; i++) { _addAddress(clientId, accounts[i]); @@ -187,6 +227,7 @@ contract ReservedBlockspaceRegistry { external onlyInitialized onlyClientAdminOrOwner(clientId) + bumpsVersion { Client storage client = _client(clientId); if (clientIdByAddress[account] != clientId) revert AddressNotRegistered(); @@ -223,13 +264,13 @@ contract ReservedBlockspaceRegistry { function getClientForAddress(address account) external view - returns (uint256 clientId, uint64 gasQuota, address admin, bool active) + returns (uint256 clientId, uint64 gasQuota, address admin, bool active, uint8 feeMode, uint64 effectiveFrom) { clientId = clientIdByAddress[account]; - if (clientId == 0) return (0, 0, address(0), false); + if (clientId == 0) return (0, 0, address(0), false, 0, 0); Client storage client = clients[clientId]; - return (clientId, client.gasQuota, client.admin, client.active); + return (clientId, client.gasQuota, client.admin, client.active, client.feeMode, client.effectiveFrom); } function isReservedAddress(address account) external view returns (bool) { @@ -237,6 +278,29 @@ contract ReservedBlockspaceRegistry { return clientId != 0 && clients[clientId].active; } + // root returns a value that changes whenever the reserved set, quotas, fee + // modes, effective heights, or limits change. Bor caches its snapshot keyed + // on this and only rebuilds when it moves (reserved-blockspace spec §4.5). + function root() external view returns (bytes32) { + return bytes32(configVersion); + } + + function setClientFeeMode(uint256 clientId, uint8 feeMode) external onlyInitialized onlyOwner bumpsVersion { + if (feeMode > FEE_MODE_ROUTED) revert InvalidFeeMode(); + _client(clientId).feeMode = feeMode; + emit ClientFeeModeUpdated(clientId, feeMode); + } + + function setClientEffectiveFrom(uint256 clientId, uint64 effectiveFrom) + external + onlyInitialized + onlyOwner + bumpsVersion + { + _client(clientId).effectiveFrom = effectiveFrom; + emit ClientEffectiveFromUpdated(clientId, effectiveFrom); + } + function getWhitelistedAddresses() external view returns (address[] memory) { uint256 count; for (uint256 i = 0; i < allClientAddresses.length; i++) { diff --git a/registry-contract/test/ReservedBlockspaceRegistry.t.sol b/registry-contract/test/ReservedBlockspaceRegistry.t.sol index c744302006..98231f2c6e 100644 --- a/registry-contract/test/ReservedBlockspaceRegistry.t.sol +++ b/registry-contract/test/ReservedBlockspaceRegistry.t.sol @@ -14,6 +14,9 @@ contract ReservedBlockspaceRegistryTest is Test { address internal senderB = address(0x200); address internal senderC = address(0x300); + uint8 internal constant FEE_FREE = 0; + uint8 internal constant FEE_ROUTED = 1; + function setUp() public { registry = new ReservedBlockspaceRegistry(); registry.initialize(owner, 80_000_000, 30_000_000); @@ -30,7 +33,7 @@ contract ReservedBlockspaceRegistryTest is Test { addresses[1] = senderB; vm.prank(owner); - uint256 clientId = registry.createClient(admin, 20_000_000, "Polymarket", addresses); + uint256 clientId = registry.createClient(admin, 20_000_000, FEE_FREE, 0, "Polymarket", addresses); assertEq(clientId, 1); assertEq(registry.totalReservedGas(), 20_000_000); @@ -38,12 +41,14 @@ contract ReservedBlockspaceRegistryTest is Test { assertTrue(registry.isReservedAddress(senderB)); assertFalse(registry.isReservedAddress(senderC)); - (uint256 resolvedClientId, uint64 gasQuota, address resolvedAdmin, bool active) = + (uint256 resolvedClientId, uint64 gasQuota, address resolvedAdmin, bool active, uint8 feeMode, uint64 effFrom) = registry.getClientForAddress(senderA); assertEq(resolvedClientId, clientId); assertEq(gasQuota, 20_000_000); assertEq(resolvedAdmin, admin); assertTrue(active); + assertEq(feeMode, FEE_FREE); + assertEq(effFrom, 0); (address returnedAdmin, uint64 returnedQuota, bool returnedActive, string memory metadata, uint256 count) = registry.getClient(clientId); @@ -94,17 +99,17 @@ contract ReservedBlockspaceRegistryTest is Test { vm.prank(owner); vm.expectRevert(ReservedBlockspaceRegistry.MaxClientReservedGasExceeded.selector); - registry.createClient(admin, 30_000_001, "too-large-client", addresses); + registry.createClient(admin, 30_000_001, FEE_FREE, 0, "too-large-client", addresses); vm.prank(owner); - registry.createClient(admin, 30_000_000, "Courtyard", addresses); + registry.createClient(admin, 30_000_000, FEE_FREE, 0, "Courtyard", addresses); address[] memory moreAddresses = new address[](1); moreAddresses[0] = senderC; vm.prank(owner); vm.expectRevert(ReservedBlockspaceRegistry.MaxTotalReservedGasExceeded.selector); - registry.createClient(admin, 20_000_000, "would-exceed-total", moreAddresses); + registry.createClient(admin, 20_000_000, FEE_FREE, 0, "would-exceed-total", moreAddresses); } function testInactiveClientIsNotReservedButKeepsAddressMapping() public { @@ -117,7 +122,7 @@ contract ReservedBlockspaceRegistryTest is Test { assertEq(registry.getClientId(senderA), clientId); assertEq(registry.totalReservedGas(), 0); - (uint256 resolvedClientId, uint64 gasQuota, address resolvedAdmin, bool active) = + (uint256 resolvedClientId, uint64 gasQuota, address resolvedAdmin, bool active,,) = registry.getClientForAddress(senderA); assertEq(resolvedClientId, clientId); assertEq(gasQuota, 20_000_000); @@ -132,7 +137,7 @@ contract ReservedBlockspaceRegistryTest is Test { addresses[0] = senderB; vm.prank(owner); - uint256 secondClientId = registry.createClient(admin, 10_000_000, "Courtyard", addresses); + uint256 secondClientId = registry.createClient(admin, 10_000_000, FEE_FREE, 0, "Courtyard", addresses); vm.prank(owner); registry.setClientActive(firstClientId, false); @@ -150,11 +155,84 @@ contract ReservedBlockspaceRegistryTest is Test { assertEq(whitelisted[0], senderB); } + // --- new: feeMode --- + + function testCreateClientStoresFeeMode() public { + address[] memory addresses = new address[](1); + addresses[0] = senderA; + + vm.prank(owner); + uint256 clientId = registry.createClient(admin, 20_000_000, FEE_ROUTED, 0, "routed-client", addresses); + + (,,,, uint8 feeMode,) = registry.getClientForAddress(senderA); + assertEq(feeMode, FEE_ROUTED); + + vm.prank(owner); + registry.setClientFeeMode(clientId, FEE_FREE); + (,,,, feeMode,) = registry.getClientForAddress(senderA); + assertEq(feeMode, FEE_FREE); + } + + function testInvalidFeeModeReverts() public { + address[] memory addresses = new address[](1); + addresses[0] = senderA; + + vm.prank(owner); + vm.expectRevert(ReservedBlockspaceRegistry.InvalidFeeMode.selector); + registry.createClient(admin, 20_000_000, 2, 0, "bad-fee-mode", addresses); + } + + // --- new: effectiveFrom --- + + function testEffectiveFromStoredAndUpdatable() public { + address[] memory addresses = new address[](1); + addresses[0] = senderA; + + vm.prank(owner); + uint256 clientId = registry.createClient(admin, 20_000_000, FEE_FREE, 1000, "scheduled-client", addresses); + + (,,,,, uint64 effFrom) = registry.getClientForAddress(senderA); + assertEq(effFrom, 1000); + + vm.prank(owner); + registry.setClientEffectiveFrom(clientId, 2000); + (,,,,, effFrom) = registry.getClientForAddress(senderA); + assertEq(effFrom, 2000); + } + + // --- new: root() change-epoch --- + + function testRootChangesOnEveryMutation() public { + bytes32 r0 = registry.root(); + + address[] memory addresses = new address[](1); + addresses[0] = senderA; + vm.prank(owner); + uint256 clientId = registry.createClient(admin, 20_000_000, FEE_FREE, 0, "c", addresses); + bytes32 r1 = registry.root(); + assertTrue(r1 != r0, "root must change after createClient"); + + vm.prank(owner); + registry.setClientQuota(clientId, 15_000_000); + bytes32 r2 = registry.root(); + assertTrue(r2 != r1, "root must change after setClientQuota"); + + vm.prank(owner); + registry.setClientFeeMode(clientId, FEE_ROUTED); + bytes32 r3 = registry.root(); + assertTrue(r3 != r2, "root must change after setClientFeeMode"); + + vm.prank(owner); + registry.setClientActive(clientId, false); + bytes32 r4 = registry.root(); + assertTrue(r4 != r3, "root must change after setClientActive"); + } + function _createSingleAddressClient(address account) internal returns (uint256 clientId) { address[] memory addresses = new address[](1); addresses[0] = account; vm.prank(owner); - clientId = registry.createClient(admin, 20_000_000, "Polymarket", addresses); + clientId = registry.createClient(admin, 20_000_000, FEE_FREE, 0, "Polymarket", addresses); } } From b031d42cf1c595a7d36ffce0fee61281979e685d Mon Sep 17 00:00:00 2001 From: marcello33 Date: Sat, 27 Jun 2026 10:51:22 +0200 Subject: [PATCH 12/19] registryreader: add per-block reserved-set Snapshot cache (POS-3574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce registryreader.Snapshot — an immutable, pure-lookup view of the reserved set built once from the registry at a given block state (spec §4.5), so the hot classification paths (txpool admission, EVM fee-skip, base-fee capacity) never do a per-transaction state read. BuildSnapshot reads the active whitelist + per-client feeMode/effectiveFrom/quota + capacity + root(); callers cache it and rebuild only when root() moves. nil-safe (no registry = classifies nothing). Extends the Reader interface with WhitelistedAddresses/TotalReservedGas (already implemented on the concrete reader and the test harness). Snapshot unit test builds from the real contract bytecode via the harness and verifies classification, feeMode, capacity, and root. --- .../bor/contract/registrytest/harness.go | 30 +++++++ .../bor/contract/registrytest/harness_test.go | 24 ++++++ consensus/bor/registryreader/reader.go | 81 +++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/consensus/bor/contract/registrytest/harness.go b/consensus/bor/contract/registrytest/harness.go index 2e8461463f..512f05831a 100644 --- a/consensus/bor/contract/registrytest/harness.go +++ b/consensus/bor/contract/registrytest/harness.go @@ -207,6 +207,36 @@ func (r *evmReader) Root(_ *state.StateDB, _ uint64, _ common.Hash) (common.Hash return common.Hash(root), nil } +func (r *evmReader) WhitelistedAddresses(_ *state.StateDB, _ uint64, _ common.Hash) ([]common.Address, error) { + values, err := r.call("getWhitelistedAddresses") + if err != nil { + return nil, err + } + if len(values) != 1 { + return nil, fmt.Errorf("getWhitelistedAddresses returned %d values", len(values)) + } + addrs, ok := values[0].([]common.Address) + if !ok { + return nil, fmt.Errorf("getWhitelistedAddresses returned %T", values[0]) + } + return addrs, nil +} + +func (r *evmReader) TotalReservedGas(_ *state.StateDB, _ uint64, _ common.Hash) (uint64, error) { + values, err := r.call("totalReservedGas") + if err != nil { + return 0, err + } + if len(values) != 1 { + return 0, fmt.Errorf("totalReservedGas returned %d values", len(values)) + } + total, ok := values[0].(uint64) + if !ok { + return 0, fmt.Errorf("totalReservedGas returned %T", values[0]) + } + return total, nil +} + func (r *evmReader) call(method string, args ...interface{}) ([]interface{}, error) { data, err := r.readerAB.Pack(method, args...) if err != nil { diff --git a/consensus/bor/contract/registrytest/harness_test.go b/consensus/bor/contract/registrytest/harness_test.go index 4ec54c8bf1..c51d62b838 100644 --- a/consensus/bor/contract/registrytest/harness_test.go +++ b/consensus/bor/contract/registrytest/harness_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/stretchr/testify/require" ) @@ -27,3 +28,26 @@ func TestHarness_DeploysAndAnswersQueries(t *testing.T) { require.NotNil(t, lookup.ClientID) require.Equal(t, int64(1), lookup.ClientID.Int64(), "createClient assigns id starting at 1") } + +func TestSnapshot_BuildsFromRegistry(t *testing.T) { + h := NewHarness(t) + + snap, err := registryreader.BuildSnapshot(h.Reader, nil, 1, common.Hash{}) + require.NoError(t, err) + require.NotNil(t, snap) + + // The whitelisted address classifies reserved; the other does not. + require.True(t, snap.IsReserved(h.ReservedAddr, 1)) + require.False(t, snap.IsReserved(h.UnreservedAddr, 1)) + + // Snapshot mirrors the registry: feeMode free (0), capacity = the one + // client's quota, and a non-zero root it can be cached against. + require.Equal(t, uint8(0), snap.FeeMode(h.ReservedAddr)) + require.Equal(t, uint64(10_000_000), snap.Capacity()) + require.NotEqual(t, common.Hash{}, snap.Root()) + + // nil snapshot (no registry) classifies nothing and is safe. + var none *registryreader.Snapshot + require.False(t, none.IsReserved(h.ReservedAddr, 1)) + require.Equal(t, uint64(0), none.Capacity()) +} diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go index c06967d360..d2ff1b2b52 100644 --- a/consensus/bor/registryreader/reader.go +++ b/consensus/bor/registryreader/reader.go @@ -40,4 +40,85 @@ type Reader interface { // whenever the reserved set or its limits change, so a snapshot keyed on it // can be reused until it moves (spec §4.5). Root(state *state.StateDB, number uint64, hash common.Hash) (common.Hash, error) + // WhitelistedAddresses returns every currently-active reserved address. + WhitelistedAddresses(state *state.StateDB, number uint64, hash common.Hash) ([]common.Address, error) + // TotalReservedGas returns the sum of active client quotas (reserved capacity). + TotalReservedGas(state *state.StateDB, number uint64, hash common.Hash) (uint64, error) +} + +// Snapshot is an immutable, pure-lookup view of the reserved set as of one +// block's state. It is built once per head/parent (and reused while root() is +// unchanged) so the hot classification paths — txpool admission, the EVM +// fee-skip stand-in, base-fee capacity — never do a per-transaction state read +// (spec §4.5). A nil *Snapshot classifies nothing (no registry / non-bor chain), +// so all methods are nil-safe. +type Snapshot struct { + root common.Hash + capacity uint64 + clients map[common.Address]ClientLookup +} + +// BuildSnapshot reads the full active reserved set from the registry at the +// given block state and returns an immutable Snapshot. Returns nil (no error) +// when no registry is configured. Callers cache it keyed on Root(). +func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common.Hash) (*Snapshot, error) { + if r == nil || !r.HasReservedRegistry() { + return nil, nil + } + root, err := r.Root(statedb, number, hash) + if err != nil { + return nil, err + } + addrs, err := r.WhitelistedAddresses(statedb, number, hash) + if err != nil { + return nil, err + } + capacity, err := r.TotalReservedGas(statedb, number, hash) + if err != nil { + return nil, err + } + clients := make(map[common.Address]ClientLookup, len(addrs)) + for _, a := range addrs { + c, err := r.ReservedClientForAddress(statedb, number, hash, a) + if err != nil { + return nil, err + } + clients[a] = c + } + return &Snapshot{root: root, capacity: capacity, clients: clients}, nil +} + +// Root is the registry root this snapshot was built at; callers reuse the +// snapshot while the live root is unchanged. +func (s *Snapshot) Root() common.Hash { + if s == nil { + return common.Hash{} + } + return s.root +} + +// IsReserved reports whether account is an active reserved sender effective at +// the given block number (active && effectiveFrom <= number). +func (s *Snapshot) IsReserved(account common.Address, number uint64) bool { + if s == nil { + return false + } + c, ok := s.clients[account] + return ok && c.Active && c.EffectiveFrom <= number +} + +// FeeMode returns the fee mode of account's client (0 = free) or 0 if not reserved. +func (s *Snapshot) FeeMode(account common.Address) uint8 { + if s == nil { + return 0 + } + return s.clients[account].FeeMode +} + +// Capacity returns the reserved capacity (sum of active client quotas). +func (s *Snapshot) Capacity() uint64 { + if s == nil { + return 0 + } + return s.capacity } From db3cd07c4a39f22af490c1dfc0df0beb1077586b Mon Sep 17 00:00:00 2001 From: marcello33 Date: Mon, 29 Jun 2026 10:35:38 +0200 Subject: [PATCH 13/19] txpool: classify reserved senders from the registry snapshot, not config (POS-3570/3574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repoint the pool's reserved classification off the config stub onto the registry: legacypool builds a registryreader.Snapshot at each reset from the new head's state, and isReserved / validation.go's tip-floor waiver query the snapshot (rebuilt per head, not per tx — spec §4.5). The fork-height gate stays in chain config (a legit chain param); only the reserved *set* now comes from the registry. TxPool.SetReservedRegistry propagates the reader to subpools. Reserved unit tests now drive a fake registryreader.Reader as the source; full txpool suite green. --- core/txpool/legacypool/legacypool.go | 62 +++++++++++++++++++++---- core/txpool/legacypool/reserved_test.go | 53 ++++++++++++++++++--- core/txpool/txpool.go | 8 ++++ core/txpool/validation.go | 20 ++++++-- 4 files changed, 124 insertions(+), 19 deletions(-) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 61cdc578c4..f2d5d93fb8 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/prque" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" @@ -326,6 +327,12 @@ type LegacyPool struct { filteredAddrs map[common.Address]struct{} // Map of addresses to filter + // Reserved-blockspace registry: reader is wired post-Init from the backend; + // the snapshot is rebuilt at each reset from the new head's state so the + // per-tx admission path never reads contract state (spec §4.5). + reservedRegistry registryreader.Reader + reservedSnapshot atomic.Pointer[registryreader.Snapshot] + // Rebroadcast tracking rebroadcastTxFeed event.Feed // Feed for stuck transaction events lastRebroadcast map[common.Hash]time.Time // Track last rebroadcast time per tx hash @@ -784,8 +791,9 @@ func (pool *LegacyPool) ValidateTxBasics(tx *types.Transaction) error { 1< Date: Mon, 29 Jun 2026 10:49:00 +0200 Subject: [PATCH 14/19] core, vm, miner: classify reserved senders from the registry snapshot in execution (POS-3570/3574) Build a per-block reserved-set snapshot from the registry at parent state and inject it into the EVM block context across the serial, V1-parallel, and V2 BlockSTM execution paths plus the miner. newStateTransition now classifies reserved senders from the snapshot instead of the config stub, keeping serial and parallel execution byte-identical. --- consensus/bor/registryreader/reader.go | 6 +++++ core/evm.go | 33 ++++++++++++++++++++++++++ core/parallel_state_processor.go | 6 +++++ core/reserved_fee_test.go | 23 +++++++++++++----- core/state_processor.go | 3 +++ core/state_transition.go | 14 +++++++---- core/vm/evm.go | 9 +++++++ miner/worker.go | 7 +++++- 8 files changed, 90 insertions(+), 11 deletions(-) diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go index d2ff1b2b52..c7a4a93636 100644 --- a/consensus/bor/registryreader/reader.go +++ b/consensus/bor/registryreader/reader.go @@ -88,6 +88,12 @@ func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common. return &Snapshot{root: root, capacity: capacity, clients: clients}, nil } +// NewSnapshot constructs a Snapshot from an explicit client set. Used by tests +// and by callers that source the reserved set outside the registry contract. +func NewSnapshot(root common.Hash, capacity uint64, clients map[common.Address]ClientLookup) *Snapshot { + return &Snapshot{root: root, capacity: capacity, clients: clients} +} + // Root is the registry root this snapshot was built at; callers reuse the // snapshot while the live root is unchanged. func (s *Snapshot) Root() common.Hash { diff --git a/core/evm.go b/core/evm.go index a74c098e00..9c7da25af5 100644 --- a/core/evm.go +++ b/core/evm.go @@ -24,10 +24,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/log" ) // ChainContext supports retrieving headers and consensus parameters from the @@ -39,6 +42,36 @@ type ChainContext interface { Engine() consensus.Engine } +// ReservedSnapshotForBlock builds the reserved-blockspace snapshot used to +// classify reserved senders while executing `header`, read from the PARENT +// registry state (statedb is the parent post-state). Reading at the parent makes +// classification deterministic across produce and verify and immune to a +// governance tx landing in the same block. Returns nil when the chain exposes no +// registry reader / none is configured, or on a read error (deterministic across +// nodes since the parent state is fixed) — a nil snapshot classifies nothing. +func ReservedSnapshotForBlock(chain ChainContext, statedb *state.StateDB, header *types.Header) *registryreader.Snapshot { + rr, ok := chain.(interface { + ReservedRegistry() registryreader.Reader + }) + if !ok { + return nil + } + reader := rr.ReservedRegistry() + if reader == nil || !reader.HasReservedRegistry() { + return nil + } + parentNumber := uint64(0) + if header.Number.Uint64() > 0 { + parentNumber = header.Number.Uint64() - 1 + } + snap, err := registryreader.BuildSnapshot(reader, statedb, parentNumber, header.ParentHash) + if err != nil { + log.Warn("Failed to build reserved-blockspace snapshot", "number", header.Number, "err", err) + return nil + } + return snap +} + // NewEVMBlockContext creates a new context for use in the EVM. func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { var ( diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 83951a2219..5f37b7269d 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -439,6 +439,9 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat } blockContext := NewEVMBlockContext(header, p.bc, author) + // Same parent-state reserved snapshot the serial processor uses, so parallel + // and serial classify identically (consensus parity). + blockContext.ReservedSnapshot = ReservedSnapshotForBlock(p.bc, statedb, header) coinbase := blockContext.Coinbase context := NewEVMBlockContext(header, p.bc.hc, author) @@ -1116,6 +1119,9 @@ func (p *V2StateProcessor) Process(block *types.Block, statedb *state.StateDB, c misc.ApplyDAOHardFork(statedb) } blockCtx := NewEVMBlockContext(header, p.chain, author) + // Reserved snapshot from the parent state, matching the serial path so V2 + // BlockSTM classification is identical (consensus parity). + blockCtx.ReservedSnapshot = ReservedSnapshotForBlock(p.chain, statedb, header) applyV2PreExecSystemCalls(block, statedb, config, cfg, blockCtx) tasks, err := buildV2Tasks(block, config, header, interruptCtx) diff --git a/core/reserved_fee_test.go b/core/reserved_fee_test.go index 52d9aab48d..0fd8f91c9b 100644 --- a/core/reserved_fee_test.go +++ b/core/reserved_fee_test.go @@ -24,6 +24,7 @@ import ( "github.com/holiman/uint256" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" @@ -53,8 +54,10 @@ func reservedTestConfig(forkBlock *big.Int, reservedSenders ...common.Address) * return &cc } -func reservedBlockCtx(coinbase common.Address, blockNumber *big.Int, baseFee *big.Int) vm.BlockContext { - return vm.BlockContext{ +// reservedBlockCtx builds a block context with a reserved-set snapshot covering +// reservedSenders (the registry-backed source the EVM now classifies from). +func reservedBlockCtx(coinbase common.Address, blockNumber *big.Int, baseFee *big.Int, reservedSenders ...common.Address) vm.BlockContext { + ctx := vm.BlockContext{ CanTransfer: CanTransfer, Transfer: Transfer, GetHash: func(n uint64) common.Hash { return common.Hash{} }, @@ -64,6 +67,14 @@ func reservedBlockCtx(coinbase common.Address, blockNumber *big.Int, baseFee *bi Time: 1, BaseFee: baseFee, } + if len(reservedSenders) > 0 { + clients := make(map[common.Address]registryreader.ClientLookup, len(reservedSenders)) + for i, a := range reservedSenders { + clients[a] = registryreader.ClientLookup{ClientID: big.NewInt(int64(i + 1)), GasQuota: 30_000_000, Active: true} + } + ctx.ReservedSnapshot = registryreader.NewSnapshot(common.HexToHash("0x1"), uint64(len(reservedSenders))*30_000_000, clients) + } + return ctx } // fundedState returns an in-memory StateDB with `sender` funded and nonce 0. @@ -91,7 +102,7 @@ func TestReservedTxSkipsFees(t *testing.T) { cc := reservedTestConfig(big.NewInt(0), sender) baseFee := big.NewInt(1_000_000_000) - blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee) + blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee, sender) initial := uint256.NewInt(1e18) sdb := fundedState(t, sender, initial) @@ -162,7 +173,7 @@ func TestReservedTxGatedByFork(t *testing.T) { cc := reservedTestConfig(big.NewInt(100), sender) // fork at 100 baseFee := big.NewInt(1_000_000_000) - blockCtx := reservedBlockCtx(common.HexToAddress("0xc0b0"), big.NewInt(1), baseFee) // pre-fork + blockCtx := reservedBlockCtx(common.HexToAddress("0xc0b0"), big.NewInt(1), baseFee, sender) // pre-fork sdb := fundedState(t, sender, uint256.NewInt(1e18)) signer := types.NewLondonSigner(cc.ChainID) @@ -198,7 +209,7 @@ func TestNonReservedSenderPaysNormally(t *testing.T) { reserved := common.HexToAddress("0x00000000000000000000000000000000000000ee") cc := reservedTestConfig(big.NewInt(0), reserved) baseFee := big.NewInt(1_000_000_000) - blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee) + blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee, reserved) sdb := fundedState(t, sender, uint256.NewInt(1e18)) signer := types.NewLondonSigner(cc.ChainID) @@ -242,7 +253,7 @@ func TestReservedTxSerialParallelParity(t *testing.T) { cc := reservedTestConfig(big.NewInt(0), sender) baseFee := big.NewInt(1_000_000_000) - blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee) + blockCtx := reservedBlockCtx(coinbase, big.NewInt(1), baseFee, sender) // Committed base state shared by both paths. memdb := rawdb.NewMemoryDatabase() diff --git a/core/state_processor.go b/core/state_processor.go index 0ac0af66e6..24d9e11f3b 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -95,6 +95,9 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg tracingStateDB = state.NewHookedState(statedb, hooks) } context = NewEVMBlockContext(header, p.chain, author) + // Reserved-blockspace classification reads the registry at the parent state + // (statedb is the parent post-state here, before block execution). + context.ReservedSnapshot = ReservedSnapshotForBlock(p.chain, statedb, header) evm := vm.NewEVM(context, tracingStateDB, p.chainConfig(), cfg) if beaconRoot := block.BeaconRoot(); beaconRoot != nil { diff --git a/core/state_transition.go b/core/state_transition.go index f21bd8afda..2495cdca47 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -294,13 +294,19 @@ type stateTransition struct { func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition { // Classify the message as reserved-blockspace here — the single point every // execution path (serial, parallel, and the ApplyMessage* variants) funnels - // through — so the zero-fee decision is identical across executors. The source - // is the config-backed stub (POS-3572 will swap in the registry without - // changing this call). + // through — so the zero-fee decision is identical across executors. The fork + // height gate comes from chain config; the reserved set comes from the parent + // registry snapshot on the block context (set per block by the consensus + // paths, nil elsewhere), matching the txpool and base-fee sources. + // + // This sender-based classification is the foundation stand-in. The spec's + // end state is position-based (reserved region 0..ReservedTxCount), set by + // the producer two-pass and checked by block validation (POS-3575/3576); + // that work supersedes this. var reserved bool if cfg := evm.ChainConfig(); cfg.Bor != nil && cfg.Bor.IsReservedBlockspace(evm.Context.BlockNumber) && - cfg.Bor.IsReservedSender(msg.From) { + evm.Context.ReservedSnapshot.IsReserved(msg.From, evm.Context.BlockNumber.Uint64()) { reserved = true } return &stateTransition{ diff --git a/core/vm/evm.go b/core/vm/evm.go index 5f19d0e5b5..a240730a08 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -25,6 +25,7 @@ import ( "github.com/holiman/uint256" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" @@ -120,6 +121,14 @@ type BlockContext struct { BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) Random *common.Hash // Provides information for PREVRANDAO + + // ReservedSnapshot is the reserved-blockspace set as of the parent block, + // used to classify reserved senders for the in-protocol fee skip. Set once + // per block by the consensus execution paths (serial + parallel processors, + // miner) from the parent state, so produce and verify agree; nil elsewhere + // (eth_call, prefetch) and on non-reserved chains. A nil snapshot classifies + // nothing, so the fee skip is inert until a registry is configured. + ReservedSnapshot *registryreader.Snapshot } // TxContext provides the EVM with information about a transaction. diff --git a/miner/worker.go b/miner/worker.go index a32804cf4c..fe76c4201b 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1282,6 +1282,11 @@ func (w *worker) makeEnv(header *types.Header, coinbase common.Address, witness state.StartPrefetcher("miner", nil, nil) } + // Build the block context once and attach the parent-state reserved snapshot + // so the producer classifies reserved senders the same way verifiers do. + blockCtx := core.NewEVMBlockContext(header, w.chain, &coinbase) + blockCtx.ReservedSnapshot = core.ReservedSnapshotForBlock(w.chain, state, header) + // Note the passed coinbase may be different with header.Coinbase. env := &environment{ signer: types.MakeSigner(w.chainConfig, header.Number, header.Time), @@ -1290,7 +1295,7 @@ func (w *worker) makeEnv(header *types.Header, coinbase common.Address, witness coinbase: coinbase, header: header, witness: state.Witness(), - evm: vm.NewEVM(core.NewEVMBlockContext(header, w.chain, &coinbase), state, w.chainConfig, w.vmConfig()), + evm: vm.NewEVM(blockCtx, state, w.chainConfig, w.vmConfig()), prefetchReader: genParams.prefetchReader, processReader: genParams.processReader, prefetchedTxHashes: genParams.prefetchedTxHashes, From f16aaccb8efa36d336f5359b853e1b92da02f800 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Mon, 29 Jun 2026 10:50:52 +0200 Subject: [PATCH 15/19] eip1559: document why reserved base-fee capacity stays on the config stub CalcBaseFee is pure (config, parent) with no parent state at most call sites, so the reserved-capacity input cannot read the registry here; it arrives via a producer-stamped header field (POS-3575/3576), mirroring the inert reserved-gas-used half. Records the decision so the retained config stub is deliberate, not an oversight. --- consensus/misc/eip1559/eip1559.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index 0bf3ca33af..b2fafca741 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -227,6 +227,16 @@ func gasTargetForLimit(config *params.ChainConfig, parent *types.Header, gasLimi // the standard curve applied to capacity that excludes the reserved quotas // (Σ active client quotas). Header.GasLimit is left untouched — this is a // formula-only input. +// +// The capacity source stays on the config stub rather than the registry +// snapshot on purpose: CalcBaseFee is a pure (config, parent) function called +// from RPC, txpool, graphql, and feehistory paths that have no parent StateDB +// (and some legitimately cannot — pruned, light, historical headers), so a +// registry state read here would break that contract and risk a parity split. +// The registry-derived capacity reaches base fee the same way reserved gas +// used does: a producer-stamped header field read from the parent (Manav's +// POS-3575/3576). Until that field lands, this half mirrors the inert +// publicGasUsed half above — deterministic, every node reads the same config. func reservedAwareGasTarget(config *params.ChainConfig, parent *types.Header) uint64 { reservedCapacity := config.Bor.ReservedCapacity() if reservedCapacity >= parent.GasLimit { From dde6dcbbac097d0562674fc6f826e96c9eb966c5 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Mon, 29 Jun 2026 12:06:42 +0200 Subject: [PATCH 16/19] core, registryreader: make registry-backed reserved classification consensus-safe (POS-3574) A registry-initialized two-miner devnet exposed two ways reserved-blockspace classification could differ between the block producer and the verifying nodes, each a consensus split: - BuildSnapshot read the registry through the EVM (ApplyMessage), which mutates the statedb it runs against. On the execution path the caller passes the live block state, so the read leaked gas/nonce/touch changes into the block and diverged the state root. Read against a throwaway state copy. - The serial and V2 block processors and the prefetcher only hold a *HeaderChain context, which did not expose the registry reader, so ReservedSnapshotForBlock fell through its type assertion to a nil snapshot on every import path. Producers (full chain) classified reserved senders and built fee-free blocks that verifiers then rejected as underpriced. Mirror the reader onto HeaderChain so produce and verify share one source. Also gate the execution-path snapshot build on the fork height so nodes syncing from genesis skip the per-block copy and registry read before the reserved fork activates. Adds a registryreader unit test (Snapshot accessors, the effectiveFrom activation boundary, BuildSnapshot error/nil propagation) and converts the end-to-end integration test to seed the registry via real initialize()/createClient() txs, asserting a reserved zero-fee tx is admitted, mined fee-free, and accepted by the peer (cross-node parity), while a non-reserved zero-fee tx is rejected at admission. --- consensus/bor/registryreader/reader.go | 10 + consensus/bor/registryreader/reader_test.go | 186 ++++++++++++++++ core/blockchain_reader.go | 7 +- core/evm.go | 6 + core/headerchain.go | 16 ++ tests/bor/reserved_blockspace_test.go | 231 ++++++++++++++++---- 6 files changed, 418 insertions(+), 38 deletions(-) create mode 100644 consensus/bor/registryreader/reader_test.go diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go index c7a4a93636..7c37e18e9f 100644 --- a/consensus/bor/registryreader/reader.go +++ b/consensus/bor/registryreader/reader.go @@ -65,6 +65,16 @@ func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common. if r == nil || !r.HasReservedRegistry() { return nil, nil } + // The registry reads run through the EVM (ApplyMessage), which mutates the + // statedb it executes against — gas accounting, sender nonce, touched/created + // accounts, revert-journal residue. On the block execution path the caller + // passes the live execution state, so reading against it would leak those + // mutations into the block and diverge the state root across producer and + // validator (a consensus split). Read against a throwaway copy so a snapshot + // build is always state-neutral. + if statedb != nil { + statedb = statedb.Copy() + } root, err := r.Root(statedb, number, hash) if err != nil { return nil, err diff --git a/consensus/bor/registryreader/reader_test.go b/consensus/bor/registryreader/reader_test.go new file mode 100644 index 0000000000..4446e1aad5 --- /dev/null +++ b/consensus/bor/registryreader/reader_test.go @@ -0,0 +1,186 @@ +package registryreader + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" +) + +// mockReader is a controllable Reader for exercising BuildSnapshot and the +// Snapshot accessors without an EVM-backed registry. +type mockReader struct { + has bool + root common.Hash + whitelist []common.Address + totalGas uint64 + clients map[common.Address]ClientLookup + rootErr error + wlErr error + totalErr error + clientErr error + clientCalls int +} + +func (m *mockReader) HasReservedRegistry() bool { return m.has } + +func (m *mockReader) IsReservedAddress(_ *state.StateDB, _ uint64, _ common.Hash, _ common.Address) (bool, error) { + return false, nil +} + +func (m *mockReader) ReservedClientForAddress(_ *state.StateDB, _ uint64, _ common.Hash, a common.Address) (ClientLookup, error) { + m.clientCalls++ + if m.clientErr != nil { + return ClientLookup{}, m.clientErr + } + return m.clients[a], nil +} + +func (m *mockReader) Root(_ *state.StateDB, _ uint64, _ common.Hash) (common.Hash, error) { + return m.root, m.rootErr +} + +func (m *mockReader) WhitelistedAddresses(_ *state.StateDB, _ uint64, _ common.Hash) ([]common.Address, error) { + return m.whitelist, m.wlErr +} + +func (m *mockReader) TotalReservedGas(_ *state.StateDB, _ uint64, _ common.Hash) (uint64, error) { + return m.totalGas, m.totalErr +} + +func addr(b byte) common.Address { return common.Address{19: b} } + +func TestBuildSnapshot(t *testing.T) { + errBoom := errors.New("boom") + a1, a2 := addr(1), addr(2) + base := func() *mockReader { + return &mockReader{ + has: true, + root: common.HexToHash("0xabc"), + whitelist: []common.Address{a1, a2}, + totalGas: 60_000_000, + clients: map[common.Address]ClientLookup{ + a1: {ClientID: big.NewInt(1), GasQuota: 30_000_000, Active: true}, + a2: {ClientID: big.NewInt(2), GasQuota: 30_000_000, Active: true}, + }, + } + } + + t.Run("nil reader yields nil snapshot", func(t *testing.T) { + snap, err := BuildSnapshot(nil, nil, 1, common.Hash{}) + if err != nil || snap != nil { + t.Fatalf("snap=%v err=%v, want nil,nil", snap, err) + } + }) + + t.Run("registry not configured yields nil snapshot", func(t *testing.T) { + snap, err := BuildSnapshot(&mockReader{has: false}, nil, 1, common.Hash{}) + if err != nil || snap != nil { + t.Fatalf("snap=%v err=%v, want nil,nil", snap, err) + } + }) + + t.Run("happy path populates root, capacity and clients", func(t *testing.T) { + r := base() + snap, err := BuildSnapshot(r, nil, 7, common.Hash{}) + if err != nil { + t.Fatal(err) + } + if snap.Root() != r.root { + t.Errorf("root=%s, want %s", snap.Root(), r.root) + } + if snap.Capacity() != 60_000_000 { + t.Errorf("capacity=%d, want 60000000", snap.Capacity()) + } + if r.clientCalls != len(r.whitelist) { + t.Errorf("client lookups=%d, want %d", r.clientCalls, len(r.whitelist)) + } + if !snap.IsReserved(a1, 7) || !snap.IsReserved(a2, 7) { + t.Error("both whitelisted addresses should be reserved") + } + }) + + for _, tc := range []struct { + name string + mutta func(*mockReader) + }{ + {"root error", func(m *mockReader) { m.rootErr = errBoom }}, + {"whitelist error", func(m *mockReader) { m.wlErr = errBoom }}, + {"total gas error", func(m *mockReader) { m.totalErr = errBoom }}, + {"per-client error", func(m *mockReader) { m.clientErr = errBoom }}, + } { + t.Run(tc.name+" propagates", func(t *testing.T) { + r := base() + tc.mutta(r) + snap, err := BuildSnapshot(r, nil, 7, common.Hash{}) + if !errors.Is(err, errBoom) { + t.Fatalf("err=%v, want boom", err) + } + if snap != nil { + t.Errorf("snap=%v, want nil on error", snap) + } + }) + } +} + +func TestSnapshotIsReserved(t *testing.T) { + a := addr(1) + snap := NewSnapshot(common.HexToHash("0x1"), 30_000_000, map[common.Address]ClientLookup{ + a: {ClientID: big.NewInt(1), GasQuota: 30_000_000, Active: true, EffectiveFrom: 100}, + addr(2): {ClientID: big.NewInt(2), GasQuota: 30_000_000, Active: false, EffectiveFrom: 0}, + }) + + tests := []struct { + name string + account common.Address + number uint64 + want bool + }{ + {"before effectiveFrom", a, 99, false}, + {"exactly at effectiveFrom", a, 100, true}, + {"after effectiveFrom", a, 101, true}, + {"inactive client", addr(2), 1, false}, + {"unknown account", addr(9), 100, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := snap.IsReserved(tt.account, tt.number); got != tt.want { + t.Errorf("IsReserved(%s, %d)=%v, want %v", tt.account, tt.number, got, tt.want) + } + }) + } +} + +func TestSnapshotNilSafe(t *testing.T) { + var snap *Snapshot + if snap.IsReserved(addr(1), 1) { + t.Error("nil snapshot must not classify anything as reserved") + } + if snap.FeeMode(addr(1)) != 0 { + t.Error("nil snapshot FeeMode must be 0") + } + if snap.Capacity() != 0 { + t.Error("nil snapshot Capacity must be 0") + } + if snap.Root() != (common.Hash{}) { + t.Error("nil snapshot Root must be zero hash") + } +} + +func TestSnapshotFeeModeAndCapacity(t *testing.T) { + a := addr(1) + snap := NewSnapshot(common.HexToHash("0x2"), 12_345, map[common.Address]ClientLookup{ + a: {ClientID: big.NewInt(1), GasQuota: 12_345, Active: true, FeeMode: 1}, + }) + if snap.FeeMode(a) != 1 { + t.Errorf("FeeMode=%d, want 1", snap.FeeMode(a)) + } + if snap.FeeMode(addr(9)) != 0 { + t.Errorf("FeeMode(unknown)=%d, want 0", snap.FeeMode(addr(9))) + } + if snap.Capacity() != 12_345 { + t.Errorf("Capacity=%d, want 12345", snap.Capacity()) + } +} diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index b5125ba1b3..d036275cda 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -563,7 +563,12 @@ func (bc *BlockChain) ReservedRegistry() registryreader.Reader { return bc.reser // SetReservedRegistry wires the reserved blockspace registry reader into the // chain. Called once at startup from the backend after the consensus engine is // ready. Passing nil is valid and disables reserved-tx awareness. -func (bc *BlockChain) SetReservedRegistry(r registryreader.Reader) { bc.reservedRegistry = r } +func (bc *BlockChain) SetReservedRegistry(r registryreader.Reader) { + bc.reservedRegistry = r + // Mirror onto the header chain so the serial/V2 processors and the prefetcher + // (which only hold a *HeaderChain context) classify against the same reader. + bc.hc.SetReservedRegistry(r) +} // Snapshots returns the blockchain snapshot tree. func (bc *BlockChain) Snapshots() *snapshot.Tree { diff --git a/core/evm.go b/core/evm.go index 9c7da25af5..4587bf77bc 100644 --- a/core/evm.go +++ b/core/evm.go @@ -60,6 +60,12 @@ func ReservedSnapshotForBlock(chain ChainContext, statedb *state.StateDB, header if reader == nil || !reader.HasReservedRegistry() { return nil } + // Classification is fork-gated, so there is nothing to classify before the + // reserved-blockspace fork. Skipping the build pre-fork avoids a per-block + // state copy and registry read on every node syncing from genesis. + if cfg := chain.Config(); cfg.Bor == nil || !cfg.Bor.IsReservedBlockspace(header.Number) { + return nil + } parentNumber := uint64(0) if header.Number.Uint64() > 0 { parentNumber = header.Number.Uint64() - 1 diff --git a/core/headerchain.go b/core/headerchain.go index 08362e4daa..ab00d05347 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" @@ -72,8 +73,23 @@ type HeaderChain struct { engine consensus.Engine stateSyncData []*types.StateSyncData // State sync data + + // reservedRegistry mirrors the BlockChain's reserved-blockspace registry + // reader so block-execution paths that only hold a *HeaderChain context + // (the serial and V2 processors, the prefetcher) can resolve the same + // reserved set as the miner. Wired by BlockChain.SetReservedRegistry. + reservedRegistry registryreader.Reader } +// ReservedRegistry returns the reserved-blockspace registry reader, or nil when +// none is wired. Lets ReservedSnapshotForBlock classify on hc-based execution +// paths, matching the miner. +func (hc *HeaderChain) ReservedRegistry() registryreader.Reader { return hc.reservedRegistry } + +// SetReservedRegistry wires the reserved-blockspace registry reader. Propagated +// from BlockChain.SetReservedRegistry so producer and verifier share one source. +func (hc *HeaderChain) SetReservedRegistry(r registryreader.Reader) { hc.reservedRegistry = r } + // NewHeaderChain creates a new HeaderChain structure. ProcInterrupt points // to the parent's interrupt semaphore. func NewHeaderChain(chainDb ethdb.Database, config *params.ChainConfig, engine consensus.Engine, procInterrupt func() bool) (*HeaderChain, error) { diff --git a/tests/bor/reserved_blockspace_test.go b/tests/bor/reserved_blockspace_test.go index f0798d3255..aad6801e83 100644 --- a/tests/bor/reserved_blockspace_test.go +++ b/tests/bor/reserved_blockspace_test.go @@ -7,23 +7,37 @@ import ( "context" "crypto/ecdsa" "math/big" + "strings" "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/params" ) +// reservedRegistrySetupABI is the minimal setter surface the test drives to seed +// the genesis-deployed registry: claim ownership, then register one client with a +// whitelisted address. Bor's own embedded ABI is read-only, so the writes live here. +const reservedRegistrySetupABI = `[ + {"inputs":[{"name":"initialOwner","type":"address"},{"name":"maxTotalGas","type":"uint64"},{"name":"maxClientGas","type":"uint64"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"admin","type":"address"},{"name":"gasQuota","type":"uint64"},{"name":"feeMode","type":"uint8"},{"name":"effectiveFrom","type":"uint64"},{"name":"metadata","type":"string"},{"name":"addresses","type":"address[]"}],"name":"createClient","outputs":[{"name":"clientId","type":"uint256"}],"stateMutability":"nonpayable","type":"function"} +]` + // TestReservedBlockspaceZeroFeeProduction is the end-to-end (in-process devnet) -// validation of the reserved-blockspace feature across all four tickets: a real -// two-node bor network produces and cross-verifies blocks; a zero-fee tx from a -// config-whitelisted reserved sender is admitted to the pool (POS-3570), mined -// past the ReservedBlockspace fork with the reserved header fields set by the -// real Prepare path (POS-3637), and executes fee-free so the sender pays only -// its call value with no gas debit (POS-3573). An identical zero-fee tx from a -// non-reserved sender is rejected at admission. +// validation of the reserved-blockspace feature against the real registry contract. +// A two-node bor network produces and cross-verifies blocks; the registry contract +// is deployed in genesis and seeded at runtime via initialize()+createClient() so a +// client's address becomes reserved through actual contract state — the same source +// (registry → Go reader → per-block snapshot) the txpool and EVM classify from in +// production. A zero-fee tx from that registered address is admitted to the pool +// (POS-3570/3574), mined past the ReservedBlockspace fork with the reserved header +// fields set by the real Prepare path (POS-3637), and executes fee-free so the +// sender pays only its call value with no gas debit (POS-3573). An identical +// zero-fee tx from an address absent from the registry is rejected at admission. // // Run with: go test -tags=integration -run TestReservedBlockspaceZeroFeeProduction ./tests/bor/ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { @@ -33,18 +47,37 @@ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { } reservedAddr := crypto.PubkeyToAddress(faucets[0].PublicKey) nonReservedAddr := crypto.PubkeyToAddress(faucets[1].PublicKey) + ownerKey := faucets[2] + ownerAddr := crypto.PubkeyToAddress(ownerKey.PublicKey) + + registryAddr := common.HexToAddress(params.DefaultReservedRegistryContract) + setupABI, err := abi.JSON(strings.NewReader(reservedRegistrySetupABI)) + if err != nil { + t.Fatal(err) + } genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 8) // Reserved fork at block 5 (Cancun is active from block 3, so the reserved // header fields encode in the post-Cancun BlockExtraData format). reservedFork := uint64(5) genesis.Config.Bor.ReservedBlockspaceBlock = new(big.Int).SetUint64(reservedFork) - genesis.Config.Bor.ReservedClients = []params.ReservedClient{ - {Addresses: []common.Address{reservedAddr}, QuotaGas: 30_000_000}, + // The registry runtime bytecode (solc 0.8.33) uses PUSH0, a Shanghai opcode. + // This genesis activates Cancun at 3 but omits Shanghai; activate it at 2 so + // the contract is callable before the reserved fork. + genesis.Config.ShanghaiBlock = big.NewInt(2) + // Deploy the registry contract in genesis (empty storage — ownership and + // clients are established at runtime, exactly as on a real network) and point + // the chain config at it so the engine wires the reader into txpool/EVM/miner. + genesis.Config.Bor.ReservedRegistryContract = params.DefaultReservedRegistryContract + genesis.Alloc[registryAddr] = types.Account{ + Balance: new(big.Int), + Code: common.FromHex(params.ReservedBlockspaceRegistryCode), } + startBalance := new(big.Int).SetUint64(1_000_000_000_000_000_000) // 1 ETH genesis.Alloc[reservedAddr] = types.Account{Balance: new(big.Int).Set(startBalance)} genesis.Alloc[nonReservedAddr] = types.Account{Balance: new(big.Int).Set(startBalance)} + genesis.Alloc[ownerAddr] = types.Account{Balance: new(big.Int).Set(startBalance)} stacks, nodes, _ := setupMiner(t, 2, genesis) defer func() { @@ -73,12 +106,94 @@ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { } } - // Build past the reserved fork before submitting. - waitForBlock(reservedFork + 2) + // waitMined blocks until tx is found in a canonical block and returns it. + waitMined := func(txHash common.Hash, what string) *types.Block { + t.Helper() + deadline := time.After(60 * time.Second) + for { + head := nodes[0].BlockChain().CurrentBlock().Number.Uint64() + for n := uint64(0); n <= head; n++ { + blk := nodes[0].BlockChain().GetBlockByNumber(n) + if blk == nil { + continue + } + for _, tx := range blk.Transactions() { + if tx.Hash() == txHash { + return blk + } + } + } + select { + case <-deadline: + t.Fatalf("timeout waiting for %s to be mined", what) + case <-time.After(200 * time.Millisecond): + } + } + } signer := types.LatestSigner(genesis.Config) - recipient := common.HexToAddress("0x00000000000000000000000000000000000000aa") + // Seed the registry: initialize() claims ownership, createClient() registers + // reservedAddr as the sole whitelisted address of a fee-free client. Both are + // ordinary fee-paying txs from the owner; they execute in nonce order. + sendOwnerTx := func(nonce uint64, data []byte) *types.Transaction { + t.Helper() + tx, err := types.SignNewTx(ownerKey, signer, &types.DynamicFeeTx{ + ChainID: genesis.Config.ChainID, + Nonce: nonce, + GasTipCap: big.NewInt(30_000_000_000), + GasFeeCap: big.NewInt(100_000_000_000), + Gas: 1_000_000, + To: ®istryAddr, + Value: big.NewInt(0), + Data: data, + }) + if err != nil { + t.Fatal(err) + } + // Submit to the producer only; p2p propagates to the peer. + if err := nodes[0].APIBackend.SendTx(context.Background(), tx); err != nil { + t.Fatalf("registry setup tx rejected: %v", err) + } + return tx + } + + initData, err := setupABI.Pack("initialize", ownerAddr, uint64(8_000_000), uint64(5_000_000)) + if err != nil { + t.Fatal(err) + } + createData, err := setupABI.Pack("createClient", + ownerAddr, uint64(5_000_000), uint8(0), uint64(0), "e2e", []common.Address{reservedAddr}) + if err != nil { + t.Fatal(err) + } + + // London activates at block 1 in this genesis; the dynamic-fee setup txs are + // rejected before then ("pool not yet in London"). + waitForBlock(2) + + oNonce, err := nodes[0].APIBackend.GetPoolNonce(context.Background(), ownerAddr) + if err != nil { + t.Fatal(err) + } + sendOwnerTx(oNonce, initData) + createTx := sendOwnerTx(oNonce+1, createData) + seedBlock := waitMined(createTx.Hash(), "createClient") + t.Logf("registry seeded: createClient mined in block %d", seedBlock.NumberU64()) + + // Land the reserved tx inside node0's primary-producer window. With sprint=8 + // and two validators, node0 is the in-turn producer for blocks 1-8 and 17-24 + // while node1 produces 9-16 (see bor_test.go). Submitting at the start of the + // 17-24 window lets node0 mine the tx canonically with room to spare, so the + // chain doesn't reorg it out at the 9/17 producer handover. The parent state by + // then long carries the client (createClient mined at block ~4), and the block + // is well past the reserved fork (5). + if seedBlock.NumberU64() >= 16 { + t.Fatalf("registry seeded too late (block %d) for the 17-24 producer window", seedBlock.NumberU64()) + } + waitForBlock(17) + + recipient := common.HexToAddress("0x00000000000000000000000000000000000000aa") zeroFeeTx := func(from *ecdsa.PrivateKey, nonce uint64) *types.Transaction { tx, err := types.SignNewTx(from, signer, &types.DynamicFeeTx{ ChainID: genesis.Config.ChainID, @@ -102,45 +217,93 @@ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { } rtx := zeroFeeTx(faucets[0], rNonce) for _, node := range nodes { - if err := node.APIBackend.SendTx(context.Background(), rtx); err != nil { + // Tolerate "already known" from the peer that received it via p2p — what + // matters is that admission classified the reserved sender, not rejected it. + if err := node.APIBackend.SendTx(context.Background(), rtx); err != nil && + !strings.Contains(err.Error(), "already known") { t.Fatalf("reserved zero-fee tx rejected by pool: %v", err) } } // Non-reserved sender: identical zero-fee tx must be rejected at admission. - oNonce, err := nodes[0].APIBackend.GetPoolNonce(context.Background(), nonReservedAddr) + nrNonce, err := nodes[0].APIBackend.GetPoolNonce(context.Background(), nonReservedAddr) if err != nil { t.Fatal(err) } - otx := zeroFeeTx(faucets[1], oNonce) + otx := zeroFeeTx(faucets[1], nrNonce) if err := nodes[0].APIBackend.SendTx(context.Background(), otx); err == nil { t.Fatal("non-reserved zero-fee tx should be rejected at admission, but was accepted") } - // Wait for the reserved tx to be mined, then locate its block. - var includedBlock *types.Block - deadline := time.After(60 * time.Second) - for includedBlock == nil { - select { - case <-deadline: - t.Fatal("timeout waiting for reserved tx inclusion") - case <-time.After(200 * time.Millisecond): - } - head := nodes[0].BlockChain().CurrentBlock().Number.Uint64() - for n := reservedFork; n <= head && includedBlock == nil; n++ { - blk := nodes[0].BlockChain().GetBlockByNumber(n) + firstSeen := waitMined(rtx.Hash(), "reserved zero-fee tx") + t.Logf("reserved zero-fee tx first seen in block %d", firstSeen.NumberU64()) + + // Let the tip settle: both miners may briefly fork at the tip (the peer can + // build a competing block before the reserved tx propagates). Wait for several + // confirmations so fork choice converges, then read the tx's settled canonical + // block. A real classification split would never converge — the validator would + // reject the producer's block as a bad block — so convergence here is itself the + // consensus-parity assertion (POS-3573/3637). + canonicalTxBlock := func(n *eth.Ethereum) *types.Block { + head := n.BlockChain().CurrentBlock().Number.Uint64() + for h := firstSeen.NumberU64(); h <= head; h++ { + blk := n.BlockChain().GetBlockByNumber(h) if blk == nil { continue } for _, tx := range blk.Transactions() { if tx.Hash() == rtx.Hash() { - includedBlock = blk + return blk + } + } + } + return nil + } + + var includedBlock, peerBlock *types.Block + settleDeadline := time.After(60 * time.Second) + for { + includedBlock = canonicalTxBlock(nodes[0]) + peerBlock = canonicalTxBlock(nodes[1]) + if includedBlock != nil && peerBlock != nil && + includedBlock.Hash() == peerBlock.Hash() && + nodes[1].BlockChain().CurrentBlock().Number.Uint64() >= includedBlock.NumberU64()+3 { + break + } + select { + case <-settleDeadline: + h0 := nodes[0].BlockChain().CurrentBlock() + h1 := nodes[1].BlockChain().CurrentBlock() + t.Logf("node0 head=%d %s | node1 head=%d %s", h0.Number.Uint64(), h0.Hash(), h1.Number.Uint64(), h1.Hash()) + if includedBlock != nil { + t.Logf("node0 tx-block=%d %s; node1 has it by hash: %v", + includedBlock.NumberU64(), includedBlock.Hash(), + nodes[1].BlockChain().GetBlockByHash(includedBlock.Hash()) != nil) + } + if peerBlock != nil { + t.Logf("node1 tx-block=%d %s; node0 has it by hash: %v", + peerBlock.NumberU64(), peerBlock.Hash(), + nodes[0].BlockChain().GetBlockByHash(peerBlock.Hash()) != nil) + } + // Compare canonical hashes at each height up to the lower head to find + // where (if anywhere) the chains forked. + lo := h0.Number.Uint64() + if h1.Number.Uint64() < lo { + lo = h1.Number.Uint64() + } + for n := uint64(1); n <= lo; n++ { + b0 := nodes[0].BlockChain().GetBlockByNumber(n) + b1 := nodes[1].BlockChain().GetBlockByNumber(n) + if b0 == nil || b1 == nil || b0.Hash() != b1.Hash() { + t.Logf("chains first differ at height %d: node0=%v node1=%v", n, b0.Hash(), b1.Hash()) break } } + t.Fatalf("nodes never converged on the reserved tx block") + case <-time.After(200 * time.Millisecond): } } - t.Logf("reserved zero-fee tx mined in block %d", includedBlock.NumberU64()) + t.Logf("reserved zero-fee tx settled in canonical block %d on both nodes", includedBlock.NumberU64()) // The producing block must carry the reserved header fields (set by Prepare). rc, rg := includedBlock.Header().GetReservedInfo(genesis.Config) @@ -148,7 +311,8 @@ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { t.Fatalf("block %d missing reserved header fields: count=%v gas=%v", includedBlock.NumberU64(), rc, rg) } - // The reserved sender paid only its call value — no gas was debited. + // The reserved sender paid only its call value — no gas was debited — and both + // nodes computed the same post-state for the block (identical hash above). state, err := nodes[0].BlockChain().StateAt(includedBlock.Root()) if err != nil { t.Fatal(err) @@ -158,11 +322,4 @@ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { if got.Cmp(want) != 0 { t.Fatalf("reserved sender balance = %s, want %s (call value only, no gas debit)", got, want) } - - // Both nodes agreed on the block (the validator verified the producer's - // header, including the reserved-field presence check). - other := nodes[1].BlockChain().GetBlockByNumber(includedBlock.NumberU64()) - if other == nil || other.Hash() != includedBlock.Hash() { - t.Fatalf("nodes disagree at block %d: %v vs %s", includedBlock.NumberU64(), other, includedBlock.Hash()) - } } From 447baafa7178ee2fb079e42bd8e46e0cde6b479e Mon Sep 17 00:00:00 2001 From: marcello33 Date: Mon, 29 Jun 2026 14:52:30 +0200 Subject: [PATCH 17/19] core, ethapi, params, bor: address reserved-blockspace review findings (POS-3574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-agent pre-PR review (/pos-code-review) surfaced fixes worth taking now; the rest are tracked as pre-activation work in the run report. - ethapi: bypass the EVM wall-clock timeout for bor-internal system-contract reads, not just the gas cap. The reserved-registry snapshot read runs through doCall, whose RPCEVMTimeout is node-local and load-dependent. A timeout on one node but not another returned a nil snapshot and flipped fee classification, diverging the state root. Both node-local limits are now bypassed together for internal consensus reads (validators, span, state receiver, registry). - params: reject scheduling ReservedBlockspaceBlock before CancunBlock (the reserved header fields only encode in the post-Cancun extra-data format, while verifyHeader requires them once the fork is active), or without a registry contract configured. Prevents a rollout height that bricks block production. - bor: add an interim header-only bound (ReservedGasUsed <= block GasUsed) in verifyReservedFields, rejecting an impossible value the base fee would otherwise silently clamp. Full value-correctness stays with POS-3576. - types: GetReservedInfo now routes through DecodeBlockExtraData instead of re-decoding the extra blob independently. - params: drop the dead config-stub classifiers (IsReservedSender/ ReservedQuotaOf — no production caller; classification is registry-sourced) and document that ReservedClients now feeds only the base-fee capacity carve-out until the producer-stamped header field lands. - registryreader: correct the Snapshot/Root/BuildSnapshot docs — the root()-keyed cross-block cache is a tracked optimization, not yet implemented on the execution path (txpool caches per head; execution rebuilds per block). Adds tests for the Cancun-order guard, the ReservedGasUsed bound, and the reserved base-fee capacity-equals-limit boundary. --- consensus/bor/bor.go | 23 +++++-- consensus/bor/bor_test.go | 25 ++++++++ consensus/bor/registryreader/reader.go | 22 ++++--- .../misc/eip1559/eip1559_reserved_test.go | 26 ++++++++ core/types/block.go | 13 +--- internal/ethapi/api.go | 14 +++-- params/config.go | 61 ++++++++++--------- params/reserved_test.go | 61 ++++++++++--------- 8 files changed, 158 insertions(+), 87 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 7abf8edde8..ad4fdd70c3 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -31,8 +31,8 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/bor/api" "github.com/ethereum/go-ethereum/consensus/bor/clerk" - "github.com/ethereum/go-ethereum/consensus/bor/registryreader" borSpan "github.com/ethereum/go-ethereum/consensus/bor/heimdall/span" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/consensus/bor/statefull" "github.com/ethereum/go-ethereum/consensus/bor/valset" "github.com/ethereum/go-ethereum/consensus/misc" @@ -111,6 +111,11 @@ var ( // block is missing the reserved tx count or reserved gas used in its extra data. errMissingReservedBlockspaceFields = errors.New("missing reserved tx count or reserved gas used in extra data") + // errReservedGasUsedExceedsBlock is returned if a header's reserved gas used + // is larger than the block's total gas used — an impossible value, since the + // reserved region is a subset of the block. + errReservedGasUsedExceedsBlock = errors.New("reserved gas used exceeds block gas used") + // errInvalidMixDigest is returned if a block's mix digest is non-zero. errInvalidMixDigest = errors.New("non-zero mix digest") @@ -1060,10 +1065,10 @@ func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraD } // verifyReservedFields checks that post-ReservedBlockspace headers carry the -// reserved-region fields. Presence only. Value correctness — that ReservedTxCount -// matches the actual reserved region and ReservedGasUsed stays within each -// client's quota — is NOT enforced yet; it lands with block validation in -// POS-3576. Until then a producer can write any present values here. +// reserved-region fields. Presence plus a header-only sanity bound on +// ReservedGasUsed. Full value correctness — that ReservedTxCount matches the +// actual reserved region and ReservedGasUsed stays within each client's quota — +// is enforced by block validation in POS-3576. func (c *Bor) verifyReservedFields(header *types.Header) error { if !c.config.IsReservedBlockspace(header.Number) { return nil @@ -1072,6 +1077,14 @@ func (c *Bor) verifyReservedFields(header *types.Header) error { if reservedTxCount == nil || reservedGasUsed == nil { return errMissingReservedBlockspaceFields } + // Interim header-only bound: the reserved region is a subset of the block, so + // its gas cannot exceed the block's gas used. This rejects an impossible value + // rather than letting CalcBaseFee silently clamp it (the base fee consumes + // parent.ReservedGasUsed). ReservedTxCount against the body and per-client + // quota are body-level checks enforced by block validation in POS-3576. + if *reservedGasUsed > header.GasUsed { + return errReservedGasUsedExceedsBlock + } return nil } diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index f559364e93..46258f5c26 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -5770,6 +5770,31 @@ func TestVerifyHeader_ReservedBlockspaceFieldsPresent(t *testing.T) { } } +func TestVerifyReservedFields_GasUsedBound(t *testing.T) { + t.Parallel() + s := newGiuglianoVerifySetup(t, true) + s.b.config.ReservedBlockspaceBlock = big.NewInt(0) + + gasTarget := uint64(15_000_000) + bfcd := uint64(64) + count := uint32(1) + reservedGasUsed := uint64(5_000) + extra := buildBlockExtraBytes(&types.BlockExtraData{ + GasTarget: &gasTarget, + BaseFeeChangeDenominator: &bfcd, + ReservedTxCount: &count, + ReservedGasUsed: &reservedGasUsed, + }) + + // Reserved gas used exceeds the block's gas used — impossible, must be rejected. + h := &types.Header{Number: big.NewInt(1), GasUsed: 4_000, Extra: extra} + require.ErrorIs(t, s.b.verifyReservedFields(h), errReservedGasUsedExceedsBlock) + + // Equal-or-below the block's gas used passes the bound. + h.GasUsed = 5_000 + require.NoError(t, s.b.verifyReservedFields(h)) +} + // TestApplyMessage_StateSyncTxContext validates if TxContext is correctly // set for state-sync transactions. func TestApplyMessage_StateSyncTxContext(t *testing.T) { diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go index 7c37e18e9f..328d8c2572 100644 --- a/consensus/bor/registryreader/reader.go +++ b/consensus/bor/registryreader/reader.go @@ -36,9 +36,11 @@ type Reader interface { HasReservedRegistry() bool IsReservedAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (bool, error) ReservedClientForAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (ClientLookup, error) - // Root returns the registry's configVersion-derived root. It changes - // whenever the reserved set or its limits change, so a snapshot keyed on it - // can be reused until it moves (spec §4.5). + // Root returns the registry's configVersion-derived root. It changes whenever + // the reserved set or its limits change, so a snapshot keyed on it can be + // reused until it moves (spec §4.5). The root()-keyed cross-block cache is a + // tracked optimization (POS-3574) not yet wired on the execution path; see + // BuildSnapshot. Root(state *state.StateDB, number uint64, hash common.Hash) (common.Hash, error) // WhitelistedAddresses returns every currently-active reserved address. WhitelistedAddresses(state *state.StateDB, number uint64, hash common.Hash) ([]common.Address, error) @@ -47,11 +49,13 @@ type Reader interface { } // Snapshot is an immutable, pure-lookup view of the reserved set as of one -// block's state. It is built once per head/parent (and reused while root() is -// unchanged) so the hot classification paths — txpool admission, the EVM +// block's state, so the hot classification paths — txpool admission, the EVM // fee-skip stand-in, base-fee capacity — never do a per-transaction state read -// (spec §4.5). A nil *Snapshot classifies nothing (no registry / non-bor chain), -// so all methods are nil-safe. +// (spec §4.5). The txpool rebuilds it once per head (on reset); the execution +// path rebuilds it once per block (gated on the fork height). Cross-block reuse +// keyed on Root() is a tracked optimization (POS-3574), not yet implemented. A +// nil *Snapshot classifies nothing (no registry / non-bor chain), so all methods +// are nil-safe. type Snapshot struct { root common.Hash capacity uint64 @@ -60,7 +64,9 @@ type Snapshot struct { // BuildSnapshot reads the full active reserved set from the registry at the // given block state and returns an immutable Snapshot. Returns nil (no error) -// when no registry is configured. Callers cache it keyed on Root(). +// when no registry is configured. Each call does one Root() read plus a +// whitelist scan and a per-address lookup; a Root()-keyed cache that skips the +// rebuild while the root is unchanged is a tracked optimization (POS-3574). func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common.Hash) (*Snapshot, error) { if r == nil || !r.HasReservedRegistry() { return nil, nil diff --git a/consensus/misc/eip1559/eip1559_reserved_test.go b/consensus/misc/eip1559/eip1559_reserved_test.go index 2b0099dbac..17938b46e3 100644 --- a/consensus/misc/eip1559/eip1559_reserved_test.go +++ b/consensus/misc/eip1559/eip1559_reserved_test.go @@ -178,6 +178,32 @@ func TestReservedBaseFee_CapacityExceedsLimitFallsBack(t *testing.T) { } } +// TestReservedBaseFee_CapacityEqualsLimitFallsBack pins the >= boundary in +// reservedAwareGasTarget: when reserved capacity equals the gas limit the public +// target would be zero, so the guard must fall back to the full target (not +// divide by a zero target). Distinguishes >= from > at the exact boundary. +func TestReservedBaseFee_CapacityEqualsLimitFallsBack(t *testing.T) { + t.Parallel() + + const gasLimit = 30_000_000 + baseFee := big.NewInt(params.InitialBaseFee) + cfg := reservedFeeConfig(big.NewInt(0), gasLimit) // capacity == limit + + parent := &types.Header{ + Number: big.NewInt(1), + GasLimit: gasLimit, + GasUsed: gasLimit / 2, // == full target → unchanged + BaseFee: baseFee, + } + + var got *big.Int + require.NotPanics(t, func() { got = CalcBaseFee(cfg, parent) }, + "CalcBaseFee must not panic when reserved capacity == gas limit") + if got.Cmp(baseFee) != 0 { + t.Errorf("fallback base fee = %s, want unchanged %s (full target)", got, baseFee) + } +} + // TestReservedBaseFee_VerifyAcceptsProducerValue checks that the reserved-aware // base fee a producer computes passes strict (pre-Lisovo) header verification, // since VerifyEIP1559Header recomputes via CalcBaseFee on that path. diff --git a/core/types/block.go b/core/types/block.go index c713d68451..da493eabc2 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -559,19 +559,10 @@ func (h *Header) GetBaseFeeParams(chainConfig *params.ChainConfig) (gasTarget *u // blocks, on decode error, or for blocks that predate the ReservedBlockspace // fork (the optional fields are absent). func (h *Header) GetReservedInfo(chainConfig *params.ChainConfig) (reservedTxCount *uint32, reservedGasUsed *uint64) { - if !chainConfig.IsCancun(h.Number) { - return nil, nil - } - - if len(h.Extra) < ExtraVanityLength+ExtraSealLength { + blockExtraData := h.DecodeBlockExtraData(chainConfig) + if blockExtraData == nil { return nil, nil } - - var blockExtraData BlockExtraData - if err := rlp.DecodeBytes(h.Extra[ExtraVanityLength:len(h.Extra)-ExtraSealLength], &blockExtraData); err != nil { - return nil, nil - } - return blockExtraData.ReservedTxCount, blockExtraData.ReservedGasUsed } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 71f0c178e6..16942eb58e 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -901,10 +901,18 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S return nil, err } + // System-contract reads issued by internal consensus code (validator set, + // span, state receiver, reserved-blockspace registry) must be deterministic + // across nodes. The RPC gas cap and the wall-clock EVM timeout are both + // node-local and load-dependent, so neither may apply here: a read that + // timed out on one node but not another would diverge state (e.g. a nil + // reserved snapshot flips fee classification). Both are bypassed together. + internalSystemCall := isBorInternalCall(ctx) && isBorSystemTx(b.ChainConfig().Bor, args.To) + // Setup context so it may be cancelled the call has completed // or, in case of unmetered gas, setup a context with a timeout. var cancel context.CancelFunc - if timeout > 0 { + if timeout > 0 && !internalSystemCall { ctx, cancel = context.WithTimeout(ctx, timeout) } else { ctx, cancel = context.WithCancel(ctx) @@ -913,9 +921,7 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S // this makes sure resources are cleaned up. defer cancel() - // Only bypass the RPC gas cap for system contract calls that originate from - // internal consensus code (marked via WithBorInternalCall ctx). - if isBorInternalCall(ctx) && isBorSystemTx(b.ChainConfig().Bor, args.To) { + if internalSystemCall { globalGasCap = 0 } gp := new(core.GasPool) diff --git a/params/config.go b/params/config.go index aac5eb97a8..81b99f7ea6 100644 --- a/params/config.go +++ b/params/config.go @@ -980,9 +980,13 @@ type BorConfig struct { ChicagoBlock *big.Int `json:"chicagoBlock"` // Chicago switch block (nil = no fork, 0 = already on chicago) ReservedBlockspaceBlock *big.Int `json:"reservedBlockspaceBlock"` // ReservedBlockspace switch block (nil = no fork, 0 = already on reservedBlockspace) - // ReservedClients is the config-backed stub source for reserved-blockspace - // classification (allowlist + per-client quota). The registry contract - // (POS-3572) will replace this source without changing the query methods below. + // ReservedClients feeds ONLY the EIP-1559 base-fee capacity carve-out + // (ReservedCapacity, consumed by CalcBaseFee, which is pure (config, parent) + // and has no parent state to read the registry from). Reserved-sender + // classification — admission, EVM fee-skip — is sourced from the registry + // contract, not this field. The two must be kept consistent until the + // registry-derived capacity arrives via a producer-stamped header field + // (POS-3575/3576), at which point this stub is retired entirely. ReservedClients []ReservedClient `json:"reservedClients,omitempty"` } @@ -1074,33 +1078,6 @@ func (c *BorConfig) IsReservedBlockspace(number *big.Int) bool { return isBlockForked(c.ReservedBlockspaceBlock, number) } -// IsReservedSender reports whether addr is a whitelisted sender of any -// reserved-blockspace client. Config-backed stub; the registry contract -// (POS-3572) replaces the source without changing this signature. -func (c *BorConfig) IsReservedSender(addr common.Address) bool { - for i := range c.ReservedClients { - for _, a := range c.ReservedClients[i].Addresses { - if a == addr { - return true - } - } - } - return false -} - -// ReservedQuotaOf returns the per-block reserved gas quota of the client that -// owns addr, or 0 if addr is not a reserved sender. -func (c *BorConfig) ReservedQuotaOf(addr common.Address) uint64 { - for i := range c.ReservedClients { - for _, a := range c.ReservedClients[i].Addresses { - if a == addr { - return c.ReservedClients[i].QuotaGas - } - } - } - return 0 -} - // ReservedCapacity returns the sum of all reserved clients' per-block gas // quotas — the capacity removed from the EIP-1559 normal region. func (c *BorConfig) ReservedCapacity() uint64 { @@ -1670,6 +1647,30 @@ func (c *ChainConfig) CheckConfigForkOrder() error { } } } + + if err := c.checkReservedBlockspaceForkOrder(); err != nil { + return err + } + return nil +} + +// checkReservedBlockspaceForkOrder enforces the rollout preconditions for the +// reserved-blockspace fork. The producer writes the reserved header fields only +// in the post-Cancun BlockExtraData format, while verifyHeader requires them on +// every post-fork block — so activating reserved blockspace at or before Cancun +// would make block production unverifiable at the boundary. The reserved set +// also has no source without the registry contract configured. +func (c *ChainConfig) checkReservedBlockspaceForkOrder() error { + if c.Bor == nil || c.Bor.ReservedBlockspaceBlock == nil { + return nil + } + if c.CancunBlock == nil || c.CancunBlock.Cmp(c.Bor.ReservedBlockspaceBlock) > 0 { + return fmt.Errorf("unsupported fork ordering: reservedBlockspaceBlock %v must be at or after cancunBlock %v", + c.Bor.ReservedBlockspaceBlock, c.CancunBlock) + } + if c.Bor.ReservedRegistryContract == "" { + return errors.New("invalid chain configuration: reservedBlockspaceBlock is scheduled but reservedRegistryContract is unset") + } return nil } diff --git a/params/reserved_test.go b/params/reserved_test.go index 793d813e68..6b44891c84 100644 --- a/params/reserved_test.go +++ b/params/reserved_test.go @@ -45,13 +45,15 @@ func TestIsReservedBlockspace(t *testing.T) { } } -func TestReservedClientClassifier(t *testing.T) { +// TestReservedCapacity covers the only live use of the ReservedClients config +// stub: the EIP-1559 base-fee capacity carve-out (Σ per-client quotas). +// Reserved-sender classification is sourced from the registry, not this config. +func TestReservedCapacity(t *testing.T) { t.Parallel() a := common.HexToAddress("0x00000000000000000000000000000000000000Aa") b := common.HexToAddress("0x00000000000000000000000000000000000000Bb") c := common.HexToAddress("0x00000000000000000000000000000000000000Cc") - other := common.HexToAddress("0x00000000000000000000000000000000000000Ff") cfg := &BorConfig{ ReservedClients: []ReservedClient{ @@ -59,38 +61,39 @@ func TestReservedClientClassifier(t *testing.T) { {Addresses: []common.Address{c}, QuotaGas: 10_000_000}, }, } - - for _, addr := range []common.Address{a, b, c} { - if !cfg.IsReservedSender(addr) { - t.Errorf("%s should be reserved", addr.Hex()) - } - } - if cfg.IsReservedSender(other) { - t.Error("non-whitelisted address must not be reserved") + if got := cfg.ReservedCapacity(); got != 30_000_000 { + t.Errorf("capacity: got %d want 30000000", got) } - // QuotaOf returns the owning client's quota; addresses of the same client share it. - if got := cfg.ReservedQuotaOf(a); got != 20_000_000 { - t.Errorf("quota of a: got %d want 20000000", got) - } - if got := cfg.ReservedQuotaOf(b); got != 20_000_000 { - t.Errorf("quota of b (same client as a): got %d want 20000000", got) - } - if got := cfg.ReservedQuotaOf(c); got != 10_000_000 { - t.Errorf("quota of c: got %d want 10000000", got) - } - if got := cfg.ReservedQuotaOf(other); got != 0 { - t.Errorf("quota of non-reserved: got %d want 0", got) + // Empty config carves out nothing. + if got := (&BorConfig{}).ReservedCapacity(); got != 0 { + t.Errorf("empty config capacity: got %d want 0", got) } +} - if got := cfg.ReservedCapacity(); got != 30_000_000 { - t.Errorf("capacity: got %d want 30000000", got) - } +func TestReservedBlockspaceForkOrder(t *testing.T) { + t.Parallel() - // Empty config classifies nothing and has zero capacity. - empty := &BorConfig{} - if empty.IsReservedSender(a) || empty.ReservedQuotaOf(a) != 0 || empty.ReservedCapacity() != 0 { - t.Error("empty reserved config should classify nothing") + const reg = "0x0000000000000000000000000000000000001002" + tests := []struct { + name string + cfg *ChainConfig + wantErr bool + }{ + {"nil bor", &ChainConfig{}, false}, + {"unscheduled", &ChainConfig{Bor: &BorConfig{}}, false}, + {"reserved before cancun", &ChainConfig{CancunBlock: big.NewInt(100), Bor: &BorConfig{ReservedBlockspaceBlock: big.NewInt(50), ReservedRegistryContract: reg}}, true}, + {"reserved without cancun", &ChainConfig{Bor: &BorConfig{ReservedBlockspaceBlock: big.NewInt(50), ReservedRegistryContract: reg}}, true}, + {"reserved at cancun", &ChainConfig{CancunBlock: big.NewInt(50), Bor: &BorConfig{ReservedBlockspaceBlock: big.NewInt(50), ReservedRegistryContract: reg}}, false}, + {"reserved after cancun", &ChainConfig{CancunBlock: big.NewInt(10), Bor: &BorConfig{ReservedBlockspaceBlock: big.NewInt(50), ReservedRegistryContract: reg}}, false}, + {"scheduled without registry", &ChainConfig{CancunBlock: big.NewInt(10), Bor: &BorConfig{ReservedBlockspaceBlock: big.NewInt(50)}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.cfg.checkReservedBlockspaceForkOrder(); (err != nil) != tt.wantErr { + t.Errorf("checkReservedBlockspaceForkOrder() error = %v, wantErr %v", err, tt.wantErr) + } + }) } } From e7b2cf688e775b14b7237fccb9bf26ec8acf10b0 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Fri, 3 Jul 2026 15:07:42 +0200 Subject: [PATCH 18/19] consensus, core, params, eip1559: tidy reserved-blockspace comments --- cmd/keeper/go.mod | 4 +- consensus/bor/bor.go | 24 ++++------ consensus/bor/contract/reserved_registry.go | 4 +- consensus/bor/registryreader/reader.go | 36 +++++---------- consensus/misc/eip1559/eip1559.go | 44 +++++++------------ core/state_transition.go | 17 +++---- core/txpool/legacypool/legacypool.go | 4 +- internal/ethapi/api.go | 10 ++--- params/config.go | 12 +++-- .../src/ReservedBlockspaceRegistry.sol | 7 ++- tests/bor/reserved_blockspace_test.go | 12 ++--- 11 files changed, 68 insertions(+), 106 deletions(-) diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index 9842ebee56..4f13be0ff6 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -48,9 +48,9 @@ require ( github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/crypto v0.50.0 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/time v0.12.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index ad4fdd70c3..2ba230d4af 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1051,12 +1051,9 @@ func (c *Bor) setGiuglianoExtraFields(header *types.Header, parent *types.Header // verifyHeader presence check, even when there are no reserved transactions. func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraData *types.BlockExtraData) { if c.config.IsReservedBlockspace(header.Number) { - // Placeholder zeros. The producer's reserved pass (POS-3575) will set the - // real ReservedTxCount / ReservedGasUsed during block building; this - // foundation only guarantees the fields are present (non-nil) so the - // header is structurally valid and passes the verifyReservedFields - // presence check. Until POS-3575 lands, every block reports zero reserved - // gas regardless of how many reserved txs it actually contains. + // Placeholder zeros so the fields are present (non-nil) and the header + // passes the verifyReservedFields presence check. The producer's reserved + // pass sets the real ReservedTxCount / ReservedGasUsed during block building. var zeroCount uint32 var zeroGas uint64 blockExtraData.ReservedTxCount = &zeroCount @@ -1065,10 +1062,9 @@ func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraD } // verifyReservedFields checks that post-ReservedBlockspace headers carry the -// reserved-region fields. Presence plus a header-only sanity bound on -// ReservedGasUsed. Full value correctness — that ReservedTxCount matches the -// actual reserved region and ReservedGasUsed stays within each client's quota — -// is enforced by block validation in POS-3576. +// reserved-region fields, plus a header-only bound on ReservedGasUsed. Full +// value correctness (ReservedTxCount against the body, per-client quota) is a +// body-level check enforced by block validation. func (c *Bor) verifyReservedFields(header *types.Header) error { if !c.config.IsReservedBlockspace(header.Number) { return nil @@ -1077,11 +1073,9 @@ func (c *Bor) verifyReservedFields(header *types.Header) error { if reservedTxCount == nil || reservedGasUsed == nil { return errMissingReservedBlockspaceFields } - // Interim header-only bound: the reserved region is a subset of the block, so - // its gas cannot exceed the block's gas used. This rejects an impossible value - // rather than letting CalcBaseFee silently clamp it (the base fee consumes - // parent.ReservedGasUsed). ReservedTxCount against the body and per-client - // quota are body-level checks enforced by block validation in POS-3576. + // The reserved region is a subset of the block, so its gas cannot exceed the + // block's gas used. Reject the impossible value rather than letting CalcBaseFee + // silently clamp it (the base fee consumes parent.ReservedGasUsed). if *reservedGasUsed > header.GasUsed { return errReservedGasUsedExceedsBlock } diff --git a/consensus/bor/contract/reserved_registry.go b/consensus/bor/contract/reserved_registry.go index d7640fa617..1d5b0014d6 100644 --- a/consensus/bor/contract/reserved_registry.go +++ b/consensus/bor/contract/reserved_registry.go @@ -120,8 +120,8 @@ func (gc *GenesisContractsClient) ReservedClientForAddress( }, nil } -// Root reads the registry's configVersion-derived root. The cache (spec §4.5) -// rebuilds its reserved-set snapshot only when this value changes. +// Root reads the registry's configVersion-derived root. Callers rebuild their +// reserved-set snapshot only when this value changes. func (gc *GenesisContractsClient) Root( state *state.StateDB, number uint64, diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go index 328d8c2572..e5e507364c 100644 --- a/consensus/bor/registryreader/reader.go +++ b/consensus/bor/registryreader/reader.go @@ -20,8 +20,7 @@ type ClientLookup struct { GasQuota uint64 Admin common.Address Active bool - // FeeMode: 0 = free (zero in-protocol fee), 1 = routed (fee credited to the - // producer). See the reserved-blockspace spec §7. + // FeeMode: 0 = free (zero in-protocol fee), 1 = routed (fee credited to the producer). FeeMode uint8 // EffectiveFrom: block from which the client's reserved status applies. // Callers gate on Active && EffectiveFrom <= number. @@ -36,11 +35,8 @@ type Reader interface { HasReservedRegistry() bool IsReservedAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (bool, error) ReservedClientForAddress(state *state.StateDB, number uint64, hash common.Hash, account common.Address) (ClientLookup, error) - // Root returns the registry's configVersion-derived root. It changes whenever - // the reserved set or its limits change, so a snapshot keyed on it can be - // reused until it moves (spec §4.5). The root()-keyed cross-block cache is a - // tracked optimization (POS-3574) not yet wired on the execution path; see - // BuildSnapshot. + // Root returns a value that changes whenever the reserved set or its limits + // change, so a snapshot keyed on it can be reused until it moves. Root(state *state.StateDB, number uint64, hash common.Hash) (common.Hash, error) // WhitelistedAddresses returns every currently-active reserved address. WhitelistedAddresses(state *state.StateDB, number uint64, hash common.Hash) ([]common.Address, error) @@ -49,13 +45,10 @@ type Reader interface { } // Snapshot is an immutable, pure-lookup view of the reserved set as of one -// block's state, so the hot classification paths — txpool admission, the EVM -// fee-skip stand-in, base-fee capacity — never do a per-transaction state read -// (spec §4.5). The txpool rebuilds it once per head (on reset); the execution -// path rebuilds it once per block (gated on the fork height). Cross-block reuse -// keyed on Root() is a tracked optimization (POS-3574), not yet implemented. A -// nil *Snapshot classifies nothing (no registry / non-bor chain), so all methods -// are nil-safe. +// block's state, so the hot classification paths never do a per-transaction +// state read. The txpool rebuilds it once per head; the execution path once per +// block (gated on the fork height). A nil *Snapshot classifies nothing (no +// registry / non-bor chain), so all methods are nil-safe. type Snapshot struct { root common.Hash capacity uint64 @@ -64,20 +57,15 @@ type Snapshot struct { // BuildSnapshot reads the full active reserved set from the registry at the // given block state and returns an immutable Snapshot. Returns nil (no error) -// when no registry is configured. Each call does one Root() read plus a -// whitelist scan and a per-address lookup; a Root()-keyed cache that skips the -// rebuild while the root is unchanged is a tracked optimization (POS-3574). +// when no registry is configured. func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common.Hash) (*Snapshot, error) { if r == nil || !r.HasReservedRegistry() { return nil, nil } - // The registry reads run through the EVM (ApplyMessage), which mutates the - // statedb it executes against — gas accounting, sender nonce, touched/created - // accounts, revert-journal residue. On the block execution path the caller - // passes the live execution state, so reading against it would leak those - // mutations into the block and diverge the state root across producer and - // validator (a consensus split). Read against a throwaway copy so a snapshot - // build is always state-neutral. + // Registry reads run through the EVM, which mutates the statedb it executes + // against. On the execution path the caller passes the live block state, so + // read against a throwaway copy to keep the build state-neutral — reading + // against the live state would leak into the block and split the state root. if statedb != nil { statedb = statedb.Copy() } diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index b2fafca741..51bce74260 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -118,21 +118,14 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { parentGasTarget := calcParentGasTarget(config, parent) parentGasUsed := parent.GasUsed - // Reserved blockspace prices the public fee market on the normal region - // only: hold the reserved capacity out of the target and net the parent's - // reserved gas out of the used figure. Reserved txs pay zero in-protocol - // fee, so counting them would let a reserved client move the public base - // fee with its own usage. Anchoring the target on capacity (not used gas) - // keeps the public market stable and isolated from reserved behaviour. - // - // Note the two halves are not yet symmetric: the capacity-side reduction - // (reservedAwareGasTarget) is live, but the used-side netting (publicGasUsed) - // reads parent.ReservedGasUsed, which the producer stubs to 0 until its - // reserved pass lands (POS-3575). Until then the netting is inert, so under - // real reserved load the public base fee runs slightly hot. This is - // deterministic — every node reads the same header field — so it is a fee - // accuracy gap, not a consensus split. It resolves once POS-3575 populates - // ReservedGasUsed; no change is needed here. + // Reserved blockspace prices the public fee market on the normal region only: + // hold reserved capacity out of the target and net the parent's reserved gas + // out of the used figure, so a reserved client paying zero fee can't move the + // public base fee with its own usage. Anchoring the target on capacity keeps + // the public market stable and isolated from reserved behaviour. The used-side + // netting stays inert until the producer populates parent.ReservedGasUsed; + // until then the public base fee runs slightly hot — deterministic (a fee + // accuracy gap, not a consensus split), so no change is needed here. if config.Bor != nil && config.Bor.IsReservedBlockspace(parent.Number) { parentGasTarget = reservedAwareGasTarget(config, parent) parentGasUsed = publicGasUsed(config, parent) @@ -223,20 +216,15 @@ func gasTargetForLimit(config *params.ChainConfig, parent *types.Header, gasLimi return gasLimit / config.ElasticityMultiplier() } -// reservedAwareGasTarget returns the EIP-1559 target for the public region: -// the standard curve applied to capacity that excludes the reserved quotas -// (Σ active client quotas). Header.GasLimit is left untouched — this is a -// formula-only input. +// reservedAwareGasTarget returns the EIP-1559 target for the public region: the +// standard curve applied to capacity that excludes the reserved quotas (Σ active +// client quotas). Header.GasLimit is left untouched — a formula-only input. // -// The capacity source stays on the config stub rather than the registry -// snapshot on purpose: CalcBaseFee is a pure (config, parent) function called -// from RPC, txpool, graphql, and feehistory paths that have no parent StateDB -// (and some legitimately cannot — pruned, light, historical headers), so a -// registry state read here would break that contract and risk a parity split. -// The registry-derived capacity reaches base fee the same way reserved gas -// used does: a producer-stamped header field read from the parent (Manav's -// POS-3575/3576). Until that field lands, this half mirrors the inert -// publicGasUsed half above — deterministic, every node reads the same config. +// The capacity source is the config rather than the registry on purpose: +// CalcBaseFee is a pure (config, parent) function called from paths that have no +// parent state to read the registry from (RPC, txpool, historical headers), so a +// state read here would break that contract. The registry-derived capacity +// reaches base fee via a producer-stamped header field instead. func reservedAwareGasTarget(config *params.ChainConfig, parent *types.Header) uint64 { reservedCapacity := config.Bor.ReservedCapacity() if reservedCapacity >= parent.GasLimit { diff --git a/core/state_transition.go b/core/state_transition.go index 2495cdca47..012628d929 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -293,16 +293,13 @@ type stateTransition struct { // newStateTransition initialises and returns a new state transition object. func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition { // Classify the message as reserved-blockspace here — the single point every - // execution path (serial, parallel, and the ApplyMessage* variants) funnels - // through — so the zero-fee decision is identical across executors. The fork - // height gate comes from chain config; the reserved set comes from the parent - // registry snapshot on the block context (set per block by the consensus - // paths, nil elsewhere), matching the txpool and base-fee sources. - // - // This sender-based classification is the foundation stand-in. The spec's - // end state is position-based (reserved region 0..ReservedTxCount), set by - // the producer two-pass and checked by block validation (POS-3575/3576); - // that work supersedes this. + // execution path (serial, parallel, ApplyMessage*) funnels through — so the + // zero-fee decision is identical across executors. The fork gate comes from + // chain config; the reserved set from the parent registry snapshot on the + // block context (set per block by the consensus paths, nil elsewhere), + // matching the txpool and base-fee sources. Classification is sender-based; + // position-based enforcement (reserved region 0..ReservedTxCount) is layered + // on by the producer two-pass and block validation. var reserved bool if cfg := evm.ChainConfig(); cfg.Bor != nil && cfg.Bor.IsReservedBlockspace(evm.Context.BlockNumber) && diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index f2d5d93fb8..e948da2d1c 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -329,7 +329,7 @@ type LegacyPool struct { // Reserved-blockspace registry: reader is wired post-Init from the backend; // the snapshot is rebuilt at each reset from the new head's state so the - // per-tx admission path never reads contract state (spec §4.5). + // per-tx admission path never reads contract state. reservedRegistry registryreader.Reader reservedSnapshot atomic.Pointer[registryreader.Snapshot] @@ -1804,7 +1804,7 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) { pool.pendingNonces = newNoncer(statedb) // Refresh the reserved-set snapshot from the new head's state so per-tx - // admission classifies without a contract read (spec §4.5). + // admission classifies without a contract read. pool.rebuildReservedSnapshot(statedb, newHead) // Inject any transactions discarded due to reorgs diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 16942eb58e..a32045c44c 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -901,12 +901,10 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S return nil, err } - // System-contract reads issued by internal consensus code (validator set, - // span, state receiver, reserved-blockspace registry) must be deterministic - // across nodes. The RPC gas cap and the wall-clock EVM timeout are both - // node-local and load-dependent, so neither may apply here: a read that - // timed out on one node but not another would diverge state (e.g. a nil - // reserved snapshot flips fee classification). Both are bypassed together. + // Internal consensus reads of system contracts must be deterministic across + // nodes. The RPC gas cap and the wall-clock EVM timeout are both node-local + // and load-dependent, so a read that timed out on one node but not another + // would diverge state — bypass both for these calls. internalSystemCall := isBorInternalCall(ctx) && isBorSystemTx(b.ChainConfig().Bor, args.To) // Setup context so it may be cancelled the call has completed diff --git a/params/config.go b/params/config.go index 81b99f7ea6..70c7b06a3e 100644 --- a/params/config.go +++ b/params/config.go @@ -980,13 +980,11 @@ type BorConfig struct { ChicagoBlock *big.Int `json:"chicagoBlock"` // Chicago switch block (nil = no fork, 0 = already on chicago) ReservedBlockspaceBlock *big.Int `json:"reservedBlockspaceBlock"` // ReservedBlockspace switch block (nil = no fork, 0 = already on reservedBlockspace) - // ReservedClients feeds ONLY the EIP-1559 base-fee capacity carve-out - // (ReservedCapacity, consumed by CalcBaseFee, which is pure (config, parent) - // and has no parent state to read the registry from). Reserved-sender - // classification — admission, EVM fee-skip — is sourced from the registry - // contract, not this field. The two must be kept consistent until the - // registry-derived capacity arrives via a producer-stamped header field - // (POS-3575/3576), at which point this stub is retired entirely. + // ReservedClients feeds only the EIP-1559 base-fee capacity carve-out + // (ReservedCapacity), since CalcBaseFee is pure (config, parent) and has no + // parent state to read the registry from. Reserved-sender classification is + // sourced from the registry contract, not this field; the two are kept + // consistent until the capacity arrives via a producer-stamped header field. ReservedClients []ReservedClient `json:"reservedClients,omitempty"` } diff --git a/registry-contract/src/ReservedBlockspaceRegistry.sol b/registry-contract/src/ReservedBlockspaceRegistry.sol index f94735a228..6aae75b57e 100644 --- a/registry-contract/src/ReservedBlockspaceRegistry.sol +++ b/registry-contract/src/ReservedBlockspaceRegistry.sol @@ -23,7 +23,7 @@ contract ReservedBlockspaceRegistry { uint64 gasQuota; bool active; // feeMode: 0 = free (zero in-protocol fee), 1 = routed (fee paid but - // credited to the producer). See the reserved-blockspace spec §7. + // credited to the producer). uint8 feeMode; // effectiveFrom: block number from which this client's reserved status // applies. Lets governance schedule/announce a change at a future height @@ -44,8 +44,7 @@ contract ReservedBlockspaceRegistry { // configVersion increments on every change to the reserved set or its // limits. Bor reads root() once per block and only rebuilds its cached - // snapshot when the value changes (reserved-blockspace spec §4.5), avoiding - // a per-transaction state read. + // snapshot when the value changes, avoiding a per-transaction state read. uint256 public configVersion; mapping(uint256 clientId => Client client) private clients; @@ -280,7 +279,7 @@ contract ReservedBlockspaceRegistry { // root returns a value that changes whenever the reserved set, quotas, fee // modes, effective heights, or limits change. Bor caches its snapshot keyed - // on this and only rebuilds when it moves (reserved-blockspace spec §4.5). + // on this and only rebuilds when it moves. function root() external view returns (bytes32) { return bytes32(configVersion); } diff --git a/tests/bor/reserved_blockspace_test.go b/tests/bor/reserved_blockspace_test.go index aad6801e83..7554fd17fa 100644 --- a/tests/bor/reserved_blockspace_test.go +++ b/tests/bor/reserved_blockspace_test.go @@ -33,11 +33,11 @@ const reservedRegistrySetupABI = `[ // is deployed in genesis and seeded at runtime via initialize()+createClient() so a // client's address becomes reserved through actual contract state — the same source // (registry → Go reader → per-block snapshot) the txpool and EVM classify from in -// production. A zero-fee tx from that registered address is admitted to the pool -// (POS-3570/3574), mined past the ReservedBlockspace fork with the reserved header -// fields set by the real Prepare path (POS-3637), and executes fee-free so the -// sender pays only its call value with no gas debit (POS-3573). An identical -// zero-fee tx from an address absent from the registry is rejected at admission. +// production. A zero-fee tx from that registered address is admitted to the pool, +// mined past the ReservedBlockspace fork with the reserved header fields set by +// the real Prepare path, and executes fee-free so the sender pays only its call +// value with no gas debit. An identical zero-fee tx from an address absent from +// the registry is rejected at admission. // // Run with: go test -tags=integration -run TestReservedBlockspaceZeroFeeProduction ./tests/bor/ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { @@ -243,7 +243,7 @@ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { // confirmations so fork choice converges, then read the tx's settled canonical // block. A real classification split would never converge — the validator would // reject the producer's block as a bad block — so convergence here is itself the - // consensus-parity assertion (POS-3573/3637). + // consensus-parity assertion. canonicalTxBlock := func(n *eth.Ethereum) *types.Block { head := n.BlockChain().CurrentBlock().Number.Uint64() for h := firstSeen.NumberU64(); h <= head; h++ { From 426a9381a181e7fc6eb27f85d5d862e675ae6a79 Mon Sep 17 00:00:00 2001 From: Manav Darji Date: Thu, 16 Jul 2026 17:00:09 +0530 Subject: [PATCH 19/19] resolve conflicts and some improvements --- consensus/bor/bor.go | 26 ++- consensus/bor/bor_test.go | 13 +- .../bor/contract/registrytest/harness_test.go | 8 +- consensus/bor/registryreader/reader.go | 112 ++++++++++--- consensus/bor/registryreader/reader_test.go | 101 ++++++++--- consensus/misc/eip1559/eip1559.go | 2 +- .../misc/eip1559/eip1559_reserved_test.go | 11 +- core/evm.go | 2 +- core/reserved_fee_test.go | 4 +- core/state_transition.go | 6 +- core/txpool/legacypool/legacypool.go | 10 +- core/txpool/legacypool/list.go | 14 +- core/txpool/legacypool/list_test.go | 20 +-- core/txpool/legacypool/queue.go | 2 +- core/txpool/legacypool/reserved_test.go | 49 +++++- core/txpool/subpool.go | 2 - core/txpool/validation.go | 2 +- core/types/block_test.go | 123 -------------- eth/backend.go | 7 +- miner/miner.go | 37 ----- miner/ordering.go | 19 +-- miner/reserved_registry.go | 121 -------------- miner/reserved_registry_test.go | 43 ----- miner/reserved_test.go | 157 +++++++----------- miner/sequencing.go | 44 ++--- miner/worker.go | 71 ++++---- params/config.go | 27 ++- tests/bor/reserved_blockspace_test.go | 7 +- 28 files changed, 405 insertions(+), 635 deletions(-) delete mode 100644 miner/reserved_registry.go delete mode 100644 miner/reserved_registry_test.go diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index e632bfe39d..b6d3ddc81f 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1046,33 +1046,29 @@ func (c *Bor) setGiuglianoExtraFields(header *types.Header, parent *types.Header } } -// setReservedBlockspaceExtraFields initializes the reserved-region header fields -// for post-ReservedBlockspace blocks. The producer fills the real values during -// block building (the reserved pass); here we ensure the fields are present -// (non-nil) so every post-fork block is structurally valid and passes the -// verifyHeader presence check, even when there are no reserved transactions. +// setReservedBlockspaceExtraFields initializes the reserved-region header field +// for post-ReservedBlockspace blocks: a placeholder zero so the field is +// present (non-nil) and the header passes the verifyReservedFields presence +// check even when there are no reserved transactions. The miner overwrites it +// with the block's real reserved gas total at the end of block building +// (worker.writeReservedGasUsed). func (c *Bor) setReservedBlockspaceExtraFields(header *types.Header, blockExtraData *types.BlockExtraData) { if c.config.IsReservedBlockspace(header.Number) { - // Placeholder zeros so the fields are present (non-nil) and the header - // passes the verifyReservedFields presence check. The producer's reserved - // pass sets the real ReservedTxCount / ReservedGasUsed during block building. - var zeroCount uint32 var zeroGas uint64 - blockExtraData.ReservedTxCount = &zeroCount blockExtraData.ReservedGasUsed = &zeroGas } } // verifyReservedFields checks that post-ReservedBlockspace headers carry the -// reserved-region fields, plus a header-only bound on ReservedGasUsed. Full -// value correctness (ReservedTxCount against the body, per-client quota) is a -// body-level check enforced by block validation. +// reserved-region gas field, plus a header-only bound on its value. Value +// correctness (the sum over the block's reserved transactions, per-client +// quota) is a body-level check that lands with the block-validation slice. func (c *Bor) verifyReservedFields(header *types.Header) error { if !c.config.IsReservedBlockspace(header.Number) { return nil } - reservedTxCount, reservedGasUsed := header.GetReservedInfo(c.chainConfig) - if reservedTxCount == nil || reservedGasUsed == nil { + reservedGasUsed := header.GetReservedGasUsed(c.chainConfig) + if reservedGasUsed == nil { return errMissingReservedBlockspaceFields } // The reserved region is a subset of the block, so its gas cannot exceed the diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 68b0fa19c1..f686f3abb0 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -5914,20 +5914,17 @@ func TestVerifyHeader_PreGiugliano_NoCheck(t *testing.T) { func TestSetReservedBlockspaceExtraFields(t *testing.T) { t.Parallel() - // Pre-fork: the reserved fields are left nil. + // Pre-fork: the reserved field is left nil. bPre := &Bor{config: ¶ms.BorConfig{ReservedBlockspaceBlock: big.NewInt(100)}} bedPre := &types.BlockExtraData{} bPre.setReservedBlockspaceExtraFields(&types.Header{Number: big.NewInt(99)}, bedPre) - require.Nil(t, bedPre.ReservedTxCount) require.Nil(t, bedPre.ReservedGasUsed) - // Post-fork: the fields are initialized to non-nil zero so the block is valid - // even before the reserved pass fills the real values. + // Post-fork: the field is initialized to non-nil zero so the block is valid + // even before the miner writes the real value. bPost := &Bor{config: ¶ms.BorConfig{ReservedBlockspaceBlock: big.NewInt(100)}} bedPost := &types.BlockExtraData{} bPost.setReservedBlockspaceExtraFields(&types.Header{Number: big.NewInt(100)}, bedPost) - require.NotNil(t, bedPost.ReservedTxCount) - require.Equal(t, uint32(0), *bedPost.ReservedTxCount) require.NotNil(t, bedPost.ReservedGasUsed) require.Equal(t, uint64(0), *bedPost.ReservedGasUsed) } @@ -5958,12 +5955,10 @@ func TestVerifyHeader_ReservedBlockspaceFieldsPresent(t *testing.T) { gasTarget := uint64(15_000_000) bfcd := uint64(64) - count := uint32(0) gasUsed := uint64(0) extra := buildBlockExtraBytes(&types.BlockExtraData{ GasTarget: &gasTarget, BaseFeeChangeDenominator: &bfcd, - ReservedTxCount: &count, ReservedGasUsed: &gasUsed, }) h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee)) @@ -5983,12 +5978,10 @@ func TestVerifyReservedFields_GasUsedBound(t *testing.T) { gasTarget := uint64(15_000_000) bfcd := uint64(64) - count := uint32(1) reservedGasUsed := uint64(5_000) extra := buildBlockExtraBytes(&types.BlockExtraData{ GasTarget: &gasTarget, BaseFeeChangeDenominator: &bfcd, - ReservedTxCount: &count, ReservedGasUsed: &reservedGasUsed, }) diff --git a/consensus/bor/contract/registrytest/harness_test.go b/consensus/bor/contract/registrytest/harness_test.go index c51d62b838..076c4b0da1 100644 --- a/consensus/bor/contract/registrytest/harness_test.go +++ b/consensus/bor/contract/registrytest/harness_test.go @@ -32,13 +32,13 @@ func TestHarness_DeploysAndAnswersQueries(t *testing.T) { func TestSnapshot_BuildsFromRegistry(t *testing.T) { h := NewHarness(t) - snap, err := registryreader.BuildSnapshot(h.Reader, nil, 1, common.Hash{}) + snap, err := registryreader.BuildSnapshot(h.Reader, nil, 1, common.Hash{}, 1) require.NoError(t, err) require.NotNil(t, snap) // The whitelisted address classifies reserved; the other does not. - require.True(t, snap.IsReserved(h.ReservedAddr, 1)) - require.False(t, snap.IsReserved(h.UnreservedAddr, 1)) + require.True(t, snap.IsReserved(h.ReservedAddr)) + require.False(t, snap.IsReserved(h.UnreservedAddr)) // Snapshot mirrors the registry: feeMode free (0), capacity = the one // client's quota, and a non-zero root it can be cached against. @@ -48,6 +48,6 @@ func TestSnapshot_BuildsFromRegistry(t *testing.T) { // nil snapshot (no registry) classifies nothing and is safe. var none *registryreader.Snapshot - require.False(t, none.IsReserved(h.ReservedAddr, 1)) + require.False(t, none.IsReserved(h.ReservedAddr)) require.Equal(t, uint64(0), none.Capacity()) } diff --git a/consensus/bor/registryreader/reader.go b/consensus/bor/registryreader/reader.go index e5e507364c..ee60303222 100644 --- a/consensus/bor/registryreader/reader.go +++ b/consensus/bor/registryreader/reader.go @@ -6,7 +6,9 @@ package registryreader import ( + "fmt" "math/big" + "slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" @@ -44,21 +46,40 @@ type Reader interface { TotalReservedGas(state *state.StateDB, number uint64, hash common.Hash) (uint64, error) } -// Snapshot is an immutable, pure-lookup view of the reserved set as of one -// block's state, so the hot classification paths never do a per-transaction -// state read. The txpool rebuilds it once per head; the execution path once per -// block (gated on the fork height). A nil *Snapshot classifies nothing (no -// registry / non-bor chain), so all methods are nil-safe. +// Client is the slim per-sender record a Snapshot stores: just what the hot +// classification and sequencing paths need. Activation state (active, +// effectiveFrom) is resolved at snapshot build time, so it never appears here. +type Client struct { + // ID is the registry contract's incremental clientId. + ID uint64 + // GasQuota is the client's per-block reserved gas allowance, charged + // against declared transaction gas limits. + GasQuota uint64 + // FeeMode: 0 = free (zero in-protocol fee), 1 = routed (fee credited to + // the producer; reserved for a future mode, unused today). + FeeMode uint8 +} + +// Snapshot is an immutable, pure-lookup view of the reserved set effective for +// one block, so the hot classification paths never do a per-transaction state +// read. Activation (active flag, effectiveFrom delay) is resolved once at +// build time: every stored entry is effective, which is why lookups take no +// block number. The txpool rebuilds it once per head; the execution path once +// per block (gated on the fork height). A nil *Snapshot classifies nothing +// (no registry / non-bor chain), so all methods are nil-safe. type Snapshot struct { - root common.Hash - capacity uint64 - clients map[common.Address]ClientLookup + root common.Hash + capacity uint64 + byAddress map[common.Address]Client + clientIDs []uint64 + quotas map[uint64]uint64 } // BuildSnapshot reads the full active reserved set from the registry at the -// given block state and returns an immutable Snapshot. Returns nil (no error) -// when no registry is configured. -func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common.Hash) (*Snapshot, error) { +// given block state and returns an immutable Snapshot classifying senders for +// block effectiveAt (clients with effectiveFrom > effectiveAt are excluded). +// Returns nil (no error) when no registry is configured. +func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common.Hash, effectiveAt uint64) (*Snapshot, error) { if r == nil || !r.HasReservedRegistry() { return nil, nil } @@ -81,21 +102,37 @@ func BuildSnapshot(r Reader, statedb *state.StateDB, number uint64, hash common. if err != nil { return nil, err } - clients := make(map[common.Address]ClientLookup, len(addrs)) + clients := make(map[common.Address]Client, len(addrs)) for _, a := range addrs { c, err := r.ReservedClientForAddress(statedb, number, hash, a) if err != nil { return nil, err } - clients[a] = c + if !c.Active || c.EffectiveFrom > effectiveAt { + continue + } + if c.ClientID == nil || !c.ClientID.IsUint64() { + return nil, fmt.Errorf("reserved registry returned invalid client id %v for %s", c.ClientID, a) + } + clients[a] = Client{ID: c.ClientID.Uint64(), GasQuota: c.GasQuota, FeeMode: c.FeeMode} } - return &Snapshot{root: root, capacity: capacity, clients: clients}, nil + return NewSnapshot(root, capacity, clients), nil } -// NewSnapshot constructs a Snapshot from an explicit client set. Used by tests -// and by callers that source the reserved set outside the registry contract. -func NewSnapshot(root common.Hash, capacity uint64, clients map[common.Address]ClientLookup) *Snapshot { - return &Snapshot{root: root, capacity: capacity, clients: clients} +// NewSnapshot constructs a Snapshot from an explicit, already-effective client +// set. Used by tests and by callers that source the reserved set outside the +// registry contract. +func NewSnapshot(root common.Hash, capacity uint64, clients map[common.Address]Client) *Snapshot { + quotas := make(map[uint64]uint64, len(clients)) + for _, c := range clients { + quotas[c.ID] = c.GasQuota + } + ids := make([]uint64, 0, len(quotas)) + for id := range quotas { + ids = append(ids, id) + } + slices.Sort(ids) + return &Snapshot{root: root, capacity: capacity, byAddress: clients, clientIDs: ids, quotas: quotas} } // Root is the registry root this snapshot was built at; callers reuse the @@ -107,14 +144,41 @@ func (s *Snapshot) Root() common.Hash { return s.root } -// IsReserved reports whether account is an active reserved sender effective at -// the given block number (active && effectiveFrom <= number). -func (s *Snapshot) IsReserved(account common.Address, number uint64) bool { +// IsReserved reports whether account is a reserved sender in this snapshot's +// effective set. +func (s *Snapshot) IsReserved(account common.Address) bool { if s == nil { return false } - c, ok := s.clients[account] - return ok && c.Active && c.EffectiveFrom <= number + _, ok := s.byAddress[account] + return ok +} + +// Lookup returns the client ID that owns account, or (_, false) if account is +// not in the effective reserved set. +func (s *Snapshot) Lookup(account common.Address) (uint64, bool) { + if s == nil { + return 0, false + } + c, ok := s.byAddress[account] + return c.ID, ok +} + +// Quota returns the per-block reserved gas allowance of clientID, or 0 for an +// unknown client. +func (s *Snapshot) Quota(clientID uint64) uint64 { + if s == nil { + return 0 + } + return s.quotas[clientID] +} + +// Clients returns the effective client IDs, sorted ascending. +func (s *Snapshot) Clients() []uint64 { + if s == nil { + return nil + } + return slices.Clone(s.clientIDs) } // FeeMode returns the fee mode of account's client (0 = free) or 0 if not reserved. @@ -122,7 +186,7 @@ func (s *Snapshot) FeeMode(account common.Address) uint8 { if s == nil { return 0 } - return s.clients[account].FeeMode + return s.byAddress[account].FeeMode } // Capacity returns the reserved capacity (sum of active client quotas). diff --git a/consensus/bor/registryreader/reader_test.go b/consensus/bor/registryreader/reader_test.go index 4446e1aad5..ae44f5d3d0 100644 --- a/consensus/bor/registryreader/reader_test.go +++ b/consensus/bor/registryreader/reader_test.go @@ -69,14 +69,14 @@ func TestBuildSnapshot(t *testing.T) { } t.Run("nil reader yields nil snapshot", func(t *testing.T) { - snap, err := BuildSnapshot(nil, nil, 1, common.Hash{}) + snap, err := BuildSnapshot(nil, nil, 1, common.Hash{}, 2) if err != nil || snap != nil { t.Fatalf("snap=%v err=%v, want nil,nil", snap, err) } }) t.Run("registry not configured yields nil snapshot", func(t *testing.T) { - snap, err := BuildSnapshot(&mockReader{has: false}, nil, 1, common.Hash{}) + snap, err := BuildSnapshot(&mockReader{has: false}, nil, 1, common.Hash{}, 2) if err != nil || snap != nil { t.Fatalf("snap=%v err=%v, want nil,nil", snap, err) } @@ -84,7 +84,7 @@ func TestBuildSnapshot(t *testing.T) { t.Run("happy path populates root, capacity and clients", func(t *testing.T) { r := base() - snap, err := BuildSnapshot(r, nil, 7, common.Hash{}) + snap, err := BuildSnapshot(r, nil, 7, common.Hash{}, 8) if err != nil { t.Fatal(err) } @@ -97,9 +97,18 @@ func TestBuildSnapshot(t *testing.T) { if r.clientCalls != len(r.whitelist) { t.Errorf("client lookups=%d, want %d", r.clientCalls, len(r.whitelist)) } - if !snap.IsReserved(a1, 7) || !snap.IsReserved(a2, 7) { + if !snap.IsReserved(a1) || !snap.IsReserved(a2) { t.Error("both whitelisted addresses should be reserved") } + if id, ok := snap.Lookup(a1); !ok || id != 1 { + t.Errorf("Lookup(a1)=(%d,%v), want (1,true)", id, ok) + } + if got := snap.Quota(2); got != 30_000_000 { + t.Errorf("Quota(2)=%d, want 30000000", got) + } + if ids := snap.Clients(); len(ids) != 2 || ids[0] != 1 || ids[1] != 2 { + t.Errorf("Clients()=%v, want [1 2] sorted", ids) + } }) for _, tc := range []struct { @@ -114,7 +123,7 @@ func TestBuildSnapshot(t *testing.T) { t.Run(tc.name+" propagates", func(t *testing.T) { r := base() tc.mutta(r) - snap, err := BuildSnapshot(r, nil, 7, common.Hash{}) + snap, err := BuildSnapshot(r, nil, 7, common.Hash{}, 8) if !errors.Is(err, errBoom) { t.Fatalf("err=%v, want boom", err) } @@ -125,39 +134,81 @@ func TestBuildSnapshot(t *testing.T) { } } -func TestSnapshotIsReserved(t *testing.T) { - a := addr(1) - snap := NewSnapshot(common.HexToHash("0x1"), 30_000_000, map[common.Address]ClientLookup{ - a: {ClientID: big.NewInt(1), GasQuota: 30_000_000, Active: true, EffectiveFrom: 100}, - addr(2): {ClientID: big.NewInt(2), GasQuota: 30_000_000, Active: false, EffectiveFrom: 0}, - }) +// TestBuildSnapshotEffectiveFiltering pins the build-time activation +// resolution: inactive clients and clients whose effectiveFrom is beyond the +// snapshot's effectiveAt block never enter the stored set, so lookups need no +// block number. +func TestBuildSnapshotEffectiveFiltering(t *testing.T) { + a1, a2 := addr(1), addr(2) + reader := func() *mockReader { + return &mockReader{ + has: true, + root: common.HexToHash("0xabc"), + whitelist: []common.Address{a1, a2}, + totalGas: 60_000_000, + clients: map[common.Address]ClientLookup{ + a1: {ClientID: big.NewInt(1), GasQuota: 30_000_000, Active: true, EffectiveFrom: 100}, + a2: {ClientID: big.NewInt(2), GasQuota: 30_000_000, Active: false}, + }, + } + } tests := []struct { - name string - account common.Address - number uint64 - want bool + name string + effectiveAt uint64 + wantA1 bool }{ - {"before effectiveFrom", a, 99, false}, - {"exactly at effectiveFrom", a, 100, true}, - {"after effectiveFrom", a, 101, true}, - {"inactive client", addr(2), 1, false}, - {"unknown account", addr(9), 100, false}, + {"before effectiveFrom", 99, false}, + {"exactly at effectiveFrom", 100, true}, + {"after effectiveFrom", 101, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := snap.IsReserved(tt.account, tt.number); got != tt.want { - t.Errorf("IsReserved(%s, %d)=%v, want %v", tt.account, tt.number, got, tt.want) + snap, err := BuildSnapshot(reader(), nil, tt.effectiveAt-1, common.Hash{}, tt.effectiveAt) + if err != nil { + t.Fatal(err) + } + if got := snap.IsReserved(a1); got != tt.wantA1 { + t.Errorf("IsReserved(a1) at %d = %v, want %v", tt.effectiveAt, got, tt.wantA1) + } + if snap.IsReserved(a2) { + t.Error("inactive client must never be reserved") + } + if snap.IsReserved(addr(9)) { + t.Error("unknown account must never be reserved") } }) } } +func TestBuildSnapshotRejectsInvalidClientID(t *testing.T) { + a1 := addr(1) + r := &mockReader{ + has: true, + whitelist: []common.Address{a1}, + clients: map[common.Address]ClientLookup{ + a1: {ClientID: nil, GasQuota: 1, Active: true}, + }, + } + if _, err := BuildSnapshot(r, nil, 1, common.Hash{}, 2); err == nil { + t.Fatal("expected error for nil client id") + } +} + func TestSnapshotNilSafe(t *testing.T) { var snap *Snapshot - if snap.IsReserved(addr(1), 1) { + if snap.IsReserved(addr(1)) { t.Error("nil snapshot must not classify anything as reserved") } + if _, ok := snap.Lookup(addr(1)); ok { + t.Error("nil snapshot Lookup must miss") + } + if snap.Quota(1) != 0 { + t.Error("nil snapshot Quota must be 0") + } + if snap.Clients() != nil { + t.Error("nil snapshot Clients must be nil") + } if snap.FeeMode(addr(1)) != 0 { t.Error("nil snapshot FeeMode must be 0") } @@ -171,8 +222,8 @@ func TestSnapshotNilSafe(t *testing.T) { func TestSnapshotFeeModeAndCapacity(t *testing.T) { a := addr(1) - snap := NewSnapshot(common.HexToHash("0x2"), 12_345, map[common.Address]ClientLookup{ - a: {ClientID: big.NewInt(1), GasQuota: 12_345, Active: true, FeeMode: 1}, + snap := NewSnapshot(common.HexToHash("0x2"), 12_345, map[common.Address]Client{ + a: {ID: 1, GasQuota: 12_345, FeeMode: 1}, }) if snap.FeeMode(a) != 1 { t.Errorf("FeeMode=%d, want 1", snap.FeeMode(a)) diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index 51bce74260..4380e7ff8e 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -239,7 +239,7 @@ func reservedAwareGasTarget(config *params.ChainConfig, parent *types.Header) ui // publicGasUsed nets the parent's reserved gas out of its total gas used, so // the base-fee controller tracks only normal-region demand. func publicGasUsed(config *params.ChainConfig, parent *types.Header) uint64 { - _, reservedGasUsed := parent.GetReservedInfo(config) + reservedGasUsed := parent.GetReservedGasUsed(config) if reservedGasUsed == nil || *reservedGasUsed > parent.GasUsed { return parent.GasUsed } diff --git a/consensus/misc/eip1559/eip1559_reserved_test.go b/consensus/misc/eip1559/eip1559_reserved_test.go index 17938b46e3..27c4b9aad6 100644 --- a/consensus/misc/eip1559/eip1559_reserved_test.go +++ b/consensus/misc/eip1559/eip1559_reserved_test.go @@ -62,16 +62,15 @@ func reservedFeeConfig(reservedBlock *big.Int, capacity uint64) *params.ChainCon return cc } -// extraWithReserved encodes a header Extra carrying the reserved-region fields. +// extraWithReserved encodes a header Extra carrying the reserved-region field. // The two preceding optional fields (GasTarget, BaseFeeChangeDenominator) must -// be non-nil for RLP to emit the trailing reserved optionals. -func extraWithReserved(t *testing.T, reservedTxCount uint32, reservedGasUsed uint64) []byte { +// be non-nil for RLP to emit the trailing reserved optional. +func extraWithReserved(t *testing.T, reservedGasUsed uint64) []byte { t.Helper() zero := uint64(0) enc, err := rlp.EncodeToBytes(&types.BlockExtraData{ GasTarget: &zero, BaseFeeChangeDenominator: &zero, - ReservedTxCount: &reservedTxCount, ReservedGasUsed: &reservedGasUsed, }) require.NoError(t, err) @@ -132,7 +131,7 @@ func TestReservedBaseFee_NetsReservedGasUsed(t *testing.T) { GasLimit: gasLimit, GasUsed: 35_000_000, BaseFee: baseFee, - Extra: extraWithReserved(t, 3, 15_000_000), + Extra: extraWithReserved(t, 15_000_000), } if got := CalcBaseFee(cfg, parent); got.Cmp(baseFee) != 0 { t.Errorf("base fee = %s, want unchanged %s (public used == target after netting)", got, baseFee) @@ -145,7 +144,7 @@ func TestReservedBaseFee_NetsReservedGasUsed(t *testing.T) { GasLimit: gasLimit, GasUsed: 35_000_000, BaseFee: baseFee, - Extra: extraWithReserved(t, 0, 0), + Extra: extraWithReserved(t, 0), } if got := CalcBaseFee(cfg, parentNoReserved); got.Cmp(baseFee) <= 0 { t.Errorf("base fee = %s, want > %s (full usage above target without netting)", got, baseFee) diff --git a/core/evm.go b/core/evm.go index 4587bf77bc..aebe94c2b7 100644 --- a/core/evm.go +++ b/core/evm.go @@ -70,7 +70,7 @@ func ReservedSnapshotForBlock(chain ChainContext, statedb *state.StateDB, header if header.Number.Uint64() > 0 { parentNumber = header.Number.Uint64() - 1 } - snap, err := registryreader.BuildSnapshot(reader, statedb, parentNumber, header.ParentHash) + snap, err := registryreader.BuildSnapshot(reader, statedb, parentNumber, header.ParentHash, header.Number.Uint64()) if err != nil { log.Warn("Failed to build reserved-blockspace snapshot", "number", header.Number, "err", err) return nil diff --git a/core/reserved_fee_test.go b/core/reserved_fee_test.go index 0fd8f91c9b..76b5e561a6 100644 --- a/core/reserved_fee_test.go +++ b/core/reserved_fee_test.go @@ -68,9 +68,9 @@ func reservedBlockCtx(coinbase common.Address, blockNumber *big.Int, baseFee *bi BaseFee: baseFee, } if len(reservedSenders) > 0 { - clients := make(map[common.Address]registryreader.ClientLookup, len(reservedSenders)) + clients := make(map[common.Address]registryreader.Client, len(reservedSenders)) for i, a := range reservedSenders { - clients[a] = registryreader.ClientLookup{ClientID: big.NewInt(int64(i + 1)), GasQuota: 30_000_000, Active: true} + clients[a] = registryreader.Client{ID: uint64(i + 1), GasQuota: 30_000_000} } ctx.ReservedSnapshot = registryreader.NewSnapshot(common.HexToHash("0x1"), uint64(len(reservedSenders))*30_000_000, clients) } diff --git a/core/state_transition.go b/core/state_transition.go index 012628d929..9ce667ee76 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -298,12 +298,12 @@ func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition // chain config; the reserved set from the parent registry snapshot on the // block context (set per block by the consensus paths, nil elsewhere), // matching the txpool and base-fee sources. Classification is sender-based; - // position-based enforcement (reserved region 0..ReservedTxCount) is layered - // on by the producer two-pass and block validation. + // per-client quota enforcement is layered on by the producer's reserved + // pass and the block-validation slice. var reserved bool if cfg := evm.ChainConfig(); cfg.Bor != nil && cfg.Bor.IsReservedBlockspace(evm.Context.BlockNumber) && - evm.Context.ReservedSnapshot.IsReserved(msg.From, evm.Context.BlockNumber.Uint64()) { + evm.Context.ReservedSnapshot.IsReserved(msg.From) { reserved = true } return &stateTransition{ diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index e948da2d1c..a2f09f2f6b 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -769,7 +769,6 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter, interrupt *atomic.B GasTipCap: uint256.MustFromBig(txs[i].GasTipCap()), Gas: txs[i].Gas(), BlobGas: txs[i].BlobGas(), - Reserved: reserved, } } pending[addr] = lazies @@ -1138,7 +1137,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, async bool) (replaced bool, e // Try to replace an existing transaction in the pending pool if list := pool.pending[from]; list != nil && list.Contains(tx.Nonce()) { // Nonce already pending, check if required price bump is met - inserted, old := list.Add(tx, pool.config.PriceBump, pool.isReserved(from)) + inserted, old := list.Add(tx, pool.config.PriceBump) if !inserted { pendingDiscardMeter.Mark(1) stage2Duration = time.Since(stage2Time) @@ -1242,7 +1241,7 @@ func (pool *LegacyPool) promoteTx(addr common.Address, hash common.Hash, tx *typ } list := pool.pending[addr] - inserted, old := list.Add(tx, pool.config.PriceBump, pool.isReserved(addr)) + inserted, old := list.Add(tx, pool.config.PriceBump) if !inserted { // An older transaction was better, discard this pool.all.Remove(hash) @@ -2316,7 +2315,7 @@ func (pool *LegacyPool) isReserved(addr common.Address) bool { if !cfg.Bor.IsReservedBlockspace(number) { return false } - return pool.reservedSnapshot.Load().IsReserved(addr, number.Uint64()) + return pool.reservedSnapshot.Load().IsReserved(addr) } // SetReservedRegistry installs the reserved-blockspace registry reader and @@ -2337,7 +2336,8 @@ func (pool *LegacyPool) rebuildReservedSnapshot(statedb *state.StateDB, head *ty if pool.reservedRegistry == nil || statedb == nil || head == nil { return } - snap, err := registryreader.BuildSnapshot(pool.reservedRegistry, statedb, head.Number.Uint64(), head.Hash()) + // The pool classifies for the next block (head+1) — see isReserved. + snap, err := registryreader.BuildSnapshot(pool.reservedRegistry, statedb, head.Number.Uint64(), head.Hash(), head.Number.Uint64()+1) if err != nil { log.Warn("Failed to build reserved-blockspace snapshot", "number", head.Number, "err", err) return diff --git a/core/txpool/legacypool/list.go b/core/txpool/legacypool/list.go index 1a5c013163..b0359a60ae 100644 --- a/core/txpool/legacypool/list.go +++ b/core/txpool/legacypool/list.go @@ -331,14 +331,14 @@ func (l *list) Contains(nonce uint64) bool { // // If the new transaction is accepted into the list, the lists' cost and gas // thresholds are also potentially updated. -func (l *list) Add(tx *types.Transaction, priceBump uint64, reserved bool) (bool, *types.Transaction) { - // If there's an older better transaction, abort +func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transaction) { + // If there's an older better transaction, abort. Reserved-blockspace + // senders get no special case: replacement is priced entirely through the + // fallback-fee fields (spec §8.2) — a zero-fee tx can never replace a + // zero-fee tx (the strict-increase check below rejects equal fees), while + // strictly positive fallback fees clear a zero-fee incumbent's threshold. old := l.txs.Get(tx.Nonce()) - // Reserved-blockspace senders replace by arrival order: two zero-fee txs at - // the same nonce can't out-bid each other, so the fee-bump rule is skipped. - // A reserved client must be able to cancel or replace a stuck tx without - // attaching a fee we told them they don't need. - if old != nil && !reserved { + if old != nil { if old.GasFeeCapCmp(tx) >= 0 || old.GasTipCapCmp(tx) >= 0 { return false, nil } diff --git a/core/txpool/legacypool/list_test.go b/core/txpool/legacypool/list_test.go index e86bab239f..ce325faaeb 100644 --- a/core/txpool/legacypool/list_test.go +++ b/core/txpool/legacypool/list_test.go @@ -49,7 +49,7 @@ func TestStrictListAdd(t *testing.T) { // Insert the transactions in a random order list := newList(true) for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump, false) + list.Add(txs[v], DefaultConfig.PriceBump) } // Verify internal state if len(list.txs.items) != len(txs) { @@ -74,7 +74,7 @@ func TestListAddVeryExpensive(t *testing.T) { gaslimit := uint64(i) tx, _ := types.SignTx(types.NewTransaction(uint64(i), common.Address{}, value, gaslimit, gasprice, nil), types.HomesteadSigner{}, key) t.Logf("cost: %x bitlen: %d\n", tx.Cost(), tx.Cost().BitLen()) - list.Add(tx, DefaultConfig.PriceBump, false) + list.Add(tx, DefaultConfig.PriceBump) } } @@ -92,7 +92,7 @@ func BenchmarkListAdd(b *testing.B) { for i := 0; i < b.N; i++ { list := newList(true) for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump, false) + list.Add(txs[v], DefaultConfig.PriceBump) list.Filter(priceLimit, DefaultConfig.PriceBump) } } @@ -120,7 +120,7 @@ func TestFilterTxConditionalKnownAccounts(t *testing.T) { // Create a transaction with no defined tx options // and add to the list. tx := transaction(0, 1000, key) - list.Add(tx, DefaultConfig.PriceBump, false) + list.Add(tx, DefaultConfig.PriceBump) // There should be no drops at this point. // No state has been modified. @@ -154,7 +154,7 @@ func TestFilterTxConditionalKnownAccounts(t *testing.T) { _ = trie tx2.PutOptions(&options) - list.Add(tx2, DefaultConfig.PriceBump, false) + list.Add(tx2, DefaultConfig.PriceBump) // There should still be no drops as no state has been modified. drops = list.FilterTxConditional(state, header) @@ -201,7 +201,7 @@ func TestFilterTxConditionalBlockNumber(t *testing.T) { // Create a transaction with no defined tx options // and add to the list. tx := transaction(0, 1000, key) - list.Add(tx, DefaultConfig.PriceBump, false) + list.Add(tx, DefaultConfig.PriceBump) // There should be no drops at this point. // No state has been modified. @@ -219,7 +219,7 @@ func TestFilterTxConditionalBlockNumber(t *testing.T) { options.BlockNumberMax = big.NewInt(110) tx2.PutOptions(&options) - list.Add(tx2, DefaultConfig.PriceBump, false) + list.Add(tx2, DefaultConfig.PriceBump) // There should still be no drops as no state has been modified. drops = list.FilterTxConditional(state, header) @@ -262,7 +262,7 @@ func TestFilterTxConditionalTimestamp(t *testing.T) { // Create a transaction with no defined tx options // and add to the list. tx := transaction(0, 1000, key) - list.Add(tx, DefaultConfig.PriceBump, false) + list.Add(tx, DefaultConfig.PriceBump) // There should be no drops at this point. // No state has been modified. @@ -283,7 +283,7 @@ func TestFilterTxConditionalTimestamp(t *testing.T) { options.TimestampMax = &maxTimestamp tx2.PutOptions(&options) - list.Add(tx2, DefaultConfig.PriceBump, false) + list.Add(tx2, DefaultConfig.PriceBump) // There should still be no drops as no state has been modified. drops = list.FilterTxConditional(state, header) @@ -374,7 +374,7 @@ func BenchmarkListCapOneTx(b *testing.B) { list := newList(true) // Insert the transactions in a random order for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump, false) + list.Add(txs[v], DefaultConfig.PriceBump) } b.StartTimer() list.Cap(list.Len() - 1) diff --git a/core/txpool/legacypool/queue.go b/core/txpool/legacypool/queue.go index cbb7cbb25e..b1ea0b050c 100644 --- a/core/txpool/legacypool/queue.go +++ b/core/txpool/legacypool/queue.go @@ -126,7 +126,7 @@ func (q *queue) add(tx *types.Transaction) (*common.Hash, error) { } // Reserved replacement-by-arrival applies to the pending pool; the future // queue keeps the standard fee-bump rule. - inserted, old := q.queued[from].Add(tx, q.config.PriceBump, false) + inserted, old := q.queued[from].Add(tx, q.config.PriceBump) if !inserted { // An older transaction was better, discard this queuedDiscardMeter.Mark(1) diff --git a/core/txpool/legacypool/reserved_test.go b/core/txpool/legacypool/reserved_test.go index 6e4698dd2f..8181638d5c 100644 --- a/core/txpool/legacypool/reserved_test.go +++ b/core/txpool/legacypool/reserved_test.go @@ -161,9 +161,12 @@ func TestReservedZeroFeeTxAdmittedAndPending(t *testing.T) { } } -// TestReservedZeroFeeReplacement verifies replacement-by-arrival: a reserved -// sender can replace a stuck zero-fee tx with another zero-fee tx at the same -// nonce, which the standard price-bump rule would reject. +// TestReservedZeroFeeReplacement pins the spec's replacement rule (§8.2): +// same-nonce replacement is priced entirely through the fallback-fee fields +// under the standard bump rule. A zero-fee tx can never replace a zero-fee tx +// ("10% over zero is still zero" — the strict-increase check rejects it), +// while strictly positive fallback fees win the slot. This keeps same-nonce +// pool content convergent across nodes regardless of arrival order. func TestReservedZeroFeeReplacement(t *testing.T) { t.Parallel() @@ -180,16 +183,44 @@ func TestReservedZeroFeeReplacement(t *testing.T) { t.Fatalf("first reserved tx rejected: %v", err) } - // Same nonce, different recipient, still zero fee — must replace by arrival. + // Same nonce, still zero fee: rejected — zero cannot out-bid zero. second := zeroFeeTx(t, cfg, reservedKey, 0, common.Address{0xbb}) - if err := pool.Add([]*types.Transaction{second}, true)[0]; err != nil { - t.Fatalf("reserved zero-fee replacement rejected: %v", err) + if err := pool.Add([]*types.Transaction{second}, true)[0]; err == nil { + t.Fatal("zero-fee replacement of a zero-fee tx must be rejected") + } + if pool.Get(first.Hash()) == nil { + t.Fatal("incumbent zero-fee tx must remain in the pool") } - if pool.Get(second.Hash()) == nil { - t.Fatal("replacement tx not in pool") + // Same nonce with positive fallback fees: wins the slot under the standard + // bump rule (strictly above zero clears both the strict-increase check and + // the zero threshold). The fallback fees stay below the 30 gwei tip floor, + // which reserved senders bypass at admission. + third, err := types.SignNewTx(reservedKey, types.LatestSigner(cfg), &types.DynamicFeeTx{ + ChainID: cfg.ChainID, + Nonce: 0, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(1), + Gas: 100_000, + To: &common.Address{0xcc}, + Value: big.NewInt(0), + }) + if err != nil { + t.Fatal(err) + } + if err := pool.Add([]*types.Transaction{third}, true)[0]; err != nil { + t.Fatalf("fallback-fee replacement rejected: %v", err) + } + if pool.Get(third.Hash()) == nil { + t.Fatal("fallback-fee replacement not in pool") } if pool.Get(first.Hash()) != nil { - t.Fatal("replaced tx should have been evicted") + t.Fatal("replaced zero-fee tx should have been evicted") + } + + // And a zero-fee tx cannot displace the fallback-fee incumbent either. + fourth := zeroFeeTx(t, cfg, reservedKey, 0, common.Address{0xdd}) + if err := pool.Add([]*types.Transaction{fourth}, true)[0]; err == nil { + t.Fatal("zero-fee tx must not replace a fallback-fee incumbent") } } diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go index d240e2164d..4b2e258930 100644 --- a/core/txpool/subpool.go +++ b/core/txpool/subpool.go @@ -43,8 +43,6 @@ type LazyTransaction struct { Gas uint64 // Amount of gas required by the transaction BlobGas uint64 // Amount of blob gas required by the transaction - - Reserved bool // Sender is a reserved-blockspace client (zero in-protocol fee, exempt from the miner's base-fee floor) } // Resolve retrieves the full transaction belonging to a lazy handle if it is still diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 9e044db809..1a21d520e5 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -186,7 +186,7 @@ func isReservedSender(opts *ValidationOptions, head *types.Header, signer types. if err != nil { return false } - return opts.ReservedSnapshot.IsReserved(from, number.Uint64()) + return opts.ReservedSnapshot.IsReserved(from) } // validateBlobTx implements the blob-transaction specific validations. diff --git a/core/types/block_test.go b/core/types/block_test.go index c633580815..db8cc1cacf 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -1051,129 +1051,6 @@ func TestSetReservedGasUsed(t *testing.T) { } } -func TestReservedBlockExtraDataRLP(t *testing.T) { - t.Parallel() - - gasTarget := uint64(15000000) - bfcd := uint64(64) - - // Pre-ReservedBlockspace: reserved fields nil -> omitted from RLP, decode as nil. - preReserved := &BlockExtraData{ - ValidatorBytes: []byte{0x01}, - TxDependency: [][]uint64{{1}}, - GasTarget: &gasTarget, - BaseFeeChangeDenominator: &bfcd, - } - encoded, err := rlp.EncodeToBytes(preReserved) - if err != nil { - t.Fatalf("encode pre-reserved: %v", err) - } - var dec BlockExtraData - if err := rlp.DecodeBytes(encoded, &dec); err != nil { - t.Fatalf("decode pre-reserved: %v", err) - } - if dec.ReservedTxCount != nil || dec.ReservedGasUsed != nil { - t.Fatalf("reserved fields should be nil pre-fork, got %v %v", dec.ReservedTxCount, dec.ReservedGasUsed) - } - - // Post-ReservedBlockspace: reserved fields set round-trip as non-nil. - count := uint32(3) - gasUsed := uint64(21000) - postReserved := &BlockExtraData{ - ValidatorBytes: []byte{0x02}, - TxDependency: [][]uint64{{2}}, - GasTarget: &gasTarget, - BaseFeeChangeDenominator: &bfcd, - ReservedTxCount: &count, - ReservedGasUsed: &gasUsed, - } - encoded, err = rlp.EncodeToBytes(postReserved) - if err != nil { - t.Fatalf("encode post-reserved: %v", err) - } - var dec2 BlockExtraData - if err := rlp.DecodeBytes(encoded, &dec2); err != nil { - t.Fatalf("decode post-reserved: %v", err) - } - if dec2.ReservedTxCount == nil || *dec2.ReservedTxCount != count { - t.Errorf("ReservedTxCount mismatch: got %v want %d", dec2.ReservedTxCount, count) - } - if dec2.ReservedGasUsed == nil || *dec2.ReservedGasUsed != gasUsed { - t.Errorf("ReservedGasUsed mismatch: got %v want %d", dec2.ReservedGasUsed, gasUsed) - } - - // Zero values must round-trip as non-nil (post-fork block with no reserved txs). - zeroCount := uint32(0) - zeroGas := uint64(0) - zero := &BlockExtraData{ - GasTarget: &gasTarget, - BaseFeeChangeDenominator: &bfcd, - ReservedTxCount: &zeroCount, - ReservedGasUsed: &zeroGas, - } - encoded, err = rlp.EncodeToBytes(zero) - if err != nil { - t.Fatalf("encode zero-reserved: %v", err) - } - var dec3 BlockExtraData - if err := rlp.DecodeBytes(encoded, &dec3); err != nil { - t.Fatalf("decode zero-reserved: %v", err) - } - if dec3.ReservedTxCount == nil || *dec3.ReservedTxCount != 0 { - t.Errorf("zero ReservedTxCount should decode non-nil 0, got %v", dec3.ReservedTxCount) - } - if dec3.ReservedGasUsed == nil || *dec3.ReservedGasUsed != 0 { - t.Errorf("zero ReservedGasUsed should decode non-nil 0, got %v", dec3.ReservedGasUsed) - } -} - -func TestGetReservedInfo(t *testing.T) { - t.Parallel() - - chainConfig := ¶ms.ChainConfig{ - ChainID: big.NewInt(137), - CancunBlock: big.NewInt(100), - } - - buildExtra := func(bed *BlockExtraData) []byte { - vanity := make([]byte, ExtraVanityLength) - seal := make([]byte, ExtraSealLength) - encoded, _ := rlp.EncodeToBytes(bed) - extra := append(vanity, encoded...) - extra = append(extra, seal...) - return extra - } - - count := uint32(2) - gasUsed := uint64(42000) - gasTarget := uint64(15000000) - bfcd := uint64(64) - bed := &BlockExtraData{ - GasTarget: &gasTarget, - BaseFeeChangeDenominator: &bfcd, - ReservedTxCount: &count, - ReservedGasUsed: &gasUsed, - } - - // Post-Cancun header with reserved fields present. - postHeader := &Header{Number: big.NewInt(200), Extra: buildExtra(bed)} - if rc, rg := postHeader.GetReservedInfo(chainConfig); rc == nil || *rc != count || rg == nil || *rg != gasUsed { - t.Errorf("post-Cancun: got count=%v gas=%v want %d %d", rc, rg, count, gasUsed) - } - - // Pre-Cancun header: getter returns nil, nil regardless of extra contents. - preHeader := &Header{Number: big.NewInt(50), Extra: buildExtra(bed)} - if rc, rg := preHeader.GetReservedInfo(chainConfig); rc != nil || rg != nil { - t.Errorf("pre-Cancun should return nil,nil; got %v %v", rc, rg) - } - - // Short extra (vanity only): getter returns nil, nil. - short := &Header{Number: big.NewInt(200), Extra: make([]byte, ExtraVanityLength)} - if rc, rg := short.GetReservedInfo(chainConfig); rc != nil || rg != nil { - t.Errorf("short extra should return nil,nil; got %v %v", rc, rg) - } -} - func TestGetBaseFeeParams(t *testing.T) { t.Parallel() diff --git a/eth/backend.go b/eth/backend.go index 01c8fb90a0..67f2fb949b 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -474,15 +474,14 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth.miner.SetPrioAddresses(config.TxPool.Locals) } - // Wire the reserved blockspace registry into chain/txpool/miner. Only Bor + // Wire the reserved blockspace registry into the chain and txpool. Only Bor // exposes a registry; non-Bor engines silently leave nil handles in place. + // The miner needs no handle of its own: block building classifies against + // the per-block snapshot the chain builds for execution (makeEnv). if borEngine, ok := eth.engine.(*bor.Bor); ok { reg := borEngine.ReservedRegistry() eth.blockchain.SetReservedRegistry(reg) eth.txPool.SetReservedRegistry(reg) - if eth.miner != nil { - eth.miner.SetReservedRegistry(reg) - } } // 1.14.8: NewOracle function definition was changed to accept (startPrice *big.Int) param. diff --git a/miner/miner.go b/miner/miner.go index 02ecb3db01..dbd4607542 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -26,7 +26,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" @@ -110,11 +109,6 @@ type Miner struct { worker *worker prio []common.Address // A list of senders to prioritize - // reservedRegistry is the read-only handle to the reserved blockspace - // registry. Nil when no registry is configured. Block builders that need - // to filter reserved txs should pull it via ReservedRegistry(). - reservedRegistry registryreader.Reader - wg sync.WaitGroup } @@ -139,28 +133,6 @@ func (miner *Miner) GetWorker() *worker { return miner.worker } -// ReservedRegistry returns the reserved blockspace registry reader wired into -// the miner, or nil when none is configured. -func (miner *Miner) ReservedRegistry() registryreader.Reader { - if miner == nil { - return nil - } - return miner.reservedRegistry -} - -// SetReservedRegistry installs the reserved blockspace registry reader. -// Called once at startup from the backend, after the consensus engine is -// available. Propagates the handle to the worker. Passing nil is valid. -func (miner *Miner) SetReservedRegistry(r registryreader.Reader) { - if miner == nil { - return - } - miner.reservedRegistry = r - if miner.worker != nil { - miner.worker.reservedRegistry = r - } -} - // update keeps track of the downloader events. Please be aware that this is a one shot type of update loop. // It's entered once and as soon as `Done` or `Failed` has been broadcasted the events are unregistered and // the loop is exited. This to prevent a major security vuln where external parties can DOS you with blocks @@ -313,15 +285,6 @@ func (miner *Miner) SetPrioAddresses(prio []common.Address) { miner.worker.setPrio(prio) } -// SetReservedRegistry installs the reserved-blockspace registry the miner -// consults during block building. Production callers leave it unset (nil) until -// the registry module and its hardfork gating land — the reserved pass is a -// no-op in that case and block building is byte-identical to before -// reserved-blockspace existed. Tests inject a *MockRegistry. -func (miner *Miner) SetReservedRegistry(r ReservedRegistry) { - miner.worker.setReservedRegistry(r) -} - // SetGasCeil sets the gaslimit to strive for when mining blocks post 1559. // For pre-1559 blocks, it sets the ceiling. func (miner *Miner) SetGasCeil(ceil uint64) { diff --git a/miner/ordering.go b/miner/ordering.go index b2a5de30b1..74cffd8fb9 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -41,19 +41,12 @@ type txWithMinerFee struct { func newTxWithMinerFee(tx *txpool.LazyTransaction, from common.Address, baseFee *uint256.Int) (*txWithMinerFee, error) { tip := new(uint256.Int).Set(tx.GasTipCap) if baseFee != nil { - // Reserved-blockspace senders pay zero in-protocol fee, so their txs are - // exempt from the base-fee floor and contribute no miner tip. Without this - // the producer would drop every reserved zero-fee tx before sealing. - if tx.Reserved { - tip = new(uint256.Int) - } else { - if tx.GasFeeCap.Cmp(baseFee) < 0 { - return nil, types.ErrGasFeeCapTooLow - } - tip = new(uint256.Int).Sub(tx.GasFeeCap, baseFee) - if tip.Gt(tx.GasTipCap) { - tip = tx.GasTipCap - } + if tx.GasFeeCap.Cmp(baseFee) < 0 { + return nil, types.ErrGasFeeCapTooLow + } + tip = new(uint256.Int).Sub(tx.GasFeeCap, baseFee) + if tip.Gt(tx.GasTipCap) { + tip = tx.GasTipCap } } return &txWithMinerFee{ diff --git a/miner/reserved_registry.go b/miner/reserved_registry.go deleted file mode 100644 index e6466ece85..0000000000 --- a/miner/reserved_registry.go +++ /dev/null @@ -1,121 +0,0 @@ -package miner - -import ( - "fmt" - "sort" - - "github.com/ethereum/go-ethereum/common" -) - -// reservedRegistry is the worker's local view of the reserved-blockspace -// registry. Production wiring leaves the worker's registry nil; tests inject a -// *MockRegistry via SetReservedRegistry. The contract-backed implementation owned by -// the registry-module work lands behind the same shape later — a swap at the -// setter site. -type reservedRegistry interface { - // Lookup returns the client ID that owns addr, or (_, false) if addr is - // not registered. Only active clients are visible, and a sender belongs - // to at most one client — uniqueness is the implementation's invariant - // (MockRegistry panics on construction; the registry contract enforces it). - Lookup(addr common.Address) (clientID uint64, ok bool) - // Quota returns the per-block reserved-region gas allowance for clientID, - // measured against declared transaction gas limits (tx.Gas), not actual - // gas used. - Quota(clientID uint64) uint64 - // CeilingGas returns the global reserved-region gas cap across all - // clients (the registry contract's ceilingGas). Zero means uncapped. - CeilingGas() uint64 - // Clients returns every registered client ID. - Clients() []uint64 - // Snapshot returns an immutable copy of the registry state effective for - // the child block of parent. The contract-backed implementation resolves - // parent state and the effectiveFrom activation delay internally; callers - // pin one snapshot per build so every pass sees a consistent view. - Snapshot(parent common.Hash) reservedRegistry -} - -// ReservedRegistry is the exported alias callers use with -// Miner.SetReservedRegistry. -type ReservedRegistry = reservedRegistry - -// ReservedClient describes one reserved-blockspace client for NewMockRegistry. Each whitelisted -// sender belongs to exactly one client. -type ReservedClient struct { - ID uint64 - Senders []common.Address - QuotaGas uint64 -} - -// MockRegistry is a throwaway in-memory reservedRegistry for tests and as a placeholder -// until the registry module lands. Immutable after construction; lock-free. -type MockRegistry struct { - addrToClient map[common.Address]uint64 - clientToQuota map[uint64]uint64 - clientIDs []uint64 - ceiling uint64 -} - -// NewMockRegistry builds a MockRegistry. Panics on a duplicate client ID or a sender claimed by -// two clients — both are test-setup bugs, not runtime conditions. -func NewMockRegistry(clients []ReservedClient) *MockRegistry { - m := &MockRegistry{ - addrToClient: make(map[common.Address]uint64), - clientToQuota: make(map[uint64]uint64), - clientIDs: make([]uint64, 0, len(clients)), - } - for _, c := range clients { - if _, dup := m.clientToQuota[c.ID]; dup { - panic(fmt.Sprintf("miner.NewMockRegistry: duplicate client ID %d", c.ID)) - } - m.clientToQuota[c.ID] = c.QuotaGas - m.clientIDs = append(m.clientIDs, c.ID) - for _, addr := range c.Senders { - if existing, ok := m.addrToClient[addr]; ok { - panic(fmt.Sprintf("miner.NewMockRegistry: address %s belongs to clients %d and %d", addr.Hex(), existing, c.ID)) - } - m.addrToClient[addr] = c.ID - } - } - sort.Slice(m.clientIDs, func(i, j int) bool { return m.clientIDs[i] < m.clientIDs[j] }) - return m -} - -// WithCeiling sets the global reserved-region gas cap (zero = uncapped) and -// returns m for construction chaining. Set before handing the MockRegistry to a -// worker; MockRegistry is treated as immutable once in use. -func (m *MockRegistry) WithCeiling(gas uint64) *MockRegistry { - m.ceiling = gas - return m -} - -func (m *MockRegistry) Lookup(addr common.Address) (uint64, bool) { - cid, ok := m.addrToClient[addr] - return cid, ok -} - -func (m *MockRegistry) Quota(clientID uint64) uint64 { return m.clientToQuota[clientID] } - -func (m *MockRegistry) CeilingGas() uint64 { return m.ceiling } - -func (m *MockRegistry) Clients() []uint64 { - out := make([]uint64, len(m.clientIDs)) - copy(out, m.clientIDs) - return out -} - -func (m *MockRegistry) Snapshot(common.Hash) reservedRegistry { - // The MockRegistry has no chain state to resolve, so the parent hash is unused; - // the copy below just guarantees snapshot isolation. - snapshot := *m - snapshot.addrToClient = make(map[common.Address]uint64, len(m.addrToClient)) - for k, v := range m.addrToClient { - snapshot.addrToClient[k] = v - } - snapshot.clientToQuota = make(map[uint64]uint64, len(m.clientToQuota)) - for k, v := range m.clientToQuota { - snapshot.clientToQuota[k] = v - } - snapshot.clientIDs = make([]uint64, len(m.clientIDs)) - copy(snapshot.clientIDs, m.clientIDs) - return &snapshot -} diff --git a/miner/reserved_registry_test.go b/miner/reserved_registry_test.go deleted file mode 100644 index 0266b14bde..0000000000 --- a/miner/reserved_registry_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package miner - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/bor/contract/registrytest" -) - -// TestMiner_ReservedRegistry_NilByDefault verifies the accessor on a Miner -// (and a typed-nil receiver) returns nil so consumers don't panic. -func TestMiner_ReservedRegistry_NilByDefault(t *testing.T) { - var miner *Miner - require.Nil(t, miner.ReservedRegistry()) - - miner = &Miner{worker: &worker{}} - require.Nil(t, miner.ReservedRegistry()) -} - -// TestMiner_ReservedRegistry_PropagatesToWorker confirms SetReservedRegistry -// installs the handle on both the Miner and its worker, and that the handle -// answers EVM-backed queries identically to the harness reader. -func TestMiner_ReservedRegistry_PropagatesToWorker(t *testing.T) { - h := registrytest.NewHarness(t) - - w := &worker{} - miner := &Miner{worker: w} - miner.SetReservedRegistry(h.Reader) - - require.NotNil(t, miner.ReservedRegistry()) - require.Same(t, h.Reader, miner.ReservedRegistry()) - require.Same(t, h.Reader, w.reservedRegistry, "worker must mirror the miner's registry handle") - - reserved, err := miner.ReservedRegistry().IsReservedAddress(nil, 0, common.Hash{}, h.ReservedAddr) - require.NoError(t, err) - require.True(t, reserved) - - reserved, err = miner.ReservedRegistry().IsReservedAddress(nil, 0, common.Hash{}, h.UnreservedAddr) - require.NoError(t, err) - require.False(t, reserved) -} diff --git a/miner/reserved_test.go b/miner/reserved_test.go index 3a0fb2c8d3..84b3e8d151 100644 --- a/miner/reserved_test.go +++ b/miner/reserved_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/txpool" @@ -18,9 +19,24 @@ import ( "github.com/ethereum/go-ethereum/params" ) -// Compile-time guard: MockRegistry must satisfy the exported registry interface so the -// Miner.SetReservedRegistry wiring can't silently break. -var _ ReservedRegistry = (*MockRegistry)(nil) +// testClient describes one reserved client for newTestSnapshot. +type testClient struct { + ID uint64 + Senders []common.Address + QuotaGas uint64 +} + +// newTestSnapshot builds a registry snapshot for sequencing tests. capacity 0 +// means uncapped (effectiveCeilingGas normalizes it to MaxUint64). +func newTestSnapshot(capacity uint64, clients []testClient) *registryreader.Snapshot { + m := make(map[common.Address]registryreader.Client) + for _, c := range clients { + for _, a := range c.Senders { + m[a] = registryreader.Client{ID: c.ID, GasQuota: c.QuotaGas} + } + } + return registryreader.NewSnapshot(common.Hash{}, capacity, m) +} func TestOrderClients(t *testing.T) { t.Parallel() @@ -63,64 +79,6 @@ func TestOrderClients(t *testing.T) { "different parent hashes should reorder >2 clients") } -func TestNewMockRegistry(t *testing.T) { - t.Parallel() - - a := common.HexToAddress("0xAa") - b := common.HexToAddress("0xBb") - c := common.HexToAddress("0xCc") - - m := NewMockRegistry([]ReservedClient{ - {ID: 7, Senders: []common.Address{a, b}, QuotaGas: 10_000_000}, - {ID: 3, Senders: []common.Address{c}, QuotaGas: 5_000_000}, - }) - - cid, ok := m.Lookup(a) - require.True(t, ok) - require.Equal(t, uint64(7), cid) - - _, ok = m.Lookup(common.HexToAddress("0xDd")) - require.False(t, ok) - - require.Equal(t, uint64(10_000_000), m.Quota(7)) - require.Equal(t, uint64(0), m.Quota(99)) - require.Equal(t, []uint64{3, 7}, m.Clients(), "Clients sorted") -} - -func TestNewMockRegistry_PanicsOnDuplicateSender(t *testing.T) { - t.Parallel() - defer func() { require.NotNil(t, recover(), "expected panic on duplicate sender") }() - - addr := common.HexToAddress("0x01") - _ = NewMockRegistry([]ReservedClient{ - {ID: 1, Senders: []common.Address{addr}, QuotaGas: 1}, - {ID: 2, Senders: []common.Address{addr}, QuotaGas: 1}, - }) -} - -// TestMockRegistrySnapshot verifies Snapshot returns an independent deep copy: mutating -// the original after snapshotting must not be observable through the snapshot. -func TestMockRegistrySnapshot(t *testing.T) { - t.Parallel() - - a := common.HexToAddress("0x0a") - m := NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{a}, QuotaGas: 100}}).WithCeiling(500) - snap := m.Snapshot(common.Hash{}) - - // Mutate the original's internal maps; the snapshot must be unaffected. - m.addrToClient[a] = 999 - m.clientToQuota[1] = 0 - m.clientIDs[0] = 999 - m.ceiling = 0 - - cid, ok := snap.Lookup(a) - require.True(t, ok) - require.Equal(t, uint64(1), cid) - require.Equal(t, uint64(100), snap.Quota(1)) - require.Equal(t, []uint64{1}, snap.Clients()) - require.Equal(t, uint64(500), snap.CeilingGas()) -} - func TestFilterReservedTxs(t *testing.T) { t.Parallel() @@ -131,20 +89,20 @@ func TestFilterReservedTxs(t *testing.T) { cases := []struct { name string - registry *MockRegistry + registry *registryreader.Snapshot input map[common.Address][]*txpool.LazyTransaction wantClients map[uint64][]common.Address wantRemaining []common.Address }{ { name: "no reserved senders present", - registry: NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{common.HexToAddress("0x99")}, QuotaGas: 1}}), + registry: newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{common.HexToAddress("0x99")}, QuotaGas: 1}}), input: map[common.Address][]*txpool.LazyTransaction{a: stubTxs(1)}, wantRemaining: []common.Address{a}, }, { name: "filters reserved senders out", - registry: NewMockRegistry([]ReservedClient{ + registry: newTestSnapshot(0, []testClient{ {ID: 7, Senders: []common.Address{a, b}, QuotaGas: 1_000_000}, {ID: 3, Senders: []common.Address{c}, QuotaGas: 500_000}, }), @@ -371,13 +329,13 @@ func TestExtractReservedTxs(t *testing.T) { t.Run("empty registry is a no-op", func(t *testing.T) { t.Parallel() pending := map[common.Address][]*txpool.LazyTransaction{a: {gasTx(100)}} - require.Nil(t, extractReservedTxs(NewMockRegistry(nil), common.Hash{}, pending, fn)) + require.Nil(t, extractReservedTxs(newTestSnapshot(0, nil), common.Hash{}, pending, fn)) require.Contains(t, pending, a, "pending untouched") }) t.Run("client within quota; normal sender untouched", func(t *testing.T) { t.Parallel() - registry := NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{a}, QuotaGas: 1_000_000}}) + registry := newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{a}, QuotaGas: 1_000_000}}) pending := map[common.Address][]*txpool.LazyTransaction{a: {gasTx(100), gasTx(100)}, n: {gasTx(100)}} groups := extractReservedTxs(registry, common.Hash{}, pending, fn) @@ -390,7 +348,7 @@ func TestExtractReservedTxs(t *testing.T) { t.Run("quota overflow re-added to pending", func(t *testing.T) { t.Parallel() - registry := NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{a}, QuotaGas: 100}}) + registry := newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{a}, QuotaGas: 100}}) pending := map[common.Address][]*txpool.LazyTransaction{a: {gasTx(100), gasTx(100)}} groups := extractReservedTxs(registry, common.Hash{}, pending, fn) @@ -420,10 +378,10 @@ func TestExtractReservedTxs_Ceiling(t *testing.T) { t.Run("ceiling caps the second client in visit order", func(t *testing.T) { t.Parallel() - registry := NewMockRegistry([]ReservedClient{ + registry := newTestSnapshot(800, []testClient{ {ID: 1, Senders: []common.Address{a}, QuotaGas: 600}, {ID: 2, Senders: []common.Address{b}, QuotaGas: 600}, - }).WithCeiling(800) + }) pending := newPending() // The first-visited client fits its full 600; the second is left @@ -439,7 +397,7 @@ func TestExtractReservedTxs_Ceiling(t *testing.T) { t.Run("zero ceiling means uncapped", func(t *testing.T) { t.Parallel() - registry := NewMockRegistry([]ReservedClient{ + registry := newTestSnapshot(0, []testClient{ {ID: 1, Senders: []common.Address{a}, QuotaGas: 600}, {ID: 2, Senders: []common.Address{b}, QuotaGas: 600}, }) @@ -492,10 +450,10 @@ func TestSequenceTxs(t *testing.T) { t.Run("priority, reserved and normal groups in order", func(t *testing.T) { t.Parallel() - w := &worker{prio: []common.Address{p}, reservedRegistry: NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 1_000_000}})} + w := &worker{prio: []common.Address{p}, reservedSnapshotOverride: newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 1_000_000}})} pending := map[common.Address][]*txpool.LazyTransaction{p: {gasTx(100)}, r: {gasTx(100)}, n: {gasTx(100)}} - seq := w.sequenceTxs(newEnv(), w.reservedRegistrySnapshot(common.Hash{}), pending) + seq := w.sequenceTxs(newEnv(), w.reservedSnapshotOverride, pending) require.Len(t, seq, 3) require.Equal(t, map[common.Address]int{p: 1}, drainBySender(seq[0]), "priority group first") require.Equal(t, map[common.Address]int{r: 1}, drainBySender(seq[1]), "reserved group next") @@ -507,24 +465,24 @@ func TestSequenceTxs(t *testing.T) { w := &worker{} pending := map[common.Address][]*txpool.LazyTransaction{n: {gasTx(100)}} - seq := w.sequenceTxs(newEnv(), w.reservedRegistrySnapshot(common.Hash{}), pending) + seq := w.sequenceTxs(newEnv(), w.reservedSnapshotOverride, pending) require.Len(t, seq, 1) require.Equal(t, map[common.Address]int{n: 1}, drainBySender(seq[0])) }) t.Run("empty pending yields no groups", func(t *testing.T) { t.Parallel() - w := &worker{prio: []common.Address{p}, reservedRegistry: NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} - seq := w.sequenceTxs(newEnv(), w.reservedRegistrySnapshot(common.Hash{}), map[common.Address][]*txpool.LazyTransaction{}) + w := &worker{prio: []common.Address{p}, reservedSnapshotOverride: newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} + seq := w.sequenceTxs(newEnv(), w.reservedSnapshotOverride, map[common.Address][]*txpool.LazyTransaction{}) require.Empty(t, seq) }) t.Run("reserved overflow joins the normal group", func(t *testing.T) { t.Parallel() - w := &worker{reservedRegistry: NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} + w := &worker{reservedSnapshotOverride: newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} pending := map[common.Address][]*txpool.LazyTransaction{r: {gasTx(100), gasTx(100)}, n: {gasTx(100)}} - seq := w.sequenceTxs(newEnv(), w.reservedRegistrySnapshot(common.Hash{}), pending) + seq := w.sequenceTxs(newEnv(), w.reservedSnapshotOverride, pending) require.Len(t, seq, 2) require.Equal(t, map[common.Address]int{r: 1}, drainBySender(seq[0]), "reserved group: one tx fits quota") require.Equal(t, map[common.Address]int{n: 1, r: 1}, drainBySender(seq[1]), "normal group: normal sender + reserved overflow") @@ -534,7 +492,7 @@ func TestSequenceTxs(t *testing.T) { t.Parallel() r2 := common.HexToAddress("0x0d") senderOf := map[uint64]common.Address{1: r, 2: r2} - w := &worker{reservedRegistry: NewMockRegistry([]ReservedClient{ + w := &worker{reservedSnapshotOverride: newTestSnapshot(0, []testClient{ {ID: 1, Senders: []common.Address{r}, QuotaGas: 1_000_000}, {ID: 2, Senders: []common.Address{r2}, QuotaGas: 1_000_000}, })} @@ -543,7 +501,7 @@ func TestSequenceTxs(t *testing.T) { env := newEnv() order := orderClients(env.header.ParentHash, []uint64{1, 2}) - seq := w.sequenceTxs(env, w.reservedRegistrySnapshot(env.header.ParentHash), pending) + seq := w.sequenceTxs(env, w.reservedSnapshotOverride, pending) require.Len(t, seq, 3) require.Equal(t, map[common.Address]int{senderOf[order[0]]: 1}, drainBySender(seq[0]), "first client in visit order") require.Equal(t, map[common.Address]int{senderOf[order[1]]: 1}, drainBySender(seq[1]), "second client in visit order") @@ -556,7 +514,7 @@ func TestSequenceTxs(t *testing.T) { // the normal pass, where standard EIP-1559 admission drops it // (GasFeeCap < BaseFee) — per spec it stays in the pool for a later // block instead of entering this one. - w := &worker{reservedRegistry: NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} + w := &worker{reservedSnapshotOverride: newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} pending := map[common.Address][]*txpool.LazyTransaction{ r: {feeGasTx(0, 0, 100), feeGasTx(0, 0, 100)}, n: {feeGasTx(200, 100, 100)}, @@ -565,7 +523,7 @@ func TestSequenceTxs(t *testing.T) { env := newEnv() env.header.BaseFee = big.NewInt(100) - seq := w.sequenceTxs(env, w.reservedRegistrySnapshot(env.header.ParentHash), pending) + seq := w.sequenceTxs(env, w.reservedSnapshotOverride, pending) require.Len(t, seq, 2) require.Equal(t, map[common.Address]int{r: 1}, drainBySender(seq[0]), "reserved group: one zero-fee tx within quota") require.Equal(t, map[common.Address]int{n: 1}, drainBySender(seq[1]), "normal group: zero-fee overflow not admitted") @@ -577,10 +535,10 @@ func TestSequenceTxs(t *testing.T) { // first, so a prioritized registered sender bypasses reserved quota // accounting and pays normal fees. Operators should not prioritize // registered senders. - w := &worker{prio: []common.Address{r}, reservedRegistry: NewMockRegistry([]ReservedClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} + w := &worker{prio: []common.Address{r}, reservedSnapshotOverride: newTestSnapshot(0, []testClient{{ID: 1, Senders: []common.Address{r}, QuotaGas: 100}})} pending := map[common.Address][]*txpool.LazyTransaction{r: {gasTx(100), gasTx(100)}} - seq := w.sequenceTxs(newEnv(), w.reservedRegistrySnapshot(common.Hash{}), pending) + seq := w.sequenceTxs(newEnv(), w.reservedSnapshotOverride, pending) require.Len(t, seq, 1, "single priority group; no reserved group") require.False(t, seq[0].reserved, "priority group uses normal ordering") require.Equal(t, map[common.Address]int{r: 2}, drainBySender(seq[0]), "both txs taken despite quota of 100") @@ -598,7 +556,7 @@ func TestReservedBuild_NilRegistry(t *testing.T) { w, b, _ := newTestWorker(t, DefaultTestConfig(), &chainConfig, engine, rawdb.NewMemoryDatabase(), false, 0) defer w.close() - require.Nil(t, w.reservedRegistry, "registry defaults to nil") + require.Nil(t, w.reservedSnapshotOverride, "snapshot override defaults to nil") sub := w.mux.Subscribe(core.NewMinedBlockEvent{}) defer sub.Unsubscribe() @@ -624,7 +582,7 @@ func TestReservedBuild_HappyPath(t *testing.T) { w, b, _ := newTestWorker(t, DefaultTestConfig(), &chainConfig, engine, rawdb.NewMemoryDatabase(), false, 0) defer w.close() - w.setReservedRegistry(NewMockRegistry([]ReservedClient{ + w.setReservedSnapshot(newTestSnapshot(0, []testClient{ {ID: 1, Senders: []common.Address{testBankAddress}, QuotaGas: 10_000_000}, })) @@ -661,12 +619,23 @@ func borUnittestCancunConfig() params.ChainConfig { return chainConfig } +// borUnittestReservedConfig returns a Cancun-enabled unittest config with the +// ReservedBlockspace fork active from genesis. The BorConfig is deep-copied so +// scheduling the fork can't leak into the shared BorUnittestChainConfig. +func borUnittestReservedConfig() params.ChainConfig { + chainConfig := borUnittestCancunConfig() + borCopy := *chainConfig.Bor + borCopy.ReservedBlockspaceBlock = big.NewInt(0) + chainConfig.Bor = &borCopy + return chainConfig +} + // TestReservedBuild_HeaderGasUsed confirms the producer records the reserved // pass's actual gas total in BlockExtraData.ReservedGasUsed. With every block // transaction committed through the reserved group, the total must equal the // header's GasUsed. func TestReservedBuild_HeaderGasUsed(t *testing.T) { - chainConfig := borUnittestCancunConfig() + chainConfig := borUnittestReservedConfig() engine, ctrl := getFakeBorFromConfig(t, &chainConfig) defer engine.Close() defer ctrl.Finish() @@ -674,7 +643,7 @@ func TestReservedBuild_HeaderGasUsed(t *testing.T) { w, b, _ := newTestWorker(t, DefaultTestConfig(), &chainConfig, engine, rawdb.NewMemoryDatabase(), false, 0) defer w.close() - w.setReservedRegistry(NewMockRegistry([]ReservedClient{ + w.setReservedSnapshot(newTestSnapshot(0, []testClient{ {ID: 1, Senders: []common.Address{testBankAddress}, QuotaGas: 10_000_000}, })) @@ -697,11 +666,11 @@ func TestReservedBuild_HeaderGasUsed(t *testing.T) { require.Equal(t, got.GasUsed(), *reserved, "all txs are reserved, so reserved gas equals block gas used") } -// TestReservedBuild_HeaderAbsentWithoutRegistry pins wire compatibility: with no -// registry wired (production default), post-Cancun blocks must not carry the +// TestReservedBuild_HeaderAbsentPreFork pins wire compatibility: before the +// ReservedBlockspace fork, post-Cancun blocks must not carry the // ReservedGasUsed field at all — their Extra encoding stays byte-identical to // pre-reserved-blockspace blocks. -func TestReservedBuild_HeaderAbsentWithoutRegistry(t *testing.T) { +func TestReservedBuild_HeaderAbsentPreFork(t *testing.T) { chainConfig := borUnittestCancunConfig() engine, ctrl := getFakeBorFromConfig(t, &chainConfig) defer engine.Close() @@ -710,7 +679,7 @@ func TestReservedBuild_HeaderAbsentWithoutRegistry(t *testing.T) { w, b, _ := newTestWorker(t, DefaultTestConfig(), &chainConfig, engine, rawdb.NewMemoryDatabase(), false, 0) defer w.close() - require.Nil(t, w.reservedRegistry, "registry defaults to nil") + require.Nil(t, w.reservedSnapshotOverride, "snapshot override defaults to nil") sub := w.mux.Subscribe(core.NewMinedBlockEvent{}) defer sub.Unsubscribe() @@ -719,7 +688,7 @@ func TestReservedBuild_HeaderAbsentWithoutRegistry(t *testing.T) { require.NoError(t, b.txPool.Add([]*types.Transaction{b.newRandomTxWithNonce(false, 0)}, false)[0]) got := waitForBlockWithTxs(t, sub, 1, 15*time.Second) - require.Nil(t, got.Header().GetReservedGasUsed(&chainConfig), "no registry: field must be absent from the wire") + require.Nil(t, got.Header().GetReservedGasUsed(&chainConfig), "pre-fork: field must be absent from the wire") } // TestReservedBuild_Positional proves the reserved-first builder preference @@ -735,7 +704,7 @@ func TestReservedBuild_Positional(t *testing.T) { w, b, _ := newTestWorker(t, DefaultTestConfig(), &chainConfig, engine, rawdb.NewMemoryDatabase(), false, 0) defer w.close() - w.setReservedRegistry(NewMockRegistry([]ReservedClient{ + w.setReservedSnapshot(newTestSnapshot(0, []testClient{ {ID: 1, Senders: []common.Address{testUserAddress}, QuotaGas: 10_000_000}, })) @@ -801,7 +770,7 @@ func TestReservedBuild_Overflow(t *testing.T) { defer w.close() // Quota fits exactly one value-transfer (gas limit params.TxGas = 21000). - w.setReservedRegistry(NewMockRegistry([]ReservedClient{ + w.setReservedSnapshot(newTestSnapshot(0, []testClient{ {ID: 1, Senders: []common.Address{testBankAddress}, QuotaGas: params.TxGas}, })) diff --git a/miner/sequencing.go b/miner/sequencing.go index 7676cfff22..5b753e8d44 100644 --- a/miner/sequencing.go +++ b/miner/sequencing.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/bor/registryreader" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/metrics" @@ -42,10 +43,10 @@ func extractPriorityTxs(prio []common.Address, pendingTxs map[common.Address][]* // filterReservedTxs removes the transactions from reserved-eligible senders from // pending transactions and returns them grouped by client ID. -func filterReservedTxs(pendingTxs map[common.Address][]*txpool.LazyTransaction, registry reservedRegistry) map[uint64]map[common.Address][]*txpool.LazyTransaction { +func filterReservedTxs(pendingTxs map[common.Address][]*txpool.LazyTransaction, snap *registryreader.Snapshot) map[uint64]map[common.Address][]*txpool.LazyTransaction { var reserved = make(map[uint64]map[common.Address][]*txpool.LazyTransaction) for addr, txs := range pendingTxs { - cid, ok := registry.Lookup(addr) + cid, ok := snap.Lookup(addr) if !ok { continue } @@ -139,10 +140,13 @@ func selectReservedTxs( return selected, used, overflow } -// effectiveCeilingGas returns the registry's global reserved-region cap, normalizing -// zero (uncapped) to MaxUint64 so the selection loop needs no special case. -func effectiveCeilingGas(registry reservedRegistry) uint64 { - if c := registry.CeilingGas(); c != 0 { +// effectiveCeilingGas returns the snapshot's reserved capacity (Σ effective +// client quotas) as a global cap on the reserved pass, normalizing zero +// (no clients) to MaxUint64 so the selection loop needs no special case. +// Defense-in-depth: per-client quotas already bound the total, and the +// registry contract enforces Σ quotas ≤ its ceiling at write time. +func effectiveCeilingGas(snap *registryreader.Snapshot) uint64 { + if c := snap.Capacity(); c != 0 { return c } return math.MaxUint64 @@ -150,15 +154,15 @@ func effectiveCeilingGas(registry reservedRegistry) uint64 { // extractReservedTxs pulls reserved-eligible transactions out of pendingTxs and // returns one quota-trimmed, ordered group per client, in the deterministic -// parent-hash-keyed client order. registry is a per-build snapshot taken by the -// caller; a nil registry (production default until the registry module lands) -// or one with no clients is a no-op. Quota overflow — per-client and global -// ceiling alike — is re-added to pendingTxs for the normal pass. -func extractReservedTxs(registry reservedRegistry, parentHash common.Hash, pendingTxs map[common.Address][]*txpool.LazyTransaction, newReservedTxSet transactionsByPriceAndNonceFn) []*transactionsByPriceAndNonce { - if registry == nil || len(registry.Clients()) == 0 { +// parent-hash-keyed client order. snap is the caller's per-build registry +// snapshot; nil (pre-fork, or no registry configured) or one with no clients +// is a no-op. Quota overflow — per-client and capacity alike — is re-added to +// pendingTxs for the normal pass. +func extractReservedTxs(snap *registryreader.Snapshot, parentHash common.Hash, pendingTxs map[common.Address][]*txpool.LazyTransaction, newReservedTxSet transactionsByPriceAndNonceFn) []*transactionsByPriceAndNonce { + if snap == nil || len(snap.Clients()) == 0 { return nil } - reservedTxs := filterReservedTxs(pendingTxs, registry) + reservedTxs := filterReservedTxs(pendingTxs, snap) if len(reservedTxs) == 0 { return nil } @@ -166,9 +170,9 @@ func extractReservedTxs(registry reservedRegistry, parentHash common.Hash, pendi // The global ceiling bounds the summed declared gas selected across all // clients. Like per-client quota, it is charged against declared gas // limits, not actual gas used. - ceilingLeft := effectiveCeilingGas(registry) + ceilingLeft := effectiveCeilingGas(snap) - clientOrder := orderClients(parentHash, registry.Clients()) + clientOrder := orderClients(parentHash, snap.Clients()) var clientGroups = make([]*transactionsByPriceAndNonce, 0, len(clientOrder)) for _, cid := range clientOrder { clientTxs := reservedTxs[cid] @@ -176,7 +180,7 @@ func extractReservedTxs(registry reservedRegistry, parentHash common.Hash, pendi continue } - selected, used, overflow := selectReservedTxs(clientTxs, min(registry.Quota(cid), ceilingLeft), newReservedTxSet) + selected, used, overflow := selectReservedTxs(clientTxs, min(snap.Quota(cid), ceilingLeft), newReservedTxSet) ceilingLeft -= used for addr, txs := range overflow { pendingTxs[addr] = append(pendingTxs[addr], txs...) @@ -196,12 +200,12 @@ func extractReservedTxs(registry reservedRegistry, parentHash common.Hash, pendi // client (deterministic, parent-hash-keyed order), then the remaining normal // transactions. Each group is an independently-ordered transactionsByPriceAndNonce // the caller commits in turn. Empty groups are omitted so the caller never spins -// up a commit pass for nothing. registry is the caller's per-build snapshot -// (see worker.reservedRegistrySnapshot); nil disables the reserved pass. +// up a commit pass for nothing. snap is the caller's per-build registry +// snapshot (see worker.sequencingSnapshot); nil disables the reserved pass. // // There is no error path — sequencing only groups and orders an in-memory // snapshot; interruption is surfaced later, by commitTransactions. -func (w *worker) sequenceTxs(env *environment, registry reservedRegistry, pendingTxs map[common.Address][]*txpool.LazyTransaction) []*transactionsByPriceAndNonce { +func (w *worker) sequenceTxs(env *environment, snap *registryreader.Snapshot, pendingTxs map[common.Address][]*txpool.LazyTransaction) []*transactionsByPriceAndNonce { w.mu.RLock() prio := w.prio w.mu.RUnlock() @@ -226,7 +230,7 @@ func (w *worker) sequenceTxs(env *environment, registry reservedRegistry, pendin // Reserved transactions, one ordered group per client. No emptiness check // here, unlike the neighbouring groups: extractReservedTxs already omits // empty groups from the slice it returns, so every element is committable. - txBatches = append(txBatches, extractReservedTxs(registry, env.header.ParentHash, pendingTxs, newReservedTxSet)...) + txBatches = append(txBatches, extractReservedTxs(snap, env.header.ParentHash, pendingTxs, newReservedTxSet)...) // Everything left (including reserved quota overflow added back above) is // normal. Heap-init time is recorded for the normal batch only, keeping diff --git a/miner/worker.go b/miner/worker.go index 3fc75871f8..b226fb723f 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -413,16 +413,12 @@ type worker struct { eth Backend chain *core.BlockChain - // reservedRegistry mirrors the Miner field. Populated via - // Miner.SetReservedRegistry — see miner.go. - reservedRegistry registryreader.Reader - prio []common.Address // A list of senders to prioritize - // reservedRegistry is the source of truth for reserved-blockspace clients - // and their per-block gas quotas. nil disables the reserved pass — block - // building is byte-identical to before reserved-blockspace existed. - reservedRegistry reservedRegistry + // reservedSnapshotOverride replaces the env's registry snapshot during + // block building. Test seam only — production sequencing reads the + // snapshot makeEnv pins on the EVM block context. + reservedSnapshotOverride *registryreader.Snapshot // Feeds pendingLogsFeed event.Feed @@ -688,12 +684,13 @@ func (w *worker) getCurrent() *environment { return w.current } -// setReservedRegistry installs the reserved-blockspace registry the worker -// consults during block building. A nil registry disables the reserved pass. -func (w *worker) setReservedRegistry(r reservedRegistry) { +// setReservedSnapshot installs a fixed reserved-registry snapshot the +// sequencing pass classifies against, bypassing the env's snapshot. Test seam +// only. +func (w *worker) setReservedSnapshot(snap *registryreader.Snapshot) { w.mu.Lock() defer w.mu.Unlock() - w.reservedRegistry = r + w.reservedSnapshotOverride = snap } // setRecommitInterval updates the interval for miner sealing work recommitting. @@ -2142,15 +2139,16 @@ func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment, gen env.pendingDuration = time.Since(pendingStart) pendingTimer.Update(env.pendingDuration) - // Pin one registry snapshot for the whole build so client order, quotas - // and membership stay consistent across groups. A future multi-pass - // (mini-block) builder must take the snapshot once per block, outside its - // pass loop. - registry := w.reservedRegistrySnapshot(env.header.ParentHash) + // One registry snapshot serves the whole build — the same parent-state + // snapshot makeEnv pinned for execution — so client order, quotas and + // membership stay consistent across batches and match what the executor + // classifies. A future multi-pass (mini-block) builder inherits this for + // free: the snapshot lives on the env, outside any pass loop. + snap := w.sequencingSnapshot(env) // Order transactions based on priority into 3 buckets - priority, reserved // and normal transactions. - txBatches := w.sequenceTxs(env, registry, pendingTxs) + txBatches := w.sequenceTxs(env, snap, pendingTxs) // Shared channels used during builder mode. Both are nil when there is no prefetcher. var builderPlanCh chan<- *types.Transaction @@ -2177,7 +2175,7 @@ func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment, gen // Record reserved gas even when a commit pass was interrupted: callers // seal the partial block, and its header must account for the reserved // transactions that did commit. - if err := w.writeReservedGasUsed(env, registry); err != nil { + if err := w.writeReservedGasUsed(env); err != nil { return err } @@ -2194,32 +2192,33 @@ func remainingGas(env *environment) uint64 { return env.gasPool.Gas() } -// reservedRegistrySnapshot pins one registry snapshot for a build, keyed by -// the parent block. Returns nil when no registry is wired — the production -// default until the registry module lands — which disables the reserved pass -// and the header write. -func (w *worker) reservedRegistrySnapshot(parent common.Hash) reservedRegistry { +// sequencingSnapshot returns the reserved-registry snapshot the sequencing +// pass classifies against: the one makeEnv pinned on the EVM block context, +// i.e. the same snapshot execution classifies with, so the sequencer and the +// executor can never disagree within one build. Nil (pre-fork, or no registry +// configured) disables the reserved pass. +func (w *worker) sequencingSnapshot(env *environment) *registryreader.Snapshot { w.mu.RLock() - registry := w.reservedRegistry + override := w.reservedSnapshotOverride w.mu.RUnlock() - if registry == nil { + if override != nil { + return override + } + if env.evm == nil { return nil } - return registry.Snapshot(parent) + return env.evm.Context.ReservedSnapshot } // writeReservedGasUsed records the build's reserved-region gas total in the -// header's BlockExtraData. The field is consensus-visible, so it is written -// only while the reserved pass is active (registry wired); an explicit zero is -// still written then, marking "reserved active, none used" on the wire. -// -// TODO(reserved-blockspace): gate on the reserved hardfork instead of the -// registry handle once the fork is defined (follow setGiuglianoExtraFields). -func (w *worker) writeReservedGasUsed(env *environment, registry reservedRegistry) error { - // BlockExtraData is only RLP-encoded into Header.Extra post-Cancun. - if registry == nil || !w.chainConfig.IsCancun(env.header.Number) { +// header's BlockExtraData, overwriting the placeholder zero that Prepare +// stamped. Post-fork headers must carry the field (verifyReservedFields), so +// it is written on every post-fork build — including interrupted ones, whose +// partial blocks still seal. +func (w *worker) writeReservedGasUsed(env *environment) error { + if w.chainConfig.Bor == nil || !w.chainConfig.Bor.IsReservedBlockspace(env.header.Number) { return nil } diff --git a/params/config.go b/params/config.go index aea7af9a87..f0dccc73fe 100644 --- a/params/config.go +++ b/params/config.go @@ -749,20 +749,19 @@ var ( BurntContract: map[string]string{"0": "0x000000000000000000000000000000000000dead"}, BlockAlloc: map[string]interface{}{}, // Bor hard forks - JaipurBlock: big.NewInt(0), - DelhiBlock: big.NewInt(0), - IndoreBlock: big.NewInt(0), - AhmedabadBlock: big.NewInt(0), - BhilaiBlock: big.NewInt(0), - RioBlock: big.NewInt(0), - MadhugiriBlock: big.NewInt(0), - MadhugiriProBlock: big.NewInt(0), - DandeliBlock: big.NewInt(0), - LisovoBlock: big.NewInt(0), - LisovoProBlock: big.NewInt(0), - ChicagoBlock: big.NewInt(0), - ValenciaBlock: big.NewInt(0), - ReservedBlockspaceBlock: big.NewInt(0), + JaipurBlock: big.NewInt(0), + DelhiBlock: big.NewInt(0), + IndoreBlock: big.NewInt(0), + AhmedabadBlock: big.NewInt(0), + BhilaiBlock: big.NewInt(0), + RioBlock: big.NewInt(0), + MadhugiriBlock: big.NewInt(0), + MadhugiriProBlock: big.NewInt(0), + DandeliBlock: big.NewInt(0), + LisovoBlock: big.NewInt(0), + LisovoProBlock: big.NewInt(0), + ChicagoBlock: big.NewInt(0), + ValenciaBlock: big.NewInt(0), }, } diff --git a/tests/bor/reserved_blockspace_test.go b/tests/bor/reserved_blockspace_test.go index 7554fd17fa..51d4a6b4d1 100644 --- a/tests/bor/reserved_blockspace_test.go +++ b/tests/bor/reserved_blockspace_test.go @@ -305,10 +305,9 @@ func TestReservedBlockspaceZeroFeeProduction(t *testing.T) { } t.Logf("reserved zero-fee tx settled in canonical block %d on both nodes", includedBlock.NumberU64()) - // The producing block must carry the reserved header fields (set by Prepare). - rc, rg := includedBlock.Header().GetReservedInfo(genesis.Config) - if rc == nil || rg == nil { - t.Fatalf("block %d missing reserved header fields: count=%v gas=%v", includedBlock.NumberU64(), rc, rg) + // The producing block must carry the reserved header field (set by Prepare). + if rg := includedBlock.Header().GetReservedGasUsed(genesis.Config); rg == nil { + t.Fatalf("block %d missing reserved gas used header field", includedBlock.NumberU64()) } // The reserved sender paid only its call value — no gas was debited — and both