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 PrecompiledContractsVerkle = PrecompiledContractsBerlin
var PrecompiledContractsMLDSA = PrecompiledContracts{
mldsa65VerifyAddr: &mldsa65VerifyPrecompile{},
}
var PrecompiledAddressesMLDSA = []common.Address{mldsa65VerifyAddr}
// PrecompiledContractsOsaka contains the set of pre-compiled Ethereum // PrecompiledContractsOsaka contains the set of pre-compiled Ethereum
// contracts used in the Osaka release. // contracts used in the Osaka release.
var PrecompiledContractsOsaka = PrecompiledContracts{ 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. // ActivePrecompiledContracts returns a copy of precompiled contracts enabled with the current configuration.
func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts { 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. // ActivePrecompiles returns the precompile addresses enabled with the current configuration.
func ActivePrecompiles(rules params.Rules) []common.Address { func ActivePrecompiles(rules params.Rules) []common.Address {
var active []common.Address
switch { switch {
case rules.IsOsaka: case rules.IsOsaka:
return PrecompiledAddressesOsaka active = PrecompiledAddressesOsaka
case rules.IsPrague: case rules.IsPrague:
return PrecompiledAddressesPrague active = PrecompiledAddressesPrague
case rules.IsCancun: case rules.IsCancun:
return PrecompiledAddressesCancun active = PrecompiledAddressesCancun
case rules.IsBerlin: case rules.IsBerlin:
return PrecompiledAddressesBerlin active = PrecompiledAddressesBerlin
case rules.IsIstanbul: case rules.IsIstanbul:
return PrecompiledAddressesIstanbul active = PrecompiledAddressesIstanbul
case rules.IsByzantium: case rules.IsByzantium:
return PrecompiledAddressesByzantium active = PrecompiledAddressesByzantium
default: 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. // RunPrecompiledContract runs and evaluates the output of a precompiled contract.

View file

@ -24,7 +24,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
) )
// precompiledTest defines the input/output pairs for precompiled contract tests. // 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{0x0f, 0x10}): &bls12381MapG2{},
common.BytesToAddress([]byte{0x0b}): &p256Verify{}, common.BytesToAddress([]byte{0x0b}): &p256Verify{},
mldsa65VerifyAddr: &mldsa65VerifyPrecompile{},
} }
// EIP-152 test vectors // EIP-152 test vectors
@ -300,6 +303,150 @@ func TestPrecompileBlake2FMalformedInput(t *testing.T) {
func TestPrecompiledEcrecover(t *testing.T) { testJson("ecRecover", "01", 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) { func testJson(name, addr string, t *testing.T) {
tests, err := loadJson(name) tests, err := loadJson(name)
if err != nil { 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/credentials v1.13.43
github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2 github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2
github.com/cespare/cp v0.1.0 github.com/cespare/cp v0.1.0
github.com/cloudflare/circl v1.6.3
github.com/cloudflare/cloudflare-go v0.114.0 github.com/cloudflare/cloudflare-go v0.114.0
github.com/cockroachdb/pebble v1.1.5 github.com/cockroachdb/pebble v1.1.5
github.com/consensys/gnark-crypto v0.18.1 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/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= 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/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 h1:ucoti4/7Exo0XQ+rzpn1H+IfVVe++zgiM+tyKtf0HUA=
github.com/cloudflare/cloudflare-go v0.114.0/go.mod h1:O7fYfFfA6wKqKFn2QIR9lhj7FDw6VQCGOY6hd2TBtd0= 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= 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 // Fork scheduling was switched from blocks to timestamps here
ShanghaiTime *uint64 `json:"shanghaiTime,omitempty"` // Shanghai switch time (nil = no fork, 0 = already on shanghai) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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 // TerminalTotalDifficulty is the amount of total difficulty reached by
// the network that triggers the consensus upgrade. // the network that triggers the consensus upgrade.
@ -595,6 +596,9 @@ func (c *ChainConfig) String() string {
if c.AmsterdamTime != nil { if c.AmsterdamTime != nil {
result += fmt.Sprintf(", AmsterdamTime: %v", *c.AmsterdamTime) result += fmt.Sprintf(", AmsterdamTime: %v", *c.AmsterdamTime)
} }
if c.MLDSAPrecompileTime != nil {
result += fmt.Sprintf(", MLDSAPrecompileTime: %v", *c.MLDSAPrecompileTime)
}
if c.VerkleTime != nil { if c.VerkleTime != nil {
result += fmt.Sprintf(", VerkleTime: %v", *c.VerkleTime) result += fmt.Sprintf(", VerkleTime: %v", *c.VerkleTime)
} }
@ -690,10 +694,13 @@ func (c *ChainConfig) Description() string {
if c.AmsterdamTime != nil { if c.AmsterdamTime != nil {
banner += fmt.Sprintf(" - Amsterdam: @%-10v blob: (%s)\n", *c.AmsterdamTime, c.BlobScheduleConfig.Amsterdam) 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 { if c.VerkleTime != nil {
banner += fmt.Sprintf(" - Verkle: @%-10v blob: (%s)\n", *c.VerkleTime, c.BlobScheduleConfig.Verkle) 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 return banner
} }
@ -866,6 +873,11 @@ func (c *ChainConfig) IsAmsterdam(num *big.Int, time uint64) bool {
return c.IsLondon(num) && isTimestampForked(c.AmsterdamTime, time) 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. // IsVerkle returns whether time is either equal to the Verkle fork time or greater.
func (c *ChainConfig) IsVerkle(num *big.Int, time uint64) bool { func (c *ChainConfig) IsVerkle(num *big.Int, time uint64) bool {
return c.IsLondon(num) && isTimestampForked(c.VerkleTime, time) 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) { if isForkTimestampIncompatible(c.AmsterdamTime, newcfg.AmsterdamTime, headTimestamp) {
return newTimestampCompatError("Amsterdam fork timestamp", c.AmsterdamTime, newcfg.AmsterdamTime) 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 return nil
} }
@ -1380,7 +1395,7 @@ type Rules struct {
IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool
IsBerlin, IsLondon bool IsBerlin, IsLondon bool
IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool
IsAmsterdam, IsVerkle bool IsAmsterdam, IsMLDSAPrecompile, IsVerkle bool
} }
// Rules ensures c's ChainID is not nil. // 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) isMerge = isMerge && c.IsLondon(num)
isVerkle := isMerge && c.IsVerkle(num, timestamp) isVerkle := isMerge && c.IsVerkle(num, timestamp)
return Rules{ return Rules{
IsHomestead: c.IsHomestead(num), IsHomestead: c.IsHomestead(num),
IsEIP150: c.IsEIP150(num), IsEIP150: c.IsEIP150(num),
IsEIP155: c.IsEIP155(num), IsEIP155: c.IsEIP155(num),
IsEIP158: c.IsEIP158(num), IsEIP158: c.IsEIP158(num),
IsByzantium: c.IsByzantium(num), IsByzantium: c.IsByzantium(num),
IsConstantinople: c.IsConstantinople(num), IsConstantinople: c.IsConstantinople(num),
IsPetersburg: c.IsPetersburg(num), IsPetersburg: c.IsPetersburg(num),
IsIstanbul: c.IsIstanbul(num), IsIstanbul: c.IsIstanbul(num),
IsBerlin: c.IsBerlin(num), IsBerlin: c.IsBerlin(num),
IsEIP2929: c.IsBerlin(num) && !isVerkle, IsEIP2929: c.IsBerlin(num) && !isVerkle,
IsLondon: c.IsLondon(num), IsLondon: c.IsLondon(num),
IsMerge: isMerge, IsMerge: isMerge,
IsShanghai: isMerge && c.IsShanghai(num, timestamp), IsShanghai: isMerge && c.IsShanghai(num, timestamp),
IsCancun: isMerge && c.IsCancun(num, timestamp), IsCancun: isMerge && c.IsCancun(num, timestamp),
IsPrague: isMerge && c.IsPrague(num, timestamp), IsPrague: isMerge && c.IsPrague(num, timestamp),
IsOsaka: isMerge && c.IsOsaka(num, timestamp), IsOsaka: isMerge && c.IsOsaka(num, timestamp),
IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp), IsAmsterdam: isMerge && c.IsAmsterdam(num, timestamp),
IsVerkle: isVerkle, IsMLDSAPrecompile: c.IsMLDSAPrecompile(num, timestamp),
IsEIP4762: isVerkle, IsVerkle: isVerkle,
IsEIP4762: isVerkle,
} }
} }

View file

@ -110,6 +110,17 @@ func TestCheckCompatible(t *testing.T) {
RewindToTime: 9, 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 { for _, test := range tests {
@ -137,6 +148,18 @@ func TestConfigRules(t *testing.T) {
if r := c.Rules(big.NewInt(0), true, stamp); !r.IsShanghai { if r := c.Rules(big.NewInt(0), true, stamp); !r.IsShanghai {
t.Errorf("expected %v to be shanghai", stamp) 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) { func TestTimestampCompatError(t *testing.T) {