reshare

package
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package reshare implements the two proactive secret-sharing primitives Lens needs to evolve a key era's share distribution without changing the persistent group public key.

  1. Refresh — same-committee zero-polynomial proactive update (Herzberg-Jakobsson-Jarecki-Krawczyk-Yung 1997, "PSS"). The committee is fixed; every party samples a random degree-(t-1) polynomial z_i(X) with z_i(0) = 0 and contributes z_i(α_j) to each peer j. Each party updates s'_j = s_j + Σ_i z_i(α_j). The master secret is unchanged because Σ_i z_i(0) = 0. Use case: periodic share-randomization within a stable validator set, to defeat a mobile adversary that compromises < t parties per epoch and accumulates shares across epochs.

  2. Reshare — validator-set resharing. The OLD committee O and NEW committee N are different (potentially disjoint, potentially different threshold). The new committee has no old shares and so cannot apply zero-polynomial deltas. Instead, a qualified subset Q ⊆ O with |Q| ≥ t_old executes a one-shot Shamir-from- Lagrange transformation: each i ∈ Q samples g_i(X) of degree t_new-1 with g_i(0) = s_i (its OWN old share as the constant term), and privately delivers g_i(β_j) to each new party j ∈ N. Each new party j computes

    s'_j = Σ_{i ∈ Q} λ^Q_i · g_i(β_j)

    where λ^Q_i are the Lagrange coefficients for Q evaluated at 0. Define G(X) = Σ_{i ∈ Q} λ^Q_i · g_i(X). Then deg(G) = t_new − 1, and G(0) = s by Lagrange interpolation over Q at X = 0. Use case: validator-set rotation at Quasar epoch boundaries.

Both primitives leave the public key X = s·G unchanged. The genesis values (G, H, X) are persistent for the entire key era. Only the share distribution changes. This is the property that lets Quasar avoid running a full DKG on every validator-set rotation.

Security model (kernel)

The kernel APIs Refresh / Reshare implement the arithmetic correctness of the two primitives, against a HONEST-BUT-CURIOUS adversary that may statically corrupt up to t-1 parties.

VSR — production deployment

The kernel is embedded in a full Verifiable Secret Resharing protocol with the following components, implemented in sibling files:

  • commit.go — Pedersen commits (g^c · h^r) to f_i (Refresh) and g_i (Reshare); recipients verify each share against the committed polynomial.
  • transcript.go — Domain-separated transcript binding the entire resharing exchange.
  • complaint.go — Complaint format, signed evidence, complaint quorum logic, deterministic disqualification.
  • keyshare.go — Wraps reshared scalar shares into complete threshold.KeyShare instances by regenerating Lambda + Seeds + MACKeys + VerificationShare.
  • pairwise.go — Authenticated pairwise X25519+Ed25519 KEX → KDF for Seeds / MACKeys under domain-separated tags.
  • activation.go — Post-reshare activation cert: the new committee threshold-signs the resharing transcript hash under the unchanged GroupKey. The chain accepts the new epoch only when the activation verifies.

Index

Constants

View Source
const TranscriptPersonalization = "lens.reshare.transcript.v1"

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.

Variables

View Source
var (
	ErrActivationFailed   = errors.New("reshare: activation cert failed to verify under group public key")
	ErrTranscriptMismatch = errors.New("reshare: activation transcript hash does not match local view")
)

Errors returned by the activation circuit-breaker.

View Source
var (
	ErrCommitMismatch      = errors.New("reshare: commitment verification failed")
	ErrCommitWrongLength   = errors.New("reshare: commit vector has wrong length")
	ErrInconsistentDigests = errors.New("reshare: cross-recipient commit digest mismatch")
)

Errors specific to commitment verification.

