mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
341 lines
9.8 KiB
Go
341 lines
9.8 KiB
Go
package infra
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testKV runs a standard suite of KV operations against any KV implementation.
|
|
// Used to verify 100% parity between consensus, NATS, and Consul backends.
|
|
func testKV(t *testing.T, kv KV) {
|
|
t.Helper()
|
|
|
|
// Put + Get
|
|
err := kv.Put("ready/node0", []byte("true"))
|
|
require.NoError(t, err)
|
|
val, err := kv.Get("ready/node0")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("true"), val)
|
|
|
|
// Get non-existent
|
|
val, err = kv.Get("ready/nonexistent")
|
|
require.NoError(t, err)
|
|
assert.Nil(t, val)
|
|
|
|
// Put more keys
|
|
require.NoError(t, kv.Put("ready/node1", []byte("true")))
|
|
require.NoError(t, kv.Put("ready/node2", []byte("true")))
|
|
require.NoError(t, kv.Put("threshold_keyinfo/wallet1", []byte(`{"threshold":2}`)))
|
|
|
|
// List with prefix
|
|
pairs, err := kv.List("ready/")
|
|
require.NoError(t, err)
|
|
assert.Len(t, pairs, 3)
|
|
keys := make(map[string]string)
|
|
for _, p := range pairs {
|
|
keys[p.Key] = string(p.Value)
|
|
}
|
|
assert.Equal(t, "true", keys["ready/node0"])
|
|
assert.Equal(t, "true", keys["ready/node1"])
|
|
assert.Equal(t, "true", keys["ready/node2"])
|
|
|
|
// List different prefix
|
|
pairs, err = kv.List("threshold_keyinfo/")
|
|
require.NoError(t, err)
|
|
assert.Len(t, pairs, 1)
|
|
|
|
// Delete
|
|
require.NoError(t, kv.Delete("ready/node1"))
|
|
val, err = kv.Get("ready/node1")
|
|
require.NoError(t, err)
|
|
assert.Nil(t, val)
|
|
|
|
// List after delete
|
|
pairs, err = kv.List("ready/")
|
|
require.NoError(t, err)
|
|
assert.Len(t, pairs, 2)
|
|
|
|
// Delete non-existent (no error)
|
|
require.NoError(t, kv.Delete("ready/nonexistent"))
|
|
|
|
// Empty prefix
|
|
pairs, err = kv.List("nothing/")
|
|
require.NoError(t, err)
|
|
assert.Len(t, pairs, 0)
|
|
|
|
// Overwrite
|
|
require.NoError(t, kv.Put("ready/node0", []byte("false")))
|
|
val, err = kv.Get("ready/node0")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("false"), val)
|
|
}
|
|
|
|
func newTestConsensusKV() *ConsensusKV {
|
|
return &ConsensusKV{
|
|
state: make(map[string][]byte),
|
|
pending: make([]KVMutation, 0),
|
|
pendingCh: make(chan struct{}, 1),
|
|
nodeID: "test-node",
|
|
validators: make(map[string]*ValidatorKeys),
|
|
threshold: 1,
|
|
chainID: "test",
|
|
currentVotes: make(map[uint64]*voteCollector),
|
|
}
|
|
}
|
|
|
|
func TestConsensusKV_KVInterface(t *testing.T) {
|
|
testKV(t, newTestConsensusKV())
|
|
}
|
|
|
|
func TestConsensusKV_Snapshot(t *testing.T) {
|
|
ckv := newTestConsensusKV()
|
|
testKV(t, ckv)
|
|
|
|
snap := ckv.Snapshot()
|
|
assert.Contains(t, snap, "ready/node0")
|
|
assert.Contains(t, snap, "ready/node2")
|
|
assert.NotContains(t, snap, "ready/node1")
|
|
|
|
ckv2 := newTestConsensusKV()
|
|
require.NoError(t, ckv2.LoadSnapshot(snap, 42))
|
|
assert.Equal(t, uint64(42), ckv2.Height())
|
|
|
|
val, _ := ckv2.Get("ready/node0")
|
|
assert.Equal(t, []byte("false"), val)
|
|
}
|
|
|
|
func TestConsensusKV_BlockHash(t *testing.T) {
|
|
block := ConsensusBlock{
|
|
Height: 1,
|
|
Timestamp: time.Date(2026, 3, 11, 0, 0, 0, 0, time.UTC),
|
|
Mutations: []KVMutation{{Op: OpPut, Key: "foo", Value: []byte("bar")}},
|
|
ProposerID: "node0",
|
|
}
|
|
hash1 := block.BlockHash()
|
|
hash2 := block.BlockHash()
|
|
assert.Equal(t, hash1, hash2, "deterministic")
|
|
|
|
block.Mutations[0].Value = []byte("baz")
|
|
assert.NotEqual(t, hash1, block.BlockHash(), "different content = different hash")
|
|
}
|
|
|
|
func TestConsensusKV_BlockApply(t *testing.T) {
|
|
ckv := newTestConsensusKV()
|
|
|
|
block := ConsensusBlock{
|
|
Height: 1,
|
|
Timestamp: time.Now().UTC(),
|
|
Mutations: []KVMutation{
|
|
{Op: OpPut, Key: "ready/node0", Value: []byte("true")},
|
|
{Op: OpPut, Key: "ready/node1", Value: []byte("true")},
|
|
},
|
|
ProposerID: "node0",
|
|
}
|
|
ckv.applyFinalized(&FinalizedBlock{
|
|
Block: block,
|
|
Votes: []Vote{{VoterID: "node0", BlockHash: block.BlockHash(), Height: 1}},
|
|
})
|
|
|
|
val, _ := ckv.Get("ready/node0")
|
|
assert.Equal(t, []byte("true"), val)
|
|
assert.Equal(t, uint64(1), ckv.Height())
|
|
|
|
// Delete block
|
|
block2 := ConsensusBlock{
|
|
Height: 2,
|
|
ParentHash: ckv.LastBlockHash(),
|
|
Timestamp: time.Now().UTC(),
|
|
Mutations: []KVMutation{{Op: OpDelete, Key: "ready/node1"}},
|
|
ProposerID: "node0",
|
|
}
|
|
ckv.applyFinalized(&FinalizedBlock{
|
|
Block: block2,
|
|
Votes: []Vote{{VoterID: "node0", BlockHash: block2.BlockHash(), Height: 2}},
|
|
})
|
|
|
|
val, _ = ckv.Get("ready/node1")
|
|
assert.Nil(t, val)
|
|
assert.Equal(t, uint64(2), ckv.Height())
|
|
}
|
|
|
|
// TestConsensusKV_DualCertificate verifies Ed25519 + ML-DSA-65 dual signing.
|
|
func TestConsensusKV_DualCertificate(t *testing.T) {
|
|
// Generate dual keys for 3 validators
|
|
edPub0, edPriv0, _ := ed25519.GenerateKey(rand.Reader)
|
|
edPub1, edPriv1, _ := ed25519.GenerateKey(rand.Reader)
|
|
edPub2, _, _ := ed25519.GenerateKey(rand.Reader)
|
|
|
|
pqPriv0, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
require.NoError(t, err)
|
|
pqPriv1, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
require.NoError(t, err)
|
|
pqPriv2, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
require.NoError(t, err)
|
|
|
|
validators := map[string]*ValidatorKeys{
|
|
"node0": {EdPubKey: edPub0, PQPubKey: pqPriv0.PublicKey},
|
|
"node1": {EdPubKey: edPub1, PQPubKey: pqPriv1.PublicKey},
|
|
"node2": {EdPubKey: edPub2, PQPubKey: pqPriv2.PublicKey},
|
|
}
|
|
|
|
// Node0 proposes a block with dual signatures
|
|
block := ConsensusBlock{
|
|
Height: 1,
|
|
Timestamp: time.Now().UTC(),
|
|
Mutations: []KVMutation{{Op: OpPut, Key: "ready/node0", Value: []byte("true")}},
|
|
ProposerID: "node0",
|
|
}
|
|
blockHash := block.BlockHash()
|
|
|
|
// Sign with Ed25519
|
|
block.EdSig = ed25519.Sign(edPriv0, blockHash[:])
|
|
assert.True(t, ed25519.Verify(edPub0, blockHash[:], block.EdSig))
|
|
|
|
// Sign with ML-DSA-65
|
|
pqSig0, err := pqPriv0.Sign(nil, blockHash[:], nil)
|
|
require.NoError(t, err)
|
|
block.PQSig = pqSig0
|
|
assert.True(t, pqPriv0.PublicKey.VerifySignature(blockHash[:], pqSig0))
|
|
|
|
// Node0 and Node1 cast dual-signed votes
|
|
pqSig0Vote, _ := pqPriv0.Sign(nil, blockHash[:], nil)
|
|
pqSig1Vote, _ := pqPriv1.Sign(nil, blockHash[:], nil)
|
|
|
|
vote0 := Vote{
|
|
VoterID: "node0",
|
|
BlockHash: blockHash,
|
|
Height: 1,
|
|
EdSig: ed25519.Sign(edPriv0, blockHash[:]),
|
|
PQSig: pqSig0Vote,
|
|
}
|
|
vote1 := Vote{
|
|
VoterID: "node1",
|
|
BlockHash: blockHash,
|
|
Height: 1,
|
|
EdSig: ed25519.Sign(edPriv1, blockHash[:]),
|
|
PQSig: pqSig1Vote,
|
|
}
|
|
|
|
// Verify both signatures on both votes
|
|
assert.True(t, ed25519.Verify(edPub0, blockHash[:], vote0.EdSig))
|
|
assert.True(t, pqPriv0.PublicKey.VerifySignature(blockHash[:], vote0.PQSig))
|
|
assert.True(t, ed25519.Verify(edPub1, blockHash[:], vote1.EdSig))
|
|
assert.True(t, pqPriv1.PublicKey.VerifySignature(blockHash[:], vote1.PQSig))
|
|
|
|
// Apply finalized block to node2
|
|
ckv := &ConsensusKV{
|
|
state: make(map[string][]byte),
|
|
pending: make([]KVMutation, 0),
|
|
pendingCh: make(chan struct{}, 1),
|
|
nodeID: "node2",
|
|
validators: validators,
|
|
threshold: 2,
|
|
chainID: "test",
|
|
currentVotes: make(map[uint64]*voteCollector),
|
|
}
|
|
|
|
finalized := FinalizedBlock{Block: block, Votes: []Vote{vote0, vote1}}
|
|
ckv.applyFinalized(&finalized)
|
|
|
|
val, _ := ckv.Get("ready/node0")
|
|
assert.Equal(t, []byte("true"), val)
|
|
assert.Equal(t, uint64(1), ckv.Height())
|
|
}
|
|
|
|
// TestConsensusKV_DualCertVerification tests that handleFinalized verifies signatures.
|
|
func TestConsensusKV_DualCertVerification(t *testing.T) {
|
|
edPub0, edPriv0, _ := ed25519.GenerateKey(rand.Reader)
|
|
edPub1, _, _ := ed25519.GenerateKey(rand.Reader)
|
|
edPub2, _, _ := ed25519.GenerateKey(rand.Reader)
|
|
|
|
pqPriv0, _ := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
pqPriv1, _ := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
pqPriv2, _ := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
|
|
validators := map[string]*ValidatorKeys{
|
|
"node0": {EdPubKey: edPub0, PQPubKey: pqPriv0.PublicKey},
|
|
"node1": {EdPubKey: edPub1, PQPubKey: pqPriv1.PublicKey},
|
|
"node2": {EdPubKey: edPub2, PQPubKey: pqPriv2.PublicKey},
|
|
}
|
|
|
|
ckv := &ConsensusKV{
|
|
state: make(map[string][]byte),
|
|
pending: make([]KVMutation, 0),
|
|
pendingCh: make(chan struct{}, 1),
|
|
nodeID: "node2",
|
|
validators: validators,
|
|
threshold: 2, // need 2 votes
|
|
chainID: "test",
|
|
currentVotes: make(map[uint64]*voteCollector),
|
|
}
|
|
|
|
block := ConsensusBlock{
|
|
Height: 1,
|
|
Timestamp: time.Now().UTC(),
|
|
Mutations: []KVMutation{{Op: OpPut, Key: "test/key", Value: []byte("val")}},
|
|
ProposerID: "node0",
|
|
}
|
|
blockHash := block.BlockHash()
|
|
|
|
// Only 1 valid dual-signed vote — should NOT apply (threshold=2)
|
|
pqSig, _ := pqPriv0.Sign(nil, blockHash[:], nil)
|
|
finalized := FinalizedBlock{
|
|
Block: block,
|
|
Votes: []Vote{{
|
|
VoterID: "node0",
|
|
BlockHash: blockHash,
|
|
Height: 1,
|
|
EdSig: ed25519.Sign(edPriv0, blockHash[:]),
|
|
PQSig: pqSig,
|
|
}},
|
|
}
|
|
|
|
data, _ := json.Marshal(finalized)
|
|
ckv.handleFinalized(data)
|
|
|
|
val, _ := ckv.Get("test/key")
|
|
assert.Nil(t, val, "insufficient votes should not apply")
|
|
assert.Equal(t, uint64(0), ckv.Height())
|
|
}
|
|
|
|
func TestConsensusKV_PQKeyGeneration(t *testing.T) {
|
|
// Verify ML-DSA-65 key sizes
|
|
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, mldsa.MLDSA65PublicKeySize, len(priv.PublicKey.Bytes())) // 1,952 bytes
|
|
assert.Equal(t, mldsa.MLDSA65PrivateKeySize, len(priv.Bytes())) // 4,032 bytes
|
|
|
|
// Sign and verify
|
|
msg := []byte("test message for post-quantum signing")
|
|
sig, err := priv.Sign(nil, msg, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, mldsa.MLDSA65SignatureSize, len(sig)) // 3,309 bytes
|
|
|
|
assert.True(t, priv.PublicKey.VerifySignature(msg, sig))
|
|
|
|
// Tampered message should fail
|
|
assert.False(t, priv.PublicKey.VerifySignature([]byte("wrong"), sig))
|
|
|
|
t.Logf("ML-DSA-65 sizes: pubkey=%d, privkey=%d, sig=%d",
|
|
len(priv.PublicKey.Bytes()), len(priv.Bytes()), len(sig))
|
|
}
|
|
|
|
func TestConsensusKV_Metadata(t *testing.T) {
|
|
ckv := newTestConsensusKV()
|
|
ckv.validators = map[string]*ValidatorKeys{
|
|
"node0": nil, "node1": nil, "node2": nil,
|
|
}
|
|
ckv.threshold = 2
|
|
|
|
assert.Equal(t, 3, ckv.ValidatorCount())
|
|
assert.Equal(t, 2, ckv.Threshold())
|
|
}
|