mirror of
https://github.com/luxfi/coronad.git
synced 2026-10-30 16:59:09 +00:00
github.com/luxfi/coronad existed only as loose files: no .git, no history, no
remote. Invisible to any sweep that looks for a .git directory, and an rm -rf
would have destroyed it unrecoverably.
Includes two dependency repairs.
1. It did not build: "unknown revision v1.16.98" — the v1.16.x line was DELETED
from github.com/luxfi/geth. The requirement is baked into published modules
(crypto/constants/warp pin the dead tag, some also `exclude` the
alternative), so requiring crypto v1.20.2 / constants v1.6.2 / warp v1.24.1
is what escapes it.
2. Dropped two replace directives pointing at absolute paths on one disk:
replace github.com/luxfi/corona => /Users/z/work/lux/corona
replace github.com/luxfi/warp => /Users/z/work/lux/warp
Those would have made this module resolvable ONLY on this machine — CI or
any other checkout could never build it. They were also unnecessary: the
published versions the `require` lines already name (corona v0.10.4) build
and test clean without them.
Verified: go build ./... exits 0, go test ./... 1 ok / 0 fail.
The compiled coronad binary is ignored rather than committed.
NOTE: no remote — luxfi/coronad does not exist on GitHub. Still one copy.
249 lines
9.9 KiB
Go
249 lines
9.9 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package coronad_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"testing"
|
|
|
|
"github.com/luxfi/coronad"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/warp"
|
|
|
|
corona "github.com/luxfi/corona/threshold"
|
|
warppulsar "github.com/luxfi/warp/pulsar"
|
|
)
|
|
|
|
// committee returns n deterministic validator identity strings.
|
|
func committee(n int) []string {
|
|
vs := make([]string, n)
|
|
for i := range vs {
|
|
vs[i] = string(rune('A'+i)) + "-corona-validator"
|
|
}
|
|
return vs
|
|
}
|
|
|
|
// fixedResolver is a warp/pulsar.CoronaGroupKeyResolver that returns one fixed
|
|
// group key + suite, regardless of routing. Used to exercise the on-chain
|
|
// registry path where the verifier holds a DESERIALIZED group key.
|
|
type fixedResolver struct {
|
|
gk *corona.GroupKey
|
|
suite string
|
|
}
|
|
|
|
func (f *fixedResolver) ResolveGroupKey(_ [32]byte, _ uint64, _ uint64) (*corona.GroupKey, string, error) {
|
|
return f.gk, f.suite, nil
|
|
}
|
|
|
|
// TestEndToEnd_CoronadSigns_WarpVerifies is the headline proof: a t-of-n coronad
|
|
// committee bootstraps a dealerless key era, threshold-signs a subject, and the
|
|
// warp Corona lane's RingtailVerifier ACCEPTS the emitted CoronaEvidence.
|
|
func TestEndToEnd_CoronadSigns_WarpVerifies(t *testing.T) {
|
|
const (
|
|
n = 5
|
|
threshold = 3
|
|
keyEraID = uint64(7)
|
|
)
|
|
chainID := ids.ID{0xC0, 0x12, 0x20, 0x06} // arbitrary source chain
|
|
era, err := coronad.Bootstrap(chainID, keyEraID, threshold, committee(n), rand.Reader)
|
|
require.NoError(t, err, "dealerless Bootstrap")
|
|
t.Logf("bootstrapped Corona era: chain=%x keyEra=%d gen=%d t=%d n=%d",
|
|
chainID[:4], era.KeyEraID(), era.Generation(), era.Threshold(), era.Size())
|
|
|
|
// The subject is a 32-byte finality digest. Use the canonical quasar
|
|
// consensus subject M to show the verifier is subject-agnostic.
|
|
subjectArr := warp.QuasarFinalitySubject(warp.QuasarFinalityParams{
|
|
ChainID: chainID,
|
|
Height: 42,
|
|
Round: 1,
|
|
BlockID: ids.ID{0xB1, 0x0C},
|
|
SignerSetID: ids.ID{0x5e, 0x70},
|
|
KeyEraID: keyEraID,
|
|
Generation: era.Generation(),
|
|
})
|
|
subject := subjectArr[:]
|
|
|
|
// coronad signs with the FULL committee.
|
|
signer := coronad.NewSigner(era, coronad.Loopback)
|
|
ev, err := signer.ThresholdSign(subject)
|
|
require.NoError(t, err, "coronad ThresholdSign (full committee)")
|
|
require.Equal(t, chainID, ev.ChainID)
|
|
require.Equal(t, keyEraID, ev.KeyEraID)
|
|
require.Equal(t, era.Generation(), ev.Generation)
|
|
require.NotEmpty(t, ev.Sig)
|
|
t.Logf("coronad emitted CoronaEvidence: %d-byte lattice threshold signature", len(ev.Sig))
|
|
|
|
// The warp Corona lane verifies it.
|
|
resolver := coronad.NewEraResolver()
|
|
resolver.Add(era)
|
|
v := warppulsar.NewRingtailVerifier(resolver)
|
|
require.NoError(t, v.VerifyRingtailThreshold(subject, ev),
|
|
"warp RingtailVerifier MUST accept coronad evidence")
|
|
t.Log("PASS: coronad signed -> warp/pulsar.RingtailVerifier accepted CoronaEvidence")
|
|
}
|
|
|
|
// TestTrueThreshold_StrictSubsetSigns proves coronad is a real t-of-n signer: an
|
|
// EXACT t-of-n strict subset (3 of the 5 committee members) produces a signature
|
|
// that verifies under the group key, while t-1 members are refused fail-closed.
|
|
func TestTrueThreshold_StrictSubsetSigns(t *testing.T) {
|
|
const n, threshold = 5, 3
|
|
chainID := ids.ID{0x5, 0x5, 0x5, 0x5}
|
|
era, err := coronad.Bootstrap(chainID, 1, threshold, committee(n), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
subject := make([]byte, 32)
|
|
_, _ = rand.Read(subject)
|
|
|
|
resolver := coronad.NewEraResolver()
|
|
resolver.Add(era)
|
|
v := warppulsar.NewRingtailVerifier(resolver)
|
|
|
|
// Strict t-subset {0, 2, 4} (omits 1 and 3) signs and verifies.
|
|
subsetSigner := coronad.NewSigner(era, coronad.Loopback, 0, 2, 4)
|
|
ev, err := subsetSigner.ThresholdSign(subject)
|
|
require.NoError(t, err, "strict t-of-n subset must sign")
|
|
require.NoError(t, v.VerifyRingtailThreshold(subject, ev),
|
|
"strict t-of-n subset signature must verify under the group key")
|
|
t.Log("PASS: strict 3-of-5 subset {0,2,4} produced a verifying signature (true t-of-n)")
|
|
|
|
// A different strict t-subset {1, 3, 4} also signs and verifies — the share
|
|
// at each evaluation point interpolates s for ANY t-subset.
|
|
subsetSigner2 := coronad.NewSigner(era, coronad.Loopback, 1, 3, 4)
|
|
ev2, err := subsetSigner2.ThresholdSign(subject)
|
|
require.NoError(t, err)
|
|
require.NoError(t, v.VerifyRingtailThreshold(subject, ev2))
|
|
|
|
// t-1 members are refused fail-closed: coronad never starts a sub-quorum.
|
|
tooFew := coronad.NewSigner(era, coronad.Loopback, 0, 1)
|
|
_, err = tooFew.ThresholdSign(subject)
|
|
require.ErrorIs(t, err, coronad.ErrInsufficientParticipants,
|
|
"a sub-threshold set must be refused")
|
|
t.Log("PASS: t-1 participants refused with ErrInsufficientParticipants")
|
|
}
|
|
|
|
// TestVerifierRejectsTamperedAndWrongEra exercises the negative paths the warp
|
|
// lane must reject.
|
|
func TestVerifierRejectsTamperedAndWrongEra(t *testing.T) {
|
|
const n, threshold = 3, 2
|
|
chainID := ids.ID{0xAB, 0xCD}
|
|
era, err := coronad.Bootstrap(chainID, 4, threshold, committee(n), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
subject := make([]byte, 32)
|
|
_, _ = rand.Read(subject)
|
|
other := make([]byte, 32)
|
|
_, _ = rand.Read(other)
|
|
|
|
signer := coronad.NewSigner(era, coronad.Loopback)
|
|
ev, err := signer.ThresholdSign(subject)
|
|
require.NoError(t, err)
|
|
|
|
resolver := coronad.NewEraResolver()
|
|
resolver.Add(era)
|
|
v := warppulsar.NewRingtailVerifier(resolver)
|
|
|
|
// Sanity: the right subject verifies.
|
|
require.NoError(t, v.VerifyRingtailThreshold(subject, ev))
|
|
|
|
// Tampered subject: the corona signature is over CoronaSigningBytes(subject),
|
|
// so verifying over a DIFFERENT subject fails the lattice check.
|
|
require.ErrorIs(t, v.VerifyRingtailThreshold(other, ev), warppulsar.ErrCoronaVerifyFailed,
|
|
"a signature must not verify against a different subject")
|
|
|
|
// Wrong generation: the resolver has no key for it -> resolver failure.
|
|
evWrongGen := ev
|
|
evWrongGen.Generation = ev.Generation + 99
|
|
require.ErrorIs(t, v.VerifyRingtailThreshold(subject, evWrongGen), warppulsar.ErrCoronaGroupKeyResolverFailed)
|
|
|
|
// Truncated signature bytes -> deserialize failure (still fail-closed).
|
|
evBadSig := ev
|
|
evBadSig.Sig = ev.Sig[:len(ev.Sig)/2]
|
|
require.Error(t, v.VerifyRingtailThreshold(subject, evBadSig))
|
|
t.Log("PASS: tampered subject, wrong generation, and corrupt signature all rejected")
|
|
}
|
|
|
|
// TestReshareAndRefreshPreserveGroupKey proves the key-era lifecycle: after a
|
|
// Refresh and a Reshare the Generation advances but signatures still verify
|
|
// under the SAME group key.
|
|
func TestReshareAndRefreshPreserveGroupKey(t *testing.T) {
|
|
const n, threshold = 5, 3
|
|
chainID := ids.ID{0x9, 0x9, 0x9}
|
|
era, err := coronad.Bootstrap(chainID, 2, threshold, committee(n), rand.Reader)
|
|
require.NoError(t, err)
|
|
gen0 := era.Generation()
|
|
|
|
subject := make([]byte, 32)
|
|
_, _ = rand.Read(subject)
|
|
|
|
resolver := coronad.NewEraResolver()
|
|
resolver.Add(era)
|
|
v := warppulsar.NewRingtailVerifier(resolver)
|
|
|
|
// Refresh (same committee) -> generation advances, group key unchanged.
|
|
require.NoError(t, era.Refresh(rand.Reader))
|
|
require.Equal(t, gen0+1, era.Generation(), "Refresh must advance Generation")
|
|
resolver.Add(era) // register the new generation
|
|
ev, err := coronad.NewSigner(era, coronad.Loopback).ThresholdSign(subject)
|
|
require.NoError(t, err)
|
|
require.Equal(t, era.Generation(), ev.Generation)
|
|
require.NoError(t, v.VerifyRingtailThreshold(subject, ev),
|
|
"post-Refresh signature must verify under the unchanged group key")
|
|
|
|
// Reshare onto a partially-new committee -> generation advances again.
|
|
newCommittee := []string{"A-corona-validator", "B-corona-validator", "X-new", "Y-new", "Z-new"}
|
|
require.NoError(t, era.Reshare(newCommittee, threshold, rand.Reader))
|
|
require.Equal(t, gen0+2, era.Generation(), "Reshare must advance Generation")
|
|
resolver.Add(era)
|
|
ev2, err := coronad.NewSigner(era, coronad.Loopback).ThresholdSign(subject)
|
|
require.NoError(t, err)
|
|
require.NoError(t, v.VerifyRingtailThreshold(subject, ev2),
|
|
"post-Reshare signature must verify under the unchanged group key")
|
|
t.Logf("PASS: Refresh+Reshare advanced gen %d->%d, key preserved", gen0, era.Generation())
|
|
}
|
|
|
|
// TestOnChainRegistryPath proves the production verify path: a destination chain
|
|
// holds the group key as WIRE BYTES, deserializes it (fresh ring Params), and
|
|
// verifies coronad's evidence with that deserialized key.
|
|
func TestOnChainRegistryPath(t *testing.T) {
|
|
const n, threshold = 3, 2
|
|
chainID := ids.ID{0x1, 0x2, 0x3}
|
|
era, err := coronad.Bootstrap(chainID, 5, threshold, committee(n), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
subject := make([]byte, 32)
|
|
_, _ = rand.Read(subject)
|
|
ev, err := coronad.NewSigner(era, coronad.Loopback).ThresholdSign(subject)
|
|
require.NoError(t, err)
|
|
|
|
// Round-trip the group key through its canonical wire form, as an on-chain
|
|
// registry would store and a destination chain would load it.
|
|
gkBytes, err := era.GroupKey().MarshalBinary()
|
|
require.NoError(t, err)
|
|
var gk2 corona.GroupKey
|
|
require.NoError(t, gk2.UnmarshalBinary(gkBytes))
|
|
|
|
v := warppulsar.NewRingtailVerifier(&fixedResolver{gk: &gk2, suite: string(warp.DefaultCoronaSuiteID)})
|
|
require.NoError(t, v.VerifyRingtailThreshold(subject, ev),
|
|
"evidence must verify under a group key loaded from wire (fresh Params)")
|
|
t.Log("PASS: evidence verified under a wire-deserialized group key (on-chain registry path)")
|
|
}
|
|
|
|
// TestSuiteMismatchRejected proves the Corona lane suite gate: a resolver that
|
|
// reports a non-Corona suite is rejected before any lattice work.
|
|
func TestSuiteMismatchRejected(t *testing.T) {
|
|
const n, threshold = 3, 2
|
|
chainID := ids.ID{0x7}
|
|
era, err := coronad.Bootstrap(chainID, 1, threshold, committee(n), rand.Reader)
|
|
require.NoError(t, err)
|
|
subject := make([]byte, 32)
|
|
_, _ = rand.Read(subject)
|
|
ev, err := coronad.NewSigner(era, coronad.Loopback).ThresholdSign(subject)
|
|
require.NoError(t, err)
|
|
|
|
v := warppulsar.NewRingtailVerifier(&fixedResolver{gk: era.GroupKey(), suite: "Pulsar-SHA3-experimental"})
|
|
require.ErrorIs(t, v.VerifyRingtailThreshold(subject, ev), warppulsar.ErrCoronaSuiteMismatch)
|
|
}
|