View Source
var (
	ErrInvalidThresholdOld = errors.New("reshare: t_old must be >= 1")
	ErrInvalidThresholdNew = errors.New("reshare: t_new must be >= 1")
	ErrTOldExceedsOldSet   = errors.New("reshare: t_old exceeds size of old committee")
	ErrTNewExceedsNewSet   = errors.New("reshare: t_new exceeds size of new committee")
	ErrEmptyOldShares      = errors.New("reshare: no old shares supplied")
	ErrEmptyNewSet         = errors.New("reshare: empty new committee")
	ErrZeroPartyID         = errors.New("reshare: party IDs must be 1-indexed (no 0)")
	ErrDuplicateNewID      = errors.New("reshare: duplicate ID in new committee")
	ErrTOldShortfall       = errors.New("reshare: fewer than t_old shares supplied; cannot reconstruct s")
)

Errors returned by the package.

View Source
var ErrInsufficientQuorum = errors.New("reshare: qualified quorum below t_old after disqualification")

ErrInsufficientQuorum signals that too many resharing parties were disqualified for the protocol to recover.

Functions

func AuthenticatedKex

func AuthenticatedKex(
	privIEph []byte,
	pubJEph []byte,
	sigJEph []byte,
	jStaticKey ed25519.PublicKey,
	transcriptHash [32]byte,
	suite hash.HashSuite,
) ([]byte, error)

AuthenticatedKex runs the pairwise authenticated key exchange and returns the auth_kex_ij value. The signed ephemeral protects against active man-in-the-middle.

suite=nil resolves to the production default (Lens-SHA3).

func CanonicalPair

func CanonicalPair(i, j int) [2]int

CanonicalPair returns the (i, j) tuple in canonical (smaller-first) order. Used as a map key for pairwise material.

func CommitDigest

func CommitDigest(commits []primitives.Point, suite hash.HashSuite) [32]byte

CommitDigest returns the canonical 32-byte digest over a commit vector under the supplied HashSuite. suite=nil resolves to the production default (Lens-SHA3).

func CommitToPoly

func CommitToPoly(
	c primitives.Curve,
	secretCoeffs, blindCoeffs []primitives.Scalar,
) ([]primitives.Point, error)

CommitToPoly produces the t Pedersen commitments to the secret-poly coefficients c_k together with the matching blinding-poly coefficients r_k.

func ComplaintHash

func ComplaintHash(c *Complaint) [32]byte

ComplaintHash returns BLAKE3 over the complaint's canonical bytes (signature included).

func ComputeDisqualifiedSet

func ComputeDisqualifiedSet(complaints []*Complaint, thresholdOld int) map[int]struct{}

ComputeDisqualifiedSet takes a slice of validated complaints and returns the set of sender IDs that meet the disqualification threshold. Every honest party that processes the same complaint set returns the same disqualified set.

Complaints are deduplicated by (sender, complainer) tuple.

func DeriveMACKeys

func DeriveMACKeys(
	K int,
	authKex map[[2]int][]byte,
	chainID, groupID []byte,
	eraID, generation uint64,
	suite hash.HashSuite,
	outLen int,
) (map[[2]int][]byte, error)

DeriveMACKeys mirrors DeriveSeeds with the "lens.reshare.mac-key.v1" tag and only off-diagonal entries.

func DeriveSeeds

func DeriveSeeds(
	K int,
	authKex map[[2]int][]byte,
	selfSeeds map[int][]byte,
	chainID, groupID []byte,
	eraID, generation uint64,
	suite hash.HashSuite,
	outLen int,
) (map[[2]int][]byte, error)

DeriveSeeds populates the per-pair PRF seed map for a committee of size K. suite=nil resolves to the production default.

func DisqualificationThreshold

func DisqualificationThreshold(thresholdOld int) int

DisqualificationThreshold returns the minimum number of distinct, validly-signed complaints needed to disqualify a sender.

Default: t_old - 1. Rationale: any single Byzantine validator can emit one false complaint to slow the protocol, but to disqualify an honest sender the adversary needs t_old - 1 collaborators — exceeding the static-corruption threshold of t_old - 1 by exactly one.

func EraseScalar

func EraseScalar(s primitives.Scalar)

EraseScalar overwrites the scalar's binary representation with zero bytes. After activation, every old share MUST be erased — failure to do so undermines the proactive-security guarantee.

func FilterQualifiedQuorum

