Documentation
¶
Overview ¶
Package ownership is the ONE canonical definition of the NFT-ownership entitlement seam: M-Chain (mpcvm) threshold-signs the fact that an address holds a specific token of a specific collection and has bound that token to a specific node; a node's chain manager verifies that signature before it activates an entitlement-gated chain.
The collection lives on an external EVM chain (the Lux Genesis collection is an ERC-721 on Ethereum mainnet). Nothing here moves, wraps or mirrors the token — the claim is a statement ABOUT the token, signed by a quorum, so the token stays the single source of truth and the attestation stays revocable and re-issuable. The reader of an attestation never touches Ethereum.
Why this package exists rather than the encoding living in mpcvm: a node must verify an attestation WITHOUT importing the M-Chain VM, because M is a plugin-only VM that luxd must never link in-process. So the digest is a value seam, not a place — a pure function of the claim's field values, in one small package that both the M-Chain VM and the node import. Never duplicated, never able to drift.
The package is deliberately dependency-free (stdlib + luxfi/crypto/secp256k1), so importing it costs a node nothing.
The domain tag, field order, digest layout and signature encoding are FROZEN. The KAT vectors in ownership_test.go pin the bytes, so drift on either surface trips a test.
Index ¶
Constants ¶
const Domain = "nft/ownership"
Domain is the attestation domain this package defines. mpcvm registers it in its domain-separator registry under exactly this name.
const DomainTag = "LUX:QuantumAttest:nft/ownership:v1"
DomainTag is the domain-separation byte string hashed into every payload, so an ownership attestation can never be replayed as any other message M signs (oracle reads/writes, session-complete, epoch beacons, bridge transfers). It follows the registry's own convention, LUX:QuantumAttest:<domain>:v1, and is written raw (no length prefix) as the first bytes of the preimage.
mpcvm MUST register this exact byte string for Domain; the shared constant is what makes that impossible to get wrong.
Variables ¶
var ( // ErrNoQuorum means fewer parties signed than the key's policy requires, so // the signature cannot represent the committee's decision. ErrNoQuorum = errors.New("ownership: signer count below quorum") // ErrNoKey means no group key was supplied, so nothing can be verified. ErrNoKey = errors.New("ownership: no group key") // ErrBadSignature means the threshold signature does not verify against the // group key over this claim's domain-bound payload. ErrBadSignature = errors.New("ownership: signature does not verify") // ErrWrongNode means the attestation is authentic but authorizes a DIFFERENT // node. This is the reason a node may not borrow a peer's entitlement. ErrWrongNode = errors.New("ownership: attestation is bound to a different node") )
Functions ¶
func Payload ¶
Payload is the preimage a quorum signs. It MUST stay byte-identical to mpcvm.ComputeAttestationPayload for this domain — that function hashes the domain separator, then the subject, then the commitment root, then the epoch big-endian, and so does this.
func Subject ¶ added in v1.7.17
Subject is the attestation's SubjectID: WHO is entitled, as opposed to Root's WHAT was verified. It is the node's 20 NodeID bytes, left-aligned in 32.
Keying the subject on the node is what makes equivocation meaningful: two attestations in this domain with the same subject and epoch but different roots are two conflicting ownership facts about one node, which is exactly the condition mpcvm's DetectEquivocation slashes.
It is a package function as well as a method because a lookup starts from a node, before any claim is in hand. One encoding, both directions.
Types ¶
type Attestation ¶
type Attestation struct {
Claim Claim `json:"claim"`
// Epoch binds the attestation in time; it is part of the signed payload.
Epoch uint64 `json:"epoch"`
// Signers is how many parties participated in the ceremony.
Signers int `json:"signers"`
// Quorum is the signing policy's K — the number of parties required. It
// travels with the attestation because a bare signature cannot say what
// threshold produced it.
Quorum int `json:"quorum"`
// Signature is secp256k1 r(32)||s(32)||v(1), the encoding every M-Chain
// ceremony artifact uses. r||s (64) is also accepted.
Signature []byte `json:"signature"`
// GroupKey is the compressed secp256k1 group public key. A verifier must
// obtain this from consensus state, never from the party being authorized —
// see the caller-side note on Authorizes.
GroupKey []byte `json:"groupKey"`
}
Attestation is a quorum's signature over a Claim plus everything a verifier needs, so verification is zero-interaction: no callback to M-Chain, no Ethereum read, no network at all.
func (*Attestation) Authorizes ¶
func (a *Attestation) Authorizes(node [20]byte) error
Authorizes reports whether this attestation authorizes [node]. It answers authenticity AND binding in ONE call, and there is deliberately no exported way to ask only one of them: a caller that could check the signature and forget the binding would let any node activate on a peer's entitlement.
Authenticity here means "a quorum of the holders of GroupKey signed this claim". It does NOT mean GroupKey is the right key — the caller must read GroupKey from consensus state the operator cannot edit. An attestation verified against an operator-supplied key proves nothing, because the operator can sign anything.
type Claim ¶
type Claim struct {
// Chain is the EVM chain id the collection lives on (1 = Ethereum mainnet).
Chain uint64 `json:"chain"`
// Collection is the ERC-721 contract address.
Collection [20]byte `json:"collection"`
// Token is the tokenId. For the Lux Genesis collection the token IS the
// validator slot.
Token uint64 `json:"token"`
// Owner is ownerOf(Token) as read at Block.
Owner [20]byte `json:"owner"`
// Node is the luxd node this entitlement binds to — the raw 20 bytes of its
// NodeID. The node proves nothing at verification time: it simply IS this
// identity, because a NodeID is a hash of the staking certificate it must
// hold to be that node at all.
Node [20]byte `json:"node"`
// Block is the block height the ownership read was taken at, so a claim is
// pinned to a point in the collection's history rather than "now".
Block uint64 `json:"block"`
}
Claim is the fact a quorum signs. Field layout is FROZEN so the digest is canonical across the M-Chain VM, every node, and any future non-Go verifier.
One attestation over one Claim authorizes exactly one node — on the strength of exactly one token, of one collection, on one chain, as read at one block — and nothing else.
func (Claim) Root ¶
Root is the canonical commitment to a claim — the CommitmentRoot an M-Chain QuantumAttestation carries.
sha256( "LUX:QuantumAttest:nft/ownership:v1" // ASCII tag, raw || uint64_BE(Chain) // 8 || Collection[20] // 20 || uint64_BE(Token) // 8 || Owner[20] // 20 || Node[20] // 20 || uint64_BE(Block) // 8 )