The Stage-1 allocate gate was owner-gated in LOGIC (pinning.IsOwner over the
block's threaded proposer) but UNENFORCEABLE by a verifying node: a Lux block
carries no verifiable proposer, and resolving one (GetValidatorSet) can be a
network call, so the gate could not run inside the pure, local block apply. The
chain was therefore unsafe for >1 validator.
Make ownership self-attested and cryptographically verifiable:
- AllocateTx gains Signer/SignerScheme/SignerPubKey/Sig + Epoch/Nonce/Fingerprint.
Sig is an ML-DSA signature (FIPS 204, the SAME scheme the validator NodeID
derives from) over the SP 800-185-framed bytes Range|Count|Epoch|Nonce|
Fingerprint. The owning proposer signs at BuildBlock with its staking key.
- Verify (applyAllocate) is pure + local: recompute owner = pinning.Owner(R,
V@epoch); require Signer == owner; re-derive the NodeID from the carried public
key (ids.DeriveMLDSA — the NodeID is a SHAKE256-384 commitment to the key) and
require it to equal Signer; ML-DSA-verify the signature. Any failure rejects the
whole block (fail closed).
- Validator-set locality: GetValidatorOutput carries no ML-DSA key, so Verify
binds the key via the NodeID commitment instead of a set lookup, and uses the
EpochFingerprint carried in the tx (recomputed from the local snapshot) as the
DESIGN 6.4 guard — the network call (set resolution) happens in BuildBlock,
never in Verify.
A non-owner's forged allocate is unverifiable by construction: it cannot sign as
the owner (no key), and claiming the owner's NodeID with a foreign key fails the
re-derivation. This is the property that replaces raft's serialized writer.
Tests (real ML-DSA keys, production NodeID derivation, no bypass): owner-signed
accepted + contiguous/monotonic; non-owner-with-VALID-key rejected; forged sig
rejected; foreign-key-for-owner-NodeID rejected; tampered tx rejected; epoch and
fingerprint mismatch rejected; verifier accepts owner-signed block regardless of
relay; state-root/manifest tests stay green. Full schain suite green under -race.
Replaces raft's serialization role for volume/fileId allocation with
owner-gated VM state riding native Lux consensus. The HRW owner of a range
is the only emitter of allocate mutations, enforced at block Verify — no
leader election.
- txs: AllocateTx{Range,Count} mirrors PutManifestTx codec (type byte + JSON);
Verify rejects empty range / zero count. Owner gate is block-level, not in
the tx (a tx cannot see the block's validator set).
- state: per-range counter alloc/<range> (GetAlloc/SetAlloc), folded into the
SHAKE256 state Root() alongside manifests via rootPrefixes; domain bumped to
SCHAIN_STATE_ROOT_V2 so the root covers allocator divergence.
- vm: applyAllocate increments alloc/<range> by Count, reserving [base,base+Count)
— pure function of committed state, deterministic, version-layer only, no I/O.
Owner gate: pinning.IsOwner(range, proposer, members) in ProcessBlock; a
non-owner (or no-validator-set) allocate fails the whole block. ProcessBlock
now Aborts staged state first so read-modify-write applies over COMMITTED
state (idempotent across BuildBlock+Verify re-application).
- BlockContext{Members,Proposer,Epoch} threads the validator set + proposer as
explicit deterministic inputs (no network I/O in Verify). BlockContextBuilder
is the seam to the real consensus runtime (master-cutover stage); empty
context = M0/no-allocate path, AllocateTx fails closed.
Tests (green, no go.sum bypass):
- owner allocates: contiguous + monotonic across blocks ([0,8) then [8,13)).
- non-owner rejected: proposer-side (BuildBlock) and verifier-side (Verify) →
errNonOwnerAllocate; counter untouched; nothing leaks to durable base.
- empty validator set: fails closed (errNoValidatorSet).
- parallel distinct ranges: independent owners allocate independently.
- same range: serializes through its one owner.
- state root covers alloc: counter change moves Root(); Verify rejects a
tampered claimed root (errStateRootMismatch).
Fork dexvm's commit discipline into a new storage VM (VMID 'schain'): dual-DB
(durable zapdb base + per-block versiondb), pure-deterministic ProcessBlock, ONE
CommitBatch at Accept (mirrors dexvm/vm.go:1194, block.go:159). M0 op: PutManifest
{bucket,object,fileIds,size,etag} staged in the version layer, durable only after
Accept; GetManifest reads committed state. Proves the on-chain-metadata / off-chain-
blob architecture for an embedded decentralized S3 'S-chain'.
schain_test.go (real on-disk zapdb.New): manifest absent in base DB before Accept,
present after — versiondb→CommitBatch proven end-to-end; +Reject-aborts-staged and
tx-codec round-trip. go vet + -race clean.
M1 notes (in-code): add a manifest STATE ROOT before multi-validator consensus
(dexvm computeStateRoot pattern); keep any future cross-chain blob-ref leg separate
from the manifest commit; add genesis/config. Next: wire the S3 Sig-V4 gateway
(PUT→needle off-chain + PutManifest on-chain; GET→manifest+needle stream).