func FilterQualifiedQuorum(
	originalQuorum []int,
	disqualified map[int]struct{},
	tOld int,
) ([]int, error)

FilterQualifiedQuorum returns the survivor set Q' = Q \ disqualified. Returns ErrInsufficientQuorum if |Q'| < tOld.

func KDFOutput

func KDFOutput(
	suite hash.HashSuite,
	tag string,
	authKex []byte,
	chainID, groupID []byte,
	eraID, generation uint64,
	partyI, partyJ int,
	outLen int,
) []byte

KDFOutput derives a fixed-length output from keying material under the supplied HashSuite's pairwise KDF. suite=nil resolves to the production default (Lens-SHA3).

The tag is folded into the chainID label with a `|` separator so two callers with distinct tags but the same remaining inputs always produce distinct bytes.

func Refresh

func Refresh(
	c primitives.Curve,
	shares map[int]primitives.Scalar,
	threshold int,
	randSource io.Reader,
) (map[int]primitives.Scalar, error)

Refresh runs the HJKY97 same-committee proactive update on a fixed committee. The set of party IDs and the threshold are unchanged: only the share values rotate to fresh independent randomness while preserving Σ_j λ^T_j · s_j = s for every threshold subset T.

Algorithm (per HJKY97 §3, "zero-polynomial" form):

  1. Each party i samples a random degree-(t-1) polynomial z_i(X) over the curve's scalar field with z_i(0) = 0 (i.e. no constant term).

  2. Each party i privately delivers z_i(α_j) to every party j in the committee, where α_j is the Shamir evaluation point of party j (here α_j = j, the 1-indexed party ID).

  3. Each party j updates its share:

    s'_j = s_j + Σ_i z_i(α_j) (mod q)

func Reshare

func Reshare(
	c primitives.Curve,
	oldShares map[int]primitives.Scalar,
	tOld int,
	newSet []int,
	tNew int,
	randSource io.Reader,
) (map[int]primitives.Scalar, error)

