diff --git a/builder/files/genesis-amoy.json b/builder/files/genesis-amoy.json index 14da93891a..ad873696b7 100644 --- a/builder/files/genesis-amoy.json +++ b/builder/files/genesis-amoy.json @@ -29,6 +29,7 @@ "lisovoBlock": 33634700, "lisovoProBlock": 34062000, "giuglianoBlock": 35573500, + "chicagoBlock": 38358000, "skipValidatorByteCheck": [26160367, 26161087, 26171567, 26173743, 26175647], "stateSyncConfirmationDelay": { "0": 128 diff --git a/builder/files/genesis-mainnet-v1.json b/builder/files/genesis-mainnet-v1.json index 180140dd27..e5b099e78d 100644 --- a/builder/files/genesis-mainnet-v1.json +++ b/builder/files/genesis-mainnet-v1.json @@ -29,6 +29,7 @@ "lisovoBlock": 83756500, "lisovoProBlock": 83756500, "giuglianoBlock": 85268500, + "chicagoBlock": 87218600, "stateSyncConfirmationDelay": { "44934656": 128 }, diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index db168eb7c1..e15b006dbe 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -50,7 +50,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/time v0.12.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum index 43778f130f..f39a4a2c48 100644 --- a/cmd/keeper/go.sum +++ b/cmd/keeper/go.sum @@ -167,6 +167,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= diff --git a/core/forkid/forkid.go b/core/forkid/forkid.go index fc287a71ee..2b4623ffab 100644 --- a/core/forkid/forkid.go +++ b/core/forkid/forkid.go @@ -335,6 +335,7 @@ func GatherForks(config *params.ChainConfig, genesisTime uint64) (heightForks [] config.Bor.LisovoBlock, config.Bor.LisovoProBlock, config.Bor.GiuglianoBlock, + config.Bor.ChicagoBlock, } { if fork != nil { heightForks = append(heightForks, fork.Uint64()) diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 8fab06ae9c..abe998c1f9 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -269,7 +269,30 @@ var PrecompiledContractsLisovoPro = PrecompiledContracts{ common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{eip7951: true}, } +// PrecompiledContractsChicago contains the set of pre-compiled Ethereum +// contracts used in the Chicago release (bor HF). +var PrecompiledContractsChicago = PrecompiledContracts{ + common.BytesToAddress([]byte{0x01}): &ecrecover{}, + common.BytesToAddress([]byte{0x02}): &sha256hash{}, + common.BytesToAddress([]byte{0x03}): &ripemd160hash{}, + common.BytesToAddress([]byte{0x04}): &dataCopy{}, + common.BytesToAddress([]byte{0x05}): &bigModExp{eip2565: true, eip7823: true, eip7883: true}, + common.BytesToAddress([]byte{0x06}): &bn256AddIstanbul{pip88: true}, + common.BytesToAddress([]byte{0x07}): &bn256ScalarMulIstanbul{pip88: true}, + common.BytesToAddress([]byte{0x08}): &bn256PairingIstanbul{pip88: true}, + common.BytesToAddress([]byte{0x09}): &blake2F{pip88: true}, + common.BytesToAddress([]byte{0x0b}): &bls12381G1Add{pip88: true}, + common.BytesToAddress([]byte{0x0c}): &bls12381G1MultiExp{pip88: true}, + common.BytesToAddress([]byte{0x0d}): &bls12381G2Add{pip88: true}, + common.BytesToAddress([]byte{0x0e}): &bls12381G2MultiExp{pip88: true}, + common.BytesToAddress([]byte{0x0f}): &bls12381Pairing{pip88: true}, + common.BytesToAddress([]byte{0x10}): &bls12381MapG1{pip88: true}, + common.BytesToAddress([]byte{0x11}): &bls12381MapG2{pip88: true}, + common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{eip7951: true}, +} + var ( + PrecompiledAddressesChicago []common.Address PrecompiledAddressesLisovoPro []common.Address PrecompiledAddressesLisovo []common.Address PrecompiledAddressesMadhugiriPro []common.Address @@ -320,10 +343,15 @@ func init() { for k := range PrecompiledContractsLisovoPro { PrecompiledAddressesLisovoPro = append(PrecompiledAddressesLisovoPro, k) } + for k := range PrecompiledContractsChicago { + PrecompiledAddressesChicago = append(PrecompiledAddressesChicago, k) + } } func activePrecompiledContracts(rules params.Rules) PrecompiledContracts { switch { + case rules.IsChicago: + return PrecompiledContractsChicago case rules.IsLisovoPro: return PrecompiledContractsLisovoPro case rules.IsLisovo: @@ -359,6 +387,8 @@ func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts { // ActivePrecompiles returns the precompile addresses enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { switch { + case rules.IsChicago: + return PrecompiledAddressesChicago case rules.IsLisovoPro: return PrecompiledAddressesLisovoPro case rules.IsLisovo: @@ -830,10 +860,15 @@ func runBn256Add(input []byte) ([]byte, error) { // bn256AddIstanbul implements a native elliptic curve point addition conforming to // Istanbul consensus rules. -type bn256AddIstanbul struct{} +type bn256AddIstanbul struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bn256AddIstanbul) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bn256AddGasIstanbulPIP88 + } return params.Bn256AddGasIstanbul } @@ -878,10 +913,15 @@ func runBn256ScalarMul(input []byte) ([]byte, error) { // bn256ScalarMulIstanbul implements a native elliptic curve scalar // multiplication conforming to Istanbul consensus rules. -type bn256ScalarMulIstanbul struct{} +type bn256ScalarMulIstanbul struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bn256ScalarMulIstanbul) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bn256ScalarMulGasIstanbulPIP88 + } return params.Bn256ScalarMulGasIstanbul } @@ -958,10 +998,15 @@ func runBn256Pairing(input []byte) ([]byte, error) { // bn256PairingIstanbul implements a pairing pre-compile for the bn256 curve // conforming to Istanbul consensus rules. -type bn256PairingIstanbul struct{} +type bn256PairingIstanbul struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bn256PairingIstanbul) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bn256PairingBaseGasIstanbulPIP88 + uint64(len(input)/192)*params.Bn256PairingPerPointGasIstanbulPIP88 + } return params.Bn256PairingBaseGasIstanbul + uint64(len(input)/192)*params.Bn256PairingPerPointGasIstanbul } @@ -990,7 +1035,9 @@ func (c *bn256PairingByzantium) Name() string { return "BN254_PAIRING" } -type blake2F struct{} +type blake2F struct { + pip88 bool +} func (c *blake2F) RequiredGas(input []byte) uint64 { // If the input is malformed, we can't calculate the gas, return 0 and let the @@ -999,7 +1046,14 @@ func (c *blake2F) RequiredGas(input []byte) uint64 { return 0 } - return uint64(binary.BigEndian.Uint32(input[0:4])) + // Per EIP-152, each blake2F operation costs GFROUND * rounds gas, where GFROUND = 1. + // PIP-88 raises GFROUND to 22 (params.GFROUNDPIP88). + // rounds is bounded by uint32 so multiplying by params.GFROUNDPIP88 cannot overflow uint64. + rounds := uint64(binary.BigEndian.Uint32(input[0:4])) + if c.pip88 { + return rounds * params.GFROUNDPIP88 + } + return rounds } const ( @@ -1070,10 +1124,15 @@ var ( ) // bls12381G1Add implements EIP-2537 G1Add precompile. -type bls12381G1Add struct{} +type bls12381G1Add struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bls12381G1Add) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bls12381G1AddGasPIP88 + } return params.Bls12381G1AddGas } @@ -1111,7 +1170,9 @@ func (c *bls12381G1Add) Name() string { } // bls12381G1MultiExp implements EIP-2537 G1MultiExp precompile. -type bls12381G1MultiExp struct{} +type bls12381G1MultiExp struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bls12381G1MultiExp) RequiredGas(input []byte) uint64 { @@ -1128,8 +1189,12 @@ func (c *bls12381G1MultiExp) RequiredGas(input []byte) uint64 { } else { discount = params.Bls12381G1MultiExpDiscountTable[dLen-1] } + mulGas := params.Bls12381G1MulGas + if c.pip88 { + mulGas = params.Bls12381G1MulGasPIP88 + } // Calculate gas and return the result - return (uint64(k) * params.Bls12381G1MulGas * discount) / 1000 + return (uint64(k) * mulGas * discount) / 1000 } func (c *bls12381G1MultiExp) Run(input []byte) ([]byte, error) { @@ -1176,10 +1241,15 @@ func (c *bls12381G1MultiExp) Name() string { } // bls12381G2Add implements EIP-2537 G2Add precompile. -type bls12381G2Add struct{} +type bls12381G2Add struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bls12381G2Add) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bls12381G2AddGasPIP88 + } return params.Bls12381G2AddGas } @@ -1218,7 +1288,9 @@ func (c *bls12381G2Add) Name() string { } // bls12381G2MultiExp implements EIP-2537 G2MultiExp precompile. -type bls12381G2MultiExp struct{} +type bls12381G2MultiExp struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bls12381G2MultiExp) RequiredGas(input []byte) uint64 { @@ -1235,8 +1307,12 @@ func (c *bls12381G2MultiExp) RequiredGas(input []byte) uint64 { } else { discount = params.Bls12381G2MultiExpDiscountTable[dLen-1] } + mulGas := params.Bls12381G2MulGas + if c.pip88 { + mulGas = params.Bls12381G2MulGasPIP88 + } // Calculate gas and return the result - return (uint64(k) * params.Bls12381G2MulGas * discount) / 1000 + return (uint64(k) * mulGas * discount) / 1000 } func (c *bls12381G2MultiExp) Run(input []byte) ([]byte, error) { @@ -1283,10 +1359,15 @@ func (c *bls12381G2MultiExp) Name() string { } // bls12381Pairing implements EIP-2537 Pairing precompile. -type bls12381Pairing struct{} +type bls12381Pairing struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bls12381Pairing) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bls12381PairingBaseGasPIP88 + uint64(len(input)/384)*params.Bls12381PairingPerPairGasPIP88 + } return params.Bls12381PairingBaseGas + uint64(len(input)/384)*params.Bls12381PairingPerPairGas } @@ -1441,10 +1522,15 @@ func encodePointG2(p *bls12381.G2Affine) []byte { } // bls12381MapG1 implements EIP-2537 MapG1 precompile. -type bls12381MapG1 struct{} +type bls12381MapG1 struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bls12381MapG1) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bls12381MapG1GasPIP88 + } return params.Bls12381MapG1Gas } @@ -1474,10 +1560,15 @@ func (c *bls12381MapG1) Name() string { } // bls12381MapG2 implements EIP-2537 MapG2 precompile. -type bls12381MapG2 struct{} +type bls12381MapG2 struct { + pip88 bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bls12381MapG2) RequiredGas(input []byte) uint64 { + if c.pip88 { + return params.Bls12381MapG2GasPIP88 + } return params.Bls12381MapG2Gas } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index ddb01122a0..612a677f1c 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -27,9 +27,12 @@ import ( "testing" "time" + "github.com/holiman/uint256" "gotest.tools/assert" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" ) @@ -536,6 +539,7 @@ func TestReinforceMultiClientPreCompilesTest(t *testing.T) { "IsMadhugiriPro", "IsLisovo", "IsLisovoPro", + "IsChicago", } if len(actual) != len(expected) { @@ -590,7 +594,7 @@ func TestLisovoCLZOpcode(t *testing.T) { // TestKZGPointEvaluationPrecompileRemoval verifies that the kzgPointEvaluation precompile // is present from Madhugiri through Lisovo, and is not present before Madhugiri and starting -// with LisovoPro. +// with LisovoPro. Chicago should also not have kzgPointEvaluation precompile enabled. func TestKZGPointEvaluationPrecompileRemoval(t *testing.T) { t.Parallel() @@ -600,6 +604,7 @@ func TestKZGPointEvaluationPrecompileRemoval(t *testing.T) { // We verify a few things in this test: // - Madhugiri, MadhugiriPro, and Lisovo have the kzg precompile enabled // - LisovoPro removes it, so it should not be enabled + // - Chicago should not have kzgPointEvaluation precompile enabled // - Hard forks before Madhugiri (for example, Prague) should not have kzg enabled type testCase struct { name string @@ -613,6 +618,7 @@ func TestKZGPointEvaluationPrecompileRemoval(t *testing.T) { {name: "MadhugiriPro", rules: params.Rules{IsMadhugiriPro: true}, shouldHaveKzg: true}, {name: "Lisovo", rules: params.Rules{IsLisovo: true}, shouldHaveKzg: true}, {name: "LisovoPro", rules: params.Rules{IsLisovoPro: true}, shouldHaveKzg: false}, + {name: "Chicago", rules: params.Rules{IsChicago: true}, shouldHaveKzg: false}, } for _, tc := range cases { precompiles := ActivePrecompiledContracts(tc.rules) @@ -628,3 +634,252 @@ func TestKZGPointEvaluationPrecompileRemoval(t *testing.T) { } } } + +// TestPIP88PrecompileGasCosts verifies pre- and post-PIP-88 gas for every +// precompile repriced by the Chicago fork. +func TestPIP88PrecompileGasCosts(t *testing.T) { + t.Parallel() + + // blake2F input: 213 bytes, rounds=12 in big-endian uint32 at [0:4]. + blake2FInput := make([]byte, 213) + blake2FInput[3] = 12 + + cases := []struct { + addr byte + input []byte + preGas uint64 + postGas uint64 + name string + }{ + {0x06, nil, params.Bn256AddGasIstanbul, params.Bn256AddGasIstanbulPIP88, "bn256Add (3.6x)"}, + {0x07, nil, params.Bn256ScalarMulGasIstanbul, params.Bn256ScalarMulGasIstanbulPIP88, "bn256ScalarMul (2.1x)"}, + {0x0b, nil, params.Bls12381G1AddGas, params.Bls12381G1AddGasPIP88, "bls12381G1Add (2.8x)"}, + {0x0d, nil, params.Bls12381G2AddGas, params.Bls12381G2AddGasPIP88, "bls12381G2Add (2.7x)"}, + {0x10, nil, params.Bls12381MapG1Gas, params.Bls12381MapG1GasPIP88, "bls12381MapG1 (2.8x)"}, + {0x11, nil, params.Bls12381MapG2Gas, params.Bls12381MapG2GasPIP88, "bls12381MapG2 (2.8x)"}, + { + addr: 0x08, + input: make([]byte, 192), + preGas: params.Bn256PairingBaseGasIstanbul + params.Bn256PairingPerPointGasIstanbul, + postGas: params.Bn256PairingBaseGasIstanbulPIP88 + params.Bn256PairingPerPointGasIstanbulPIP88, + name: "bn256Pairing k=1 (1.5x)", + }, + { + addr: 0x0f, + input: make([]byte, 384), + preGas: params.Bls12381PairingBaseGas + params.Bls12381PairingPerPairGas, + postGas: params.Bls12381PairingBaseGasPIP88 + params.Bls12381PairingPerPairGasPIP88, + name: "bls12381Pairing k=1 (2.9x)", + }, + // MSM at k=1: discount table[0]=1000, so gas = mulGas. + {0x0c, make([]byte, 160), params.Bls12381G1MulGas, params.Bls12381G1MulGasPIP88, "bls12381G1MultiExp k=1 (6.1x)"}, + {0x0e, make([]byte, 288), params.Bls12381G2MulGas, params.Bls12381G2MulGasPIP88, "bls12381G2MultiExp k=1 (6.4x)"}, + {0x09, blake2FInput, 12, 12 * params.GFROUNDPIP88, "blake2F rounds=12 (22x)"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + addr := common.BytesToAddress([]byte{tc.addr}) + + pre, ok := PrecompiledContractsLisovoPro[addr] + if !ok { + t.Fatalf("0x%02x missing from PrecompiledContractsLisovoPro", tc.addr) + } + post, ok := PrecompiledContractsChicago[addr] + if !ok { + t.Fatalf("0x%02x missing from PrecompiledContractsChicago", tc.addr) + } + + if got := pre.RequiredGas(tc.input); got != tc.preGas { + t.Errorf("pre-PIP-88 gas: got %d, want %d", got, tc.preGas) + } + if got := post.RequiredGas(tc.input); got != tc.postGas { + t.Errorf("post-PIP-88 gas: got %d, want %d", got, tc.postGas) + } + }) + } +} + +// TestPIP88SStoreGas walks every branch of makeGasSStoreFuncPIP88 and verifies +// the gas charged and refund-pool delta match closed-form values. +func TestPIP88SStoreGas(t *testing.T) { + t.Parallel() + + addr := common.Address{0xaa} + slot := common.BigToHash(big.NewInt(1)) + val42 := common.BigToHash(big.NewInt(0x42)) + val99 := common.BigToHash(big.NewInt(0x99)) + + cases := []struct { + name string + original common.Hash // committed value before this tx + current common.Hash // dirty value (only applied if != original) + value common.Hash // value being written by SSTORE + warm bool + wantGas uint64 + wantRefundDelta int64 + }{ + { + name: "cold reset existing slot (EIP-2929 invariant: total = 5000)", + original: val42, current: val42, value: val99, warm: false, + wantGas: params.SstoreResetGasEIP2200, wantRefundDelta: 0, + }, + { + name: "warm reset existing slot", + original: val42, current: val42, value: val99, warm: true, + wantGas: params.SstoreResetGasEIP2200 - params.ColdSstoreCostPIP88, wantRefundDelta: 0, + }, + { + name: "cold create slot", + original: common.Hash{}, current: common.Hash{}, value: val99, warm: false, + wantGas: params.ColdSstoreCostPIP88 + params.SstoreSetGasEIP2200, wantRefundDelta: 0, + }, + { + name: "cold delete clean slot (clearingRefund = SstoreClearsScheduleRefundPIP88)", + original: val42, current: val42, value: common.Hash{}, warm: false, + wantGas: params.SstoreResetGasEIP2200, + wantRefundDelta: int64(params.SstoreClearsScheduleRefundPIP88), + }, + { + name: "cold noop (current == value)", + original: val42, current: val42, value: val42, warm: false, + wantGas: params.ColdSstoreCostPIP88 + params.WarmStorageReadCostEIP2929, wantRefundDelta: 0, + }, + { + name: "reset to original existing slot (refund = (RESET - cold) - warm)", + original: val42, current: val99, value: val42, warm: true, + wantGas: params.WarmStorageReadCostEIP2929, + wantRefundDelta: int64((params.SstoreResetGasEIP2200 - params.ColdSstoreCostPIP88) - params.WarmStorageReadCostEIP2929), + }, + { + name: "reset to clean zero (refund = SstoreSet - warm, unchanged from EIP-3529)", + original: common.Hash{}, current: val99, value: common.Hash{}, warm: true, + wantGas: params.WarmStorageReadCostEIP2929, + wantRefundDelta: int64(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.CreateAccount(addr) + + // Move dirty storage to pending so it appears as the "original" + // (committed) value. Finalise(false) skips empty-object deletion; + // using `true` would drop our account since it has no balance/code. + if tc.original != (common.Hash{}) { + statedb.SetState(addr, slot, tc.original) + statedb.Finalise(false) + } + if tc.current != tc.original { + statedb.SetState(addr, slot, tc.current) + } + if tc.warm { + statedb.AddSlotToAccessList(addr, slot) + } + + evm := NewEVM( + BlockContext{BlockNumber: big.NewInt(1), Time: 1, Random: &common.Hash{}}, + statedb, params.MergedTestChainConfig, Config{}, + ) + contract := NewContract(common.Address{}, addr, uint256.NewInt(0), 1_000_000, nil) + + stack := newstack() + stack.push(new(uint256.Int).SetBytes(tc.value.Bytes())) // Back(1) = value + stack.push(new(uint256.Int).SetBytes(slot.Bytes())) // peek = slot + + refundBefore := statedb.GetRefund() + gas, err := gasSStorePIP88(evm, contract, stack, NewMemory(), 0) + if err != nil { + t.Fatalf("gasSStorePIP88 returned error: %v", err) + } + refundDelta := int64(statedb.GetRefund()) - int64(refundBefore) + + if gas != tc.wantGas { + t.Errorf("gas: got %d, want %d", gas, tc.wantGas) + } + if refundDelta != tc.wantRefundDelta { + t.Errorf("refund delta: got %d, want %d", refundDelta, tc.wantRefundDelta) + } + }) + } +} + +// TestPIP88ForkBoundary verifies that the Chicago fork dispatch flips at the +// configured block: precompile set and SLOAD instruction-set gas function both +// switch from EIP-3529/LisovoPro to PIP-88 at block N (with N-1 still old). +func TestPIP88ForkBoundary(t *testing.T) { + t.Parallel() + + const chicagoBlock = 100 + + // Clone MergedTestChainConfig and push Chicago to a specific block so we + // have a real boundary to test against. + cfg := *params.MergedTestChainConfig + borCfg := *cfg.Bor + borCfg.ChicagoBlock = big.NewInt(chicagoBlock) + cfg.Bor = &borCfg + + addr := common.Address{0xaa} + slot := common.BigToHash(big.NewInt(1)) + + cases := []struct { + name string + block int64 + wantIsChicago bool + wantBn256AddGas uint64 // probe for ActivePrecompiledContracts dispatch + wantColdSloadGas uint64 // probe for LookupInstructionSet dispatch + }{ + { + name: "block N-1 (pre-Chicago)", + block: chicagoBlock - 1, + wantIsChicago: false, + wantBn256AddGas: params.Bn256AddGasIstanbul, + wantColdSloadGas: params.ColdSloadCostEIP2929, + }, + { + name: "block N (Chicago active)", + block: chicagoBlock, + wantIsChicago: true, + wantBn256AddGas: params.Bn256AddGasIstanbulPIP88, + wantColdSloadGas: params.ColdSloadCostPIP88, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rules := cfg.Rules(big.NewInt(tc.block), false, 0) + if rules.IsChicago != tc.wantIsChicago { + t.Fatalf("rules.IsChicago: got %v, want %v", rules.IsChicago, tc.wantIsChicago) + } + + // Precompile dispatch probe. + bn256Add := ActivePrecompiledContracts(rules)[common.BytesToAddress([]byte{0x06})] + if got := bn256Add.RequiredGas(nil); got != tc.wantBn256AddGas { + t.Errorf("bn256Add gas via ActivePrecompiledContracts: got %d, want %d", got, tc.wantBn256AddGas) + } + + // Instruction-set dispatch probe via SLOAD's dynamicGas on a cold slot. + jt, err := LookupInstructionSet(rules) + if err != nil { + t.Fatalf("LookupInstructionSet: %v", err) + } + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + evm := NewEVM( + BlockContext{BlockNumber: big.NewInt(tc.block), Time: 1, Random: &common.Hash{}}, + statedb, &cfg, Config{}, + ) + contract := NewContract(common.Address{}, addr, uint256.NewInt(0), 1_000_000, nil) + stack := newstack() + stack.push(new(uint256.Int).SetBytes(slot.Bytes())) + + gas, err := jt[SLOAD].dynamicGas(evm, contract, stack, NewMemory(), 0) + if err != nil { + t.Fatalf("SLOAD dynamicGas: %v", err) + } + if gas != tc.wantColdSloadGas { + t.Errorf("cold SLOAD gas via LookupInstructionSet: got %d, want %d", gas, tc.wantColdSloadGas) + } + }) + } +} diff --git a/core/vm/eips.go b/core/vm/eips.go index 4a804153e1..64e01584c8 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -174,6 +174,15 @@ func enable3529(jt *JumpTable) { jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP3529 } +// enablePIP88 applies PIP-88: +// - Increases the cost of cold SLOAD to params.ColdSloadCostPIP88 (2.6x) +// - Increases the cost of cold SSTORE to params.ColdSstoreCostPIP88 (1.4x) +// - Recomputes the slot-clear refund to params.SstoreClearsScheduleRefundPIP88 +func enablePIP88(jt *JumpTable) { + jt[SLOAD].dynamicGas = gasSLoadPIP88 + jt[SSTORE].dynamicGas = gasSStorePIP88 +} + // enable3198 applies EIP-3198 (BASEFEE Opcode) // - Adds an opcode that returns the current block's base fee. func enable3198(jt *JumpTable) { diff --git a/core/vm/evm.go b/core/vm/evm.go index 56ed629a6b..b395ed81c3 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -162,6 +162,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon evm.precompiles = activePrecompiledContracts(evm.chainRules) switch { + case evm.chainRules.IsChicago: + evm.table = &chicagoInstructionSet case evm.chainRules.IsLisovoPro: evm.table = &lisovoProInstructionSet case evm.chainRules.IsLisovo: diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 283f8cf397..a9ec4e0fe3 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -65,6 +65,7 @@ var ( osakaInstructionSet = newOsakaInstructionSet() lisovoInstructionSet = newLisovoInstructionSet() lisovoProInstructionSet = newLisovoProInstructionSet() + chicagoInstructionSet = newChicagoInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -101,6 +102,12 @@ func newLisovoProInstructionSet() JumpTable { return validate(instructionSet) } +func newChicagoInstructionSet() JumpTable { + instructionSet := newLisovoProInstructionSet() + enablePIP88(&instructionSet) // PIP-88: cold-storage repricing + return validate(instructionSet) +} + func newOsakaInstructionSet() JumpTable { instructionSet := newPragueInstructionSet() enable7939(&instructionSet) // EIP-7939 (CLZ opcode) diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go index 4a325d47e3..fdde29f73b 100644 --- a/core/vm/jump_table_export.go +++ b/core/vm/jump_table_export.go @@ -28,6 +28,8 @@ func LookupInstructionSet(rules params.Rules) (JumpTable, error) { switch { // Note: geth only returns an error for the verkle-fork. // Return nil for other forks. + case rules.IsChicago: + return newChicagoInstructionSet(), nil case rules.IsLisovoPro: return newLisovoProInstructionSet(), nil case rules.IsLisovo: diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index effa46e415..c6c1a59549 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -94,6 +94,79 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } } +func makeGasSStoreFuncPIP88(clearingRefund uint64) gasFunc { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + // If we fail the minimum gas availability invariant, fail (0) + if contract.Gas <= params.SstoreSentryGasEIP2200 { + return 0, errors.New("not enough gas for reentrancy sentry") + } + // Gas sentry honoured, do the actual gas calculation based on the stored value + var ( + y, x = stack.Back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) + cost = uint64(0) + ) + // Check slot presence in the access list + if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + // PIP-88: charge ColdSstoreCostPIP88. Must match the constant + // subtracted in the SSTORE_RESET formula below so the EIP-2929 + // invariant `cold + (RESET - cold) == RESET` (5000) still holds. + cost = params.ColdSstoreCostPIP88 + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + } + + value := common.Hash(y.Bytes32()) + + if current == value { // noop (1) + // EIP 2200 original clause: + // return params.SloadGasEIP2200, nil + return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS + } + if original == current { + if original == (common.Hash{}) { // create slot (2.1.1) + return cost + params.SstoreSetGasEIP2200, nil + } + + if value == (common.Hash{}) { // delete slot (2.1.2b) + evm.StateDB.AddRefund(clearingRefund) + } + // EIP-2200 original clause: + // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + // PIP-88: use ColdSstoreCostPIP88. + return cost + (params.SstoreResetGasEIP2200 - params.ColdSstoreCostPIP88), nil // write existing slot (2.1.2) + } + + if original != (common.Hash{}) { + if current == (common.Hash{}) { // recreate slot (2.2.1.1) + evm.StateDB.SubRefund(clearingRefund) + } else if value == (common.Hash{}) { // delete slot (2.2.1.2) + evm.StateDB.AddRefund(clearingRefund) + } + } + + if original == value { + if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1) + // EIP 2200 Original clause: + //evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.SloadGasEIP2200) + evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929) + } else { // reset to original existing slot (2.2.2.2) + // EIP 2200 Original clause: + // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200) + // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST) + // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST + // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST + // PIP-88: use ColdSstoreCostPIP88. + evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSstoreCostPIP88) - params.WarmStorageReadCostEIP2929) + } + } + // EIP-2200 original clause: + //return params.SloadGasEIP2200, nil // dirty update (2.2) + return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2) + } +} + // gasSLoadEIP2929 calculates dynamic gas for SLOAD according to EIP-2929 // For SLOAD, if the (address, storage_key) pair (where address is the address of the contract // whose storage is being read) is not yet in accessed_storage_keys, @@ -113,6 +186,26 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me return params.WarmStorageReadCostEIP2929, nil } +// gasSLoadPIP88 calculates dynamic gas for SLOAD according to PIP-88 +// For SLOAD, if the (address, storage_key) pair (where address is the address of the contract +// whose storage is being read) is not yet in accessed_storage_keys, +// charge 2100 gas and add the pair to accessed_storage_keys. +// If the pair is already in accessed_storage_keys, charge 100 gas. +// Warm storage read cost remains the same as EIP-2929. +func gasSLoadPIP88(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + loc := stack.peek() + slot := common.Hash(loc.Bytes32()) + // Check slot presence in the access list + if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + // If the caller cannot afford the cost, this change will be rolled back + // If he does afford it, we can skip checking the same thing later on, during execution + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + return params.ColdSloadCostPIP88, nil + } + + return params.WarmStorageReadCostEIP2929, nil +} + // gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929 // EIP spec: // > If the target is not in accessed_addresses, @@ -228,6 +321,9 @@ var ( // gasSStoreEIP3529 implements gas cost for SSTORE according to EIP-3529 // Replace `SSTORE_CLEARS_SCHEDULE` with `SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST` (4,800) gasSStoreEIP3529 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP3529) + + // gasSStorePIP88 implements gas cost for SSTORE according to PIP-88 + gasSStorePIP88 = makeGasSStoreFuncPIP88(params.SstoreClearsScheduleRefundPIP88) ) // makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-3529 diff --git a/internal/cli/server/chains/amoy.go b/internal/cli/server/chains/amoy.go index 9e70924aa6..1c5e4b430e 100644 --- a/internal/cli/server/chains/amoy.go +++ b/internal/cli/server/chains/amoy.go @@ -42,6 +42,7 @@ var amoyTestnet = &Chain{ LisovoBlock: big.NewInt(33634700), LisovoProBlock: big.NewInt(34062000), GiuglianoBlock: big.NewInt(35573500), + ChicagoBlock: big.NewInt(38358000), StateSyncConfirmationDelay: map[string]uint64{ "0": 128, }, diff --git a/internal/cli/server/chains/mainnet.go b/internal/cli/server/chains/mainnet.go index f895c78908..fab530ccf3 100644 --- a/internal/cli/server/chains/mainnet.go +++ b/internal/cli/server/chains/mainnet.go @@ -43,6 +43,7 @@ var mainnetBor = &Chain{ LisovoBlock: big.NewInt(83756500), LisovoProBlock: big.NewInt(83756500), GiuglianoBlock: big.NewInt(85268500), + ChicagoBlock: big.NewInt(87218600), StateSyncConfirmationDelay: map[string]uint64{ "44934656": 128, }, diff --git a/params/config.go b/params/config.go index b03fdbd325..2b35b885f1 100644 --- a/params/config.go +++ b/params/config.go @@ -346,6 +346,7 @@ var ( LisovoBlock: big.NewInt(33634700), LisovoProBlock: big.NewInt(34062000), GiuglianoBlock: big.NewInt(35573500), + ChicagoBlock: big.NewInt(38358000), StateSyncConfirmationDelay: map[string]uint64{ "0": 128, }, @@ -433,6 +434,7 @@ var ( LisovoBlock: big.NewInt(83756500), LisovoProBlock: big.NewInt(83756500), GiuglianoBlock: big.NewInt(85268500), + ChicagoBlock: big.NewInt(87218600), StateSyncConfirmationDelay: map[string]uint64{ "44934656": 128, }, @@ -738,6 +740,7 @@ var ( DandeliBlock: big.NewInt(0), LisovoBlock: big.NewInt(0), LisovoProBlock: big.NewInt(0), + ChicagoBlock: big.NewInt(0), }, } @@ -954,6 +957,7 @@ type BorConfig struct { LisovoBlock *big.Int `json:"lisovoBlock"` // Lisovo switch block (nil = no fork, 0 = already on lisovo) 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) } // String implements the stringer interface, returning the consensus engine details. @@ -1029,6 +1033,10 @@ func (c *BorConfig) IsGiugliano(number *big.Int) bool { return isBlockForked(c.GiuglianoBlock, number) } +func (c *BorConfig) IsChicago(number *big.Int) bool { + return isBlockForked(c.ChicagoBlock, number) +} + // 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. @@ -1237,6 +1245,9 @@ func (c *ChainConfig) Description() string { if c.Bor.GiuglianoBlock != nil { banner += fmt.Sprintf(" - Giugliano: #%-8v\n", c.Bor.GiuglianoBlock) } + if c.Bor.ChicagoBlock != nil { + banner += fmt.Sprintf(" - Chicago: #%-8v\n", c.Bor.ChicagoBlock) + } return banner } @@ -1875,6 +1886,7 @@ type Rules struct { IsMadhugiriPro bool IsLisovo bool IsLisovoPro bool + IsChicago bool } // Rules ensures c's ChainID is not nil. @@ -1910,5 +1922,6 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, _ uint64) Rules { 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), } } diff --git a/params/protocol_params.go b/params/protocol_params.go index 17d9fafae8..3e88b7d3c2 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -70,12 +70,22 @@ const ( ColdSloadCostEIP2929 = uint64(2100) // COLD_SLOAD_COST WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST + // PIP-88: cold-storage repricing for the Chicago hard fork. + // EIP-2929 uses a single COLD_SLOAD_COST for both SLOAD and SSTORE. + // PIP-88 splits it into two so SLOAD and SSTORE can scale independently. + ColdSloadCostPIP88 uint64 = 5460 // 2100 * 2.6 - COLD_SLOAD_COST surcharge + ColdSstoreCostPIP88 uint64 = 2940 // 2100 * 1.4 - COLD_SSTORE_COST surcharge + // In EIP-2200: SstoreResetGas was 5000. // In EIP-2929: SstoreResetGas was changed to '5000 - COLD_SLOAD_COST'. // In EIP-3529: SSTORE_CLEARS_SCHEDULE is defined as SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST // Which becomes: 5000 - 2100 + 1900 = 4800 SstoreClearsScheduleRefundEIP3529 uint64 = SstoreResetGasEIP2200 - ColdSloadCostEIP2929 + TxAccessListStorageKeyGas + // PIP-88: SSTORE_CLEARS_SCHEDULE recomputed against the PIP-88 COLD_SSTORE_COST. + // Which becomes: 5000 - 2940 + 1900 = 3960. + SstoreClearsScheduleRefundPIP88 uint64 = SstoreResetGasEIP2200 - ColdSstoreCostPIP88 + TxAccessListStorageKeyGas + JumpdestGas uint64 = 1 // Once per JUMPDEST operation. EpochDuration uint64 = 30000 // Duration between proof-of-work epochs. @@ -179,6 +189,24 @@ const ( P256VerifyGas uint64 = 3450 P256VerifyGasEIP7951 uint64 = 6900 + // PIP-88: precompile gas repricing activated by the Chicago hard fork. + + GFROUNDPIP88 uint64 = 22 // PIP-88 GFROUND for blake2F operation + + Bn256AddGasIstanbulPIP88 uint64 = 540 // 150 * 3.6 + Bn256ScalarMulGasIstanbulPIP88 uint64 = 12600 // 6000 * 2.1 + Bn256PairingBaseGasIstanbulPIP88 uint64 = 67500 // 45000 * 1.5 + Bn256PairingPerPointGasIstanbulPIP88 uint64 = 51000 // 34000 * 1.5 + + Bls12381G1AddGasPIP88 uint64 = 1050 // 375 * 2.8 + Bls12381G1MulGasPIP88 uint64 = 73200 // 12000 * 6.1 (k=1) + Bls12381G2AddGasPIP88 uint64 = 1620 // 600 * 2.7 + Bls12381G2MulGasPIP88 uint64 = 144000 // 22500 * 6.4 (k=1) + Bls12381PairingBaseGasPIP88 uint64 = 109330 // 37700 * 2.9 + Bls12381PairingPerPairGasPIP88 uint64 = 94540 // 32600 * 2.9 + Bls12381MapG1GasPIP88 uint64 = 15400 // 5500 * 2.8 + Bls12381MapG2GasPIP88 uint64 = 66640 // 23800 * 2.8 + // The Refund Quotient is the cap on how much of the used gas can be refunded. Before EIP-3529, // up to half the consumed gas could be refunded. Redefined as 1/5th in EIP-3529 RefundQuotient uint64 = 2 diff --git a/params/version.go b/params/version.go index 7f992c7cf0..492329d484 100644 --- a/params/version.go +++ b/params/version.go @@ -24,8 +24,8 @@ import ( const ( VersionMajor = 2 // Major version component of the current release - VersionMinor = 7 // Minor version component of the current release - VersionPatch = 3 // Patch version component of the current release + VersionMinor = 8 // Minor version component of the current release + VersionPatch = 0 // Patch version component of the current release VersionMeta = "" // Version metadata to append to the version string )