Adding MLSDA65 precompile to begin adding quantum hardening support to Ethereum smart contracts

This commit is contained in:
support@gravitylab.llc 2026-03-25 21:54:30 -05:00
parent 453d0f9299
commit 57e8860dfe
8 changed files with 529 additions and 40 deletions

View file

@ -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.

View file

@ -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 {

View file

@ -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"
}

View file

@ -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`

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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,
}
}

View file

@ -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) {