mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
274 lines
8.5 KiB
Go
274 lines
8.5 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package block
|
|
|
|
import (
|
|
stdbytes "bytes"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/node/staking"
|
|
"github.com/luxfi/zap"
|
|
)
|
|
|
|
var (
|
|
_ SignedBlock = (*statelessBlock)(nil)
|
|
|
|
errUnexpectedSignature = errors.New("signature provided when none was expected")
|
|
errInvalidCertificate = errors.New("invalid certificate")
|
|
errUnknownProposerScheme = errors.New("proposervm block: unknown proposer identity scheme")
|
|
errMLDSAProposerSigInvalid = errors.New("proposervm block: ML-DSA proposer signature invalid")
|
|
errNonCanonicalWire = errors.New("proposervm block: non-canonical wire encoding")
|
|
)
|
|
|
|
// Proposer-identity scheme tags stored as the FIRST byte of the offCert slot,
|
|
// so a block is self-describing about which primitive proves its proposer.
|
|
// They are the SAME bytes as the canonical wire NodeID scheme (ids.NodeIDScheme):
|
|
// 0x90 classical secp256k1/ECDSA TLS leaf, 0x42 strict-PQ ML-DSA-65. Keeping the
|
|
// tag in the block means a verifier dispatches to the correct verifier + the
|
|
// correct NodeID derivation WITHOUT consulting the chain profile — the block
|
|
// carries its own identity discriminator, exactly like a TypedNodeID does on the
|
|
// handshake. The proposer NodeID a signed block reports MUST equal the NodeID the
|
|
// windower elected; the windower reads the P-chain validator set, whose NodeIDs
|
|
// are ids.NodeIDFromCert (classical) or DeriveMLDSA(ids.Empty, pub) (strict-PQ) —
|
|
// so the two branches below reproduce exactly those two derivations.
|
|
const (
|
|
schemeSecp256k1 = byte(ids.NodeIDSchemeSecp256k1) // 0x90
|
|
schemeMLDSA65 = byte(ids.NodeIDSchemeMLDSA65) // 0x42
|
|
)
|
|
|
|
// Epoch represents a P-Chain epoch for validator set coordination
|
|
type Epoch struct {
|
|
PChainHeight uint64 `json:"pChainHeight"`
|
|
Number uint64 `json:"number"`
|
|
StartTime int64 `json:"startTime"`
|
|
}
|
|
|
|
type Block interface {
|
|
ID() ids.ID
|
|
ParentID() ids.ID
|
|
Block() []byte
|
|
Bytes() []byte
|
|
|
|
initialize(bytes []byte) error
|
|
verify(chainID ids.ID) error
|
|
}
|
|
|
|
type SignedBlock interface {
|
|
Block
|
|
|
|
PChainHeight() uint64
|
|
PChainEpoch() Epoch
|
|
Timestamp() time.Time
|
|
|
|
// Proposer returns the ID of the node that proposed this block. If no node
|
|
// signed this block, [ids.EmptyNodeID] will be returned.
|
|
Proposer() ids.NodeID
|
|
|
|
// HasClassicalProposer reports whether this block carries a CLASSICAL
|
|
// (secp256k1/ECDSA) proposer identity. A strict-PQ chain refuses such a block
|
|
// — its proposer must be ML-DSA-65 to match the ML-DSA-keyed validator set.
|
|
// Unsigned blocks (transition / single-validator) return false.
|
|
HasClassicalProposer() bool
|
|
|
|
// Data Availability fields (v1.1 spec)
|
|
DARoot() [32]byte // Root of DA commitments
|
|
WitnessRoot() [32]byte // Root of witnesses/proofs
|
|
MessagesOutRoot() [32]byte // Root of outgoing cross-chain messages
|
|
BlobCount() uint32 // Number of DA blobs in block
|
|
}
|
|
|
|
// statelessBlock is zap-backed: msg is the unsigned body buffer (a
|
|
// self-delimiting zap message with blkSigned at offset 0), Signature is the
|
|
// optional proposer signature carried in the appended suffix buffer. All block
|
|
// fields are read from msg via fixed offsets — the struct IS the wire.
|
|
type statelessBlock struct {
|
|
msg *zap.Message // unsigned body buffer
|
|
Signature []byte // proposer signature (appended suffix); exported for tests
|
|
|
|
id ids.ID
|
|
timestamp time.Time
|
|
scheme byte // proposer-identity scheme (0 for unsigned)
|
|
cert *staking.Certificate // set for the classical (secp256k1) scheme
|
|
mldsaPub *mldsa.PublicKey // set for the strict-PQ (ML-DSA-65) scheme
|
|
proposer ids.NodeID
|
|
bytes []byte // full signed bytes = unsigned ‖ sig
|
|
}
|
|
|
|
func (b *statelessBlock) ID() ids.ID {
|
|
return b.id
|
|
}
|
|
|
|
func (b *statelessBlock) ParentID() ids.ID {
|
|
return ids.ID(read32(b.msg.Root(), offParentID))
|
|
}
|
|
|
|
func (b *statelessBlock) Block() []byte {
|
|
return b.msg.Root().Bytes(offBlock)
|
|
}
|
|
|
|
func (b *statelessBlock) Bytes() []byte {
|
|
return b.bytes
|
|
}
|
|
|
|
// initialize binds the block's fields from its signed bytes. This is the one
|
|
// bytes→fields entry, shared by the Build* constructors (fresh buffer) and
|
|
// Parse (wire/disk buffer); the bytes are authoritative and never re-encoded.
|
|
func (b *statelessBlock) initialize(bytes []byte) error {
|
|
b.bytes = bytes
|
|
|
|
// The signed form is unsigned_buffer ‖ sig_buffer (both self-delimiting).
|
|
// Split on the leading message length; the ID is the hash of that prefix.
|
|
n, err := zapLen(bytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
msg, err := zap.Parse(bytes[:n])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.msg = msg
|
|
b.id = hash.ComputeHash256Array(bytes[:n])
|
|
|
|
if len(bytes) > n {
|
|
sigBytes := bytes[n:]
|
|
sigMsg, err := zap.Parse(sigBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s := sigMsg.Root().Bytes(offSig); len(s) > 0 {
|
|
b.Signature = append([]byte(nil), s...)
|
|
}
|
|
// The signature suffix is outside the block ID, so accepting multiple
|
|
// encodings of it would let a peer persist and re-serve arbitrary bytes
|
|
// under somebody else's valid block ID. Rebuild the only canonical form
|
|
// and require byte-for-byte equality; this also rejects trailing data.
|
|
if !stdbytes.Equal(sigBytes, buildSigBuffer(b.Signature)) {
|
|
return errNonCanonicalWire
|
|
}
|
|
}
|
|
|
|
root := b.msg.Root()
|
|
b.timestamp = decodeTimestamp(root.Int64(offTimestamp))
|
|
|
|
// Proposer-identity slot: [scheme:1B | identity]. Empty ⇒ unsigned block.
|
|
idSlot := root.Bytes(offCert)
|
|
if len(idSlot) == 0 {
|
|
if len(bytes) != n {
|
|
return errNonCanonicalWire
|
|
}
|
|
return nil
|
|
}
|
|
b.scheme = idSlot[0]
|
|
identity := idSlot[1:]
|
|
switch b.scheme {
|
|
case schemeMLDSA65:
|
|
// Strict-PQ: identity is the raw ML-DSA-65 public key. Derive the proposer
|
|
// NodeID the SAME way the node derives its own canonical NodeID and the
|
|
// windower reads it — DeriveMLDSA over the empty chain id (node.go boots with
|
|
// DeriveNodeID(ids.Empty)). This is what makes Proposer() == the windower's
|
|
// elected NodeID under strict-PQ.
|
|
pub, err := mldsa.PublicKeyFromBytes(identity, mldsa.MLDSA65)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errInvalidCertificate, err)
|
|
}
|
|
b.mldsaPub = pub
|
|
nodeID, _, err := ids.NodeIDSchemeMLDSA65.DeriveMLDSA(ids.Empty, identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.proposer = nodeID
|
|
case schemeSecp256k1:
|
|
// Classical: identity is the DER TLS cert. NodeID = hash of the cert, the
|
|
// legacy upstream derivation used on non-strict-PQ chains.
|
|
b.cert, err = staking.ParseCertificate(identity)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", errInvalidCertificate, err)
|
|
}
|
|
b.proposer = ids.NodeIDFromCert(&ids.Certificate{
|
|
Raw: b.cert.Raw,
|
|
PublicKey: b.cert.PublicKey,
|
|
})
|
|
default:
|
|
return fmt.Errorf("%w: 0x%02x", errUnknownProposerScheme, b.scheme)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *statelessBlock) verify(chainID ids.ID) error {
|
|
if b.cert == nil && b.mldsaPub == nil {
|
|
if len(b.Signature) > 0 {
|
|
return errUnexpectedSignature
|
|
}
|
|
return nil
|
|
}
|
|
|
|
header, err := BuildHeader(chainID, b.ParentID(), b.id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
headerBytes := header.Bytes()
|
|
|
|
if b.mldsaPub != nil {
|
|
// FIPS 204 §5.2 domain-separated verification: the same proposervm context
|
|
// the signer bound, so a proposer signature can never be replayed as any
|
|
// other ML-DSA message (UTXO auth, consensus vote) the validator produces.
|
|
if !b.mldsaPub.VerifySignatureCtx(headerBytes, b.Signature, proposerSigCtx) {
|
|
return errMLDSAProposerSigInvalid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return staking.CheckSignature(
|
|
b.cert,
|
|
headerBytes,
|
|
b.Signature,
|
|
)
|
|
}
|
|
|
|
func (b *statelessBlock) PChainHeight() uint64 {
|
|
return b.msg.Root().Uint64(offPChainHt)
|
|
}
|
|
|
|
func (b *statelessBlock) PChainEpoch() Epoch {
|
|
root := b.msg.Root()
|
|
return Epoch{
|
|
PChainHeight: root.Uint64(offEpochHt),
|
|
Number: root.Uint64(offEpochNum),
|
|
StartTime: root.Int64(offEpochStart),
|
|
}
|
|
}
|
|
|
|
func (b *statelessBlock) Timestamp() time.Time {
|
|
return b.timestamp
|
|
}
|
|
|
|
func (b *statelessBlock) Proposer() ids.NodeID {
|
|
return b.proposer
|
|
}
|
|
|
|
func (b *statelessBlock) HasClassicalProposer() bool {
|
|
return b.scheme == schemeSecp256k1
|
|
}
|
|
|
|
func (b *statelessBlock) DARoot() [32]byte {
|
|
return read32(b.msg.Root(), offDARoot)
|
|
}
|
|
|
|
func (b *statelessBlock) WitnessRoot() [32]byte {
|
|
return read32(b.msg.Root(), offWitnessRoot)
|
|
}
|
|
|
|
func (b *statelessBlock) MessagesOutRoot() [32]byte {
|
|
return read32(b.msg.Root(), offMsgOutRoot)
|
|
}
|
|
|
|
func (b *statelessBlock) BlobCount() uint32 {
|
|
return b.msg.Root().Uint32(offBlobCount)
|
|
}
|