mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
308 lines
8.7 KiB
Go
308 lines
8.7 KiB
Go
// safekatdump generates real known-answer test (KAT) vectors and the
|
|
// EXACT precompile input bytes for the Corona (0x012206), Pulsar (0x012204),
|
|
// Magnetar (0x012207) and CGGMP21 (0x0800..0003) verify precompiles, so the
|
|
// safe-{corona,pulsar,magnetar,cggmp21} Foundry tests can assert that their
|
|
// Solidity calldata framing matches the on-chain precompile wire format
|
|
// byte-for-byte.
|
|
//
|
|
// Output is JSON to stdout: one object per scheme with the precompile
|
|
// address, the verify context, the component fields, and the full framed
|
|
// `input` blob (== what the precompile's Run() parses).
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
|
|
csign "github.com/luxfi/corona/sign"
|
|
cthr "github.com/luxfi/corona/threshold"
|
|
"github.com/luxfi/crypto"
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/luxfi/crypto/slhdsa"
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
)
|
|
|
|
type katOut struct {
|
|
Scheme string `json:"scheme"`
|
|
Address string `json:"address"`
|
|
Context string `json:"context"`
|
|
Mode string `json:"mode,omitempty"`
|
|
Threshold uint32 `json:"threshold,omitempty"`
|
|
Parties uint32 `json:"parties,omitempty"`
|
|
PubKeyHex string `json:"pubkey,omitempty"`
|
|
SigHex string `json:"sig"`
|
|
MsgHex string `json:"msg,omitempty"`
|
|
MsgHash string `json:"msgHash,omitempty"`
|
|
InputHex string `json:"input"`
|
|
}
|
|
|
|
func must(err error) {
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "FATAL:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// ---- Pulsar (0x012204): byte-equal to single-party ML-DSA-65 -------------
|
|
// Wire: mode(1) || pk || msgLen:uint256(32) || msg || sig
|
|
func pulsarKAT() katOut {
|
|
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
must(err)
|
|
ctx := []byte("lux-evm-precompile-pulsar-v1")
|
|
// The Safe message is always the 32-byte Safe-tx hash.
|
|
msg := sha256.Sum256([]byte("safe-pulsar KAT message"))
|
|
sigBytes, err := priv.SignCtx(rand.Reader, msg[:], ctx)
|
|
must(err)
|
|
pub := priv.PublicKey.Bytes()
|
|
|
|
var input bytes.Buffer
|
|
input.WriteByte(0x65) // ML-DSA-65 mode
|
|
input.Write(pub)
|
|
var ml [32]byte
|
|
binary.BigEndian.PutUint64(ml[24:], uint64(len(msg)))
|
|
input.Write(ml[:])
|
|
input.Write(msg[:])
|
|
input.Write(sigBytes)
|
|
|
|
return katOut{
|
|
Scheme: "pulsar",
|
|
Address: "0x0000000000000000000000000000000000012204",
|
|
Context: string(ctx),
|
|
Mode: "0x65",
|
|
PubKeyHex: hex.EncodeToString(pub),
|
|
SigHex: hex.EncodeToString(sigBytes),
|
|
MsgHash: hex.EncodeToString(msg[:]),
|
|
InputHex: hex.EncodeToString(input.Bytes()),
|
|
}
|
|
}
|
|
|
|
// ---- Magnetar (0x012207): byte-equal to single-party SLH-DSA-SHA2-128s ----
|
|
// Wire: mode(1) || pkLen:uint16 || pk || msgLen:uint16 || msg || sig
|
|
func magnetarKAT() katOut {
|
|
priv, err := slhdsa.GenerateKey(rand.Reader, slhdsa.SHA2_128s)
|
|
must(err)
|
|
ctx := []byte("lux-evm-precompile-magnetar-v1")
|
|
msg := sha256.Sum256([]byte("safe-magnetar KAT message"))
|
|
sigBytes, err := priv.SignCtx(rand.Reader, msg[:], ctx)
|
|
must(err)
|
|
pub := priv.PublicKey.Bytes()
|
|
|
|
var input bytes.Buffer
|
|
input.WriteByte(0x00) // SHA2-128s mode
|
|
var pl [2]byte
|
|
binary.BigEndian.PutUint16(pl[:], uint16(len(pub)))
|
|
input.Write(pl[:])
|
|
input.Write(pub)
|
|
var mlh [2]byte
|
|
binary.BigEndian.PutUint16(mlh[:], uint16(len(msg)))
|
|
input.Write(mlh[:])
|
|
input.Write(msg[:])
|
|
input.Write(sigBytes)
|
|
|
|
return katOut{
|
|
Scheme: "magnetar",
|
|
Address: "0x0000000000000000000000000000000000012207",
|
|
Context: string(ctx),
|
|
Mode: "0x00",
|
|
PubKeyHex: hex.EncodeToString(pub),
|
|
SigHex: hex.EncodeToString(sigBytes),
|
|
MsgHash: hex.EncodeToString(msg[:]),
|
|
InputHex: hex.EncodeToString(input.Bytes()),
|
|
}
|
|
}
|
|
|
|
// ---- Corona (0x012206): real 2-round LWE threshold signature -------------
|
|
// Wire: t:uint32 || n:uint32 || msgHash:32 || serializedSig
|
|
// serializedSig (per precompile contract.go deserializeSignature):
|
|
//
|
|
// c || z[N] || Delta[M] || A[M][N] || bTilde[M], each poly = N()*8 bytes BE.
|
|
func coronaKAT(t, n uint32) katOut {
|
|
shares, groupKey, err := cthr.GenerateKeysTrustedDealer(int(t), int(n), rand.Reader)
|
|
must(err)
|
|
|
|
prfKey := make([]byte, csign.KeySize)
|
|
_, _ = rand.Read(prfKey)
|
|
|
|
signers := make([]int, n)
|
|
for i := range signers {
|
|
signers[i] = i
|
|
}
|
|
sessionID := 1
|
|
|
|
tsigners := make([]*cthr.Signer, n)
|
|
for i, sh := range shares {
|
|
tsigners[i] = cthr.NewSigner(sh)
|
|
}
|
|
|
|
// 32-byte message hash exactly as the precompile receives it.
|
|
var msgHash [32]byte
|
|
copy(msgHash[:], []byte("corona-kat-msg"))
|
|
signMessage := fmt.Sprintf("lux-evm-precompile-corona-v1|%x", msgHash[:])
|
|
|
|
round1 := make(map[int]*cthr.Round1Data)
|
|
for i, s := range tsigners {
|
|
r1, err := s.Round1(sessionID, prfKey, signers)
|
|
must(err)
|
|
round1[i] = r1
|
|
}
|
|
round2 := make(map[int]*cthr.Round2Data)
|
|
for i, s := range tsigners {
|
|
r2, err := s.Round2(sessionID, signMessage, prfKey, signers, round1)
|
|
must(err)
|
|
round2[i] = r2
|
|
}
|
|
sig, err := tsigners[0].Finalize(round2)
|
|
must(err)
|
|
|
|
if !cthr.Verify(groupKey, signMessage, sig) {
|
|
must(fmt.Errorf("corona sanity verify failed pre-serialization"))
|
|
}
|
|
|
|
sigBytes, err := coronaSerialize(groupKey.Params, sig, groupKey)
|
|
must(err)
|
|
|
|
var input bytes.Buffer
|
|
var tn [8]byte
|
|
binary.BigEndian.PutUint32(tn[0:4], t)
|
|
binary.BigEndian.PutUint32(tn[4:8], n)
|
|
input.Write(tn[:])
|
|
input.Write(msgHash[:])
|
|
input.Write(sigBytes)
|
|
|
|
return katOut{
|
|
Scheme: "corona",
|
|
Address: "0x0000000000000000000000000000000000012206",
|
|
Context: "lux-evm-precompile-corona-v1",
|
|
Threshold: t,
|
|
Parties: n,
|
|
SigHex: hex.EncodeToString(sigBytes),
|
|
MsgHash: hex.EncodeToString(msgHash[:]),
|
|
InputHex: hex.EncodeToString(input.Bytes()),
|
|
}
|
|
}
|
|
|
|
// coronaSerialize replicates precompile/corona/contract_test.go serializeSignature
|
|
// EXACTLY (8-byte big-endian coefficients, component order
|
|
// c || z[N] || Delta[M] || A[M][N] || bTilde[M]).
|
|
func coronaSerialize(params *cthr.Params, sig *cthr.Signature, groupKey *cthr.GroupKey) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
r := params.R
|
|
rXi := params.RXi
|
|
rNu := params.RNu
|
|
|
|
cCopy := *sig.C.CopyNew()
|
|
r.IMForm(cCopy, cCopy)
|
|
r.INTT(cCopy, cCopy)
|
|
if err := serializePoly(&buf, r, cCopy); err != nil {
|
|
return nil, err
|
|
}
|
|
for i := 0; i < csign.N; i++ {
|
|
zCopy := *sig.Z[i].CopyNew()
|
|
r.IMForm(zCopy, zCopy)
|
|
r.INTT(zCopy, zCopy)
|
|
if err := serializePoly(&buf, r, zCopy); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for i := 0; i < csign.M; i++ {
|
|
if err := serializePoly(&buf, rNu, sig.Delta[i]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for i := 0; i < csign.M; i++ {
|
|
for j := 0; j < csign.N; j++ {
|
|
aCopy := *groupKey.A[i][j].CopyNew()
|
|
r.IMForm(aCopy, aCopy)
|
|
r.INTT(aCopy, aCopy)
|
|
if err := serializePoly(&buf, r, aCopy); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
for i := 0; i < csign.M; i++ {
|
|
if err := serializePoly(&buf, rXi, groupKey.BTilde[i]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func serializePoly(buf *bytes.Buffer, r *ring.Ring, poly ring.Poly) error {
|
|
coeffs := make([]*big.Int, r.N())
|
|
r.PolyToBigint(poly, 1, coeffs)
|
|
for _, coeff := range coeffs {
|
|
cb := make([]byte, 8)
|
|
coeff.FillBytes(cb)
|
|
if _, err := buf.Write(cb); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---- CGGMP21 (0x0800..0003): threshold ECDSA, byte-equal to single-party --
|
|
// secp256k1 ECDSA. The precompile parses a standard ECDSA signature and
|
|
// asserts the recovered pubkey equals the supplied aggregated pubkey, so a
|
|
// real KAT is just a genuine secp256k1 signature whose key is the group key.
|
|
// Wire: t:uint32(4) || n:uint32(4) || pubkey(65: 0x04||x||y) || msgHash(32) ||
|
|
//
|
|
// sig(65: r||s||v).
|
|
func cggmp21KAT(t, n uint32) katOut {
|
|
priv, err := crypto.GenerateKey()
|
|
must(err)
|
|
|
|
// 32-byte message hash exactly as the precompile receives it.
|
|
msg := sha256.Sum256([]byte("safe-cggmp21 KAT message"))
|
|
|
|
// crypto.Sign produces [R || S || V] with V in {0,1}, the exact 65-byte
|
|
// layout the precompile parses at input[105:170].
|
|
sigBytes, err := crypto.Sign(msg[:], priv)
|
|
must(err)
|
|
|
|
// Uncompressed group public key: 0x04 || x || y (65 bytes).
|
|
pub := crypto.FromECDSAPub(&priv.PublicKey)
|
|
if len(pub) != 65 {
|
|
must(fmt.Errorf("cggmp21: unexpected pubkey length %d", len(pub)))
|
|
}
|
|
|
|
var input bytes.Buffer
|
|
var tn [8]byte
|
|
binary.BigEndian.PutUint32(tn[0:4], t)
|
|
binary.BigEndian.PutUint32(tn[4:8], n)
|
|
input.Write(tn[:])
|
|
input.Write(pub)
|
|
input.Write(msg[:])
|
|
input.Write(sigBytes)
|
|
|
|
return katOut{
|
|
Scheme: "cggmp21",
|
|
Address: "0x0800000000000000000000000000000000000003",
|
|
Context: "",
|
|
Threshold: t,
|
|
Parties: n,
|
|
PubKeyHex: hex.EncodeToString(pub),
|
|
SigHex: hex.EncodeToString(sigBytes),
|
|
MsgHash: hex.EncodeToString(msg[:]),
|
|
InputHex: hex.EncodeToString(input.Bytes()),
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
out := []katOut{
|
|
pulsarKAT(),
|
|
magnetarKAT(),
|
|
coronaKAT(2, 3),
|
|
cggmp21KAT(2, 3),
|
|
}
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
must(enc.Encode(out))
|
|
}
|