Files
warp/pulsar/pulsar_test.go
zeekay 7161310660 warp: flip pulsar (R-LWE) imports to corona
Cross-chain message Pulse path now wires github.com/luxfi/corona
(the R-LWE production threshold, formerly github.com/luxfi/pulsar)
instead of the pre-rename path. Drops compound-word aliases per
the family rename discipline:

  pulsarHash "github.com/luxfi/pulsar/hash"   → "github.com/luxfi/corona/hash"
  pulsarKernel "github.com/luxfi/pulsar/threshold" → corona "github.com/luxfi/corona/threshold"

Use-sites become hash.X and corona.X -- single-word, value-qualified.

Wire-format customization tags (WARP-PULSAR-ENVELOPE-v1,
Pulsar-SHA3) stay stable -- they are signature-bound bytes, not
identifiers.

go.mod replace ../pulsar → ../corona. Full warp/pulsar test sweep
passes (4.756s).
2026-05-11 22:31:20 -07:00

234 lines
7.1 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pulsar
import (
"bytes"
"crypto/rand"
"errors"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/warp"
"github.com/stretchr/testify/require"
corona "github.com/luxfi/corona/threshold"
)
// runPulsarCeremony runs a full t-of-n threshold signing ceremony
// against the given message and returns the resulting Signature plus
// the GroupKey verifiers can match against.
func runPulsarCeremony(t *testing.T, n, threshold int, message string) (*corona.Signature, *corona.GroupKey) {
t.Helper()
shares, gk, err := corona.GenerateKeys(threshold, n, rand.Reader)
require.NoError(t, err)
require.Len(t, shares, n)
signers := make([]int, n)
for i := range signers {
signers[i] = i
}
prfKey := make([]byte, 32)
_, err = rand.Read(prfKey)
require.NoError(t, err)
parties := make([]*corona.Signer, n)
for i := range parties {
parties[i] = corona.NewSigner(shares[i])
}
r1 := make(map[int]*corona.Round1Data, n)
for i, p := range parties {
r1[i] = p.Round1(1, prfKey, signers)
}
r2 := make(map[int]*corona.Round2Data, n)
for i, p := range parties {
d, err := p.Round2(1, message, prfKey, signers, r1)
require.NoError(t, err)
r2[i] = d
}
sig, err := parties[0].Finalize(r2)
require.NoError(t, err)
require.True(t, corona.Verify(gk, message, sig))
return sig, gk
}
func envFixture(t *testing.T, eraID, generation uint64) (*warp.EnvelopeV2, *warp.Message) {
t.Helper()
const networkID = uint32(1)
chainID := ids.ID{0xC1, 0xC2, 0xC3, 0xC4}
payload := []byte("warp-pulsar-roundtrip")
unsigned, err := warp.NewUnsignedMessage(networkID, chainID, payload)
require.NoError(t, err)
signers := warp.NewBitSet()
signers.Add(0)
signers.Add(2)
var sigBytes [bls.SignatureLen]byte
copy(sigBytes[:], bytes.Repeat([]byte{0xAB}, bls.SignatureLen))
v1Sig := warp.NewBitSetSignature(signers, sigBytes)
v1, err := warp.NewMessage(unsigned, v1Sig)
require.NoError(t, err)
env := &warp.EnvelopeV2{
Message: v1,
SourceKeyEraID: eraID,
SourceGeneration: generation,
HashSuiteID: warp.DefaultHashSuiteID,
}
return env, v1
}
type stubResolver struct {
gk *corona.GroupKey
suiteID string
err error
}
func (s *stubResolver) ResolveGroupKey(_ [32]byte, _ uint64, _ uint64) (*corona.GroupKey, string, error) {
return s.gk, s.suiteID, s.err
}
func TestSerializeDeserializePulseRoundTrip(t *testing.T) {
sig, gk := runPulsarCeremony(t, 3, 2, "round-trip")
wire, err := SerializePulse(sig)
require.NoError(t, err)
require.NotEmpty(t, wire)
parsed, err := DeserializePulse(wire)
require.NoError(t, err)
require.True(t, corona.Verify(gk, "round-trip", parsed))
}
func TestKernelVerifierAcceptsValidPulse(t *testing.T) {
env, _ := envFixture(t, 7, 11)
msgBytes := env.Message.UnsignedMessage.Bytes()
signing := BuildSigningBytes(env, msgBytes)
sig, gk := runPulsarCeremony(t, 3, 2, string(signing))
pulse, err := SerializePulse(sig)
require.NoError(t, err)
env.PulsarPulse = pulse
v := NewKernelVerifier(&stubResolver{gk: gk, suiteID: warp.DefaultHashSuiteID})
require.NoError(t, v.VerifyPulse(env, msgBytes))
}
func TestKernelVerifierRejectsTamperedEnvelopeFields(t *testing.T) {
env, _ := envFixture(t, 7, 11)
msgBytes := env.Message.UnsignedMessage.Bytes()
signing := BuildSigningBytes(env, msgBytes)
sig, gk := runPulsarCeremony(t, 3, 2, string(signing))
pulse, err := SerializePulse(sig)
require.NoError(t, err)
env.PulsarPulse = pulse
// Tamper with KeyEraID after signing — verification MUST fail
// because BuildSigningBytes binds KeyEraID into the transcript.
env.SourceKeyEraID = 999
v := NewKernelVerifier(&stubResolver{gk: gk, suiteID: warp.DefaultHashSuiteID})
err = v.VerifyPulse(env, msgBytes)
require.ErrorIs(t, err, ErrPulseVerifyFailed)
}
func TestKernelVerifierRejectsOnAbsentPulse(t *testing.T) {
env, _ := envFixture(t, 1, 1)
v := NewKernelVerifier(&stubResolver{suiteID: warp.DefaultHashSuiteID})
err := v.VerifyPulse(env, env.Message.UnsignedMessage.Bytes())
require.ErrorIs(t, err, ErrPulseAbsent)
}
func TestKernelVerifierRejectsResolverError(t *testing.T) {
env, _ := envFixture(t, 1, 1)
env.PulsarPulse = bytes.Repeat([]byte{0xFF}, 32)
v := NewKernelVerifier(&stubResolver{err: errors.New("registry down")})
err := v.VerifyPulse(env, env.Message.UnsignedMessage.Bytes())
require.ErrorIs(t, err, ErrGroupKeyResolverFailed)
}
func TestKernelVerifierRejectsNilGroupKey(t *testing.T) {
env, _ := envFixture(t, 1, 1)
env.PulsarPulse = bytes.Repeat([]byte{0xFF}, 32)
v := NewKernelVerifier(&stubResolver{gk: nil, suiteID: warp.DefaultHashSuiteID})
err := v.VerifyPulse(env, env.Message.UnsignedMessage.Bytes())
require.ErrorIs(t, err, ErrGroupKeyResolverFailed)
}
func TestKernelVerifierRejectsSuiteMismatch(t *testing.T) {
env, _ := envFixture(t, 1, 1)
env.PulsarPulse = bytes.Repeat([]byte{0xFF}, 32)
_, gk := runPulsarCeremony(t, 3, 2, "x")
v := NewKernelVerifier(&stubResolver{gk: gk, suiteID: "Pulsar-SHA3-experimental"})
err := v.VerifyPulse(env, env.Message.UnsignedMessage.Bytes())
require.ErrorIs(t, err, ErrSuiteMismatch)
}
func TestBuildSigningBytesBindsAllTranscriptFields(t *testing.T) {
env1, _ := envFixture(t, 7, 11)
env1.SourceNebulaRoot = [32]byte{0x01, 0x02}
msg1 := env1.Message.UnsignedMessage.Bytes()
bytes1 := BuildSigningBytes(env1, msg1)
// Changing each transcript field must change the signing bytes.
env2 := *env1
env2.SourceKeyEraID = 8
require.NotEqual(t, bytes1, BuildSigningBytes(&env2, msg1))
env3 := *env1
env3.SourceGeneration = 12
require.NotEqual(t, bytes1, BuildSigningBytes(&env3, msg1))
env4 := *env1
env4.SourceNebulaRoot = [32]byte{0x99}
require.NotEqual(t, bytes1, BuildSigningBytes(&env4, msg1))
env5 := *env1
env5.HashSuiteID = "Pulsar-SHA3-other"
require.NotEqual(t, bytes1, BuildSigningBytes(&env5, msg1))
// Different msg bytes → different signing bytes.
require.NotEqual(t, bytes1, BuildSigningBytes(env1, append([]byte("X"), msg1...)))
}
func TestBuildSigningBytesPrefixIsConstant(t *testing.T) {
env, _ := envFixture(t, 1, 1)
out := BuildSigningBytes(env, env.Message.UnsignedMessage.Bytes())
require.True(t, bytes.HasPrefix(out, []byte(SigningPrefix)))
}
func TestHorizonFromEnvelopePopulatesAllLanes(t *testing.T) {
env, v1 := envFixture(t, 7, 11)
env.SourceNebulaRoot = [32]byte{0x42}
env.PulsarPulse = bytes.Repeat([]byte{0xCC}, 16)
env.MLDSACertSet = bytes.Repeat([]byte{0xDD}, 192)
h, err := HorizonFromEnvelope(env)
require.NoError(t, err)
require.Equal(t, env.Message.UnsignedMessage.SourceChainID[:], h.SourceChainID[:])
require.Equal(t, v1.Signature.Bytes(), h.Beam)
require.Equal(t, env.PulsarPulse, h.Pulse)
require.Equal(t, env.MLDSACertSet, h.MLDSACertSet)
require.Equal(t, env.SourceNebulaRoot, h.SourceNebulaRoot)
require.Equal(t, env.SourceKeyEraID, h.SourceKeyEraID)
require.Equal(t, env.SourceGeneration, h.SourceGeneration)
require.Equal(t, env.HashSuiteOrDefault(), h.HashSuiteID)
require.Equal(t, env.Message.UnsignedMessage.Bytes(), h.UnsignedMessageBytes)
}
func TestHorizonFromEnvelopeRejectsNil(t *testing.T) {
_, err := HorizonFromEnvelope(nil)
require.Error(t, err)
}