mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-18 21:01:38 +00:00
Adding MLSDA65 precompile to begin adding quantum hardening support to Ethereum smart contracts
This commit is contained in:
parent
453d0f9299
commit
57e8860dfe
8 changed files with 529 additions and 40 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
54
core/vm/precompile_mldsa65.go
Normal file
54
core/vm/precompile_mldsa65.go
Normal 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"
|
||||||
|
}
|
||||||
230
docs/mldsa65-precompile-example.md
Normal file
230
docs/mldsa65-precompile-example.md
Normal 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
1
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/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
2
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/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=
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue