quantumvm: a validator key parses its secret once and keeps it

Sign now asks the key for its ML-DSA form instead of rebuilding one per
call. A key that arrives as bytes parses on first use and reuses that
key for every signature after; a freshly generated key already holds its
parsed form. A secret that will not parse fails closed on every call.

Takes crypto v1.20.5, where a private key copies the secret it is given
and hands back a copy, so the bytes a caller persists out of a generated
key stay readable for as long as the caller holds them.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-15 00:52:02 -07:00
co-authored by Hanzo Dev
parent 9427d78408
commit 5493e3a2ed
4 changed files with 148 additions and 12 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/luxfi/ai v0.1.0
github.com/luxfi/consensus v1.36.2
github.com/luxfi/constants v1.6.2
github.com/luxfi/crypto v1.20.2
github.com/luxfi/crypto v1.20.5
github.com/luxfi/database v1.21.1
github.com/luxfi/dex v1.14.2
github.com/luxfi/evm v1.104.30
+2
View File
@@ -387,6 +387,8 @@ github.com/luxfi/corona v0.10.4 h1:/+Uy5iOWBMkr+XACnRRiyrbb5ebsZLsUXjHfJW3sFyw=
github.com/luxfi/corona v0.10.4/go.mod h1:44Tjnm2uRG22kmmLfCzR8QEO8OXZ3jR/OnUKLsnjVJ8=
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
github.com/luxfi/crypto v1.20.5 h1:kL69Ewyr90A/wQsTnIK8Zj6cHUPYvM1WHhgSErc2Yh0=
github.com/luxfi/crypto v1.20.5/go.mod h1:gic3n9puSJhZFonBfmIwc5Cck/SMqetxpcfzrk9gna8=
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.21.1 h1:GNnoWVa82l+n2dK7x2aG8LR2NEToq6ZCRX0sQjmK0OM=
+19 -11
View File
@@ -63,7 +63,22 @@ type MLDSAValidatorKey struct {
PublicKey []byte
PrivateKey []byte
Nonce []byte
mldsaPriv *mldsa.PrivateKey
parse sync.Once
mldsaPriv *mldsa.PrivateKey
parseErr error
}
// signer returns the ML-DSA key for this identity. A key that arrives as bytes
// is parsed on first use and reused for every signature after that; a key that
// was just generated already holds its parsed form.
func (k *MLDSAValidatorKey) signer(mode mldsa.Mode) (*mldsa.PrivateKey, error) {
k.parse.Do(func() {
if k.mldsaPriv == nil {
k.mldsaPriv, k.parseErr = mldsa.PrivateKeyFromBytes(mode, k.PrivateKey)
}
})
return k.mldsaPriv, k.parseErr
}
// NewQuantumSigner creates a new quantum signer with real ML-DSA
@@ -128,16 +143,9 @@ func (qs *QuantumSigner) Sign(message []byte, key *MLDSAValidatorKey) (*QuantumS
return nil, ErrInvalidCoronaKey
}
// Restore ML-DSA key if not cached
var mldsaPriv *mldsa.PrivateKey
if key.mldsaPriv != nil {
mldsaPriv = key.mldsaPriv
} else {
var err error
mldsaPriv, err = mldsa.PrivateKeyFromBytes(qs.mldsaMode, key.PrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to restore ML-DSA key: %w", err)
}
mldsaPriv, err := key.signer(qs.mldsaMode)
if err != nil {
return nil, fmt.Errorf("failed to restore ML-DSA key: %w", err)
}
// Generate quantum stamp
+126
View File
@@ -0,0 +1,126 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quantum
import (
"bytes"
"runtime"
"testing"
"time"
"github.com/luxfi/log"
)
// runtimeCollect gives the collector a chance to run any finalizer that a
// discarded key armed.
func runtimeCollect() {
runtime.GC()
time.Sleep(5 * time.Millisecond)
runtime.GC()
}
func signer(t *testing.T) *QuantumSigner {
t.Helper()
return NewQuantumSigner(log.NewNoOpLogger(), AlgorithmMLDSA65, 0, time.Minute, 8)
}
// stored is a validator key in the shape it takes coming back from storage or
// the wire: the exported fields only, with nothing parsed yet.
func stored(t *testing.T, qs *QuantumSigner) *MLDSAValidatorKey {
t.Helper()
gen, err := qs.GenerateCoronaKey()
if err != nil {
t.Fatalf("GenerateCoronaKey: %v", err)
}
return &MLDSAValidatorKey{
Version: gen.Version,
PublicKey: gen.PublicKey,
PrivateKey: gen.PrivateKey,
Nonce: gen.Nonce,
}
}
// TestSignParsesStoredSecretOnce pins that repeated signing reuses one parsed
// key rather than building a new one per signature.
func TestSignParsesStoredSecretOnce(t *testing.T) {
qs := signer(t)
key := stored(t, qs)
msg := []byte("round digest")
sig, err := qs.Sign(msg, key)
if err != nil {
t.Fatalf("first Sign: %v", err)
}
if err := qs.Verify(msg, sig); err != nil {
t.Fatalf("first signature did not verify: %v", err)
}
first := key.mldsaPriv
if first == nil {
t.Fatal("Sign left the parsed key unset, so the next Sign parses again")
}
sig, err = qs.Sign(msg, key)
if err != nil {
t.Fatalf("second Sign: %v", err)
}
if key.mldsaPriv != first {
t.Fatal("Sign parsed the stored secret a second time")
}
if err := qs.Verify(msg, sig); err != nil {
t.Fatalf("second signature did not verify: %v", err)
}
}
// TestSignHoldsUpUnderCollection signs repeatedly across collection cycles: a
// key whose secret outlives every signature keeps producing valid signatures.
func TestSignHoldsUpUnderCollection(t *testing.T) {
qs := signer(t)
key := stored(t, qs)
msg := []byte("round digest")
for i := 0; i < 8; i++ {
sig, err := qs.Sign(msg, key)
if err != nil {
t.Fatalf("Sign %d: %v", i, err)
}
if err := qs.Verify(msg, sig); err != nil {
t.Fatalf("signature %d did not verify: %v", i, err)
}
runtimeCollect()
}
}
// TestGeneratedKeyOwnsItsSecret pins that the bytes a caller persists out of a
// generated key stay readable for as long as the caller holds them, whatever
// becomes of the key they came from.
func TestGeneratedKeyOwnsItsSecret(t *testing.T) {
qs := signer(t)
gen, err := qs.GenerateCoronaKey()
if err != nil {
t.Fatalf("GenerateCoronaKey: %v", err)
}
persisted := gen.PrivateKey
want := bytes.Clone(persisted)
gen = nil
runtimeCollect()
runtimeCollect()
if !bytes.Equal(persisted, want) {
t.Fatal("collecting the generated key changed the secret already read out of it")
}
}
// TestSignRejectsUnparseableSecret pins that a key that cannot be parsed fails
// closed, every time, rather than signing with whatever is in the buffer.
func TestSignRejectsUnparseableSecret(t *testing.T) {
qs := signer(t)
key := &MLDSAValidatorKey{PrivateKey: []byte("too short to be a key")}
for i := 0; i < 2; i++ {
if _, err := qs.Sign([]byte("round digest"), key); err == nil {
t.Fatalf("Sign %d accepted an unparseable secret", i)
}
}
}