mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user