Files
corona/reshare/transcript.go
T
Hanzo AI a2c1d1da31 corona: symmetric domain separation — PULSAR-* tags -> CORONA-*
Leftover from Corona's "Pulsar-R" lineage. Pulsar (M-LWE) and Corona
(R-LWE) are independent constructions with separate hardness assumptions,
so their cSHAKE personalisation strings must be distinct.

Changes (Go, non-luxcpp):
  - hash tags: PULSAR-HC-v1 -> CORONA-HC-v1, etc. (HU, TRANSCRIPT, PRF, MAC, PAIRWISE)
  - profile IDs: "Pulsar-SHA3" -> "Corona-SHA3", "Pulsar-BLAKE3" -> "Corona-BLAKE3"
  - context strings: pulsar.dkg2.A.v1 -> corona.dkg2.A.v1, etc.
  - env vars: PULSAR_RESHARE_KAT_PATH -> CORONA_RESHARE_KAT_PATH, etc.
  - struct names: pulsarSHA3 -> coronaSHA3
  - KAT derive roots: sign_e2e_pulsar -> sign_e2e_corona

What's preserved (different scope):
  - luxcpp/crypto/pulsar/* path references in comments (separate repo,
    out of scope; the C++ side will rename in its own commit)
  - Cross-runtime KAT files on disk (will regenerate next CI run)

All 11 packages test green: dkg, dkg2, hash, keyera, networking,
primitives, reshare, sign, threshold, utils, wire.
2026-05-13 14:19:13 -07:00

128 lines
4.7 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package reshare
// Transcript binding for Verifiable Secret Resharing.
//
// Every resharing message — Round 1 commits + private share deliveries,
// Round 1.5 commit-digest broadcasts, Round 2 complaints, and the final
// activation certificate — is bound to a transcript hash that commits
// to the (chain_id, network_id, group_id, era, generations, epochs,
// sets, thresholds, group_pk_hash, nebula_root, hash_suite_id,
// implementation_version) tuple. The transcript hash is a single point
// of equality the new committee uses when it threshold-signs the
// activation message under the unchanged GroupKey: the chain accepts
// the new epoch only when the activation signature verifies against
// this hash.
//
// Domain separation. Every input field is unambiguously length-prefixed
// (TupleHash framing under Corona-SHA3, hand-rolled length prefixes
// under the legacy Corona-BLAKE3 suite). The customization tag is
// "CORONA-TRANSCRIPT-v1" — distinct from any DKG transcript tag (e.g.
// "corona.dkg2.commit.v1") and from any Corona Sign transcript tag.
import (
"bytes"
"encoding/binary"
"github.com/luxfi/corona/hash"
)
// TranscriptPersonalization is the legacy BLAKE3 personalization
// string. Kept as an exported symbol because some external consumers
// (cross-language KAT loaders) reference it; the canonical transcript
// binding is now produced via the HashSuite layer.
const TranscriptPersonalization = "corona.reshare.transcript.v1"
// TranscriptInputs holds the public binding fields for one resharing
// invocation. All fields are mandatory; the transcript hash is well-
// defined only when every byte below is fixed.
type TranscriptInputs struct {
ChainID []byte
NetworkID []byte
GroupID []byte
KeyEraID uint64
OldGeneration uint64
NewGeneration uint64
OldEpochID uint64
NewEpochID uint64
OldSetHash [32]byte
NewSetHash [32]byte
ThresholdOld uint32
ThresholdNew uint32
GroupPublicKeyHash [32]byte
NebulaRoot [32]byte
HashSuiteID string
ImplementationVersion string
Variant string
}
// Hash returns the canonical 32-byte transcript binding for the inputs
// under the supplied HashSuite. nil resolves to the production default
// (Corona-SHA3); pass hash.NewCoronaBLAKE3() to reproduce legacy bytes.
func (t *TranscriptInputs) Hash(suite hash.HashSuite) [32]byte {
s := hash.Resolve(suite)
parts := buildTranscriptParts(t)
return s.TranscriptHash(parts...)
}
// buildTranscriptParts canonicalizes a TranscriptInputs into the
// ordered byte-string list that drives TranscriptHash.
func buildTranscriptParts(t *TranscriptInputs) [][]byte {
enc64 := func(v uint64) []byte {
var b [8]byte
binary.BigEndian.PutUint64(b[:], v)
return b[:]
}
enc32 := func(v uint32) []byte {
var b [4]byte
binary.BigEndian.PutUint32(b[:], v)
return b[:]
}
return [][]byte{
[]byte("variant"), []byte(t.Variant),
[]byte("chain_id"), t.ChainID,
[]byte("network_id"), t.NetworkID,
[]byte("group_id"), t.GroupID,
[]byte("key_era_id"), enc64(t.KeyEraID),
[]byte("old_generation"), enc64(t.OldGeneration),
[]byte("new_generation"), enc64(t.NewGeneration),
[]byte("old_epoch_id"), enc64(t.OldEpochID),
[]byte("new_epoch_id"), enc64(t.NewEpochID),
[]byte("old_set_hash"), t.OldSetHash[:],
[]byte("new_set_hash"), t.NewSetHash[:],
[]byte("threshold_old"), enc32(t.ThresholdOld),
[]byte("threshold_new"), enc32(t.ThresholdNew),
[]byte("group_public_key_hash"), t.GroupPublicKeyHash[:],
[]byte("nebula_root"), t.NebulaRoot[:],
[]byte("hash_suite_id"), []byte(t.HashSuiteID),
[]byte("implementation_version"), []byte(t.ImplementationVersion),
}
}
// ValidatorSetHash returns the canonical hash of a validator set.
// suite=nil resolves to the production default (Corona-SHA3).
func ValidatorSetHash(publicKeys [][]byte, suite hash.HashSuite) [32]byte {
s := hash.Resolve(suite)
sorted := make([][]byte, len(publicKeys))
copy(sorted, publicKeys)
for i := 1; i < len(sorted); i++ {
for j := i; j > 0 && bytes.Compare(sorted[j-1], sorted[j]) > 0; j-- {
sorted[j-1], sorted[j] = sorted[j], sorted[j-1]
}
}
parts := make([][]byte, 0, 2+len(sorted))
parts = append(parts, []byte("corona.reshare.validator-set.v1"))
var nBuf [4]byte
binary.BigEndian.PutUint32(nBuf[:], uint32(len(sorted)))
parts = append(parts, nBuf[:])
for _, pk := range sorted {
if len(pk) > 0xFFFF {
panic("reshare: validator public key longer than 65535 bytes")
}
parts = append(parts, pk)
}
return s.TranscriptHash(parts...)
}