Reshare runs the proactive secret-resharing protocol described in the package doc on the supplied curve. Inputs:

  • c — the curve s lives in (must match the GroupKey's curve).
  • oldShares — partyID → secret-share scalar (1-indexed evaluation point). The map MUST contain at least t_old entries.
  • tOld — old reconstruction threshold.
  • newSet — new committee (slice of distinct 1-indexed party IDs).
  • tNew — new reconstruction threshold (≤ |newSet|).
  • randSource — randomness source for fresh polynomial coefficients (nil → crypto/rand.Reader).

Returns the new share map keyed by new partyID. The output share at partyID j satisfies the (t_new, |newSet|)-Shamir relation against the SAME master secret s as the input shares.

func SignEphemeral

func SignEphemeral(
	priv ed25519.PrivateKey,
	pubEph []byte,
	transcriptHash [32]byte,
) []byte

SignEphemeral produces the signature j ships with its ephemeral public key.

func ValidatorSetHash

func ValidatorSetHash(publicKeys [][]byte, suite hash.HashSuite) [32]byte

ValidatorSetHash returns the canonical hash of a validator set. suite=nil resolves to the production default (Lens-SHA3).

func Verify

func Verify(c primitives.Curve, shares map[int]primitives.Scalar, t int) (primitives.Scalar, error)

Verify is a debugging helper: it Lagrange-interpolates the input shares at X=0 (using the smallest-ID t-subset) and returns the reconstructed master secret.

IMPORTANT: This recovers s, so it MUST NOT be used in production — only in tests and KAT verification. Calling it gives the caller the secret.

func VerifyActivation

func VerifyActivation(
	cert *ActivationCert,
	localTranscriptHash [32]byte,
	localExchangeHash [32]byte,
	suite hash.HashSuite,
	verify func(message []byte, signature []byte) bool,
) error

VerifyActivation runs the chain-level activation check. suite=nil resolves to the production default (Lens-SHA3).

func VerifyShareAgainstCommits

func VerifyShareAgainstCommits(
	c primitives.Curve,
	share, blind primitives.Scalar,
	commits []primitives.Point,
	x primitives.Scalar,
) error

VerifyShareAgainstCommits checks the recipient-side equation

share·G + blind·H ?= Σ_{k=0..t-1} x^k · C_k

func X25519Pair

func X25519Pair(privA, pubB []byte) ([]byte, error)

X25519Pair runs an X25519 key exchange and returns the 32-byte shared secret.

Types

type ActivationCert

type ActivationCert struct {
	Message   ActivationMessage
	Signature []byte
}

ActivationCert wraps the threshold signature emitted by the new committee over an ActivationMessage.

type ActivationMessage

type ActivationMessage struct {
	Transcript        TranscriptInputs
	ReshareTranscript ReshareTranscript
}

ActivationMessage is the canonical bytes-to-be-signed for the post-reshare circuit-breaker.

func (*ActivationMessage) SignableBytes

func (a *ActivationMessage) SignableBytes(suite hash.HashSuite) []byte

SignableBytes returns the canonical bytes the new committee threshold-signs to produce an activation cert.

type Complaint

type Complaint struct {
	TranscriptHash [32]byte
	SenderID       int
	ComplainerID   int
	Reason         ComplaintReason
	Evidence       []byte
	Signature      []byte
	ComplainerKey  ed25519.PublicKey
}

Complaint is a signed assertion that sender PartyID misbehaved during the resharing protocol.

func (*Complaint) Bytes

func (c *Complaint) Bytes() []byte

Bytes returns the canonical signed payload for a complaint. The Signature field is excluded (it is computed OVER these bytes).

func (*Complaint) Sign

func (c *Complaint) Sign(priv ed25519.PrivateKey)

Sign produces a complaint signature using the provided Ed25519 private key.

func (*Complaint) Verify

func (c *Complaint) Verify() error

Verify checks the Ed25519 signature against ComplainerKey. Returns nil iff the signature is valid.

type ComplaintReason

type ComplaintReason uint8

ComplaintReason enumerates the failure modes that justify a complaint.

const (
	// ComplaintBadDelivery — share_{i→j} fails the commitment check.
	ComplaintBadDelivery ComplaintReason = 1

	// ComplaintEquivocation — sender i shipped different commits to
	// different recipients (detected via Round 1.5 digest cross-check).
	ComplaintEquivocation ComplaintReason = 2

	// ComplaintMissing — sender i failed to deliver share_{i→j} by
	// the round-1 deadline.
	ComplaintMissing ComplaintReason = 3

	// ComplaintMalformedCommit — sender i's commit vector has the
	// wrong length, has nil entries, or fails internal sanity checks.
	ComplaintMalformedCommit ComplaintReason = 4
)

func (ComplaintReason) String

func (r ComplaintReason) String() string

String returns a human-readable name for the reason.

type PairwiseKeyMaterial

type PairwiseKeyMaterial struct {
	PartyI         int
	PartyJ         int
	AuthKex        []byte
	TranscriptHash [32]byte
}

PairwiseKeyMaterial holds the X25519 keys + signed ephemerals that together produce auth_kex_ij. One instance per pair (canonicalized by the smaller party ID first).

type ReshareTranscript

type ReshareTranscript struct {
	CommitDigests       map[int][32]byte
	ComplaintHashes     [][32]byte
	DisqualifiedSenders []int
	QualifiedQuorum     []int
}

ReshareTranscript is the structured digest of the resharing exchange.

func (*ReshareTranscript) Hash

func (rt *ReshareTranscript) Hash(suite hash.HashSuite) [32]byte

Hash returns the canonical 32-byte digest of the ReshareTranscript. suite=nil resolves to the production default.

type TranscriptInputs

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 // "refresh" or "reshare"
	CurveName             string // "ed25519" / "secp256k1" / "ristretto255"
}

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.

func (*TranscriptInputs) Hash

func (t *TranscriptInputs) Hash(suite hash.HashSuite) [32]byte

Hash returns the canonical 32-byte transcript binding for the inputs under the supplied HashSuite. nil resolves to the production default (Lens-SHA3); pass hash.NewLensBLAKE3() to reproduce legacy bytes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL