diff --git a/api/api_eth.go b/api/api_eth.go index 901df8afa..a888c21b0 100644 --- a/api/api_eth.go +++ b/api/api_eth.go @@ -34,6 +34,7 @@ import ( "time" "github.com/kaiachain/kaia/blockchain" + "github.com/kaiachain/kaia/blockchain/forkid" "github.com/kaiachain/kaia/blockchain/state" "github.com/kaiachain/kaia/blockchain/types" "github.com/kaiachain/kaia/blockchain/vm" @@ -1547,3 +1548,67 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH prevTracer = tracer } } + +type config struct { + // As mentioned in KIP-276, Kaia doesn't include ActivationTime as a field. + // ActivationTime uint64 `json:"activationTime"` + BlobSchedule *params.BlobConfig `json:"blobSchedule"` + ChainId *hexutil.Big `json:"chainId"` + ForkId hexutil.Bytes `json:"forkId"` + Precompiles map[string]common.Address `json:"precompiles"` + SystemContracts map[string]common.Address `json:"systemContracts"` +} + +type configResponse struct { + Current *config `json:"current"` + Next *config `json:"next"` + Last *config `json:"last"` +} + +// Config implements the KIP-276(EIP-7910) eth_config method. +func (api *EthAPI) Config(ctx context.Context) (*configResponse, error) { + b := api.kaiaBlockChainAPI.b + genesis, err := b.HeaderByNumber(ctx, 0) + if err != nil { + return nil, fmt.Errorf("unable to load genesis: %w", err) + } + assemble := func(c *params.ChainConfig, head *big.Int) *config { + if head == nil { + return nil + } + + var ( + rules = c.Rules(head) + precompiles = make(map[string]common.Address) + ) + for addr, c := range vm.ActivePrecompiledContracts(rules) { + precompiles[c.Name()] = addr + } + + id := forkid.NewID(c, genesis.Hash(), head.Uint64()).Hash + return &config{ + BlobSchedule: c.BlobConfig(head.Uint64()), + ChainId: (*hexutil.Big)(c.ChainID), + ForkId: id[:], + Precompiles: precompiles, + SystemContracts: b.GetActiveSystemContracts(c, genesis.Hash(), head), + } + } + var ( + c = b.ChainConfig() + bn = b.CurrentBlock().Number() + ) + currentForkCompatibleBlock := forkid.LatestForkCompatibleBlock(c, bn) + nextForkCompatibleBlock := forkid.NextForkCompatibleBlock(c, bn) + lastForkCompatibleBlock := forkid.LastForkCompatibleBlock(c) + resp := configResponse{ + Next: assemble(c, nextForkCompatibleBlock), + Current: assemble(c, currentForkCompatibleBlock), + Last: assemble(c, lastForkCompatibleBlock), + } + // Nil out last if no future-fork is configured. + if resp.Next == nil { + resp.Last = nil + } + return &resp, nil +} diff --git a/api/api_eth_test.go b/api/api_eth_test.go index 3df0ddb92..43c86df32 100644 --- a/api/api_eth_test.go +++ b/api/api_eth_test.go @@ -16,6 +16,7 @@ import ( mock_accounts "github.com/kaiachain/kaia/accounts/mocks" mock_api "github.com/kaiachain/kaia/api/mocks" "github.com/kaiachain/kaia/blockchain" + "github.com/kaiachain/kaia/blockchain/forkid" "github.com/kaiachain/kaia/blockchain/state" "github.com/kaiachain/kaia/blockchain/types" "github.com/kaiachain/kaia/blockchain/types/accountkey" @@ -2688,3 +2689,163 @@ func TestEthAPI_EstimateGas(t *testing.T) { return api.EstimateGas(context.Background(), args, nil, overrides) }) } + +// TestEthAPI_Config tests the eth_config method. +func TestEthAPI_Config(t *testing.T) { + expectedChainConfig := params.TestChainConfig + expectedChainConfig.IstanbulCompatibleBlock = big.NewInt(75373312) + expectedChainConfig.LondonCompatibleBlock = big.NewInt(80295291) + expectedChainConfig.PragueCompatibleBlock = big.NewInt(187930000) + genesisHeader := &types.Header{ + Number: big.NewInt(0), + Root: common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), + } + genesisForkID := forkid.NewID(expectedChainConfig, genesisHeader.Hash(), 0).Hash + istanbulForkID := forkid.NewID(expectedChainConfig, genesisHeader.Hash(), 75373312).Hash + lonondonForkID := forkid.NewID(expectedChainConfig, genesisHeader.Hash(), 80295291).Hash + pragueForkID := forkid.NewID(expectedChainConfig, genesisHeader.Hash(), 187930000).Hash + var ( + genesisRules = expectedChainConfig.Rules(big.NewInt(0)) + istanbulRules = expectedChainConfig.Rules(big.NewInt(75373312)) + lonondonRules = expectedChainConfig.Rules(big.NewInt(80295291)) + pragueRules = expectedChainConfig.Rules(big.NewInt(187930000)) + genesisPrecompiles = make(map[string]common.Address) + istanbulPrecompiles = make(map[string]common.Address) + lonondonPrecompiles = make(map[string]common.Address) + praguePrecompiles = make(map[string]common.Address) + ) + for addr, c := range vm.ActivePrecompiledContracts(genesisRules) { + genesisPrecompiles[c.Name()] = addr + } + for addr, c := range vm.ActivePrecompiledContracts(istanbulRules) { + istanbulPrecompiles[c.Name()] = addr + } + for addr, c := range vm.ActivePrecompiledContracts(lonondonRules) { + lonondonPrecompiles[c.Name()] = addr + } + for addr, c := range vm.ActivePrecompiledContracts(pragueRules) { + praguePrecompiles[c.Name()] = addr + } + tests := []struct { + name string + blockNumber uint64 + expectedCurrent *config + expectedNext *config + expectedLast *config + }{ + { + name: "Genesis block", + blockNumber: 0, + expectedCurrent: &config{ + BlobSchedule: nil, + ChainId: (*hexutil.Big)(expectedChainConfig.ChainID), + ForkId: genesisForkID[:], + Precompiles: genesisPrecompiles, + SystemContracts: nil, + }, + expectedNext: &config{ + BlobSchedule: nil, + ChainId: (*hexutil.Big)(expectedChainConfig.ChainID), + ForkId: istanbulForkID[:], + Precompiles: istanbulPrecompiles, + SystemContracts: nil, + }, + expectedLast: &config{ + BlobSchedule: nil, + ChainId: (*hexutil.Big)(expectedChainConfig.ChainID), + ForkId: pragueForkID[:], + Precompiles: praguePrecompiles, + SystemContracts: nil, + }, + }, + { + name: "Istanbul block", + blockNumber: 75373312, + expectedCurrent: &config{ + BlobSchedule: nil, + ChainId: (*hexutil.Big)(expectedChainConfig.ChainID), + ForkId: istanbulForkID[:], + Precompiles: istanbulPrecompiles, + SystemContracts: nil, + }, + expectedNext: &config{ + BlobSchedule: nil, + ChainId: (*hexutil.Big)(expectedChainConfig.ChainID), + ForkId: lonondonForkID[:], + Precompiles: lonondonPrecompiles, + SystemContracts: nil, + }, + expectedLast: &config{ + BlobSchedule: nil, + ChainId: (*hexutil.Big)(expectedChainConfig.ChainID), + ForkId: pragueForkID[:], + Precompiles: praguePrecompiles, + SystemContracts: nil, + }, + }, + { + name: "Latest fork block (Prague)", + blockNumber: 187930000, + expectedCurrent: &config{ + BlobSchedule: nil, + ChainId: (*hexutil.Big)(expectedChainConfig.ChainID), + ForkId: pragueForkID[:], + Precompiles: praguePrecompiles, + SystemContracts: nil, + }, + expectedNext: nil, // No more forks + expectedLast: nil, // Should be nil when Next is nil + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCtrl, mockBackend, api := testInitForEthApi(t) + defer mockCtrl.Finish() + + // Mock chain config + mockBackend.EXPECT().ChainConfig().Return(expectedChainConfig).AnyTimes() + + // Mock genesis block (block 0) - this is called first in Config method + mockBackend.EXPECT().HeaderByNumber(gomock.Any(), gomock.Any()).Return( + genesisHeader, + nil, + ) + + // Mock current block - this is called after genesis + mockBackend.EXPECT().CurrentBlock().Return( + types.NewBlockWithHeader(&types.Header{Number: new(big.Int).SetUint64(tt.blockNumber)}), + ) + mockBackend.EXPECT().GetActiveSystemContracts(gomock.Any(), gomock.Any(), gomock.Any()).Return( + nil, + ).AnyTimes() + + // Call the method + config, err := api.Config(context.Background()) + require.NoError(t, err) + require.NotNil(t, config) + + // Validate configs + assert.Equal(t, tt.expectedCurrent, config.Current) + assert.Equal(t, tt.expectedNext, config.Next) + assert.Equal(t, tt.expectedLast, config.Last) + }) + } +} + +func TestEthAPI_Config_ErrorCases(t *testing.T) { + t.Run("Genesis block not found", func(t *testing.T) { + mockCtrl, mockBackend, api := testInitForEthApi(t) + defer mockCtrl.Finish() + + mockBackend.EXPECT().ChainConfig().Return(params.KairosChainConfig).AnyTimes() + mockBackend.EXPECT().HeaderByNumber(gomock.Any(), gomock.Any()).Return( + nil, errors.New("genesis block not found"), + ) + + config, err := api.Config(context.Background()) + assert.Error(t, err) + assert.Nil(t, config) + assert.Contains(t, err.Error(), "unable to load genesis") + }) +} diff --git a/api/backend.go b/api/backend.go index 9c1cf2f6e..edb0310ff 100644 --- a/api/backend.go +++ b/api/backend.go @@ -94,6 +94,7 @@ type Backend interface { TxPoolContent() (map[common.Address]types.Transactions, map[common.Address]types.Transactions) SubscribeNewTxsEvent(chan<- blockchain.NewTxsEvent) event.Subscription + GetActiveSystemContracts(c *params.ChainConfig, genesis common.Hash, head *big.Int) map[string]common.Address ChainConfig() *params.ChainConfig CurrentBlock() *types.Block diff --git a/api/mocks/backend_mock.go b/api/mocks/backend_mock.go index c90ecf1c4..ab5cd0963 100644 --- a/api/mocks/backend_mock.go +++ b/api/mocks/backend_mock.go @@ -195,6 +195,20 @@ func (mr *MockBackendMockRecorder) FeeHistory(arg0, arg1, arg2, arg3 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FeeHistory", reflect.TypeOf((*MockBackend)(nil).FeeHistory), arg0, arg1, arg2, arg3) } +// GetActiveSystemContracts mocks base method. +func (m *MockBackend) GetActiveSystemContracts(arg0 *params.ChainConfig, arg1 common.Hash, arg2 *big.Int) map[string]common.Address { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActiveSystemContracts", arg0, arg1, arg2) + ret0, _ := ret[0].(map[string]common.Address) + return ret0 +} + +// GetActiveSystemContracts indicates an expected call of GetActiveSystemContracts. +func (mr *MockBackendMockRecorder) GetActiveSystemContracts(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSystemContracts", reflect.TypeOf((*MockBackend)(nil).GetActiveSystemContracts), arg0, arg1, arg2) +} + // GetBlockReceipts mocks base method. func (m *MockBackend) GetBlockReceipts(arg0 context.Context, arg1 common.Hash) types.Receipts { m.ctrl.T.Helper() diff --git a/blockchain/forkid/forkid.go b/blockchain/forkid/forkid.go new file mode 100644 index 000000000..79e41b3b3 --- /dev/null +++ b/blockchain/forkid/forkid.go @@ -0,0 +1,143 @@ +// Copyright 2025 The Kaia Authors +// This file is part of the Kaia library. +// +// The Kaia 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 Kaia 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 Kaia library. If not, see . + +package forkid + +import ( + "encoding/binary" + "hash/crc32" + "math/big" + "reflect" + "slices" + "strings" + + "github.com/kaiachain/kaia/common" + "github.com/kaiachain/kaia/params" +) + +// ID is a fork identifier as defined by EIP-2124. +type ID struct { + Hash [4]byte // CRC32 checksum of the genesis block and passed fork block numbers + Next uint64 // Block number of the next upcoming fork, or 0 if no forks are known +} + +// NewID calculates the Kaia fork ID from the chain config, genesis hash, head. +func NewID(config *params.ChainConfig, genesis common.Hash, head uint64) ID { + // Calculate the starting checksum from the genesis hash + hash := crc32.ChecksumIEEE(genesis[:]) + + // Calculate the current fork checksum and the next fork block + var next uint64 + for _, fork := range gatherForks(config) { + if fork <= head { + // Fork already passed, checksum the previous hash and the fork number + hash = checksumUpdate(hash, fork) + continue + } + next = fork + break + } + return ID{Hash: checksumToBytes(hash), Next: next} +} + +// LatestForkCompatibleBlock returns the latest fork compatible block or genesis(0) if no forks are known. +func LatestForkCompatibleBlock(config *params.ChainConfig, head *big.Int) *big.Int { + latestForkCompatibleBlock := common.Big0 + for _, fork := range gatherForks(config) { + if new(big.Int).SetUint64(fork).Cmp(head) <= 0 { + latestForkCompatibleBlock = new(big.Int).SetUint64(fork) + continue + } else { + break + } + } + return latestForkCompatibleBlock +} + +// NextForkCompatibleBlock returns the next fork compatible block or nil if no forks are known. +func NextForkCompatibleBlock(config *params.ChainConfig, head *big.Int) *big.Int { + var nextForkCompatibleBlock *big.Int + for _, fork := range gatherForks(config) { + if new(big.Int).SetUint64(fork).Cmp(head) <= 0 { + continue + } + nextForkCompatibleBlock = new(big.Int).SetUint64(fork) + break + } + return nextForkCompatibleBlock +} + +// LastForkCompatibleBlock returns the last fork compatible block or genesis(0) if no forks are known. +func LastForkCompatibleBlock(config *params.ChainConfig) *big.Int { + forks := gatherForks(config) + if len(forks) == 0 { + return common.Big0 + } + return new(big.Int).SetUint64(forks[len(forks)-1]) +} + +// checksumUpdate calculates the next IEEE CRC32 checksum based on the previous +// one and a fork block number (equivalent to CRC32(original-blob || fork)). +func checksumUpdate(hash uint32, fork uint64) uint32 { + var blob [8]byte + binary.BigEndian.PutUint64(blob[:], fork) + return crc32.Update(hash, crc32.IEEETable, blob[:]) +} + +// checksumToBytes converts a uint32 checksum into a [4]byte array. +func checksumToBytes(hash uint32) [4]byte { + var blob [4]byte + binary.BigEndian.PutUint32(blob[:], hash) + return blob +} + +// gatherForks gathers all the known forks and creates a sorted list out of them. +func gatherForks(config *params.ChainConfig) []uint64 { + // Gather all the fork block numbers via reflection + kind := reflect.TypeFor[params.ChainConfig]() + conf := reflect.ValueOf(config).Elem() + + var forks []uint64 + for i := 0; i < kind.NumField(); i++ { + // Fetch the next field and skip non-fork rules + field := kind.Field(i) + if !strings.HasSuffix(field.Name, "CompatibleBlock") { + continue + } + if field.Type != reflect.TypeFor[*big.Int]() { + continue + } + // Extract the fork rule block number and aggregate it + rule := conf.Field(i).Interface().(*big.Int) + if rule != nil { + forks = append(forks, rule.Uint64()) + } + } + // Sort the fork block numbers to permit chronologival XOR + slices.Sort(forks) + // Deduplicate block numbers applying multiple forks + for i := 1; i < len(forks); i++ { + if forks[i] == forks[i-1] { + forks = append(forks[:i], forks[i+1:]...) + i-- + } + } + // Skip any forks in block 0, that's the genesis ruleset + if len(forks) > 0 && forks[0] == 0 { + forks = forks[1:] + } + return forks +} diff --git a/blockchain/forkid/forkid_test.go b/blockchain/forkid/forkid_test.go new file mode 100644 index 000000000..6176bd911 --- /dev/null +++ b/blockchain/forkid/forkid_test.go @@ -0,0 +1,167 @@ +// Copyright 2025 The Kaia Authors +// This file is part of the Kaia library. +// +// The Kaia 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 Kaia 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 Kaia library. If not, see . + +package forkid + +import ( + "math/big" + "testing" + + "github.com/kaiachain/kaia/common" + "github.com/kaiachain/kaia/params" +) + +// TestCreation tests that different genesis and fork rule combinations result in +// the correct fork ID. +func TestCreation(t *testing.T) { + type testcase struct { + head uint64 + want ID + } + tests := []struct { + config *params.ChainConfig + genesis common.Hash + cases []testcase + }{ + // Mainnet test cases + { + params.MainnetChainConfig, + params.MainnetGenesisHash, + []testcase{ + {0, ID{Hash: checksumToBytes(0xdd58eb45), Next: 86816005}}, // Unsynced + {86816004, ID{Hash: checksumToBytes(0xdd58eb45), Next: 86816005}}, // Last Genesis block + {86816005, ID{Hash: checksumToBytes(0x7b1131bb), Next: 99841497}}, // First Istanbul+London+EthTxType block + {99841496, ID{Hash: checksumToBytes(0x7b1131bb), Next: 99841497}}, // Last Istanbul+London+EthTxType block + {99841497, ID{Hash: checksumToBytes(0x8b3961e6), Next: 119750400}}, // First Magma block + {119750399, ID{Hash: checksumToBytes(0x8b3961e6), Next: 119750400}}, // Last Magma block + {119750400, ID{Hash: checksumToBytes(0x171d8904), Next: 135456000}}, // First Kore+Kip103 block + {135455999, ID{Hash: checksumToBytes(0x171d8904), Next: 135456000}}, // Last Kore+Kip103 block + {135456000, ID{Hash: checksumToBytes(0x68717c3d), Next: 147534000}}, // First Shanghai block + {147533999, ID{Hash: checksumToBytes(0x68717c3d), Next: 147534000}}, // Last Shanghai block + {147534000, ID{Hash: checksumToBytes(0x75771543), Next: 162900480}}, // First Cancun+Randao block + {162900479, ID{Hash: checksumToBytes(0x75771543), Next: 162900480}}, // Last Cancun+Randao block + {162900480, ID{Hash: checksumToBytes(0x3ab6dcda), Next: 190670000}}, // First Kaia+Kip160 block + {190669999, ID{Hash: checksumToBytes(0x3ab6dcda), Next: 190670000}}, // Last Kaia+Kip160 block + {190670000, ID{Hash: checksumToBytes(0xc00bab0e), Next: 0}}, // First Prague block + {198489578, ID{Hash: checksumToBytes(0xc00bab0e), Next: 0}}, // Today Prague block + }, + }, + // Kairos test cases + { + params.KairosChainConfig, + params.KairosGenesisHash, + []testcase{ + {0, ID{Hash: checksumToBytes(0x5e10f192), Next: 75373312}}, // Unsynced + {9, ID{Hash: checksumToBytes(0x5e10f192), Next: 75373312}}, // Last Genesis block + {75373312, ID{Hash: checksumToBytes(0x3640372e), Next: 80295291}}, // First Istanbul block + {80295290, ID{Hash: checksumToBytes(0x3640372e), Next: 80295291}}, // Last Istanbul block + {80295291, ID{Hash: checksumToBytes(0xb1d92301), Next: 86513895}}, // First London block + {86513894, ID{Hash: checksumToBytes(0xb1d92301), Next: 86513895}}, // Last London block + {86513895, ID{Hash: checksumToBytes(0xf2be985d), Next: 98347376}}, // First EthTxType block + {98347375, ID{Hash: checksumToBytes(0xf2be985d), Next: 98347376}}, // Last EthTxType block + {98347376, ID{Hash: checksumToBytes(0xf99f3ae7), Next: 111736800}}, // First Magma block + {111736799, ID{Hash: checksumToBytes(0xf99f3ae7), Next: 111736800}}, // Last Magma block + {111736800, ID{Hash: checksumToBytes(0x40c759b1), Next: 119145600}}, // First Kore block + {119145599, ID{Hash: checksumToBytes(0x40c759b1), Next: 119145600}}, // Last Kore block + {119145600, ID{Hash: checksumToBytes(0x989f8b4a), Next: 131608000}}, // First Kip103 block + {131607999, ID{Hash: checksumToBytes(0x989f8b4a), Next: 131608000}}, // Last Kip103 block + {131608000, ID{Hash: checksumToBytes(0xff2bb36d), Next: 141367000}}, // First Shanghai block + {141366999, ID{Hash: checksumToBytes(0xff2bb36d), Next: 141367000}}, // Last Shanghai block + {141367000, ID{Hash: checksumToBytes(0x897592ea), Next: 156660000}}, // First Cancun+Randao block + {156659999, ID{Hash: checksumToBytes(0x897592ea), Next: 156660000}}, // Last Cancun+Randao block + {156660000, ID{Hash: checksumToBytes(0xf00fbab3), Next: 187930000}}, // First Kaia+Kip160 block + {187929999, ID{Hash: checksumToBytes(0xf00fbab3), Next: 187930000}}, // Last Kaia+Kip160 block + {187930000, ID{Hash: checksumToBytes(0x0d29cd98), Next: 0}}, // First Prague block + {198978293, ID{Hash: checksumToBytes(0x0d29cd98), Next: 0}}, // Last Prague block + }, + }, + } + for i, tt := range tests { + for j, ttt := range tt.cases { + if have := NewID(tt.config, tt.genesis, ttt.head); have != ttt.want { + t.Errorf("test %d, case %d: fork ID mismatch: have %x, want %x", i, j, have, ttt.want) + } + } + } +} + +func TestCheckForkCompatibleBlock(t *testing.T) { + type expectedNums struct { + currentForkCompatibleBlock *big.Int + nextForkCompatibleBlock *big.Int + lastForkCompatibleBlock *big.Int + } + type testcase struct { + head uint64 + expectedNums expectedNums + } + tests := []struct { + config *params.ChainConfig + cases []testcase + }{ + // Mainnet test cases + { + params.MainnetChainConfig, + []testcase{ + {0, expectedNums{big.NewInt(0), big.NewInt(86816005), big.NewInt(190670000)}}, // head is Genesis block + {86816005, expectedNums{big.NewInt(86816005), big.NewInt(99841497), big.NewInt(190670000)}}, // head is Istanbul+London+EthTxType block + {99841497, expectedNums{big.NewInt(99841497), big.NewInt(119750400), big.NewInt(190670000)}}, // head is Magma block + {119750400, expectedNums{big.NewInt(119750400), big.NewInt(135456000), big.NewInt(190670000)}}, // head is Kore+Kip103 block + {135456000, expectedNums{big.NewInt(135456000), big.NewInt(147534000), big.NewInt(190670000)}}, // head is Shanghai block + {147534000, expectedNums{big.NewInt(147534000), big.NewInt(162900480), big.NewInt(190670000)}}, // head is Cancun+Randao block + {162900480, expectedNums{big.NewInt(162900480), big.NewInt(190670000), big.NewInt(190670000)}}, // head is Kaia+Kip160 block + {190670000, expectedNums{big.NewInt(190670000), nil, big.NewInt(190670000)}}, // head is Prague block + }, + }, + // Kairos test cases + { + params.KairosChainConfig, + []testcase{ + {0, expectedNums{big.NewInt(0), big.NewInt(75373312), big.NewInt(187930000)}}, // head is Genesis block + {75373312, expectedNums{big.NewInt(75373312), big.NewInt(80295291), big.NewInt(187930000)}}, // head is Istanbul block + {80295291, expectedNums{big.NewInt(80295291), big.NewInt(86513895), big.NewInt(187930000)}}, // head is London block + {86513895, expectedNums{big.NewInt(86513895), big.NewInt(98347376), big.NewInt(187930000)}}, // head is EthTxType block + {98347376, expectedNums{big.NewInt(98347376), big.NewInt(111736800), big.NewInt(187930000)}}, // head is Magma block + {111736800, expectedNums{big.NewInt(111736800), big.NewInt(119145600), big.NewInt(187930000)}}, // head is Kore block + {119145600, expectedNums{big.NewInt(119145600), big.NewInt(131608000), big.NewInt(187930000)}}, // head is Kip103 block + {131608000, expectedNums{big.NewInt(131608000), big.NewInt(141367000), big.NewInt(187930000)}}, // head is Shanghai block + {141367000, expectedNums{big.NewInt(141367000), big.NewInt(156660000), big.NewInt(187930000)}}, // head is Cancun+Randao block + {156660000, expectedNums{big.NewInt(156660000), big.NewInt(187930000), big.NewInt(187930000)}}, // head is Kaia+Kip160 block + {187930000, expectedNums{big.NewInt(187930000), nil, big.NewInt(187930000)}}, // head is Prague block + }, + }, + } + for i, tt := range tests { + for j, ttt := range tt.cases { + latestForkCompatibleBlock := LatestForkCompatibleBlock(tt.config, new(big.Int).SetUint64(ttt.head)) + nextForkCompatibleBlock := NextForkCompatibleBlock(tt.config, new(big.Int).SetUint64(ttt.head)) + lastForkCompatibleBlock := LastForkCompatibleBlock(tt.config) + if latestForkCompatibleBlock.Cmp(ttt.expectedNums.currentForkCompatibleBlock) != 0 { + t.Errorf("test %d, case %d: latest fork compatible block mismatch: have %x, want %x", i, j, latestForkCompatibleBlock, ttt.expectedNums.currentForkCompatibleBlock) + } + if nextForkCompatibleBlock == nil { + if ttt.expectedNums.nextForkCompatibleBlock != nil { + t.Errorf("test %d, case %d: next fork compatible block is nil, but expected %x", i, j, ttt.expectedNums.nextForkCompatibleBlock) + } + } else if nextForkCompatibleBlock.Cmp(ttt.expectedNums.nextForkCompatibleBlock) != 0 { + t.Errorf("test %d, case %d: next fork compatible block mismatch: have %x, want %x", i, j, nextForkCompatibleBlock, ttt.expectedNums.nextForkCompatibleBlock) + } + if lastForkCompatibleBlock.Cmp(ttt.expectedNums.lastForkCompatibleBlock) != 0 { + t.Errorf("test %d, case %d: last fork compatible block mismatch: have %x, want %x", i, j, lastForkCompatibleBlock, ttt.expectedNums.lastForkCompatibleBlock) + } + } + } +} diff --git a/blockchain/system/util.go b/blockchain/system/util.go new file mode 100644 index 000000000..d4a496f33 --- /dev/null +++ b/blockchain/system/util.go @@ -0,0 +1,51 @@ +// Copyright 2025 The Kaia Authors +// This file is part of the Kaia library. +// +// The Kaia 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 Kaia 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 Kaia library. If not, see . + +package system + +import ( + "math/big" + + "github.com/kaiachain/kaia/common" + "github.com/kaiachain/kaia/params" +) + +// ActiveSystemContracts returns the currently active system contracts at the +// given block number. +func ActiveSystemContracts(c *params.ChainConfig, genesis common.Hash, head *big.Int) map[string]common.Address { + active := make(map[string]common.Address) + + if c.IsPragueForkEnabled(head) { + active["HISTORY_STORAGE_ADDRESS"] = params.HistoryStorageAddress + } + if c.IsKip160ForkEnabled(head) { + active["KIP160"] = c.Kip160ContractAddress + } + if c.IsRandaoForkEnabled(head) { + active["REGISTRY"] = RegistryAddr + } + if c.IsKip103ForkEnabled(head) { + active["KIP103"] = c.Kip103ContractAddress + } + + // These contracts are active from genesis. + if genesis == params.MainnetGenesisHash { + active["MAINNET_CREDIT"] = MainnetCreditAddr + } + active["ADDRESS_BOOK"] = AddressBookAddr + + return active +} diff --git a/blockchain/system/util_test.go b/blockchain/system/util_test.go new file mode 100644 index 000000000..0d1faac0f --- /dev/null +++ b/blockchain/system/util_test.go @@ -0,0 +1,117 @@ +// Copyright 2025 The Kaia Authors +// This file is part of the Kaia library. +// +// The Kaia 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 Kaia 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 Kaia library. If not, see . + +package system + +import ( + "math/big" + "testing" + + "github.com/kaiachain/kaia/common" + "github.com/kaiachain/kaia/params" + "github.com/stretchr/testify/assert" +) + +func TestActiveSystemContracts(t *testing.T) { + mainnetConfig := params.MainnetChainConfig + mainnetConfig.Kip160ContractAddress = common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + mainnetConfig.Kip103ContractAddress = common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + + cases := []struct { + name string + head *big.Int + chainConfig *params.ChainConfig + genesisHash common.Hash + expected map[string]common.Address + }{ + { + name: "Genesis block - testnet", + head: big.NewInt(0), + chainConfig: params.KairosChainConfig, + genesisHash: params.KairosGenesisHash, + expected: map[string]common.Address{ + "ADDRESS_BOOK": AddressBookAddr, + }, + }, + { + name: "Genesis block - mainnet", + head: big.NewInt(0), + chainConfig: mainnetConfig, + genesisHash: params.MainnetGenesisHash, + expected: map[string]common.Address{ + "MAINNET_CREDIT": MainnetCreditAddr, + "ADDRESS_BOOK": AddressBookAddr, + }, + }, + { + name: "Kip103 head - mainnet", + head: big.NewInt(119750400), + chainConfig: mainnetConfig, + genesisHash: params.MainnetGenesisHash, + expected: map[string]common.Address{ + "KIP103": mainnetConfig.Kip103ContractAddress, + "MAINNET_CREDIT": MainnetCreditAddr, + "ADDRESS_BOOK": AddressBookAddr, + }, + }, + { + name: "Randao head - mainnet", + head: big.NewInt(147534000), + chainConfig: mainnetConfig, + genesisHash: params.MainnetGenesisHash, + expected: map[string]common.Address{ + "REGISTRY": RegistryAddr, + "KIP103": mainnetConfig.Kip103ContractAddress, + "MAINNET_CREDIT": MainnetCreditAddr, + "ADDRESS_BOOK": AddressBookAddr, + }, + }, + { + name: "Kip160 head - mainnet", + head: big.NewInt(162900480), + chainConfig: mainnetConfig, + genesisHash: params.MainnetGenesisHash, + expected: map[string]common.Address{ + "KIP160": mainnetConfig.Kip160ContractAddress, + "REGISTRY": RegistryAddr, + "KIP103": mainnetConfig.Kip103ContractAddress, + "MAINNET_CREDIT": MainnetCreditAddr, + "ADDRESS_BOOK": AddressBookAddr, + }, + }, + { + name: "Prague head - mainnet", + head: big.NewInt(190670000), + chainConfig: mainnetConfig, + genesisHash: params.MainnetGenesisHash, + expected: map[string]common.Address{ + "HISTORY_STORAGE_ADDRESS": params.HistoryStorageAddress, + "KIP160": mainnetConfig.Kip160ContractAddress, + "REGISTRY": RegistryAddr, + "KIP103": mainnetConfig.Kip103ContractAddress, + "MAINNET_CREDIT": MainnetCreditAddr, + "ADDRESS_BOOK": AddressBookAddr, + }, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + actual := ActiveSystemContracts(tt.chainConfig, tt.genesisHash, tt.head) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/blockchain/vm/contracts.go b/blockchain/vm/contracts.go index 324921435..782ad74ff 100644 --- a/blockchain/vm/contracts.go +++ b/blockchain/vm/contracts.go @@ -75,6 +75,8 @@ type PrecompiledContract interface { // Run runs the precompiled contract // contract, evm is only exists in Kaia, those are not used in go-ethereum Run(input []byte, contract *Contract, evm *EVM) ([]byte, error) + + Name() string } // PrecompiledContractsByzantium contains the default set of pre-compiled Kaia @@ -233,19 +235,9 @@ func init() { // ActivePrecompiles returns the precompiles enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { var precompiledContractAddrs []common.Address - switch { - case rules.IsOsaka: - precompiledContractAddrs = PrecompiledAddressOsaka - case rules.IsPrague: - precompiledContractAddrs = PrecompiledAddressPrague - case rules.IsCancun: - precompiledContractAddrs = PrecompiledAddressCancun - case rules.IsIstanbul: - precompiledContractAddrs = PrecompiledAddressIstanbul - default: - precompiledContractAddrs = PrecompiledAddressesByzantium + for addr := range ActivePrecompiledContracts(rules) { + precompiledContractAddrs = append(precompiledContractAddrs, addr) } - // After istanbulCompatible hf, need to support for vmversion0 contracts, too. // VmVersion0 contracts are deployed before istanbulCompatible and they use byzantiumCompatible precompiled contracts. // VmVersion0 contracts are the contracts deployed before istanbulCompatible hf. @@ -257,6 +249,26 @@ func ActivePrecompiles(rules params.Rules) []common.Address { } } +// ActivePrecompiledContracts returns the precompiled contracts enabled with the current configuration. +// This function doesn't support for vmversion0 contracts, it only supports for istanbulCompatible hf. +func ActivePrecompiledContracts(rules params.Rules) map[common.Address]PrecompiledContract { + var precompiledContracts map[common.Address]PrecompiledContract + switch { + case rules.IsOsaka: + precompiledContracts = PrecompiledContractsOsaka + case rules.IsPrague: + precompiledContracts = PrecompiledContractsPrague + case rules.IsCancun: + precompiledContracts = PrecompiledContractsCancun + case rules.IsIstanbul: + precompiledContracts = PrecompiledContractsIstanbul + default: + precompiledContracts = PrecompiledContractsByzantium + } + + return precompiledContracts +} + // RunPrecompiledContract runs and evaluates the output of a precompiled contract. func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract, evm *EVM) (ret []byte, computationCost uint64, err error) { gas, computationCost := p.GetRequiredGasAndComputationCost(input) @@ -305,6 +317,10 @@ func (c *ecrecover) Run(input []byte, contract *Contract, evm *EVM) ([]byte, err return common.LeftPadBytes(crypto.Keccak256(pubKey[1:])[12:], 32), nil } +func (c *ecrecover) Name() string { + return "ECREC" +} + // SHA256 implemented as a native contract. type sha256hash struct{} @@ -325,6 +341,10 @@ func (c *sha256hash) Run(input []byte, contract *Contract, evm *EVM) ([]byte, er return h[:], nil } +func (c *sha256hash) Name() string { + return "SHA256" +} + // RIPEMD160 implemented as a native contract. type ripemd160hash struct{} @@ -346,6 +366,10 @@ func (c *ripemd160hash) Run(input []byte, contract *Contract, evm *EVM) ([]byte, return common.LeftPadBytes(ripemd.Sum(nil), 32), nil } +func (c *ripemd160hash) Name() string { + return "RIPEMD160" +} + // data copy implemented as a native contract. type dataCopy struct{} @@ -364,6 +388,10 @@ func (c *dataCopy) Run(in []byte, contract *Contract, evm *EVM) ([]byte, error) return in, nil } +func (c *dataCopy) Name() string { + return "ID" +} + // bigModExp implements a native big integer exponential modular operation. type bigModExp struct { eip2565 bool @@ -371,6 +399,10 @@ type bigModExp struct { eip7883 bool } +func (c *bigModExp) Name() string { + return "MODEXP" +} + // byzantiumMultComplexity implements the bigModexp multComplexity formula, as defined in EIP-198. // // def mult_complexity(x): @@ -685,6 +717,10 @@ func (c *bn256AddIstanbul) Run(input []byte, contract *Contract, evm *EVM) ([]by return runBn256Add(input) } +func (c *bn256AddIstanbul) Name() string { + return "BN254_ADD" +} + // bn256AddByzantium implements a native elliptic curve point addition // conforming to Byzantium consensus rules. type bn256AddByzantium struct{} @@ -697,6 +733,10 @@ func (c *bn256AddByzantium) Run(input []byte, contract *Contract, evm *EVM) ([]b return runBn256Add(input) } +func (c *bn256AddByzantium) Name() string { + return "BN254_ADD" +} + // runBn256ScalarMul implements the Bn256ScalarMul precompile, referenced by // both Constantionple and Istanbul operations. func runBn256ScalarMul(input []byte) ([]byte, error) { @@ -721,6 +761,10 @@ func (c *bn256ScalarMulIstanbul) Run(input []byte, contract *Contract, evm *EVM) return runBn256ScalarMul(input) } +func (c *bn256ScalarMulIstanbul) Name() string { + return "BN254_MUL" +} + // bn256ScalarMulByzantium implements a native elliptic curve scalar // multiplication conforming to Byzantium consensus rules. type bn256ScalarMulByzantium struct{} @@ -733,6 +777,10 @@ func (c *bn256ScalarMulByzantium) Run(input []byte, contract *Contract, evm *EVM return runBn256ScalarMul(input) } +func (c *bn256ScalarMulByzantium) Name() string { + return "BN254_MUL" +} + var ( // true32Byte is returned if the bn256 pairing check succeeds. true32Byte = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} @@ -789,6 +837,10 @@ func (c *bn256PairingIstanbul) Run(input []byte, contract *Contract, evm *EVM) ( return runBn256Pairing(input) } +func (c *bn256PairingIstanbul) Name() string { + return "BN254_PAIRING" +} + // bn256PairingByzantium implements a pairing pre-compile for the bn256 curve // conforming to Byzantium consensus rules. type bn256PairingByzantium struct{} @@ -803,6 +855,10 @@ func (c *bn256PairingByzantium) Run(input []byte, contract *Contract, evm *EVM) return runBn256Pairing(input) } +func (c *bn256PairingByzantium) Name() string { + return "BN254_PAIRING" +} + type blake2F struct{} const ( @@ -865,6 +921,10 @@ func (c *blake2F) Run(input []byte, contract *Contract, evm *EVM) ([]byte, error return output, nil } +func (c *blake2F) Name() string { + return "BLAKE2F" +} + // kzgPointEvaluation implements the EIP-4844 point evaluation precompile. type kzgPointEvaluation struct{} @@ -921,6 +981,10 @@ func (b *kzgPointEvaluation) Run(input []byte, contract *Contract, evm *EVM) ([] return common.Hex2Bytes(blobPrecompileReturnValue), nil } +func (b *kzgPointEvaluation) Name() string { + return "KZG_POINT_EVALUATION" +} + // kZGToVersionedHash implements kzg_to_versioned_hash from EIP-4844 func kZGToVersionedHash(kzg kzg4844.Commitment) common.Hash { h := sha256.Sum256(kzg[:]) @@ -953,6 +1017,10 @@ func (c *vmLog) Run(input []byte, contract *Contract, evm *EVM) ([]byte, error) return nil, nil } +func (c *vmLog) Name() string { + return "VMLOG" +} + type feePayer struct{} func (c *feePayer) GetRequiredGasAndComputationCost(input []byte) (uint64, uint64) { @@ -963,6 +1031,10 @@ func (c *feePayer) Run(input []byte, contract *Contract, evm *EVM) ([]byte, erro return contract.FeePayerAddress.Bytes(), nil } +func (c *feePayer) Name() string { + return "FEE_PAYER" +} + type validateSender struct{} func (c *validateSender) GetRequiredGasAndComputationCost(input []byte) (uint64, uint64) { @@ -981,6 +1053,10 @@ func (c *validateSender) Run(input []byte, contract *Contract, evm *EVM) ([]byte return []byte{1}, nil } +func (c *validateSender) Name() string { + return "VALIDATE_SENDER" +} + func (c *validateSender) validateSender(input []byte, picker types.AccountKeyPicker, currentBlockNumber uint64) error { ptr := input @@ -1068,6 +1144,10 @@ func (c *bls12381G1Add) Run(input []byte, contract *Contract, evm *EVM) ([]byte, return encodePointG1(p0), nil } +func (c *bls12381G1Add) Name() string { + return "BLS12_G1ADD" +} + // bls12381G1MultiExp implements EIP-2537 G1MultiExp precompile. type bls12381G1MultiExp struct{} @@ -1129,6 +1209,10 @@ func (c *bls12381G1MultiExp) Run(input []byte, contract *Contract, evm *EVM) ([] return encodePointG1(r), nil } +func (c *bls12381G1MultiExp) Name() string { + return "BLS12_G1MSM" +} + // bls12381G2Add implements EIP-2537 G2Add precompile. type bls12381G2Add struct{} @@ -1166,6 +1250,10 @@ func (c *bls12381G2Add) Run(input []byte, contract *Contract, evm *EVM) ([]byte, return encodePointG2(r), nil } +func (c *bls12381G2Add) Name() string { + return "BLS12_G2ADD" +} + // bls12381G2MultiExp implements EIP-2537 G2MultiExp precompile. type bls12381G2MultiExp struct{} @@ -1227,6 +1315,10 @@ func (c *bls12381G2MultiExp) Run(input []byte, contract *Contract, evm *EVM) ([] return encodePointG2(r), nil } +func (c *bls12381G2MultiExp) Name() string { + return "BLS12_G2MSM" +} + // bls12381Pairing implements EIP-2537 Pairing precompile. type bls12381Pairing struct{} @@ -1292,6 +1384,10 @@ func (c *bls12381Pairing) Run(input []byte, contract *Contract, evm *EVM) ([]byt return out, nil } +func (c *bls12381Pairing) Name() string { + return "BLS12_PAIRING_CHECK" +} + func decodePointG1(in []byte) (*bls12381.G1Affine, error) { if len(in) != 128 { return nil, errors.New("invalid g1 point length") @@ -1410,6 +1506,10 @@ func (c *bls12381MapG1) Run(input []byte, contract *Contract, evm *EVM) ([]byte, return encodePointG1(&r), nil } +func (c *bls12381MapG1) Name() string { + return "BLS12_MAP_FP_TO_G1" +} + // bls12381MapG2 implements EIP-2537 MapG2 precompile. type bls12381MapG2 struct{} @@ -1443,6 +1543,10 @@ func (c *bls12381MapG2) Run(input []byte, contract *Contract, evm *EVM) ([]byte, return encodePointG2(&r), nil } +func (c *bls12381MapG2) Name() string { + return "BLS12_MAP_FP2_TO_G2" +} + // consoleLog implements solidity console.log for local networks. type consoleLog struct{} @@ -1458,6 +1562,10 @@ func (c *consoleLog) Run(input []byte, contract *Contract, evm *EVM) ([]byte, er return nil, nil } +func (c *consoleLog) Name() string { + return "CONSOLE_LOG" +} + // Hardhat console.log accepts format string, however we don't support it. // Instead, we just join all the parameters with a space. func (c *consoleLog) toLogString(input []byte) (string, error) { @@ -1577,3 +1685,7 @@ func (c *p256Verify) Run(input []byte, contract *Contract, evm *EVM) ([]byte, er } return nil, nil } + +func (c *p256Verify) Name() string { + return "P256VERIFY" +} diff --git a/console/web3ext/web3ext.go b/console/web3ext/web3ext.go index 0b4bc622d..e7acda563 100644 --- a/console/web3ext/web3ext.go +++ b/console/web3ext/web3ext.go @@ -185,6 +185,11 @@ web3._extend({ params: 3, inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter, null] }), + new web3._extend.Method({ + name: 'config', + call: 'eth_config', + params: 0, + }), ], properties: [ new web3._extend.Property({ diff --git a/node/cn/api_backend.go b/node/cn/api_backend.go index c4c4db464..8322b51fd 100644 --- a/node/cn/api_backend.go +++ b/node/cn/api_backend.go @@ -33,6 +33,7 @@ import ( "github.com/kaiachain/kaia/blockchain" "github.com/kaiachain/kaia/blockchain/bloombits" "github.com/kaiachain/kaia/blockchain/state" + "github.com/kaiachain/kaia/blockchain/system" "github.com/kaiachain/kaia/blockchain/types" "github.com/kaiachain/kaia/blockchain/vm" "github.com/kaiachain/kaia/common" @@ -72,6 +73,10 @@ func (b *CNAPIBackend) GetTxLookupInfoAndReceiptInCache(txHash common.Hash) (*ty return b.cn.blockchain.GetTxLookupInfoAndReceiptInCache(txHash) } +func (b *CNAPIBackend) GetActiveSystemContracts(c *params.ChainConfig, genesis common.Hash, head *big.Int) map[string]common.Address { + return system.ActiveSystemContracts(c, genesis, head) +} + func (b *CNAPIBackend) ChainConfig() *params.ChainConfig { return b.cn.chainConfig } diff --git a/params/config.go b/params/config.go index b82d8cf25..b248351e8 100644 --- a/params/config.go +++ b/params/config.go @@ -325,6 +325,13 @@ func (c *IstanbulConfig) String() string { return "istanbul" } +// TODO-Kaia Add BlobConfig +type BlobConfig struct{} + +func (c *ChainConfig) BlobConfig(head uint64) *BlobConfig { + return nil +} + // String implements the fmt.Stringer interface. func (c *ChainConfig) String() string { var engine interface{} @@ -428,6 +435,16 @@ func (c *ChainConfig) IsKaiaForkEnabled(num *big.Int) bool { return isForked(c.KaiaCompatibleBlock, num) } +// IsKip103ForkEnabled returns whether num is either equal to the kip103 block or greater. +func (c *ChainConfig) IsKip103ForkEnabled(num *big.Int) bool { + return isForked(c.Kip103CompatibleBlock, num) +} + +// IsKip160ForkEnabled returns whether num is either equal to the kip160 block or greater. +func (c *ChainConfig) IsKip160ForkEnabled(num *big.Int) bool { + return isForked(c.Kip160CompatibleBlock, num) +} + // IsRandaoForkEnabled returns whether num is either equal to the randao block or greater. func (c *ChainConfig) IsRandaoForkEnabled(num *big.Int) bool { return isForked(c.RandaoCompatibleBlock, num)