feat(thresholdvm): stage design docs and protocol scaffolding

Stages the ThresholdVM engineering record under chains/thresholdvm/:

Docs (engineering record, 502 LOC):
- DESIGN.md, LLM.md, README.md
- docs/ARCHITECTURE.md, docs/F_CHAIN_INTEGRATION.md, docs/M_CHAIN_INTEGRATION.md

Source scaffolding (1289 LOC):
- cert/      — QuasarCertLane registration + subject binding (M/F shared)
- types/     — ceremony, participant, proof, share substrate types
- protocol/  — package surfaces for cggmp21, frost, corona_general, tfhe_keygen
- runtime/   — F-chain and M-chain adapters

All Go files carry the Lux license header. Nothing here is generated or
fixture data; this is the signed-off design surface for the ThresholdVM
library used by M-chain and F-chain.
This commit is contained in:
2026-04-28 09:17:47 -07:00
parent 2a86eaedde
commit e032ec4b05
20 changed files with 1791 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
# ThresholdVM Design
> Status: Final for Quasar 3.0 activation (2025-12-25)
> Supersedes the T-Chain monolith (LP-5013), per LP-134
## 1. Position in the topology
```mermaid
flowchart LR
subgraph PChain["P-Chain (Platform / staking)"]
Stake[Validator stake + delegation]
end
subgraph Substrate["thresholdvm (library, this repo)"]
SM[Ceremony state machine]
Share[Share envelope: offset/len]
Lane[QuasarCertLane registration]
Subject[certificate_subject binding]
end
subgraph MChain["M-Chain (MPC)"]
MV[M-Chain validator subset]
MProto[CGGMP21 / FROST / Corona-gen]
MRoot[mchain_ceremony_root]
end
subgraph FChain["F-Chain (FHE)"]
FV[F-Chain validator subset]
FProto[TFHE / bootstrap]
FRoot[fchain_fhe_root]
end
subgraph Quasar["Quasar 3.0 cert pipeline"]
Cert[QuasarCert artifact]
end
Stake -->|delegate| MV
Stake -->|delegate| FV
Substrate -.imported.-> MChain
Substrate -.imported.-> FChain
MChain -->|MChainCGGMP21 / FROST / CoronaGen lanes| Quasar
FChain -->|FChainTFHE / FChainBootstrap lanes| Quasar
MChain -->|bootstrap-key handoff envelope| FChain
Quasar -->|certificate_subject binds both roots| MRoot
Quasar -->|certificate_subject binds both roots| FRoot
```
The substrate has **zero** runtime presence. It compiles into both
chains' binaries.
## 2. Why M and F are separate operational chains
| Dimension | M-Chain | F-Chain |
|---|---|---|
| Workload | sign-oriented (latency-critical, sub-second) | compute-oriented (throughput-critical, multi-second) |
| Block cadence | ~500 ms | ~2 s typical |
| Validator profile | bandwidth + low latency | GPU + memory bandwidth (TFHE bootstrap is GPU-bound) |
| Failure cost | a missed signature delays a bridge tx | a slow FHE compute delays a confidential contract |
| Stake profile | wide / many small validators (Bitcoin-like) | narrower / GPU operators self-select |
| Ceremony root | `mchain_ceremony_root` | `fchain_fhe_root` |
| QuasarMode | Nebula (DAG of partials) | Nebula (computation graph) |
Sharing one validator set would force the GPU operators on F-Chain to
also keep up with M-Chain's 500 ms cadence (or vice versa). Keeping
the chains separate lets each scale on its own validator subset, both
permissionlessly carved out of P-Chain stake.
## 3. The shared state machine
```
StateRegistered -- all participants for ceremony C have committed stake
|
v
StateRound1 -- each participant broadcasts commitments (per-protocol payload)
|
v
StateRound2 -- each participant distributes shares (per-protocol payload)
|
v
StateFinalized -- aggregator emits CertLane artifact, ceremony root advances
```
Round semantics are protocol-specific (FROST is 2-round, CGGMP21
pre-sign + sign, TFHE keygen is multi-round) but the **envelope** is
identical: per-round payload is carried by `(offset, len)` indirection
into a single artifact buffer.
The state machine is implemented in `types/ceremony.go` as a simple
explicit-transition function. No goroutines, no timers in the
substrate — each chain's runtime drives transitions on block ticks.
## 4. Share envelope (per-protocol payload via offset/len)
```go
// types/share.go
type Share struct {
CeremonyID [32]byte // hash(ceremony_descriptor)
ParticipantID uint32 // index into ParticipantSet
Round uint8 // 1 or 2 for FROST/CGGMP21; up to 4 for Corona/TFHE
Lane CertLane // dispatches per-protocol verifier
PayloadOffset uint32 // offset into ceremony's payload arena
PayloadLen uint32 // length of this share's payload
Signature [64]byte // BLS or ML-DSA signature over (CeremonyID, Round, payload)
}
```
This matches the `(artifact_offset, artifact_len)` indirection used by
`QuasarCertIngress` (LP-132 §drain_cert_lane). New protocols append a
new `CertLane` enum value and a new verifier; **the envelope never
changes**.
## 5. QuasarCertLane registration
```go
// cert/lane.go
type LaneVerifier interface {
Lane() CertLane
Verify(subject [32]byte, share Share, payload []byte) error
}
type LaneRegistry interface {
Register(LaneVerifier) error
Verifier(CertLane) (LaneVerifier, error)
}
```
- M-Chain's runtime calls `Register(MChainCGGMP21Verifier{})`,
`Register(MChainFROSTVerifier{})`,
`Register(MChainCoronaGenVerifier{})` at boot.
- F-Chain's runtime calls `Register(FChainTFHEVerifier{})`,
`Register(FChainBootstrapVerifier{})` at boot.
- Each chain's registry instance is private to that chain's process.
The substrate provides the type, not a global.
## 6. Certificate-subject binding
LP-134 extends `QuasarRoundDescriptor`:
```cpp
uint8_t mchain_ceremony_root[32];
uint8_t fchain_fhe_root[32];
// ... and seven other roots ...
uint8_t certificate_subject[32]; // binds all of them
```
`certificate_subject = H(parent_block || state_root || exec_root || pchain_validator_root || qchain_ceremony_root || zchain_vk_root || achain_attestation_root || bchain_bridge_root || mchain_ceremony_root || fchain_fhe_root)`
The substrate's `cert/subject.go` exposes `BindSubject(...)` and
`Verify(subject, lane, share)`. **Both** `mchain_ceremony_root` and
`fchain_fhe_root` are inputs, regardless of which chain emitted the
ceremony — this is the structural property that makes
cross-chain-replay impossible (a cert artifact for an M-Chain
ceremony cannot be replayed against F-Chain because the F-Chain root
on the destination round disagrees by construction).
## 7. Permissionless participation
The substrate enforces, via `types/participant.go`:
1. `ParticipantSet` is derived **deterministically** from
`pchain_validator_root` + chain-specific delegation. No allowlist.
2. Selection is done by VRF-weighted sampling on stake delegated to
M-Chain or F-Chain respectively. Same security analysis as Lux's
existing P-Chain selection.
3. Threshold parameters (`t-of-n`) are policy controlled by the
chain's governance, but minimum `t >= n/2 + 1` is enforced at the
library level.
4. Resharing (LP-077 LSS) is supported via the same envelope —
resharing is just a ceremony whose output is a new key share for a
new participant set.
## 8. Cross-chain handoff (M → F)
Most ceremonies live entirely on one chain. The exception is **TFHE
bootstrap-key generation**, which uses MPC (M-Chain) to generate a
key the FHE engine (F-Chain) consumes:
```
1. M-Chain runs FROST DKG over the TFHE secret-key polynomial.
2. On finalize, M-Chain emits a CertLane artifact with lane =
MChainFROST. The artifact's payload encodes the TFHE evaluation
key + bootstrap material.
3. F-Chain ingests the artifact at its next round via lane =
FChainBootstrap (which wraps the upstream MChainFROST cert).
4. fchain_fhe_root advances to commit the new key.
5. Quasar 3.0's certificate_subject in the round binds both
mchain_ceremony_root (with the FROST output) and fchain_fhe_root
(with the new bootstrap key) — atomic from the network's view.
```
The handoff envelope is just a `Share` with `Lane=FChainBootstrap`
that wraps the upstream `Lane=MChainFROST` artifact via offset/len
into the same buffer. No new envelope type.
## 9. Migration adapters (T-Chain → M/F)
The substrate hosts:
- `cert/lane.go`: legacy `TChainSign` and `TChainFHE` lane numbers
are accepted during the one-epoch grace window after activation
(2025-12-25 → next epoch). The verifier dispatches them to
`MChain*` and `FChain*` respectively.
- After grace, the dispatch table drops the legacy entries. The
substrate has no grace logic of its own — it just exposes
`RegisterLegacyAlias(legacyLane, modernLane)` which the chain's
runtime wires up at boot for one epoch.
## 10. Type-level orthogonality
The split is enforced by Go interface, not convention:
- `runtime.MChainAdapter` exposes only MPC ceremony hooks (`OnDKG`,
`OnSign`, `OnReshare`).
- `runtime.FChainAdapter` exposes only FHE compute hooks (`OnKeygen`,
`OnEval`, `OnBootstrap`).
- The substrate's lane registry refuses to accept a verifier whose
`Lane()` value is in the M-Chain range (5–7) from the F-Chain
registry, and vice versa. A misconfigured chain fails at boot, not
at runtime.
This makes "M-Chain accidentally hosts FHE compute" a compile-time
or boot-time failure, not a silent semantic bug.
## 11. References
| LP | Topic |
|---|---|
| LP-019 | Threshold MPC for Bridge Signing |
| LP-013 | FHE on GPU |
| LP-076 | Universal Threshold Cryptography Framework |
| LP-077 | Linear Secret Sharing (resharing) |
| LP-132 | QuasarGPU Execution Adapter |
| LP-134 | Lux Chain Topology |
| LP-020 | Quasar Consensus 3.0 |
| LP-5013 | T-Chain MPC Custody (deprecated) |
+48
View File
@@ -0,0 +1,48 @@
# LLM context — `chains/thresholdvm/`
## What this is
ThresholdVM is a **library substrate**, not a Lux chain. It is imported by:
- `chains/mchain/` — MPC ceremonies (CGGMP21, FROST, Corona-general)
- `chains/fchain/` — FHE compute (TFHE bootstrap, encrypted EVM)
The substrate hosts the ceremony state machine, share envelope,
QuasarCertLane registration, and certificate-subject binding logic
shared by both chains.
## What an agent must NOT do here
- Do **not** turn this back into a chain. No `factory.go` exposed to
`chains.Manager`. No new VM ID. M-Chain and F-Chain are the chains.
- Do **not** import M-Chain or F-Chain code from this package. The
dependency graph is `mchain → thresholdvm` and `fchain → thresholdvm`;
reverse edges are forbidden.
- Do **not** add a protocol implementation here. Protocol packages
expose **interfaces only**; impls live in the chain that runs the
protocol (CGGMP21 in M-Chain, TFHE keygen straddles M-Chain →
F-Chain via the handoff envelope).
- Do **not** introduce `t-chain` / `tchain` types here. The legacy
`vm.go`, `block.go`, `factory.go`, `fhe/`, `cmd/` files in the
parent directory are the deprecated T-Chain shim from LP-5013.
They exist only for one-epoch grace-window migration, then are
removed.
## Status
- Substrate ships with Quasar 3.0 activation on **2025-12-25**.
- LP-5013 (T-Chain) is **deprecated** by LP-134.
- Cert-lane enums `MChainCGGMP21=5`, `MChainFROST=6`,
`MChainCoronaGen=7`, `FChainTFHE=8`, `FChainBootstrap=9`. Never
reorder; appends only.
## Where to read next
| File | Why |
|---|---|
| `DESIGN.md` | architecture, state machine, lane integration |
| `docs/ARCHITECTURE.md` | how M-Chain / F-Chain plug in |
| `docs/M_CHAIN_INTEGRATION.md` | M-Chain adapter contract |
| `docs/F_CHAIN_INTEGRATION.md` | F-Chain adapter contract |
| `types/ceremony.go` | the state machine in code |
| `cert/subject.go` | how `certificate_subject` binds both roots |
+116
View File
@@ -0,0 +1,116 @@
# ThresholdVM
**ThresholdVM is a Go library, not a chain.**
It is the shared substrate that **M-Chain** (MPC ceremonies) and **F-Chain**
(FHE compute) build on. Both chains import this package; neither chain *is*
this package.
```
+--------------------------------+
| ThresholdVM |
| (ceremony state machine, |
| share envelope, lane API, |
| subject binding) |
+----------------+---------------+
|
+-------------------+-------------------+
| |
+--------v--------+ +--------v--------+
| M-Chain | | F-Chain |
| (MPC: CGGMP21, | | (FHE: TFHE, |
| FROST, | | bootstrap-key |
| Corona-gen) | --bootstrap key--> | gen, encrypted |
| | | EVM) |
+-----------------+ +-----------------+
ceremony_root: ceremony_root:
mchain_ceremony_root fchain_fhe_root
block time ~500ms block time ~2s
sign-oriented compute-oriented
```
## What this directory contains
| Path | Role |
|---|---|
| `types/` | Ceremony, Participant, Share, Proof — pure data types, no chain logic |
| `protocol/` | Protocol-specific verifier interfaces (impl ships with the chain that uses it) |
| `cert/` | QuasarCertLane integration: lane registration + certificate-subject binding |
| `runtime/` | `MChainAdapter` and `FChainAdapter` interfaces the two chains implement |
| `docs/` | Architecture + per-chain integration notes |
## What this directory is **not**
- Not a Lux VM. There is no `factory.go` exposed to `chains.Manager`.
- Not a chain ID. M-Chain and F-Chain are the chains; they have their
own validator sets, ceremony roots, and block cadence.
- Not a registry of secrets, keys, or signed material. State lives on
M-Chain or F-Chain, never here.
## Why split
LP-134 deprecates LP-5013 ("T-Chain MPC Custody"). T-Chain previously
mixed MPC ceremonies and FHE compute under one validator set, one
ceremony root, one block cadence. Sign-oriented (sub-second, latency
critical) and compute-oriented (multi-second, throughput critical)
workloads have incompatible scaling profiles. Splitting them lets each
scale on its own validator set.
The substrate stays one piece because the **ceremony state machine** is
identical across both: registered participants → round 1 commitments →
round 2 shares → finalized cert. Only the per-protocol payload
(carried by `(offset, len)` indirection in the share envelope) differs.
## Permissionless participation
Both M-Chain and F-Chain inherit P-Chain's permissionless validator
model. Anyone with stake on P-Chain can delegate to the M-Chain or
F-Chain validator subset (or both). No allowlist. No central authority.
Bitcoin-like decentralization carries through — the threshold scheme
itself enforces honest-majority safety, the chain enforces liveness.
## Cross-chain handoff
When M-Chain finishes a ceremony whose output feeds F-Chain (e.g., a
TFHE bootstrap key generated by FROST DKG on M-Chain), the output
crosses via a CertLane artifact:
1. M-Chain finalizes ceremony, emits cert with lane `MChainFROST` and
`mchain_ceremony_root` updated.
2. F-Chain consumes the artifact via its `FChainBootstrap` lane,
binds it into `fchain_fhe_root`.
3. Quasar 3.0's `certificate_subject` binds **both** roots in the same
round, making cross-chain replay structurally impossible.
The handoff envelope is defined in `cert/lane.go`; the typed adapter
in `runtime/`.
## Migration from T-Chain
LP-5013 is deprecated. The legacy chain at `chains/thresholdvm/` (this
directory's `vm.go`, `block.go`, `factory.go`, `fhe/`, `cmd/`) hosts
the migration shims that route legacy `TChain*` cert lanes to the
appropriate `MChain*` / `FChain*` verifiers during the one-epoch grace
window. After the grace window closes, the shims are removed and only
the substrate (this README's directory tree) remains.
## Activation
ThresholdVM substrate activates with **Quasar 3.0 on 2025-12-25**,
alongside the M-Chain and F-Chain genesis events. Versions before that
date target the legacy T-Chain layout.
## References
| LP | Topic |
|---|---|
| LP-019 | Threshold MPC for Bridge Signing (M-Chain protocols) |
| LP-013 | FHE on GPU (F-Chain compute) |
| LP-076 | Universal Threshold Cryptography Framework |
| LP-134 | Lux Chain Topology — defines the M/F split |
| LP-132 | QuasarGPU Execution Adapter — lane verifiers run here |
| LP-020 | Quasar Consensus 3.0 — certificate_subject binding |
## License
Copyright (C) 2019-2025 Lux Industries Inc. MIT licensed.
+143
View File
@@ -0,0 +1,143 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package cert provides the QuasarCertLane registration and
// certificate-subject binding logic shared by M-Chain and F-Chain.
//
// The lane registry is **not** a global. Each chain constructs its
// own LaneRegistry at boot, registers only the verifiers it owns
// (M-Chain registers lanes 5..7, F-Chain registers lanes 8..9), and
// the substrate refuses cross-registration at runtime — a misrouted
// verifier fails fast.
package cert
import (
"errors"
"fmt"
"github.com/luxfi/chains/thresholdvm/types"
)
// LaneVerifier is the contract a chain implements to validate shares
// on a given cert lane. Verifiers are stateless with respect to the
// substrate; any mutable state lives in the chain's own runtime.
type LaneVerifier interface {
// Lane returns the lane identifier this verifier handles.
Lane() types.CertLane
// Verify validates a share's payload against the certificate
// subject. The payload was extracted via Share.PayloadFrom on
// the ceremony's PayloadArena.
Verify(subject [32]byte, share types.Share, payload []byte) error
}
// Owner identifies which operational chain owns a lane registry. The
// substrate uses Owner to enforce orthogonality: an M-Chain registry
// refuses F-Chain lanes and vice versa.
type Owner uint8
const (
OwnerUnknown Owner = 0
OwnerMChain Owner = 1
OwnerFChain Owner = 2
)
// LaneRegistry holds the verifier dispatch table for one chain.
//
// One registry per chain process. Constructed at boot, written
// during boot, read-only thereafter. No locks needed: registrations
// happen serially before the chain starts accepting blocks.
type LaneRegistry struct {
owner Owner
verifier map[types.CertLane]LaneVerifier
// aliases lets the host wire legacy-lane-id → modern-lane during
// the LP-134 grace window. Empty after the grace epoch closes.
aliases map[types.CertLane]types.CertLane
}
// NewRegistry constructs a registry owned by the given chain.
func NewRegistry(owner Owner) *LaneRegistry {
return &LaneRegistry{
owner: owner,
verifier: make(map[types.CertLane]LaneVerifier),
aliases: make(map[types.CertLane]types.CertLane),
}
}
// Register adds a verifier to the registry. Returns an error if the
// lane does not belong to the registry's owner — this enforces M/F
// orthogonality at the type level.
func (r *LaneRegistry) Register(v LaneVerifier) error {
if v == nil {
return errors.New("registry: nil verifier")
}
lane := v.Lane()
switch r.owner {
case OwnerMChain:
if !lane.IsMChain() {
return fmt.Errorf("registry: lane %d is not owned by M-Chain", lane)
}
case OwnerFChain:
if !lane.IsFChain() {
return fmt.Errorf("registry: lane %d is not owned by F-Chain", lane)
}
default:
return errors.New("registry: owner unset")
}
if _, exists := r.verifier[lane]; exists {
return fmt.Errorf("registry: lane %d already registered", lane)
}
r.verifier[lane] = v
return nil
}
// RegisterLegacyAlias maps a legacy LP-5013 T-Chain lane to the
// modern M/F lane during the grace window. After the window closes,
// the host calls ClearAliases() and any legacy share is rejected.
func (r *LaneRegistry) RegisterLegacyAlias(legacy, modern types.CertLane) error {
if _, ok := r.verifier[modern]; !ok {
return fmt.Errorf("registry: cannot alias to unregistered lane %d", modern)
}
r.aliases[legacy] = modern
return nil
}
// ClearAliases removes all legacy aliases. Called by the host at the
// end of the grace epoch.
func (r *LaneRegistry) ClearAliases() {
r.aliases = make(map[types.CertLane]types.CertLane)
}
// Verifier resolves a lane (including grace-window aliases) to its
// verifier. Returns an error if no verifier is registered.
func (r *LaneRegistry) Verifier(lane types.CertLane) (LaneVerifier, error) {
if alias, ok := r.aliases[lane]; ok {
lane = alias
}
v, ok := r.verifier[lane]
if !ok {
return nil, fmt.Errorf("registry: no verifier for lane %d", lane)
}
return v, nil
}
// Verify dispatches a share to its lane verifier. Convenience wrapper
// for the chain runtime — validates the share envelope, extracts the
// payload window, and calls the verifier.
func (r *LaneRegistry) Verify(subject [32]byte, share types.Share, arena []byte, set *types.ParticipantSet) error {
if err := share.Validate(set); err != nil {
return err
}
v, err := r.Verifier(share.Lane)
if err != nil {
return err
}
payload, err := share.PayloadFrom(arena)
if err != nil {
return err
}
return v.Verify(subject, share, payload)
}
// Owner reports which chain owns this registry. Used by tests and
// boot-time sanity checks.
func (r *LaneRegistry) Owner() Owner { return r.owner }
+112
View File
@@ -0,0 +1,112 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package cert
import (
"strings"
"testing"
"github.com/luxfi/chains/thresholdvm/types"
)
type stubVerifier struct{ lane types.CertLane }
func (s stubVerifier) Lane() types.CertLane { return s.lane }
func (s stubVerifier) Verify(_ [32]byte, _ types.Share, _ []byte) error {
return nil
}
// Orthogonality: the M-Chain registry refuses F-Chain lanes.
func TestOrthogonality_MChainRejectsFChainLane(t *testing.T) {
r := NewRegistry(OwnerMChain)
err := r.Register(stubVerifier{lane: types.LaneFChainTFHE})
if err == nil {
t.Fatal("expected M-Chain registry to reject FChainTFHE lane")
}
if !strings.Contains(err.Error(), "not owned by M-Chain") {
t.Fatalf("expected ownership error, got: %v", err)
}
}
// Orthogonality: the F-Chain registry refuses M-Chain lanes.
func TestOrthogonality_FChainRejectsMChainLane(t *testing.T) {
r := NewRegistry(OwnerFChain)
err := r.Register(stubVerifier{lane: types.LaneMChainCGGMP21})
if err == nil {
t.Fatal("expected F-Chain registry to reject MChainCGGMP21 lane")
}
if !strings.Contains(err.Error(), "not owned by F-Chain") {
t.Fatalf("expected ownership error, got: %v", err)
}
}
// Happy path: M-Chain registers all three of its lanes.
func TestRegister_MChainOwnedLanes(t *testing.T) {
r := NewRegistry(OwnerMChain)
for _, lane := range []types.CertLane{
types.LaneMChainCGGMP21,
types.LaneMChainFROST,
types.LaneMChainCoronaGen,
} {
if err := r.Register(stubVerifier{lane: lane}); err != nil {
t.Fatalf("register %d: %v", lane, err)
}
}
for _, lane := range []types.CertLane{
types.LaneMChainCGGMP21,
types.LaneMChainFROST,
types.LaneMChainCoronaGen,
} {
if _, err := r.Verifier(lane); err != nil {
t.Fatalf("lookup %d: %v", lane, err)
}
}
}
// Happy path: F-Chain registers its two lanes.
func TestRegister_FChainOwnedLanes(t *testing.T) {
r := NewRegistry(OwnerFChain)
for _, lane := range []types.CertLane{
types.LaneFChainTFHE,
types.LaneFChainBootstrap,
} {
if err := r.Register(stubVerifier{lane: lane}); err != nil {
t.Fatalf("register %d: %v", lane, err)
}
}
}
// Double-registration is rejected.
func TestRegister_RejectsDoubleRegistration(t *testing.T) {
r := NewRegistry(OwnerMChain)
if err := r.Register(stubVerifier{lane: types.LaneMChainFROST}); err != nil {
t.Fatalf("first register: %v", err)
}
if err := r.Register(stubVerifier{lane: types.LaneMChainFROST}); err == nil {
t.Fatal("expected second registration to fail")
}
}
// Legacy alias dispatch: a legacy lane id resolves to the modern verifier.
func TestLegacyAlias_DispatchesToModern(t *testing.T) {
r := NewRegistry(OwnerMChain)
if err := r.Register(stubVerifier{lane: types.LaneMChainCGGMP21}); err != nil {
t.Fatalf("register: %v", err)
}
const legacyTChainSign types.CertLane = 200 // arbitrary legacy id
if err := r.RegisterLegacyAlias(legacyTChainSign, types.LaneMChainCGGMP21); err != nil {
t.Fatalf("alias: %v", err)
}
v, err := r.Verifier(legacyTChainSign)
if err != nil {
t.Fatalf("legacy lookup: %v", err)
}
if v.Lane() != types.LaneMChainCGGMP21 {
t.Fatalf("legacy alias should dispatch to MChainCGGMP21, got %d", v.Lane())
}
r.ClearAliases()
if _, err := r.Verifier(legacyTChainSign); err == nil {
t.Fatal("after ClearAliases, legacy id should not resolve")
}
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package cert
import (
"crypto/sha256"
"errors"
)
// Roots is the set of upstream roots a Quasar 3.0 certificate_subject
// binds, mirroring the QuasarRoundDescriptor in LP-134.
//
// All seven roots are required, including both MChain and FChain —
// this is the structural property that makes cross-chain replay
// impossible. The substrate does not let a caller skip a root: a zero
// value is still a value, and the chain that owns the root is
// expected to provide its current root on every round.
type Roots struct {
ParentBlock [32]byte
StateRoot [32]byte
ExecRoot [32]byte
PChainValidator [32]byte
QChainCeremony [32]byte
ZChainVK [32]byte
AChainAttestation [32]byte
BChainBridge [32]byte
MChainCeremony [32]byte
FChainFHE [32]byte
}
// BindSubject computes certificate_subject = H(... all roots ...).
// The hash domain is sha256 with a 1-byte version tag (0x01 for
// LP-134 v3.1) so future descriptor extensions can change the input
// set without colliding.
func BindSubject(r Roots) [32]byte {
h := sha256.New()
h.Write([]byte{0x01}) // LP-134 v3.1 descriptor version
h.Write(r.ParentBlock[:])
h.Write(r.StateRoot[:])
h.Write(r.ExecRoot[:])
h.Write(r.PChainValidator[:])
h.Write(r.QChainCeremony[:])
h.Write(r.ZChainVK[:])
h.Write(r.AChainAttestation[:])
h.Write(r.BChainBridge[:])
h.Write(r.MChainCeremony[:])
h.Write(r.FChainFHE[:])
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// VerifySubject re-computes the subject from its inputs and checks it
// matches the value carried on the wire. Used by both M-Chain and
// F-Chain at certificate-ingress time.
func VerifySubject(claimed [32]byte, r Roots) error {
if BindSubject(r) != claimed {
return errors.New("subject: claimed certificate_subject does not match descriptor roots")
}
return nil
}
// RequireBothChains reports an error if either MChainCeremony or
// FChainFHE is zero — the LP-134 invariant that every round binds
// **both** chain roots, even on rounds where one chain does not
// finalize a ceremony (the unchanged root from the previous round
// satisfies the binding).
func (r Roots) RequireBothChains() error {
if r.MChainCeremony == ([32]byte{}) {
return errors.New("subject: mchain_ceremony_root is zero — boot-time root not yet wired")
}
if r.FChainFHE == ([32]byte{}) {
return errors.New("subject: fchain_fhe_root is zero — boot-time root not yet wired")
}
return nil
}
+119
View File
@@ -0,0 +1,119 @@
# Architecture
ThresholdVM is the **shared library substrate** beneath M-Chain and
F-Chain. The substrate has zero runtime presence; it compiles into
both chains' binaries.
## The three pieces
```
+-----------------------------------------------------------+
| Quasar 3.0 |
| (consensus, certificate_subject binds all chain roots) |
+-----------+--------------------------+--------------------+
| |
| mchain_ceremony_root | fchain_fhe_root
| |
+--------v---------+ +---------v--------+
| M-Chain | | F-Chain |
| validators | | validators |
| + MPC drivers | | + GPU drivers |
| + lane registry | | + lane registry |
| (MChainCGGMP21, | | (FChainTFHE, |
| MChainFROST, | | FChainBootstr) |
| MChainRingTl) | | |
+--------+---------+ +---------+--------+
| |
| imports |
| |
+----------+---------------+
|
+--------v---------+
| thresholdvm |
| (this library) |
| |
| types/ |
| protocol/*/iface |
| cert/lane,subj |
| runtime/*Adapter |
+------------------+
```
## What lives where
| Concern | Owner |
|---|---|
| Ceremony state machine (Registered → Round1 → Round2 → Finalized) | substrate (`types/ceremony.go`) |
| Share envelope `(offset, len)` indirection | substrate (`types/share.go`) |
| Participant set + Selector contract | substrate (`types/participant.go`) |
| Cert lane registry (per-owner enforcement) | substrate (`cert/lane.go`) |
| `certificate_subject` binding | substrate (`cert/subject.go`) |
| CGGMP21 / FROST / Corona-general drivers | M-Chain (`chains/mchain/protocol/*/`) |
| TFHE bootstrap-key generation | M-Chain producer + F-Chain consumer |
| TFHE compute kernels | F-Chain (`chains/fchain/`) |
| Block production, gossip, p2p | each chain's VM |
| Validator selection (stake-weighted VRF) | each chain's `Selector` impl |
| Persistence | each chain's database |
## Why orthogonal at the type level
The substrate's `LaneRegistry` carries an `Owner` field. `Register`
checks the verifier's lane against the owner's allowed range:
- `OwnerMChain` accepts lanes 5..7
- `OwnerFChain` accepts lanes 8..9
A misrouted verifier (e.g. F-Chain trying to register a CGGMP21
verifier) returns an error from `Register` at boot. The chain fails
to start, not at runtime in the middle of a ceremony.
Similarly, `runtime.MChainAdapter` and `runtime.FChainAdapter` are
disjoint interfaces. A chain implementing both is a code smell that
shows up in code review (and in `go doc` output).
## Why one substrate, two chains
The state machine is identical. The envelope is identical. The cert
binding is identical. The only thing that differs across the two
chains is the per-protocol payload — which is opaque to the
substrate and dispatched by lane.
If we kept M-Chain and F-Chain on separate substrates, we would have
two copies of the state machine, two copies of the share envelope,
two copies of the subject binding. Drift is inevitable; bugs would
fix on one side and not the other.
The substrate is the orthogonal piece. The chains are the
operational pieces. PHILOSOPHY.md §3.
## Cross-chain handoff
The only cross-chain ceremony today is **TFHE bootstrap-key gen**:
1. M-Chain runs FROST DKG on the TFHE secret-key polynomial. The
ceremony has `Kind = KindTFHEKeygen`, `Lane = LaneMChainFROST`.
2. M-Chain finalizes; the proof's payload is the serialized TFHE
evaluation key.
3. The substrate's handoff envelope wraps the M-Chain proof into a
F-Chain `Share` with `Lane = LaneFChainBootstrap`. The wrapper's
payload is `(offset, len)` into the upstream artifact buffer.
4. F-Chain ingests via `OnBootstrapHandoff`, verifies the upstream
M-Chain ceremony root from the round descriptor, installs the
key, advances `fchain_fhe_root`.
5. Quasar 3.0's `certificate_subject` for the round binds both
`mchain_ceremony_root` (post-FROST) and `fchain_fhe_root`
(post-install). Atomic from the network's view.
No new envelope type. The handoff is just two shares on two lanes
with a deterministic wrapping.
## References
| LP | Topic |
|---|---|
| LP-019 | Threshold MPC |
| LP-013 | FHE on GPU |
| LP-076 | Universal Threshold Cryptography |
| LP-132 | QuasarGPU Execution Adapter |
| LP-134 | Lux Chain Topology |
| LP-020 | Quasar Consensus 3.0 |
+101
View File
@@ -0,0 +1,101 @@
# F-Chain Integration
This document describes how `chains/fchain/` plugs into the
ThresholdVM substrate.
## Boot sequence
```
1. F-Chain VM starts.
2. F-Chain constructs cert.NewRegistry(cert.OwnerFChain).
3. F-Chain registers each protocol verifier:
reg.Register(tfheVerifier{...}) // LaneFChainTFHE
reg.Register(bootstrapVerifier{...}) // LaneFChainBootstrap
4. During the LP-134 grace epoch, F-Chain wires legacy aliases:
reg.RegisterLegacyAlias(LaneTChainFHE, LaneFChainTFHE)
5. F-Chain implements runtime.FChainAdapter.
6. F-Chain begins accepting blocks.
```
## Validator subset
F-Chain validators are the subset of P-Chain validators with stake
delegated to F-Chain. GPU operators self-select by delegating here
— no allowlist; the chain rejects participants who can't keep up
with bootstrap latency, but only at the validator-set level (slow
nodes get pruned by economic incentives, not by an admin key).
## Ceremony cadence
F-Chain block time targets **~2 s** to amortize TFHE bootstrap
latency. This means:
- A TFHE compute attestation ceremony finalizes in ~4 s.
- A bootstrap proof finalizes in ~2 s (single-round attestation).
- Bootstrap-key handoff from M-Chain takes one F-Chain block to
ingest.
## Lanes F-Chain owns
| Lane | Verifier | Driver |
|---|---|---|
| LaneFChainTFHE | F-Chain TFHE compute attestation verifier | F-Chain `drain_fhe` service |
| LaneFChainBootstrap | F-Chain bootstrap-handoff verifier (`protocol/tfhe_keygen.Consumer`) | none — bootstrap proofs come from M-Chain |
F-Chain **does not** own LaneMChain*. The registry refuses
registration of MChain lanes under OwnerFChain.
## Bootstrap handoff (M → F)
When M-Chain finalizes a KindTFHEKeygen ceremony:
```
M-Chain proof:
Lane = LaneMChainFROST
Payload = serialized TFHE evaluation key
Wrapped F-Chain share:
Lane = LaneFChainBootstrap
PayloadOffset/Len = window into the upstream artifact buffer
Verifier (F-Chain): tfhe_keygen.Consumer.VerifyHandoff(...)
checks the upstream M-Chain ceremony root
referenced by certificate_subject.
```
F-Chain's `runtime.FChainAdapter.OnBootstrapHandoff` takes the
upstream proof, verifies it under the M-Chain ceremony root in the
round descriptor, installs the new TFHE evaluation key into the
F-Chain key arena, and advances `fchain_fhe_root`.
In the same Quasar 3.0 round, `certificate_subject` binds both
`mchain_ceremony_root` (post-FROST) and `fchain_fhe_root`
(post-install). The handoff is atomic from the network's view —
either both roots advance or neither does.
## TFHE compute attestations
For each `fhe_*` precompile call on C-Chain, F-Chain runs a small
attestation ceremony:
1. The C-Chain transaction triggers a `FheCompute` event consumed by
F-Chain's `drain_fhe` service (LP-132 §FheCompute).
2. F-Chain validators run the TFHE op on GPU, produce a compute
transcript, and emit a `LaneFChainTFHE` share carrying the
transcript's hash.
3. The substrate's `LaneRegistry.Verify` dispatches to F-Chain's
TFHE verifier, which checks the transcript matches the input
ciphertexts and the published evaluation key.
4. On finalize, the transcript hash advances `fchain_fhe_root` and
the ciphertext output is delivered to C-Chain.
## Migration from T-Chain
LP-5013's T-Chain hosted FHE compute under a different chain ID.
The migration path is:
- All FHE compute moves to F-Chain unchanged. Kernels (LP-013)
port across — they were never tied to T-Chain semantics.
- During the grace epoch, F-Chain accepts legacy `LaneTChainFHE`
shares via `RegisterLegacyAlias` and dispatches them to
`LaneFChainTFHE`.
- After the grace epoch, F-Chain calls `ClearAliases()`.
+96
View File
@@ -0,0 +1,96 @@
# M-Chain Integration
This document describes how `chains/mchain/` plugs into the
ThresholdVM substrate.
## Boot sequence
```
1. M-Chain VM starts.
2. M-Chain constructs cert.NewRegistry(cert.OwnerMChain).
3. M-Chain registers each protocol verifier:
reg.Register(cggmp21Verifier{...}) // LaneMChainCGGMP21
reg.Register(frostVerifier{...}) // LaneMChainFROST
reg.Register(coronaGenVerifier{...}) // LaneMChainCoronaGen
4. During the LP-134 grace epoch, M-Chain wires legacy aliases:
reg.RegisterLegacyAlias(LaneTChainSign, LaneMChainCGGMP21)
// ... per the LP-134 deprecation table.
5. M-Chain implements runtime.MChainAdapter and hands itself to the
substrate's ceremony driver.
6. M-Chain begins accepting blocks. Each ceremony round is an entry
in the block, decoded as types.Share, validated via reg.Verify(),
and accumulated into a types.CeremonyRound. At round-2 close,
the substrate calls OnFinalize and the M-Chain validator subset
emits the cert.
```
## Validator subset
M-Chain validators are the subset of P-Chain validators with stake
delegated to M-Chain. The substrate's `Selector` interface (see
`types/participant.go`) is implemented by M-Chain over its own
delegation table.
VRF seed: `H(epoch || pchain_validator_root || ceremony_kind)`.
Stake-weighted: probability of selection ∝ delegated stake at the
epoch cutoff. Same security analysis as Lux's existing P-Chain VRF.
Permissionless: any P-Chain validator can delegate to M-Chain. No
allowlist, no admin key.
## Ceremony cadence
M-Chain block time targets **~500 ms**. This means:
- A 2-round CGGMP21 / FROST ceremony finalizes in ~1 s.
- A pre-sign + sign CGGMP21 sequence is ~1.5 s.
- Corona-general (2-round) is ~1 s.
GPU acceleration (LP-132) is optional; current M-Chain validators
are CPU-only. F-Chain is where GPU lives.
## Lanes M-Chain owns
| Lane | Verifier | Driver |
|---|---|---|
| LaneMChainCGGMP21 | `protocol/cggmp21.Verifier` | `protocol/cggmp21.Driver` |
| LaneMChainFROST | `protocol/frost.Verifier` | `protocol/frost.Driver` |
| LaneMChainCoronaGen | `protocol/corona_general.Verifier` | `protocol/corona_general.Driver` |
M-Chain **does not** own LaneFChainTFHE or LaneFChainBootstrap. The
substrate's registry refuses to register them under OwnerMChain.
## TFHE keygen — the cross-chain ceremony
M-Chain runs the FROST DKG that produces the TFHE evaluation key.
Concretely:
```go
// chains/mchain/ceremony/tfhe_keygen.go
ceremony := types.Ceremony{
Kind: types.KindTFHEKeygen, // cross-chain
// ... rest as for any FROST DKG ...
}
proof, err := frostDriver.Finalize(ctx, ceremony.ID, round2Payloads)
// proof.Lane == LaneMChainFROST
// proof.Payload == serialized TFHE evaluation key
adapter.OnFinalize(ctx, ceremony, proof)
```
The substrate notices `Kind == KindTFHEKeygen` and routes the proof
into the F-Chain handoff envelope (see
`docs/F_CHAIN_INTEGRATION.md` §"Bootstrap handoff").
## Migration from T-Chain
LP-5013's T-Chain hosted both MPC and FHE. After the LP-134
activation epoch:
- All T-Chain MPC ceremonies move to M-Chain unchanged. Same
protocol semantics, same threshold parameters; only the chain ID
and root labels change.
- During the grace epoch, M-Chain's `LaneRegistry` accepts legacy
T-Chain lane numbers via `RegisterLegacyAlias` and dispatches
them to the modern `MChain*` verifiers.
- After the grace epoch, M-Chain calls `ClearAliases()` and drops
legacy support. Any client still emitting T-Chain shares fails.
+48
View File
@@ -0,0 +1,48 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package cggmp21 declares the M-Chain CGGMP21 protocol surface.
//
// This package contains interfaces only. The concrete implementation
// (key generation, pre-signing, signing, verification, identifiable
// abort) lives in chains/mchain/protocol/cggmp21/. The substrate
// imports this package; M-Chain implements it.
package cggmp21
import (
"context"
"github.com/luxfi/chains/thresholdvm/types"
)
// Verifier validates a CGGMP21 share payload. It is registered into
// the M-Chain LaneRegistry as the LaneMChainCGGMP21 verifier.
type Verifier interface {
// VerifyShare checks that the per-round payload is a well-formed
// CGGMP21 commitment / signature share. It does not produce or
// hold any secret material.
VerifyShare(subject [32]byte, share types.Share, payload []byte) error
// VerifyFinal checks that the aggregated proof is a valid
// secp256k1 ECDSA signature under the group public key encoded in
// the proof.
VerifyFinal(subject [32]byte, proof types.Proof) error
}
// Driver runs the protocol on a participant's behalf. Only validators
// selected into the ceremony's participant set instantiate a Driver;
// everyone else only verifies.
type Driver interface {
// Round1 emits the participant's round-1 payload (commitments).
Round1(ctx context.Context, ceremony types.CeremonyID) ([]byte, error)
// Round2 emits the participant's round-2 payload (encrypted
// shares), given the round-1 commitments from the rest of the
// participant set.
Round2(ctx context.Context, ceremony types.CeremonyID, round1 [][]byte) ([]byte, error)
// Finalize aggregates all round-2 payloads into the final proof.
// Returns ErrIdentifiableAbort if a participant is detected
// cheating; the abort proof identifies the cheater.
Finalize(ctx context.Context, ceremony types.CeremonyID, round2 [][]byte) (types.Proof, error)
}
@@ -0,0 +1,34 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package corona_general declares the M-Chain general-purpose
// Corona (Ring-LWE) threshold protocol surface.
//
// This is **not** consensus-Corona. Consensus uses Corona at
// fixed parameters as the post-quantum cert lane (Q-Chain runs that
// ceremony, see LP-073). General-purpose Corona is for app-level
// threshold signing under a different parameter set; it lives on
// M-Chain.
//
// Interface-only; implementation in chains/mchain/protocol/corona/.
package corona_general
import (
"context"
"github.com/luxfi/chains/thresholdvm/types"
)
// Verifier validates Corona-general share payloads. Registered as
// the LaneMChainCoronaGen verifier in the M-Chain LaneRegistry.
type Verifier interface {
VerifyShare(subject [32]byte, share types.Share, payload []byte) error
VerifyFinal(subject [32]byte, proof types.Proof) error
}
// Driver runs the 2-round lattice protocol for a selected participant.
type Driver interface {
Round1(ctx context.Context, ceremony types.CeremonyID) ([]byte, error)
Round2(ctx context.Context, ceremony types.CeremonyID, round1 [][]byte) ([]byte, error)
Finalize(ctx context.Context, ceremony types.CeremonyID, round2 [][]byte) (types.Proof, error)
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package frost declares the M-Chain FROST protocol surface.
//
// FROST is a 2-round Schnorr threshold scheme over Ed25519. This
// package is interface-only; the implementation lives in
// chains/mchain/protocol/frost/.
package frost
import (
"context"
"github.com/luxfi/chains/thresholdvm/types"
)
// Verifier validates FROST share payloads. Registered as the
// LaneMChainFROST verifier in the M-Chain LaneRegistry.
type Verifier interface {
VerifyShare(subject [32]byte, share types.Share, payload []byte) error
VerifyFinal(subject [32]byte, proof types.Proof) error
}
// Driver runs FROST on a selected participant's behalf.
type Driver interface {
// Round1 emits the participant's nonce commitments (D_i, E_i).
Round1(ctx context.Context, ceremony types.CeremonyID) ([]byte, error)
// Round2 emits the signature share z_i.
Round2(ctx context.Context, ceremony types.CeremonyID, round1 [][]byte) ([]byte, error)
// Finalize aggregates {z_i} into the single Ed25519 (R, z).
Finalize(ctx context.Context, ceremony types.CeremonyID, round2 [][]byte) (types.Proof, error)
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package tfhe_keygen declares the cross-chain TFHE bootstrap-key
// generation surface.
//
// The ceremony **runs on M-Chain** (it is an MPC ceremony of FROST
// shape over the TFHE secret-key polynomial), but its **output is
// consumed by F-Chain** as the new bootstrap key. The handoff is
// represented in the substrate as a single Ceremony with kind
// KindTFHEKeygen and Lane=LaneMChainFROST at finalize, then ingested
// on F-Chain via Lane=LaneFChainBootstrap that wraps the upstream
// artifact.
//
// Interface-only; implementations split between
// chains/mchain/protocol/tfhe_keygen/ (the producer) and
// chains/fchain/protocol/tfhe_bootstrap/ (the consumer).
package tfhe_keygen
import (
"context"
"github.com/luxfi/chains/thresholdvm/types"
)
// Producer is implemented by M-Chain. It runs the MPC ceremony that
// produces a TFHE evaluation key.
type Producer interface {
Round1(ctx context.Context, ceremony types.CeremonyID) ([]byte, error)
Round2(ctx context.Context, ceremony types.CeremonyID, round1 [][]byte) ([]byte, error)
// Finalize emits a Proof whose Payload is the serialized TFHE
// evaluation key plus bootstrap material.
Finalize(ctx context.Context, ceremony types.CeremonyID, round2 [][]byte) (types.Proof, error)
}
// Consumer is implemented by F-Chain. It accepts the M-Chain proof,
// re-verifies under the M-Chain ceremony root, and binds the new key
// into fchain_fhe_root.
type Consumer interface {
// Ingest verifies the upstream proof and installs the bootstrap
// key for use by F-Chain's FHE compute pipeline (LP-013).
Ingest(ctx context.Context, mchainSubject [32]byte, proof types.Proof) error
// VerifyHandoff is the per-share verifier on F-Chain's
// LaneFChainBootstrap. It validates that a F-Chain bootstrap
// share correctly references an M-Chain TFHE-keygen ceremony.
VerifyHandoff(subject [32]byte, share types.Share, payload []byte) error
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package runtime
import (
"context"
"github.com/luxfi/chains/thresholdvm/cert"
"github.com/luxfi/chains/thresholdvm/types"
)
// FChainAdapter is the contract F-Chain implements. Same state
// machine as M-Chain, different hooks: F-Chain consumes upstream MPC
// outputs and emits FHE compute attestations.
type FChainAdapter interface {
// Selector returns the F-Chain participant selector. Stake-weighted
// over P-Chain stake delegated to F-Chain. GPU operators
// self-select by delegating here rather than to M-Chain.
Selector() types.Selector
// Registry returns the F-Chain LaneRegistry. Must be owned by
// OwnerFChain and have verifiers for LaneFChainTFHE,
// LaneFChainBootstrap registered.
Registry() *cert.LaneRegistry
// FHERoot returns fchain_fhe_root at the current epoch. Bound
// into certificate_subject alongside mchain_ceremony_root.
FHERoot(ctx context.Context, epoch uint64) ([32]byte, error)
// OnBootstrapHandoff is called by the substrate when M-Chain
// finalizes a KindTFHEKeygen ceremony and hands off the bootstrap
// key. F-Chain re-verifies under the M-Chain ceremony root and
// installs the key.
OnBootstrapHandoff(ctx context.Context, mchainSubject [32]byte, proof types.Proof) error
// OnEval is called for a TFHE compute ceremony. The participant
// set is the F-Chain GPU operators selected by VRF.
OnEval(ctx context.Context, ceremony types.Ceremony, set *types.ParticipantSet) error
// OnFinalize emits the finalized cert artifact onto Quasar 3.0's
// CertLane and advances fchain_fhe_root.
OnFinalize(ctx context.Context, ceremony types.Ceremony, proof types.Proof) error
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package runtime defines the adapter contracts the host chains
// (M-Chain and F-Chain) implement to plug into the ThresholdVM
// substrate.
//
// The substrate calls into adapters; adapters never call into the
// substrate. This is the orthogonality boundary at the type level:
// MChainAdapter exposes only MPC hooks, FChainAdapter exposes only
// FHE hooks. A chain that implements both interfaces is a
// configuration error.
package runtime
import (
"context"
"github.com/luxfi/chains/thresholdvm/cert"
"github.com/luxfi/chains/thresholdvm/types"
)
// MChainAdapter is the contract M-Chain implements. The substrate
// drives ceremonies by calling these hooks; M-Chain provides the
// validator-set selection, persistence, and cert-emission.
type MChainAdapter interface {
// Selector returns the M-Chain participant selector. Stake-weighted
// VRF over P-Chain stake delegated to M-Chain.
Selector() types.Selector
// Registry returns the M-Chain LaneRegistry. Must be owned by
// OwnerMChain and have verifiers for LaneMChainCGGMP21,
// LaneMChainFROST, LaneMChainCoronaGen registered.
Registry() *cert.LaneRegistry
// CeremonyRoot returns mchain_ceremony_root at the current epoch.
// Bound into certificate_subject by Quasar 3.0.
CeremonyRoot(ctx context.Context, epoch uint64) ([32]byte, error)
// OnDKG is called by the substrate when a new DKG ceremony enters
// StateRegistered. M-Chain persists the participant set and
// allocates the payload arena.
OnDKG(ctx context.Context, ceremony types.Ceremony, set *types.ParticipantSet) error
// OnSign is called for a signing ceremony. Same shape as OnDKG;
// distinguished only by Ceremony.Kind.
OnSign(ctx context.Context, ceremony types.Ceremony, set *types.ParticipantSet) error
// OnReshare is called for an LSS resharing ceremony (LP-077). The
// new participant set replaces the old one atomically at
// finalize.
OnReshare(ctx context.Context, ceremony types.Ceremony, oldSet, newSet *types.ParticipantSet) error
// OnFinalize emits the finalized cert artifact onto Quasar 3.0's
// CertLane and advances mchain_ceremony_root. If the ceremony's
// kind is KindTFHEKeygen, the proof is also handed off to F-Chain
// via the cross-chain envelope.
OnFinalize(ctx context.Context, ceremony types.Ceremony, proof types.Proof) error
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package types defines the data types of the ThresholdVM substrate.
//
// These types are imported by both M-Chain (chains/mchain) and F-Chain
// (chains/fchain). The substrate has no runtime; types here are pure
// values, the state machine is an explicit-transition function, and
// the registry types in cert/ are constructed (not singletons) by the
// host chain.
package types
import (
"errors"
"fmt"
)
// CeremonyState is the position of a ceremony in the shared state
// machine. The same machine is used by every protocol on M-Chain and
// F-Chain; only the per-round payload differs.
type CeremonyState uint8
const (
StateUnknown CeremonyState = 0
StateRegistered CeremonyState = 1 // participant set committed, no shares yet
StateRound1 CeremonyState = 2 // round-1 commitments published
StateRound2 CeremonyState = 3 // round-2 shares published
StateFinalized CeremonyState = 4 // cert artifact emitted, root advanced
StateAborted CeremonyState = 5 // identifiable abort (CGGMP21 etc.) or timeout
)
// String returns a stable label for logs.
func (s CeremonyState) String() string {
switch s {
case StateRegistered:
return "registered"
case StateRound1:
return "round1"
case StateRound2:
return "round2"
case StateFinalized:
return "finalized"
case StateAborted:
return "aborted"
default:
return "unknown"
}
}
// CeremonyKind names the protocol the ceremony executes. The kind
// determines which CertLane the finalized artifact uses and which
// verifier validates each round's payload. Adding a new kind appends
// here and adds a CertLane in cert/lane.go — never reorders.
type CeremonyKind uint8
const (
KindUnknown CeremonyKind = 0
KindCGGMP21 CeremonyKind = 1 // M-Chain: ECDSA threshold
KindFROST CeremonyKind = 2 // M-Chain: Schnorr/EdDSA threshold
KindCoronaGen CeremonyKind = 3 // M-Chain: PQ general-purpose threshold
KindTFHEKeygen CeremonyKind = 4 // M-Chain → F-Chain: TFHE bootstrap-key gen
KindTFHECompute CeremonyKind = 5 // F-Chain: encrypted compute attestation
KindTFHEBootstrap CeremonyKind = 6 // F-Chain: blind-rotate / bootstrap proof
)
// IsMChain reports whether the kind belongs to the M-Chain operational chain.
func (k CeremonyKind) IsMChain() bool {
return k == KindCGGMP21 || k == KindFROST || k == KindCoronaGen
}
// IsFChain reports whether the kind belongs to the F-Chain operational chain.
func (k CeremonyKind) IsFChain() bool {
return k == KindTFHECompute || k == KindTFHEBootstrap
}
// IsHandoff reports whether the kind is a cross-chain (M → F) ceremony.
// Handoff ceremonies originate on M-Chain and finalize into F-Chain via
// the bootstrap-handoff envelope.
func (k CeremonyKind) IsHandoff() bool {
return k == KindTFHEKeygen
}
// CeremonyID is the canonical identifier for a ceremony, derived from
// the Quasar 3.0 round descriptor and the ceremony kind. It is the
// 32-byte hash that ties shares, proofs, and cert subjects together.
type CeremonyID [32]byte
// Ceremony describes a single threshold ceremony on either chain.
//
// Ceremony is a pure data type. State transitions are produced by
// Transition() in this package; the host chain (M-Chain or F-Chain)
// drives transitions on its own block ticks.
type Ceremony struct {
ID CeremonyID
Kind CeremonyKind
State CeremonyState
Round uint8 // 1..N, valid only when State is Round1 or Round2
Threshold uint16
Total uint16 // total participants
StartEpoch uint64
Subject [32]byte // certificate_subject the ceremony binds into
// PayloadArena is the per-ceremony buffer that all share payloads
// index into via (offset, len). Owned by the host chain; the
// substrate never allocates.
PayloadArena []byte
}
// Validate checks structural invariants. Use at boundaries only; the
// substrate trusts its own callers (the host chain) for routine
// transitions per PHILOSOPHY.md "defensive programming against
// yourself".
func (c *Ceremony) Validate() error {
if c == nil {
return errors.New("ceremony: nil")
}
if c.Kind == KindUnknown {
return errors.New("ceremony: kind unset")
}
if c.Total == 0 {
return errors.New("ceremony: total participants is zero")
}
// Honest-majority safety floor (LP-076 §Security).
if uint32(c.Threshold)*2 <= uint32(c.Total) {
return fmt.Errorf("ceremony: threshold %d <= n/2 for total %d", c.Threshold, c.Total)
}
if c.Threshold > c.Total {
return fmt.Errorf("ceremony: threshold %d > total %d", c.Threshold, c.Total)
}
return nil
}
// Transition advances the ceremony state. It is the only function
// that mutates State. Returns the post-transition copy or an error
// if the transition is illegal.
//
// Legal transitions:
//
// Registered -> Round1
// Round1 -> Round2
// Round2 -> Finalized
// * -> Aborted (always permitted)
func (c Ceremony) Transition(next CeremonyState) (Ceremony, error) {
if next == StateAborted {
c.State = StateAborted
return c, nil
}
switch c.State {
case StateRegistered:
if next != StateRound1 {
return c, fmt.Errorf("ceremony: registered -> %s not legal", next)
}
c.State = StateRound1
c.Round = 1
case StateRound1:
if next != StateRound2 {
return c, fmt.Errorf("ceremony: round1 -> %s not legal", next)
}
c.State = StateRound2
c.Round = 2
case StateRound2:
if next != StateFinalized {
return c, fmt.Errorf("ceremony: round2 -> %s not legal", next)
}
c.State = StateFinalized
default:
return c, fmt.Errorf("ceremony: %s -> %s not legal", c.State, next)
}
return c, nil
}
// CeremonyRound is the per-round bag of share commitments for a single
// ceremony. The host chain accumulates these in memory while the
// ceremony is in Round1 / Round2 and finalizes them into a CertLane
// artifact at Finalize.
type CeremonyRound struct {
CeremonyID CeremonyID
Round uint8
Shares []Share // populated as each participant publishes
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package types
import "testing"
func newCeremony() Ceremony {
return Ceremony{
ID: CeremonyID{0xAA},
Kind: KindFROST,
State: StateRegistered,
Threshold: 3,
Total: 5,
}
}
func TestCeremony_LegalTransitions(t *testing.T) {
c := newCeremony()
if err := c.Validate(); err != nil {
t.Fatalf("validate: %v", err)
}
c, err := c.Transition(StateRound1)
if err != nil || c.State != StateRound1 || c.Round != 1 {
t.Fatalf("registered->round1: %v state=%s round=%d", err, c.State, c.Round)
}
c, err = c.Transition(StateRound2)
if err != nil || c.State != StateRound2 || c.Round != 2 {
t.Fatalf("round1->round2: %v state=%s round=%d", err, c.State, c.Round)
}
c, err = c.Transition(StateFinalized)
if err != nil || c.State != StateFinalized {
t.Fatalf("round2->finalized: %v state=%s", err, c.State)
}
}
func TestCeremony_IllegalTransitionsRejected(t *testing.T) {
c := newCeremony()
if _, err := c.Transition(StateFinalized); err == nil {
t.Fatal("registered -> finalized should be rejected")
}
if _, err := c.Transition(StateRound2); err == nil {
t.Fatal("registered -> round2 should be rejected")
}
}
func TestCeremony_AbortAlwaysAllowed(t *testing.T) {
c := newCeremony()
c, err := c.Transition(StateAborted)
if err != nil || c.State != StateAborted {
t.Fatalf("registered -> aborted: %v state=%s", err, c.State)
}
}
func TestCeremony_HonestMajorityFloor(t *testing.T) {
c := Ceremony{Kind: KindFROST, Threshold: 2, Total: 5}
if err := c.Validate(); err == nil {
t.Fatal("threshold <= n/2 should be rejected")
}
}
func TestCeremonyKind_OrthogonalityFlags(t *testing.T) {
if !KindFROST.IsMChain() || KindFROST.IsFChain() {
t.Fatal("FROST is M-Chain only")
}
if !KindTFHEBootstrap.IsFChain() || KindTFHEBootstrap.IsMChain() {
t.Fatal("TFHEBootstrap is F-Chain only")
}
if !KindTFHEKeygen.IsHandoff() {
t.Fatal("TFHEKeygen is the cross-chain handoff kind")
}
}
+107
View File
@@ -0,0 +1,107 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package types
import (
"crypto/sha256"
"encoding/binary"
"errors"
"sort"
)
// NodeID identifies a Lux validator. Same definition as the rest of
// luxfi/node — 32 bytes, content-addressed.
type NodeID [32]byte
// Participant is one validator selected (via stake-weighted VRF) into
// the participant set of a ceremony. The substrate does not pick
// participants; the host chain (M-Chain or F-Chain) does, then hands
// the resulting set here.
type Participant struct {
Node NodeID
Index uint32 // canonical index inside the ParticipantSet (0..n-1)
Weight uint64 // stake weight at selection time, for accounting only
}
// ParticipantSet is the deterministic, ordered set of validators for
// one ceremony. The set is fixed at registration; resharing is a new
// ceremony with a new set, never an in-place mutation.
type ParticipantSet struct {
CeremonyID CeremonyID
Members []Participant // sorted ascending by NodeID; Index == position
}
// NewParticipantSet builds a ParticipantSet, sorting members by NodeID
// and assigning Index = position. Returns an error if the input has
// duplicate nodes or is empty.
func NewParticipantSet(ceremonyID CeremonyID, members []Participant) (*ParticipantSet, error) {
if len(members) == 0 {
return nil, errors.New("participant: empty set")
}
out := make([]Participant, len(members))
copy(out, members)
sort.Slice(out, func(i, j int) bool {
for k := 0; k < len(out[i].Node); k++ {
if out[i].Node[k] != out[j].Node[k] {
return out[i].Node[k] < out[j].Node[k]
}
}
return false
})
for i := 1; i < len(out); i++ {
if out[i-1].Node == out[i].Node {
return nil, errors.New("participant: duplicate node in set")
}
}
for i := range out {
out[i].Index = uint32(i)
}
return &ParticipantSet{CeremonyID: ceremonyID, Members: out}, nil
}
// Lookup returns the participant for a node, or false if absent.
func (ps *ParticipantSet) Lookup(node NodeID) (Participant, bool) {
for _, p := range ps.Members {
if p.Node == node {
return p, true
}
}
return Participant{}, false
}
// Digest returns the canonical 32-byte digest of the set. Used as the
// participant_root when a ceremony is registered, and as input to the
// stake-weighted VRF for the next ceremony.
func (ps *ParticipantSet) Digest() [32]byte {
h := sha256.New()
h.Write(ps.CeremonyID[:])
var buf [12]byte
for _, m := range ps.Members {
h.Write(m.Node[:])
binary.BigEndian.PutUint32(buf[0:4], m.Index)
binary.BigEndian.PutUint64(buf[4:12], m.Weight)
h.Write(buf[:])
}
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// Selector is implemented by the host chain (M-Chain or F-Chain). The
// substrate consumes a Selector to produce a ParticipantSet at
// ceremony registration time.
//
// Selectors must be:
//
// - deterministic: same input -> same output, byte for byte
// - permissionless: any validator with stake delegated to the chain
// is eligible (no allowlist)
// - stake-weighted: probability of selection is proportional to
// stake delegated to the chain at the cutoff epoch
//
// The substrate does not implement Selector; M-Chain and F-Chain each
// provide one wired to their stake delegation.
type Selector interface {
Select(ceremonyID CeremonyID, total uint16, seed [32]byte) (*ParticipantSet, error)
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package types
// Proof is the verification artifact emitted at ceremony Finalize. It
// is consumed by Quasar 3.0's cert pipeline (LP-020) and bound into
// certificate_subject (LP-134).
//
// The substrate defines the envelope; the per-protocol payload (the
// actual signature, group public key, FHE key, etc.) is opaque bytes
// indexed by Lane. Verifiers in cert/lane.go decode it.
type Proof struct {
CeremonyID CeremonyID
Kind CeremonyKind
Lane CertLane
// Payload is the protocol-specific final artifact: a signature
// for sign-oriented ceremonies, a TFHE evaluation key for keygen
// ceremonies, etc.
Payload []byte
// Aggregate is the aggregate signature over the participant
// commitments (used when the chain itself attests to the
// ceremony before forwarding to Quasar).
Aggregate [64]byte
}
// Empty reports whether p is the zero value.
func (p Proof) Empty() bool {
return p.Kind == KindUnknown && len(p.Payload) == 0
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package types
import (
"errors"
"fmt"
)
// CertLane mirrors LP-134 §QuasarCertLane. We declare it here in the
// substrate because the share envelope is the wire form on which
// every chain's verifier dispatches. The values must match LP-134
// exactly. New lanes append; values never move.
type CertLane uint8
const (
LaneBLS CertLane = 0
LaneCorona CertLane = 1
LaneMLDSAGroth16 CertLane = 2
LaneAChainAttest CertLane = 3
LaneBChainBridge CertLane = 4
LaneMChainCGGMP21 CertLane = 5
LaneMChainFROST CertLane = 6
LaneMChainCoronaGen CertLane = 7
LaneFChainTFHE CertLane = 8
LaneFChainBootstrap CertLane = 9
)
// IsMChain reports whether the lane is owned by M-Chain.
func (l CertLane) IsMChain() bool {
return l == LaneMChainCGGMP21 || l == LaneMChainFROST || l == LaneMChainCoronaGen
}
// IsFChain reports whether the lane is owned by F-Chain.
func (l CertLane) IsFChain() bool {
return l == LaneFChainTFHE || l == LaneFChainBootstrap
}
// Share is the per-participant, per-round share envelope.
//
// The envelope is fixed; per-protocol payload is carried via
// (PayloadOffset, PayloadLen) indirection into a Ceremony's
// PayloadArena. This matches the QuasarCertIngress wire ABI from
// LP-132 §drain_cert_lane: the verifier never decodes the share
// itself, only the payload window it points at.
type Share struct {
CeremonyID CeremonyID
ParticipantID uint32 // index into ParticipantSet.Members
Round uint8 // 1..N, ceremony-kind specific
Lane CertLane // dispatches to the per-protocol verifier
PayloadOffset uint32
PayloadLen uint32
Signature [64]byte // BLS or ML-DSA over (CeremonyID, Round, payload)
}
// PayloadFrom returns a slice into arena described by the share. It
// returns an error only if the share is structurally inconsistent
// with the arena length; payload semantics are the verifier's
// responsibility.
func (s Share) PayloadFrom(arena []byte) ([]byte, error) {
end := uint64(s.PayloadOffset) + uint64(s.PayloadLen)
if end > uint64(len(arena)) {
return nil, fmt.Errorf("share: payload [%d..%d] out of arena (%d)",
s.PayloadOffset, end, len(arena))
}
return arena[s.PayloadOffset : s.PayloadOffset+s.PayloadLen], nil
}
// Validate checks the envelope-level invariants. Payload validation
// is the verifier's job (each protocol verifies its own payload).
func (s Share) Validate(set *ParticipantSet) error {
if set == nil {
return errors.New("share: nil participant set")
}
if s.CeremonyID != set.CeremonyID {
return errors.New("share: ceremony id does not match participant set")
}
if s.ParticipantID >= uint32(len(set.Members)) {
return fmt.Errorf("share: participant %d >= set size %d",
s.ParticipantID, len(set.Members))
}
if s.Round == 0 {
return errors.New("share: round 0 reserved")
}
if s.PayloadLen == 0 {
return errors.New("share: empty payload")
}
return nil
}