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.
157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package block
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"time"
|
|
|
|
"github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/node/staking"
|
|
)
|
|
|
|
// proposerSigCtx domain-separates proposervm block signatures (FIPS 204 §5.2)
|
|
// from every other ML-DSA signature a validator makes (UTXO auth, consensus
|
|
// votes), so a proposer signature can never be replayed as another protocol's
|
|
// message. The classical (ECDSA) path gets its cross-chain separation from the
|
|
// chainID bound into the signed header.
|
|
var proposerSigCtx = []byte("lux-proposervm-block-v1")
|
|
|
|
func BuildUnsigned(
|
|
parentID ids.ID,
|
|
timestamp time.Time,
|
|
pChainHeight uint64,
|
|
epoch Epoch,
|
|
blockBytes []byte,
|
|
) (SignedBlock, error) {
|
|
// No identity, no signature: the block bytes are just the unsigned body. This
|
|
// is the pre-fork→post-fork transition block and every K=1 block (no proposer
|
|
// window), so small local nets never exercise a signed path.
|
|
unsignedBytes := buildUnsignedBuffer(parentID, encodeTimestamp(timestamp), pChainHeight, epoch, nil, blockBytes)
|
|
|
|
block := &statelessBlock{}
|
|
if err := block.initialize(unsignedBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
return block, nil
|
|
}
|
|
|
|
// Build produces a CLASSICAL (secp256k1/ECDSA TLS-leaf) signed block: identity
|
|
// slot = [schemeSecp256k1 | cert DER], signature = ECDSA over SHA256(header).
|
|
// Used on chains whose validator set is keyed by classical NodeIDs.
|
|
func Build(
|
|
parentID ids.ID,
|
|
timestamp time.Time,
|
|
pChainHeight uint64,
|
|
epoch Epoch,
|
|
cert *staking.Certificate,
|
|
blockBytes []byte,
|
|
chainID ids.ID,
|
|
key crypto.Signer,
|
|
) (SignedBlock, error) {
|
|
return buildSigned(parentID, timestamp, pChainHeight, epoch,
|
|
schemeSecp256k1, cert.Raw, blockBytes, chainID,
|
|
func(headerBytes []byte) ([]byte, error) {
|
|
// ECDSA signs the digest; CheckSignature re-hashes the header to match.
|
|
return key.Sign(rand.Reader, hash.ComputeHash256(headerBytes), crypto.SHA256)
|
|
})
|
|
}
|
|
|
|
// BuildMLDSA produces a strict-PQ (ML-DSA-65, FIPS 204) signed block: identity
|
|
// slot = [schemeMLDSA65 | raw ML-DSA-65 pubkey], signature = ML-DSA over the raw
|
|
// header with the proposervm domain-separation context. The block's Proposer()
|
|
// NodeID derives via DeriveMLDSA, so it EQUALS the strict-PQ NodeID the windower
|
|
// elected — the fix for the height≥2 "unexpected proposer" verify failure.
|
|
func BuildMLDSA(
|
|
parentID ids.ID,
|
|
timestamp time.Time,
|
|
pChainHeight uint64,
|
|
epoch Epoch,
|
|
mldsaPub []byte,
|
|
blockBytes []byte,
|
|
chainID ids.ID,
|
|
key *mldsa.PrivateKey,
|
|
) (SignedBlock, error) {
|
|
return buildSigned(parentID, timestamp, pChainHeight, epoch,
|
|
schemeMLDSA65, mldsaPub, blockBytes, chainID,
|
|
func(headerBytes []byte) ([]byte, error) {
|
|
return key.SignCtx(rand.Reader, headerBytes, proposerSigCtx)
|
|
})
|
|
}
|
|
|
|
// buildSigned is the one signed-block core: it composes the [scheme|identity]
|
|
// proposer slot, encodes the unsigned body, derives the block ID, builds the
|
|
// (chainID, parentID, blockID) header, calls the scheme's signer over it, and
|
|
// appends the signature buffer. Build / BuildMLDSA differ ONLY in the scheme tag,
|
|
// the identity bytes, and the sign closure — DRY across primitives.
|
|
func buildSigned(
|
|
parentID ids.ID,
|
|
timestamp time.Time,
|
|
pChainHeight uint64,
|
|
epoch Epoch,
|
|
scheme byte,
|
|
identity []byte,
|
|
blockBytes []byte,
|
|
chainID ids.ID,
|
|
sign func(headerBytes []byte) ([]byte, error),
|
|
) (SignedBlock, error) {
|
|
idSlot := make([]byte, 0, 1+len(identity))
|
|
idSlot = append(idSlot, scheme)
|
|
idSlot = append(idSlot, identity...)
|
|
|
|
unsignedBytes := buildUnsignedBuffer(parentID, encodeTimestamp(timestamp), pChainHeight, epoch, idSlot, blockBytes)
|
|
|
|
// The block ID is the hash of the unsigned prefix; the proposer signs a
|
|
// header binding (chainID, parentID, blockID).
|
|
id := hash.ComputeHash256Array(unsignedBytes)
|
|
|
|
header, err := BuildHeader(chainID, parentID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sig, err := sign(header.Bytes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
signedBytes := concat(unsignedBytes, buildSigBuffer(sig))
|
|
|
|
block := &statelessBlock{}
|
|
if err := block.initialize(signedBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
return block, nil
|
|
}
|
|
|
|
func BuildHeader(
|
|
chainID ids.ID,
|
|
parentID ids.ID,
|
|
bodyID ids.ID,
|
|
) (Header, error) {
|
|
return &statelessHeader{
|
|
Chain: chainID,
|
|
Parent: parentID,
|
|
Body: bodyID,
|
|
bytes: buildHeaderBuffer(chainID, parentID, bodyID),
|
|
}, nil
|
|
}
|
|
|
|
// BuildOption the option block
|
|
// [parentID] is the ID of this option's wrapper parent block
|
|
// [innerBytes] is the byte representation of a child option block
|
|
func BuildOption(
|
|
parentID ids.ID,
|
|
innerBytes []byte,
|
|
) (Block, error) {
|
|
block := &option{}
|
|
if err := block.initialize(buildOptionBuffer(parentID, innerBytes)); err != nil {
|
|
return nil, err
|
|
}
|
|
return block, nil
|
|
}
|