mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
87cb72352a |
schain: an authorization is spent once, and a block commits what it contains
An AllocateTx's nonce was covered by the owner's signature and compared to nothing. Every gate it passed the first time passed again for the identical bytes: the epoch was still current, the fingerprint still matched, the signer was still the range owner, the ML-DSA signature still verified. Anyone who saw an owner's allocate could resubmit it and consume the range's id space again, for free, as often as they liked. A range now carries the nonce of the allocation that last moved it, in the same key as its cursor, and takes only a strictly greater one. The state root covers it, so a chain cannot disagree about what has been spent and share a root. Accept committed whatever was staged. Every apply starts by dropping the version layer, so the layer belongs to whichever block was processed LAST — verify a sibling between a block's Verify and its Accept and the commit carried the sibling's writes under this block's id. Accept re-applies its own transactions from committed state, commits, and only then advances the chain in memory, under the lock it was mutating without. Block position is now checked before block content: the parent must be a block this node holds, the height must be parent+1, and the timestamp must not rewind or run past a skew ceiling floored at the parent. None of it was checked, so a block could claim any height — and the height it claimed became the chain's height and part of every state root computed after it. A proposer now leaves out what it cannot build rather than failing the whole build: one unauthorized allocate anyone could submit used to drain the mempool and destroy every manifest queued behind it. A verifier still refuses the whole block, which is the half that carries the safety property. Also: the height lookup reads an index instead of scanning the block map and returning whichever entry map iteration reached first; abandoned blocks are released; a rejected block leaves the index; boot no longer panics on a nil logger when the manager supplies no runtime; and the empty-set check in the owner gate is gone — pinning.Owner already answers that question, and answers the wider one the length check missed. Coverage 69.9% -> 97.9%, with 32 guards mutation-checked. The stale premise in SECURITY_review.md is recorded at its head: it reviewed a checkout that did not contain this VM. |
||
|
|
6552bd69cd |
schain: Stage 2 — cryptographically verifiable signed pinned-writer (deletes raft's serialized writer)
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. |
||
|
|
64163f8e02 |
schain: Stage 1 — leaderless pinned-writer allocator (AllocateTx + owner gate)
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).
|
||
|
|
929bf86938 |
schain: M1 — manifest state root (multi-validator safety) + S3 object split
PART A — manifest STATE ROOT (the >1-validator prerequisite M0 omitted):
- state.Root() hashes the committed manifest keyspace deterministically via the
zapdb prefix iterator NewIteratorWithStartAndPrefix(nil, prefixManifest),
folding each entry length-prefixed in lexicographic key order. Mirrors
dexvm/state/state.go:395 StateHash, narrowed to the one keyspace.
- VM.computeStateRoot folds blockHash+height with state.Root() (dexvm/vm.go:1260);
ProcessBlock stamps BlockResult.StateRoot (dexvm/vm.go:584).
- The root travels in the block HEADER (new 32B field after parentID; block id
commits to it). Block.Verify recomputes it and REJECTS a mismatch
(errStateRootMismatch) + Aborts staged writes — the safety gate dexvm computed
but never compared.
- Tests: determinism across write order, change-sensitivity (object set / etag /
size / fileIds), Verify-rejects-tampered-root.
PART B — S3 OBJECT path, on-chain-metadata / off-chain-blob split, end to end
(schain/object/):
- Store.PutObject streams the blob to a Volume (OFF chain) -> fid, then commits
only PutManifest{bucket,object,[fid],size,etag} to the VM through a real block
(ON chain). GetObject reads the manifest back and reconstructs from the volume.
- object.Volume is the seam; MemVolume satisfies it for M1 with a fid shape
mirroring hanzo/s3 needle.FileId and etag = base64(md5(blob)) (= ContentMd5).
- M2 PLUG POINT (object/volume.go): swap MemVolume for a github.com/hanzoai/s3
s3/operation.SubmitFiles adapter; nothing in the VM or object.Store changes.
- Test proves blob bytes appear in NO accepted block, the block stays
manifest-small, the manifest IS on chain, and GET byte-reconstructs.
luxfi packages only; zapdb-native; no protobuf in state; SDKROOT needed on macOS.
|
||
|
|
0a20786095 |
schain: M0 storage-chain VM skeleton (manifest → zapdb through a real block)
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).
|