diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 867746acc8..2701aea794 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -147,6 +147,12 @@ var PrecompiledContractsBLS = PrecompiledContractsPrague var PrecompiledContractsVerkle = PrecompiledContractsBerlin +var PrecompiledContractsMLDSA = PrecompiledContracts{ + mldsa65VerifyAddr: &mldsa65VerifyPrecompile{}, +} + +var PrecompiledAddressesMLDSA = []common.Address{mldsa65VerifyAddr} + // PrecompiledContractsOsaka contains the set of pre-compiled Ethereum // contracts used in the Osaka release. var PrecompiledContractsOsaka = PrecompiledContracts{ @@ -234,27 +240,37 @@ func activePrecompiledContracts(rules params.Rules) PrecompiledContracts { // ActivePrecompiledContracts returns a copy of precompiled contracts enabled with the current configuration. func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts { - return maps.Clone(activePrecompiledContracts(rules)) + precompiles := maps.Clone(activePrecompiledContracts(rules)) + if rules.IsMLDSAPrecompile { + maps.Copy(precompiles, PrecompiledContractsMLDSA) + } + return precompiles } // ActivePrecompiles returns the precompile addresses enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { + var active []common.Address switch { case rules.IsOsaka: - return PrecompiledAddressesOsaka + active = PrecompiledAddressesOsaka case rules.IsPrague: - return PrecompiledAddressesPrague + active = PrecompiledAddressesPrague case rules.IsCancun: - return PrecompiledAddressesCancun + active = PrecompiledAddressesCancun case rules.IsBerlin: - return PrecompiledAddressesBerlin + active = PrecompiledAddressesBerlin case rules.IsIstanbul: - return PrecompiledAddressesIstanbul + active = PrecompiledAddressesIstanbul case rules.IsByzantium: - return PrecompiledAddressesByzantium + active = PrecompiledAddressesByzantium default: - return PrecompiledAddressesHomestead + active = PrecompiledAddressesHomestead } + if !rules.IsMLDSAPrecompile { + return active + } + addresses := append([]common.Address{}, active...) + return append(addresses, PrecompiledAddressesMLDSA...) } // RunPrecompiledContract runs and evaluates the output of a precompiled contract. diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index 51bd7101ff..403dd0cb04 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -24,7 +24,9 @@ import ( "testing" "time" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" ) // precompiledTest defines the input/output pairs for precompiled contract tests. @@ -68,6 +70,7 @@ var allPrecompiles = map[common.Address]PrecompiledContract{ common.BytesToAddress([]byte{0x0f, 0x10}): &bls12381MapG2{}, common.BytesToAddress([]byte{0x0b}): &p256Verify{}, + mldsa65VerifyAddr: &mldsa65VerifyPrecompile{}, } // EIP-152 test vectors @@ -300,6 +303,150 @@ func TestPrecompileBlake2FMalformedInput(t *testing.T) { func TestPrecompiledEcrecover(t *testing.T) { testJson("ecRecover", "01", t) } +func TestActivePrecompiledContractsMLDSA65(t *testing.T) { + rules := params.Rules{IsCancun: true} + if _, ok := ActivePrecompiledContracts(rules)[mldsa65VerifyAddr]; ok { + t.Fatal("unexpected ML-DSA precompile before activation") + } + for _, addr := range ActivePrecompiles(rules) { + if addr == mldsa65VerifyAddr { + t.Fatal("unexpected ML-DSA precompile address before activation") + } + } + + rules.IsMLDSAPrecompile = true + if _, ok := ActivePrecompiledContracts(rules)[mldsa65VerifyAddr]; !ok { + t.Fatal("missing ML-DSA precompile after activation") + } + found := false + for _, addr := range ActivePrecompiles(rules) { + if addr == mldsa65VerifyAddr { + found = true + break + } + } + if !found { + t.Fatal("missing ML-DSA precompile address after activation") + } +} + +func TestPrecompiledMLDSA65Verify(t *testing.T) { + pk, sk, err := mldsa65.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + msg := []byte("hello geth pq") + + sig := make([]byte, mldsa65.SignatureSize) + if err := mldsa65.SignTo(sk, msg, nil, true, sig); err != nil { + t.Fatal(err) + } + pkBytes, err := pk.MarshalBinary() + if err != nil { + t.Fatal(err) + } + + in := append(append(pkBytes, sig...), msg...) + out, _, err := RunPrecompiledContract(&mldsa65VerifyPrecompile{}, in, 10_000_000, nil) + if err != nil { + t.Fatal(err) + } + if want := common.LeftPadBytes([]byte{1}, 32); !bytes.Equal(out, want) { + t.Fatalf("expected 1, got %x", out) + } +} + +func TestPrecompiledMLDSA65VerifyInvalidInputs(t *testing.T) { + zero := make([]byte, 32) + + t.Run("too short", func(t *testing.T) { + in := make([]byte, mldsa65.PublicKeySize+mldsa65.SignatureSize-1) + out, _, err := RunPrecompiledContract(&mldsa65VerifyPrecompile{}, in, 10_000_000, nil) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(out, zero) { + t.Fatalf("expected 0, got %x", out) + } + }) + + t.Run("tampered msg", func(t *testing.T) { + pk, sk, err := mldsa65.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + msg := []byte("hello geth pq") + sig := make([]byte, mldsa65.SignatureSize) + if err := mldsa65.SignTo(sk, msg, nil, true, sig); err != nil { + t.Fatal(err) + } + pkBytes, err := pk.MarshalBinary() + if err != nil { + t.Fatal(err) + } + badMsg := []byte("hello geth PQ") + in := append(append(pkBytes, sig...), badMsg...) + out, _, err := RunPrecompiledContract(&mldsa65VerifyPrecompile{}, in, 10_000_000, nil) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(out, zero) { + t.Fatalf("expected 0, got %x", out) + } + }) + + t.Run("tampered sig", func(t *testing.T) { + pk, sk, err := mldsa65.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + msg := []byte("hello geth pq") + sig := make([]byte, mldsa65.SignatureSize) + if err := mldsa65.SignTo(sk, msg, nil, true, sig); err != nil { + t.Fatal(err) + } + sig[0] ^= 0xff + pkBytes, err := pk.MarshalBinary() + if err != nil { + t.Fatal(err) + } + in := append(append(pkBytes, sig...), msg...) + out, _, err := RunPrecompiledContract(&mldsa65VerifyPrecompile{}, in, 10_000_000, nil) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(out, zero) { + t.Fatalf("expected 0, got %x", out) + } + }) +} + +func BenchmarkPrecompiledMLDSA65Verify(b *testing.B) { + pk, sk, err := mldsa65.GenerateKey(nil) + if err != nil { + b.Fatal(err) + } + msg := make([]byte, 1024) + sig := make([]byte, mldsa65.SignatureSize) + if err := mldsa65.SignTo(sk, msg, nil, true, sig); err != nil { + b.Fatal(err) + } + pkBytes, err := pk.MarshalBinary() + if err != nil { + b.Fatal(err) + } + in := append(append(pkBytes, sig...), msg...) + p := &mldsa65VerifyPrecompile{} + gas := p.RequiredGas(in) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, _, err := RunPrecompiledContract(p, in, gas, nil); err != nil { + b.Fatal(err) + } + } +} + func testJson(name, addr string, t *testing.T) { tests, err := loadJson(name) if err != nil { diff --git a/core/vm/precompile_mldsa65.go b/core/vm/precompile_mldsa65.go new file mode 100644 index 0000000000..c5a2a7ce43 --- /dev/null +++ b/core/vm/precompile_mldsa65.go @@ -0,0 +1,54 @@ +package vm + +import ( + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/ethereum/go-ethereum/common" +) + +// Address: 0x0000000000000000000000000000000000000101. +var mldsa65VerifyAddr = common.HexToAddress("0101") + +const ( + mldsa65GasBase uint64 = 250_000 + mldsa65GasPerWord uint64 = 12 +) + +type mldsa65VerifyPrecompile struct{} + +func (p *mldsa65VerifyPrecompile) RequiredGas(input []byte) uint64 { + pkLen := mldsa65.PublicKeySize + sigLen := mldsa65.SignatureSize + if len(input) <= pkLen+sigLen { + return mldsa65GasBase + } + msgLen := len(input) - pkLen - sigLen + words := uint64((msgLen + 31) / 32) + return mldsa65GasBase + mldsa65GasPerWord*words +} + +func (p *mldsa65VerifyPrecompile) Run(input []byte) ([]byte, error) { + one := common.LeftPadBytes([]byte{1}, 32) + zero := make([]byte, 32) + + pkLen := mldsa65.PublicKeySize + sigLen := mldsa65.SignatureSize + if len(input) < pkLen+sigLen { + return zero, nil + } + pkBytes := input[:pkLen] + sig := input[pkLen : pkLen+sigLen] + msg := input[pkLen+sigLen:] + + var pk mldsa65.PublicKey + if err := pk.UnmarshalBinary(pkBytes); err != nil { + return zero, nil + } + if mldsa65.Verify(&pk, msg, nil, sig) { + return one, nil + } + return zero, nil +} + +func (p *mldsa65VerifyPrecompile) Name() string { + return "MLDSA65V" +} diff --git a/docs/mldsa65-precompile-example.md b/docs/mldsa65-precompile-example.md new file mode 100644 index 0000000000..128d0b6d2e --- /dev/null +++ b/docs/mldsa65-precompile-example.md @@ -0,0 +1,230 @@ +# ML-DSA-65 Precompile Contract Integration + +This note documents the Solidity-side calling convention for the ML-DSA-65 verification precompile introduced at address `0x0000000000000000000000000000000000000101`. + +It is intended as an implementation example for smart wallets, account abstraction flows, and EIP companion documentation. + +## What The Precompile Does + +The precompile verifies an ML-DSA-65 signature and returns a single 32-byte word: + +- `0x000...0001` when verification succeeds +- `0x000...0000` otherwise + +It does not revert on invalid signatures. + +## Call Interface + +The precompile does not expose a Solidity ABI in the usual sense. Contracts call it via `staticcall` with raw bytes. + +Input encoding: + +```text +input = publicKey || signature || message +``` + +For ML-DSA-65, the fixed sizes are: + +- public key: `1952` bytes +- signature: `3309` bytes +- message: variable length + +Minimal adapter shape: + +```solidity +library MLDSA65 { + address internal constant PRECOMPILE = address(0x0101); + uint256 internal constant PUBLIC_KEY_SIZE = 1952; + uint256 internal constant SIGNATURE_SIZE = 3309; + + function verify( + bytes memory publicKey, + bytes memory signature, + bytes memory message + ) internal view returns (bool) { + require(publicKey.length == PUBLIC_KEY_SIZE, "bad ML-DSA public key length"); + require(signature.length == SIGNATURE_SIZE, "bad ML-DSA signature length"); + + bytes memory input = bytes.concat(publicKey, signature, message); + (bool ok, bytes memory out) = PRECOMPILE.staticcall(input); + if (!ok || out.length != 32) { + return false; + } + uint256 word; + assembly { + word := mload(add(out, 0x20)) + } + return word == 1; + } +} +``` + +## Recommended Signing Payload + +Applications should not verify signatures over arbitrary free-form messages. The signed payload should bind the authorization to a specific chain, contract, and action. + +At minimum, include: + +- chain ID +- contract address +- nonce +- target address +- ETH value +- calldata hash +- optional deadline + +That avoids replay across chains, contracts, or previously authorized actions. + +## Example Smart Wallet Pattern + +The following pattern uses: + +- a stored ML-DSA-65 public key +- per-wallet nonce replay protection +- a deterministic signed digest +- precompile-backed verification before executing a call + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +library MLDSA65 { + address internal constant PRECOMPILE = address(0x0101); + uint256 internal constant PUBLIC_KEY_SIZE = 1952; + uint256 internal constant SIGNATURE_SIZE = 3309; + + function verify( + bytes memory publicKey, + bytes memory signature, + bytes memory message + ) internal view returns (bool) { + require(publicKey.length == PUBLIC_KEY_SIZE, "bad ML-DSA public key length"); + require(signature.length == SIGNATURE_SIZE, "bad ML-DSA signature length"); + + bytes memory input = bytes.concat(publicKey, signature, message); + (bool ok, bytes memory out) = PRECOMPILE.staticcall(input); + if (!ok || out.length != 32) { + return false; + } + + uint256 word; + assembly { + word := mload(add(out, 0x20)) + } + return word == 1; + } +} + +contract PQSmartWallet { + bytes public ownerPublicKey; + uint256 public nonce; + + event Executed(address indexed target, uint256 value, bytes data, uint256 nonce); + event PublicKeyRotated(bytes newPublicKey); + + constructor(bytes memory initialPublicKey) { + require(initialPublicKey.length == MLDSA65.PUBLIC_KEY_SIZE, "bad ML-DSA public key length"); + ownerPublicKey = initialPublicKey; + } + + function digestForExecute( + address target, + uint256 value, + bytes calldata data, + uint256 expectedNonce, + uint256 deadline + ) public view returns (bytes32) { + return keccak256( + abi.encode( + bytes32("MLDSA65_WALLET_EXECUTE"), + block.chainid, + address(this), + expectedNonce, + deadline, + target, + value, + keccak256(data) + ) + ); + } + + function execute( + address target, + uint256 value, + bytes calldata data, + uint256 deadline, + bytes calldata signature + ) external payable { + require(block.timestamp <= deadline, "signature expired"); + + uint256 currentNonce = nonce; + bytes32 digest = digestForExecute(target, value, data, currentNonce, deadline); + bytes memory message = abi.encodePacked(digest); + + require(MLDSA65.verify(ownerPublicKey, signature, message), "invalid ML-DSA signature"); + + nonce = currentNonce + 1; + + (bool ok, ) = target.call{value: value}(data); + require(ok, "call failed"); + + emit Executed(target, value, data, currentNonce); + } + + function rotateKey( + bytes calldata newPublicKey, + uint256 deadline, + bytes calldata signature + ) external { + require(newPublicKey.length == MLDSA65.PUBLIC_KEY_SIZE, "bad ML-DSA public key length"); + require(block.timestamp <= deadline, "signature expired"); + + uint256 currentNonce = nonce; + bytes32 digest = keccak256( + abi.encode( + bytes32("MLDSA65_WALLET_ROTATE"), + block.chainid, + address(this), + currentNonce, + deadline, + keccak256(newPublicKey) + ) + ); + + require( + MLDSA65.verify(ownerPublicKey, signature, abi.encodePacked(digest)), + "invalid ML-DSA signature" + ); + + ownerPublicKey = newPublicKey; + nonce = currentNonce + 1; + + emit PublicKeyRotated(newPublicKey); + } +} +``` + +## Security Notes + +This precompile only provides post-quantum verification as a building block. Whether a contract is quantum-hardened depends on the authorization policy around it. + +Good pattern: + +- contract state changes require a valid ML-DSA signature +- no ECDSA owner bypass exists +- key rotation is also ML-DSA authorized +- replay protection is present + +Weak pattern: + +- `msg.sender` remains the real authority +- ECDSA is accepted as a fallback +- signed messages omit chain ID, nonce, or contract binding + +The security gain is in authorization of state transitions. This does not encrypt storage or make legacy EOAs quantum-safe. + +## Reference Files + +- Client precompile implementation: `core/vm/precompile_mldsa65.go` +- Precompile activation wiring: `params/config.go` +- Client-side tests: `core/vm/contracts_test.go` diff --git a/go.mod b/go.mod index e15d29a6c5..b06173c737 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.13.43 github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2 github.com/cespare/cp v0.1.0 + github.com/cloudflare/circl v1.6.3 github.com/cloudflare/cloudflare-go v0.114.0 github.com/cockroachdb/pebble v1.1.5 github.com/consensys/gnark-crypto v0.18.1 diff --git a/go.sum b/go.sum index fe2a64eaec..949c0da023 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/cloudflare-go v0.114.0 h1:ucoti4/7Exo0XQ+rzpn1H+IfVVe++zgiM+tyKtf0HUA= github.com/cloudflare/cloudflare-go v0.114.0/go.mod h1:O7fYfFfA6wKqKFn2QIR9lhj7FDw6VQCGOY6hd2TBtd0= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= diff --git a/params/config.go b/params/config.go index 197ed56f8a..9a5d91f4f5 100644 --- a/params/config.go +++ b/params/config.go @@ -456,17 +456,18 @@ type ChainConfig struct { // Fork scheduling was switched from blocks to timestamps here - ShanghaiTime *uint64 `json:"shanghaiTime,omitempty"` // Shanghai switch time (nil = no fork, 0 = already on shanghai) - CancunTime *uint64 `json:"cancunTime,omitempty"` // Cancun switch time (nil = no fork, 0 = already on cancun) - PragueTime *uint64 `json:"pragueTime,omitempty"` // Prague switch time (nil = no fork, 0 = already on prague) - OsakaTime *uint64 `json:"osakaTime,omitempty"` // Osaka switch time (nil = no fork, 0 = already on osaka) - BPO1Time *uint64 `json:"bpo1Time,omitempty"` // BPO1 switch time (nil = no fork, 0 = already on bpo1) - BPO2Time *uint64 `json:"bpo2Time,omitempty"` // BPO2 switch time (nil = no fork, 0 = already on bpo2) - BPO3Time *uint64 `json:"bpo3Time,omitempty"` // BPO3 switch time (nil = no fork, 0 = already on bpo3) - BPO4Time *uint64 `json:"bpo4Time,omitempty"` // BPO4 switch time (nil = no fork, 0 = already on bpo4) - BPO5Time *uint64 `json:"bpo5Time,omitempty"` // BPO5 switch time (nil = no fork, 0 = already on bpo5) - AmsterdamTime *uint64 `json:"amsterdamTime,omitempty"` // Amsterdam switch time (nil = no fork, 0 = already on amsterdam) - VerkleTime *uint64 `json:"verkleTime,omitempty"` // Verkle switch time (nil = no fork, 0 = already on verkle) + ShanghaiTime *uint64 `json:"shanghaiTime,omitempty"` // Shanghai switch time (nil = no fork, 0 = already on shanghai) + CancunTime *uint64 `json:"cancunTime,omitempty"` // Cancun switch time (nil = no fork, 0 = already on cancun) + PragueTime *uint64 `json:"pragueTime,omitempty"` // Prague switch time (nil = no fork, 0 = already on prague) + OsakaTime *uint64 `json:"osakaTime,omitempty"` // Osaka switch time (nil = no fork, 0 = already on osaka) + BPO1Time *uint64 `json:"bpo1Time,omitempty"` // BPO1 switch time (nil = no fork, 0 = already on bpo1) + BPO2Time *uint64 `json:"bpo2Time,omitempty"` // BPO2 switch time (nil = no fork, 0 = already on bpo2) + BPO3Time *uint64 `json:"bpo3Time,omitempty"` // BPO3 switch time (nil = no fork, 0 = already on bpo3) + BPO4Time *uint64 `json:"bpo4Time,omitempty"` // BPO4 switch time (nil = no fork, 0 = already on bpo4) + BPO5Time *uint64 `json:"bpo5Time,omitempty"` // BPO5 switch time (nil = no fork, 0 = already on bpo5) + AmsterdamTime *uint64 `json:"amsterdamTime,omitempty"` // Amsterdam switch time (nil = no fork, 0 = already on amsterdam) + MLDSAPrecompileTime *uint64 `json:"mldsaPrecompileTime,omitempty"` // ML-DSA precompile switch time (nil = disabled) + VerkleTime *uint64 `json:"verkleTime,omitempty"` // Verkle switch time (nil = no fork, 0 = already on verkle) // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. @@ -595,6 +596,9 @@ func (c *ChainConfig) String() string { if c.AmsterdamTime != nil { result += fmt.Sprintf(", AmsterdamTime: %v", *c.AmsterdamTime) } + if c.MLDSAPrecompileTime != nil { + result += fmt.Sprintf(", MLDSAPrecompileTime: %v", *c.MLDSAPrecompileTime) + } if c.VerkleTime != nil { result += fmt.Sprintf(", VerkleTime: %v", *c.VerkleTime) } @@ -690,10 +694,13 @@ func (c *ChainConfig) Description() string { if c.AmsterdamTime != nil { banner += fmt.Sprintf(" - Amsterdam: @%-10v blob: (%s)\n", *c.AmsterdamTime, c.BlobScheduleConfig.Amsterdam) } + if c.MLDSAPrecompileTime != nil { + banner += fmt.Sprintf(" - ML-DSA precompile: @%-10v\n", *c.MLDSAPrecompileTime) + } if c.VerkleTime != nil { banner += fmt.Sprintf(" - Verkle: @%-10v blob: (%s)\n", *c.VerkleTime, c.BlobScheduleConfig.Verkle) } - banner += fmt.Sprintf("\nAll fork specifications can be found at https://ethereum.github.io/execution-specs/src/ethereum/forks/\n") + banner += "\nAll fork specifications can be found at https://ethereum.github.io/execution-specs/src/ethereum/forks/\n" return banner } @@ -866,6 +873,11 @@ func (c *ChainConfig) IsAmsterdam(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.AmsterdamTime, time) } +// IsMLDSAPrecompile returns whether the ML-DSA precompile is enabled at the given time. +func (c *ChainConfig) IsMLDSAPrecompile(num *big.Int, time uint64) bool { + return c.IsLondon(num) && isTimestampForked(c.MLDSAPrecompileTime, time) +} + // IsVerkle returns whether time is either equal to the Verkle fork time or greater. func (c *ChainConfig) IsVerkle(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.VerkleTime, time) @@ -1125,6 +1137,9 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, if isForkTimestampIncompatible(c.AmsterdamTime, newcfg.AmsterdamTime, headTimestamp) { return newTimestampCompatError("Amsterdam fork timestamp", c.AmsterdamTime, newcfg.AmsterdamTime) } + if isForkTimestampIncompatible(c.MLDSAPrecompileTime, newcfg.MLDSAPrecompileTime, headTimestamp) { + return newTimestampCompatError("ML-DSA precompile timestamp", c.MLDSAPrecompileTime, newcfg.MLDSAPrecompileTime) + } return nil } @@ -1380,7 +1395,7 @@ type Rules struct { IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsBerlin, IsLondon bool IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool - IsAmsterdam, IsVerkle bool + IsAmsterdam, IsMLDSAPrecompile, IsVerkle bool } // Rules ensures c's ChainID is not nil. @@ -1389,24 +1404,25 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules isMerge = isMerge && c.IsLondon(num) isVerkle := isMerge && c.IsVerkle(num, timestamp) return Rules{ - 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: isMerge && c.IsShanghai(num, timestamp), - IsCancun: isMerge && c.IsCancun(num, timestamp), - IsPrague: isMerge && c.IsPrague(num, timestamp), - IsOsaka: isMerge && c.IsOsaka(num, timestamp), - IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp), - IsVerkle: isVerkle, - IsEIP4762: isVerkle, + 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: isMerge && c.IsShanghai(num, timestamp), + IsCancun: isMerge && c.IsCancun(num, timestamp), + IsPrague: isMerge && c.IsPrague(num, timestamp), + IsOsaka: isMerge && c.IsOsaka(num, timestamp), + IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp), + IsMLDSAPrecompile: c.IsMLDSAPrecompile(num, timestamp), + IsVerkle: isVerkle, + IsEIP4762: isVerkle, } } diff --git a/params/config_test.go b/params/config_test.go index f658c336dc..c34695af19 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -110,6 +110,17 @@ func TestCheckCompatible(t *testing.T) { RewindToTime: 9, }, }, + { + stored: &ChainConfig{MLDSAPrecompileTime: newUint64(10)}, + new: &ChainConfig{MLDSAPrecompileTime: newUint64(20)}, + headTimestamp: 25, + wantErr: &ConfigCompatError{ + What: "ML-DSA precompile timestamp", + StoredTime: newUint64(10), + NewTime: newUint64(20), + RewindToTime: 9, + }, + }, } for _, test := range tests { @@ -137,6 +148,18 @@ func TestConfigRules(t *testing.T) { if r := c.Rules(big.NewInt(0), true, stamp); !r.IsShanghai { t.Errorf("expected %v to be shanghai", stamp) } + + mldsaTime := uint64(700) + c = &ChainConfig{ + LondonBlock: new(big.Int), + MLDSAPrecompileTime: &mldsaTime, + } + if r := c.Rules(big.NewInt(0), false, mldsaTime-1); r.IsMLDSAPrecompile { + t.Errorf("expected ML-DSA precompile to stay disabled before timestamp") + } + if r := c.Rules(big.NewInt(0), false, mldsaTime); !r.IsMLDSAPrecompile { + t.Errorf("expected ML-DSA precompile to activate without merge gating") + } } func TestTimestampCompatError(t *testing.T) {