Commit Graph
334 Commits
Author SHA1 Message Date
zeekay 07d6f6a9bb docs: chain routes are /v1/chain
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181):

  /v1/bc/<alias>/...   -> /v1/chain/<alias>/...
  /ext/bc/<alias>/...  -> /v1/chain/<alias>/...

Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
2026-08-30 17:43:25 -07:00
zeekayandHanzo Dev e41949ffc0 the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:48:08 -07:00
zeekayandHanzo Dev a442eedf98 the standalone venue can be told which chain it is
dexd run could not start. The VM refuses to found a chain from a creation
document it was not given — correctly, since a genesis the binary derived
is one no other node would derive — and standalone called Initialize with
no document at all, so every invocation ended at that refusal. The mode
the binary's own help describes was unreachable.

It takes --genesis now: under luxd the document arrives as
InitializeRequest.GenesisBytes, and standalone has no P-Chain to read it
from, so an operator names it. The bytes are opaque; what matters is that
somebody chose them, so two nodes told to be the same chain are.

The surface also mounted a segment twice. httpHandlers returns keys that
already carry the chain's namespace — "/dex/<method>", the same segment a
node serves under /v1/bc/<D>/ — so a prefix of "/v1/dex" produced
/v1/dex/dex/<method>: a path no client written against a node would try,
and not the one the flag promises. The prefix supplies the version now.

Verified by running it: the venue starts against a document and answers
dex_get_markets and dex_get_trades at /v1/dex/<method>.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:48:07 -07:00
zeekayandHanzo Dev 91a588ffc7 sdk/ts: describe the surface that exists, not the one it was written against
The clean-room posture is worth keeping; the surface it described had moved.

Settlement is 0x9999, which carries the V4 PoolManager ABI this path already
encodes. The LXPool precompile at 0x9010 it pointed to is deprecated —
eth_getCode returns empty for it on mainnet, so the calls it built went to an
address with no code behind it.

FIX is FIXT.1.1 at the session layer and FIX.5.0SP2 (EP307) at the application
layer. pkg/fix asserts Parse errors on 8=FIX.4.4 and 8=FIX.4.2, so the 4.4
profile advertised here could not open a session.

The ws subpath now says nothing serves it: pkg/api compiles and is tested, but
NewWebSocketServer and NewVenueWS have no caller outside those tests and no
binary in cmd/ imports the package. A client for a listener nobody started is
worth shipping only if it says so.

zap and the precompile path were checked the same way and both reach something
live.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:48:06 -07:00
zeekayandHanzo Dev 69c401ddae client: drop the unpublished @luxfi/dex-client stub
Two source files behind a package name that was never published, and
nothing in the tree imports it — every reference is the package naming
itself in its own README, manifest and lockfile. The grpc client it
describes is served by @luxfi/trading, which is published and is what
the 33 call sites actually use.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:48:04 -07:00
zeekayandHanzo Dev 522761da1d sdk: drop the @luxfi/dex stub that squats the published name
sdk/typescript declared itself @luxfi/[email protected] with a prepublish script,
but @luxfi/dex on npm is the viem precompile client and its registry
history starts at 2.0.1 — 1.0.0 was never published and nothing here
imports this package. Two codebases claiming one name is a publish
accident waiting to happen, and the axios/ws REST client it holds is not
what that name means any more.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:48:03 -07:00
zeekayandHanzo Dev 3815c14d9c the docs described an ordering the engine does not have
flare.mdx published an OrderTransactions that sorts by priority fee,
highest first. No such function exists, nothing anywhere reads a priority
fee, and the mempool says the opposite in as many words: ordering is
arrival order, and a fee-priority mempool is a future concern the venue
does not have.

That matters more than an ordinary doc error. This repository is public,
and a venue documented as selling position is a different venue from one
where order ids are (height << 32) | txIndex and cannot be bid for. The
page now describes the comparator that runs — price, then the id, FIFO
within a level — and says plainly that fee priority is not it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:48:02 -07:00
zeekay 8d4749c4fb the docs layout imports the face it uses
docs/app/layout.tsx referenced ZenMono without importing it and without @hanzo/font in its dependencies at all, so the docs site could not compile: tsc reports "Cannot find name 'ZenMono'" on the bare tree. Both faces are imported from the package now, and the package is declared. The README hero card still asked for Inter.
2026-08-28 11:28:44 -07:00
zeekay ff0f0c2bc7 fix(type): the binding is named for the face it holds
The docs layout named three different faces in one place: a binding called
`inter` or `geist`, constructed from next/font/google so it actually held Inter,
publishing `--font-geist-sans`, which @hanzo/font does not set. Whichever a
reader trusted, two were wrong.

It takes Zen and ZenMono from @hanzo/font — named `sans` and `mono` for the role
they fill, setting the properties the package publishes.
2026-08-28 09:26:46 -07:00
zeekayandHanzo Dev 1d4ad3b282 Make an unsigned rebind a decided reject, not a chain halt
TxOpenMarket does not require auth and execute() wrote its (base,quote)
binding unconditionally, so any unsigned open_market could rebind a live
market. A maker that rested a sell had locked its base; after a rebind
the settle path spent a different, unlocked asset and returned
ErrInsufficientLocked out of execute() as a block abort — so every later
BuildBlock/Verify failed identically and the chain could not advance past
that height. A market's assets are now decided once: bind when unbound,
no-op an identical rebind, and reject a conflicting one as a decided tx
while the block still succeeds. Separately, the maker leg was read from
the taker-side byte with opposite defaults in makerSettleKey and
decrementMakerReserves, so an out-of-domain side (the byte is uncontrolled
on the wire) made them name different legs; both now read one canonical
makerOf, unchanged on the valid {buy,sell} domain.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.37
2026-08-21 14:09:52 -07:00
zeekayandHanzo Dev 81fe9fa775 Bind authorization scope on the strict-PQ order path
The classical SignedOrder binds ChainID, NetworkID and Expiry so a
captured order cannot replay onto another chain or after it expires.
The strict-PQ order carried none of them, so an ML-DSA-signed order was
good on every chain forever — the exact replay the classical path
already closed. Mirror the three scope fields onto SignedOrderPQ and
fold them into the shared SigningHash, so one digest binds the same
scope under either scheme. Mutating any scope field now breaks the
signature (regression test).

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 10:56:48 -07:00
zeekayandHanzo Dev 33157d5fed dex: an order signature binds the chain, the network, and an expiry
The signing digest covered the order's economic fields but named no chain, no
network, and no deadline, so a signature made for one chain recovered its signer
on every chain and never expired. On a permissionless exchange a signed order
could be lifted onto another D-Chain, or replayed forever.

ChainID, NetworkID and Expiry are packed into SigningHash as the authorization
scope, ahead of the economic fields, in the same fixed-width hand-written layout
so a future field cannot silently drop out of the signature. The D-Chain tx
digest binds the same scope for the same reason: a signature must name the chain
it spends on to spend on only one.

COORDINATED: this changes the order signing digest, so every signer and every
verifier moves together. It is a protocol change, not a lone patch, and the
order-authorization layout belongs in an LP alongside the finality-message spec.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.36
2026-08-21 08:39:10 -07:00
zeekayandHanzo Dev e0a8a27436 dex: a signature covers the size the matcher acts on
SigningHash packed uint64(math.Round(Size*1e8)), so the digest committed to a
rounded tick count while the matcher's crossing loop and RemainingSize read the
float. Two sizes inside one rounding interval produced the SAME digest: a
signature taken over one authorized the other. On a permissionless exchange that
is a signed order whose quantity the holder can change.

An off-tick size is refused rather than snapped. Snapping decides, silently, that
the signer meant a quantity they did not write, and that is the one thing a
signature exists to prevent. Two on-tick sizes differ by at least a tick, so
their digests differ; an off-tick size cannot be signed at all. The collision is
unreachable from either side.

The epsilon absorbs the binary representation error of a decimal quantity and
nothing more — it is orders of magnitude below one tick, so it can never let two
distinct ticks meet.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.35
2026-08-21 01:23:21 -07:00
zeekayandHanzo Dev 247b110b9d dex: bound the GPU book indices, and let a market sell actually cross
The C matcher takes the book pointer and the indices but no book LENGTH, so it
cannot bounds-check, and the Go wrapper did not either. MatchOrderCPU guards the
same field because indices genuinely can be out of range — one field, two
readers, one guard. On a real Metal device that wrote past the end of a Go slice
and answered 420 where the oracle answered 490.

An empty book is the same hole from the other side: &book[0] panicked where the
CPU path returned cleanly, so one build was total and the other fatal on
identical input. Both cases hand off to the oracle rather than restate its bounds
rule here; a second copy of a bounds check is a second chance to get it wrong.

The shadow gate gave every market order the MAXIMAL price, "so it crosses every
resting level". That is true for a market BUY, which sits on the bid and crosses
any ask at or below it. A market SELL sits on the ask and crosses any bid at or
above it, and no bid is ever above +inf — so every market sell matched nothing,
compared empty against empty, and was counted VERIFIED. The mechanism the GPU
path is assured by was measuring nothing for an entire order type, while an
operator watched Verified climb and Divergences stay at zero.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.34
2026-08-21 01:18:02 -07:00
zooqueen 2e25239bd7 deps: consensus v1.36.3 2026-08-20 21:50:30 -07:00
zeekay f82eddfe32 track the name agents read, so a clone documents itself
LLM.md is the source and stays the source; CLAUDE.md is a symlink to it, so there is
one file and it cannot drift from itself. What changes is that the link is committed
rather than left for each checkout to recreate.

Left untracked, the doc was conditional on a step outside the repo: a fresh clone had
no CLAUDE.md, nothing loaded LLM.md, and an agent worked here having never read what
this repo already explains. Most of the estate already tracks theirs; this is the rest
agreeing with them.
2026-08-20 16:36:21 -07:00
zooqueen f950525c0d go 1.26.4 again — hanzos3/go v1.0.2, zapdb v1.10.6
My previous commit here raised this module's go directive from 1.26.4 to 1.26.5.
That was not intended and not needed: `go mod tidy` writes the local toolchain's
version, and a go directive is the MAXIMUM of the module's own and every
dependency's — so hanzos3/go v1.0.1 and zapdb v1.10.4, both of which had picked
up 1.26.5 the same way, dragged this one up with them.

Both are fixed upstream (v1.0.2 back to 1.26.1, v1.10.6 back to 1.26.4), so this
declares 1.26.4 again, which is what it asked for before I touched it. Still zero
minio; build passes.
2026-08-10 17:48:49 -07:00
zooqueen a0ee037dda no minio in the graph
The S3 client moves from hanzos3/go-sdk to hanzos3/go, and zapdb comes up to
v1.10.4.

go-sdk pointed at our checksum forks with `replace`. Go ignores `replace` in a
non-main module, so that redirect applied when you built go-sdk and to none of
its consumers — this repo resolved the real minio packages regardless of how the
arrows were drawn. hanzos3/go requires the forks by name, which does travel, and
zapdb v1.10.4 is the release that stops pulling go-sdk back in on every tidy.

go.mod names zero minio modules. build passes.
2026-08-10 17:24:48 -07:00
zeekayandHanzo Dev 0f70be9c6c dchain: say why the funding rail has no method, where someone would add one
The method table is the only caller of submitTx, submitTx is the only caller of
mempool.Add, and there is no gossip handler — so a tx type absent from this
table cannot exist. TxImport and TxExport are both absent, which is the whole
reason the rail moves nothing today and why every C->D deposit would lock on
activation: C debits into custody, stages a claim, and nothing on D can consume
it.

That is a decision, not an oversight, and it was recorded nowhere near the place
someone would undo it. An import method is not a method — it is an
authorization model. TxImport carries no signature by design, so the ingress is
an unauthenticated, free-to-call, value-moving endpoint, and what stands between
it and a chain halt is that the proposer screens what it cannot prove and that
an already-consumed claim needs no proof. Both are new this week, and neither
had ever run in a test before the block authenticator was switched on.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.31
2026-08-07 21:49:38 -07:00
zeekayandHanzo Dev df71b108f5 dchain: a signature named no chain, so it authorized every chain
txAuthDigest bound the type, the nonce and the whole frozen body. Nothing in
that preimage names a chain, and nothing in a tx body does either. Account16
folds a key to an account with no network in the fold, so one key is the same
account on every D-Chain there is, and the native asset is all-zero on all of
them.

So a signed frame was valid wherever it could be parsed. An export signed on a
devnet is a valid export of the same account's mainnet balance to the same C
address, and anyone who saw the frame can submit it there. The signer authorized
one withdrawal and paid for as many chains as exist. Nothing had to be forged
and no key had to leak — the frame is replayed verbatim.

Put the network and the chain in the preimage. A frame now verifies on exactly
the chain its signer meant and is bytes on every other. This covers every signed
type, not only the export: a signed order was equally portable, and there is one
digest, so there is one place to fix it.

verifyTxSignature takes the scope from the VM, which fixes it at Initialize and
never changes it, so the check stays a pure function of the frame plus a
constant — still runnable at the RPC boundary before the mempool and identically
at consensus.

Account16 is deliberately NOT scoped. Its network-independence is what makes one
key one identity across the stack, and with the signature bound it is no longer
load-bearing for replay. It also keys every genesis allocation, and three chains
are about to be founded on those values.

Test VMs and the test signer now share one scope, because a signer and the chain
it signs for have to agree on it — which is the property.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.30
2026-08-07 21:41:48 -07:00
zeekayandHanzo Dev f4968a0f7c dchain: the import authenticator had never run, and it halted the chain
verifyImports gates on normalOp. No test in the package ever set it, so every
rail test proved its property with the block-level check returning at its first
line: the forged-object rejection, the absent-object rejection and the bootstrap
gate were assertions nothing had executed. Turn normalOp on in the rail harness
and TestRail_ClaimImportsOnce — a test whose whole subject is replay — fails
immediately.

What it was hiding is a permanent halt reachable by an honest party. Delivery is
permissionless by design, so two relayers delivering the same claim, or one
retrying after a timeout, is the steady state. The second delivery is well
formed and carries the real recorded bytes; the object is simply gone from
shared memory, because this chain's own accepted Remove took it. The
authenticator read that as unbacked and rejected the block, Reject requeued
every tx in it, and the next build drained the same delivery and failed the same
way. Measured over four rounds, the chain never left the height it stopped at.

The consumed set is the committed fact that explains the absence, so consult it
first: an import that cannot credit needs no proof. executeImport's replay guard
runs before it decodes or credits anything, so a consumed claim pays out nothing
whatever bytes ride with it.

That is the safety half. The liveness half is that a validator has no lesser
answer than rejecting a block carrying an unprovable import — the proposer's
execution root already contains the credit — which makes proposing one
self-harm. So the proposer screens the same predicate at build and holds what it
cannot prove. Held, not dropped: a claim C has recorded but not yet flushed here
is unprovable now and provable in a moment, and that lag is the rail's known
liveness property, not an invalidity.

One predicate, proveClaim, with two callers doing two different jobs.

Also covers what running the authenticator buys back: forged bytes, an
unrecorded claim id, the bootstrap gate in both positions, an over-export
writing no claim, and commitAtomic's no-batch rule and commit-then-apply order.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.29
2026-08-07 21:27:28 -07:00
zeekayandHanzo Dev d9da4dd317 dchain: a C chain id is not a rail, and the import half thought it was
executeImport refused only when the C chain id was empty. executeExport also
refused when shared memory was nil. The plugin server sets a C chain id
unconditionally and leaves shared memory nil whenever the node wired no atomic
server, which is the state of every live network today — so on the import side
60 self-authored bytes named a beneficiary and an amount and became real,
spendable balance, with no C-side debit anywhere behind them.

verifyImports exists to catch exactly that, and returned nil on the same nil
handle. Its reason was that "executeImport rejects without a C chain id": true,
and never the condition that holds. Both guards keyed on the wrong fact, and the
two holes covered for each other.

Bind both halves to the same condition. Execution refuses the credit without
shared memory; the block that carried the import is rejected rather than
accepted unproven. Either alone is sufficient, which is why there are two. A
block carrying no import is unaffected — the check is per-import, so an absent
rail is not an error, only crossing on one is.

commitAtomic's nil-shared-memory branch asserted this was already unreachable
while committing the state anyway and dropping the op. Now that both producers
really do refuse, make the claim self-enforcing: accumulated ops with nothing to
apply them to stop the node, the same answer an Apply that errors already gets.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.28
2026-08-07 21:19:24 -07:00
zeekayandHanzo Dev dc4ae7c55a dexprotocol: the import path minted money from a struct literal
Import took a bare Claim. Every other value-creating path in this package
already had the unforgeable-value discipline — VerifiedExecution,
VerifiedOrder, AcceptedBlock — and the export side has three storage
domains. The import side, the ONLY path that creates value on D, took an
exported struct with exported fields, so

  Claim{Beneficiary: attacker, Asset: usdc, Amount: 1e30, ...}

returned nil and credited 1e30. Validate() checks that fields are
PRESENT, which is a different question from whether anyone said them.

It survived because permissionless delivery reads as a safety property:
"anyone may deliver a claim, because delivering it can only credit the
account it already names." That holds only if the claim is genuine. The
argument for the feature quietly assumed the check that was missing.

Import now takes a VerifiedClaim, produced only by VerifyClaim, with an
unexported interface method as the sentinel — unexported fields alone are
insufficient because T{} is zero-constructible anywhere. The strongest
statement is not a test: c.Import(Claim{...}) no longer compiles.

The commitment covers the corridor, not just the payment, so an
attestation for one direction cannot be replayed as the other and one for
a different pair of chains cannot be replayed here. Destination is the
verifier's own view, never the witness's claim about itself.

Second, same class: VerifyOrder read `ctx.Nonces != nil && ...`, so a
caller that forgot to wire the nonce set got no replay protection and no
error. Two lines above, a nil Signer fails closed. They agree now — the
quietest way to lose a safety property is to make its absence look like a
configuration choice.

87 tests.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.27
2026-08-07 21:08:21 -07:00
zeekayandHanzo Dev 93543e35b2 docs(llm): the v1.5 -> v1.14 D-Chain bump is a re-genesis, not an upgrade
The two lineages derive different genesis blocks. A fleet that keeps its v1.5
store keeps working, but any node whose store is wiped — or any validator added
later — adopts the v1.14 genesis, disagrees at height 0, and is stranded there
forever with isBootstrapped:false and no error naming the cause.

Found by wiping one devnet validator's D store after the roll: it came back on
genesis mpxP6xF3... while the fleet carried Muetn.... Wiping D on all five
converged them, and a subsequent wipe of one node then re-derived every block to
the fleet's exact root.

A matching height is not enough to verify this — every node reports height 0
either way. Compare lastAccepted.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 21:02:25 -07:00
zeekayandHanzo Dev e2c5f8dd70 dchain: say rail where the code moves value, seam only where it ingests
Co-authored-by: Hanzo Dev <[email protected]>
v1.14.26
2026-08-07 17:44:38 -07:00
zeekayandHanzo Dev 4784571595 dchain: prove the funding path, and prove what it does not do
Three tests against the real VM and real shared memory:

  - a deposit crosses, credits the beneficiary's OWN account, and places NO
    order — asserted as a negative, by counting resting rows across the
    crossing. The beneficiary then trades that money with an ordinary signed
    order, which is the whole reason the crossing is allowed to be this boring.
  - a re-delivered claim credits nothing. The consumed set, not the shared-memory
    Remove, is what makes that true, so a crash between the state commit and the
    apply cannot resurrect it.
  - an export cannot move value without its owner's signature: the constructor
    refuses to build one, the parser refuses the hand-forged frame, and a
    frame signed by someone else moves nothing.

The wire golden is pinned on both sides of the seam against the same strings,
including the id derivation — a drift there does not corrupt value, it loses it:
one side writes the claim under one key and the other looks under another.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:36:42 -07:00
zeekayandHanzo Dev 4a90738c69 dchain: the funding rail carries value, and nothing else
Move once, trade many. Value crosses the C<->D boundary twice in its life; in
between, D owns it and every order, match and trade is a local balance movement.
So the crossing has exactly one job — move ownership exactly once — and the
object shrinks to what that job needs:

    beneficiary(20) | asset(32) | amount(8)   = 60 bytes

Out of the wire: the 49-byte market/side/limit/size tail, the `spent` execution
witness, and the lane byte that told C which trading pot to debit. Out of the
package: the per-order escrow, the per-order ledger account, the proposer-side
drive that scanned shared memory for orders to run, and TxOrderSubmit. An
import now credits the beneficiary's OWN account — the one their signed orders
already draw from — so the money is tradable the moment it lands, with no second
crossing and no order created by the crossing.

TxExport is now signed. It used to draw on a per-order account whose only
possible payee was the escrow's recorded owner, so the object was its own
authority. Draining a real account to a caller-named address is not, so the
body asserts its owner and txWireUser binds that to the recovered signer, the
same way a withdraw does.

One claim id, both directions, both repos: SHA-256(domain | source | dest |
sourceTx | index). It replaces DeriveOrderID and deriveSeamOutputID, which were
two derivations of the same fact.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:18:14 -07:00
zeekayandHanzo Dev c98afc12da dchain: a chain cannot be founded from a document that never arrived
An empty database and an empty creation document was the one combination
the three-way genesis check could not judge. Nothing delivered, nothing
stored, so no view contradicted another and Initialize returned the binary's
own default as the chain's genesis — the exact defect the check exists to
remove, reached through the absence of its inputs rather than their conflict.

A D-Chain's document is the dchain.json recorded in its P-Chain
CreateChainTx and reaches the VM as InitializeRequest.GenesisBytes. An empty
one therefore means the bytes did not arrive, which is a delivery failure,
not a chain that has none. Founding is refused, and the refusal names the
genesis it declined to use so the two cases are distinguishable in a log.

Existing chains are untouched: this fires only when the database holds no
chain at all. A chain already on disk that is handed no document still fails
the stored-vs-expected comparison, as before, and one born under a build that
kept no record still recovers from its own height-0 head.

Nineteen tests founded chains with no document and now supply one; the four
that model chains already on disk deliberately still supply none.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.25
2026-08-07 17:08:20 -07:00
zeekayandHanzo Dev b240160d91 dchain: pin the block root, not just the genesis root
Every determinism test in this package compares ComposeRoot against
ComposeRoot, so both sides move together and a change to the composition
stays green. That is the pattern that let the ledger operand into v1.14.0
unseen. Genesis is now pinned against the written construction; every root
from height 1 on was not pinned at all, which is the same fork one block
later.

TestBlockRootGolden fixes distinct non-zero operands and pins the value,
recomputed on the spec side with a separate keccak so the vector cannot be
brought back into agreement by editing the code it checks. Height is pinned
at two values, and operand swaps are rejected, so a reorder is caught as
surely as an added term.

Injecting an operand into ComposeRoot fails it; so does moving height ahead
of the ledger.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.24
2026-08-07 16:50:31 -07:00
zeekayandHanzo Dev 52905e489a dchain: genesis is its own construction, not a block with an empty parent
Height 0 was built by calling the block composition with the creation
document's digest in the slot that means "parent" everywhere else. The slot
was unused, so the convention held — and nothing enforced it. When the custody
ledger entered ComposeRoot in v1.14.0 every chain's genesis moved with it,
silently, because genesis was downstream of a composition that has nothing to
do with chain identity.

Genesis now has its own domain-separated construction and no longer touches
the block root:

    GenesisDigest = keccak256("dex/genesis/v1"      | init.Genesis)
    GenesisRoot   = keccak256("dex/genesis-root/v1" | digest | book | trade | tx | ledger)
    BlockRoot     = keccak256("dex/block-root/v1"   | parent | book | trade | tx | ledger | height)

Different creation document -> different digest -> different genesis root ->
different root at every descendant height. That is now structural rather than
a property of a field that happens to be free.

GenesisDigest is defined for every input including none, so a running chain
always has one; whether a chain may run without a creation document is a
policy question and stays where it was answered.

The vectors verify the derivation against the written construction, using a
separate keccak and the empty sub-roots read from pinned hex, so they do not
drift by being updated to match. They also hold the three fleet documents to
their on-chain sha256, and pin the genesis against a VM carrying arbitrary
runtime state: same bytes in, same chain out, whatever the binary is doing.

Perturbing the block root now leaves every fleet genesis unmoved; routing
genesis back through the block composition fails the vector.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.23
2026-08-07 16:44:15 -07:00
zeekayandHanzo Dev d338ca0df6 dexprotocol: the money always lands, the order is best effort
The arriving-trader path: bring value across from C and place an order
with it in one D transition. The semantics look wrong until you see the
failure they prevent.

The tempting design makes the pair atomic in the strong sense, so a
trader never ends up funded without the order they wanted. That strands
people. A deadline can expire in transit — C accepts the export at t, D
imports at t+d, and an order whose deadline fell in between is dead on
arrival through nobody's fault. Failing as a unit leaves the value in the
in-flight state with its only consuming transaction permanently invalid,
and the trader has to be rescued by hand.

So the import is unconditional and the order is best effort. Order
failure is DATA, not transaction failure: expired deadline, bad
signature, consumed nonce, wrong asset, insufficient balance all come
back as a Rejected Result carrying a Reason, with the value sitting
available on D — a state the trader can act on alone, by placing another
order or exporting home.

The one real failure is an inadmissible claim. That is not an order
problem; there is nothing to land, so it is an error rather than a
Result.

Two details that follow. A rejected order does not burn the nonce — the
trader never used it, and an unordered nonce is a position they chose; a
test re-signs with the same nonce afterwards and it works. And Cancel
derives its refund from the lots it actually retired, never from a
caller-supplied amount, which is how a cancel comes to refund more than
the order ever committed; with three of ten lots committed to a match,
cancelling refunds seven and leaves the three held.

83 tests, including the full arriving-trader round trip: import and
place, partially trade, cancel the remainder, export home — one boundary
crossing in, one out, everything between it purely local.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.22
2026-08-07 16:40:05 -07:00
zeekayandHanzo Dev 518fdf778c dchain: pin the golden vector to the CreateChainTx, not the config file
The chain-creation documents I pinned came from luxfi/genesis's dchain.json. That
file created none of the three live D-Chains. The bytes in each fleet's
CreateChainTx differ from it: mainnet and testnet carry no chainId at all and end
with a newline (356 bytes); devnet puts chainId first in sorted order and has no
trailing newline (375 bytes — the same length as the config file and not the same
bytes, so a size check passes and proves nothing).

Pinned now: all three documents verbatim, each keyed on its keccak256, recovered
from the CreateChainTx and confirmed byte for byte against every pod's own
/data/genesis.json dChainGenesis. Two independent sources.

Two traps make the document opaque by necessity, which is how the VM already
treats it — hashed raw, never parsed, never re-encoded. The trailing newline is
load-bearing and present on only two of three. The em dash is the literal six
characters —, so a JSON round trip changes both length and hash.

TestGenesisLiveFleets now asserts the defect directly: not one live D-Chain has
the genesis its own creation record implies. All three were born from the binary.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.21
2026-08-07 16:27:40 -07:00
zeekayandHanzo Dev fddf4f85a4 dchain: a head block the chain disowns cannot be its genesis
Recovery blessed meta:headBlock as the genesis without checking the head pointer
named it. On a database where the two disagree that recorded a genesis out of thin
air — the same invention the record exists to prevent, reached by a shorter route.
Assert the head block is the head before trusting it, and record nothing when it
is not.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 16:23:28 -07:00
zeekayandHanzo Dev 47928ea08d dchain: the genesis is chain-creation data, not a property of the binary
The D-Chain derived its height-0 block from compiled-in defaults and threw away
the genesis bytes the node handed it. Two consequences, both live:

  * dex v1.4.2..v1.13.0 and v1.14.0+ derive DIFFERENT genesis blocks. v1.14.0
    (e58e2a3) put the custody ledger into ComposeRoot, taking the keccak preimage
    from 136 to 168 bytes, so the empty-state root moved. lux-mainnet and
    lux-testnet run MuetnVSbs1UnPUDTH5q4kgbKcfRx97DjmzZXVxcCdQ52H9sES; lux-devnet
    runs mpxP6xF3ahpjdwP6YgjtagSWQCZSR6pziEqDoDv573k2V3WJA. The fleets are split
    and nothing said so.
  * every network shared one genesis, because none of them reached the VM.

A wiped validator therefore adopted whatever its binary believed, looked healthy
alone, and was partitioned for good. Wiping chainData is the ordinary repair for
a stuck chain, so the ordinary repair was the thing that stranded the node.

Genesis now comes from block.Init.Genesis — the dchain.json recorded in the
P-Chain CreateChainTx, carried across the plugin boundary as
InitializeRequest.GenesisBytes and, until now, dropped on the floor. The document
is opaque: its digest occupies genesis's parent-root slot, which nothing used, so
the chain begins from its configuration and every later root descends from it. No
schema of the document enters consensus.

meta:genesis records the height-0 image in the batch that commits it. Every start
reconciles three views — the creation record, this binary, the database — and a
disagreement refuses to start naming all three. A chain born without the record
recovers it at height 0 from its own head block, which is durable data, not a
guess; past height 0 there is nothing honest to recover and it refuses.

Removed with it: loadHeadBlock's rebuild-genesis-from-code path, and readRoot's
silent zero for a missing root, which would have rebased the chain just as quietly.

genesis_test.go pins the images byte for byte, including both live fleet genesis
blocks read from luxd, so this cannot drift again unnoticed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 16:21:48 -07:00
zeekayandHanzo Dev aae6cd0857 dexprotocol: the third conservation proof, denominated in lots
Three separate proofs, none able to stand in for another:

  cross-chain  C + C->D + D + D->C constant          token base units
  D-local      owned == available + reserved         token base units
  order-local  original == open + reserved
                          + traded + canceled        LOTS

The units stay apart. Merging them is how a lot count gets added to a wei
balance and type-checks; the market's lot size connects the domains and
one test asserts the two numbers differ, so a future merge has to delete
an assertion rather than quietly succeed.

Four terms, not three. Open lots are restable and cancellable; reserved
lots are committed to a match already in flight. Collapsing them loses
the property that matters: Cancel takes the OPEN quantity only, because a
cancel reaching committed lots would let the same lots be cancelled and
traded, which is the order-book form of a double spend. The committed
remainder resolves when its match does, and the trader may cancel again
to retire whatever came back.

Cancel is a no-op on an order with nothing open rather than an error.
"Retire whatever you can" is the honest reading of the request, and
failing it would force callers to inspect state to avoid an error they
cannot prevent.

Every transition moves lots between two terms and never writes a total,
so conservation is a property of the shape. A 3000-step random run
asserts it after each: 548 trades, 451 cancels, 528 returns, 257 orders
replenished. The run replenishes exhausted orders deliberately — without
that it did 67 real transitions in 3000 steps, and a floor assertion now
fails if it degrades back to flattering the step count.

75 tests.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.20
2026-08-07 16:21:20 -07:00
zeekayandHanzo Dev 9061f81167 dexprotocol: a reclaimed export cannot become writable
The D->C leg is the sharpest monetary edge in the system. D debits the
owner and writes an object for C to consume; if the write is lost, the
value has left D and never arrives. Refunding on a timer is the obvious
repair and is a double-spend waiting to happen — the moment a refund and
a late delivery can both take effect, the value exists twice.

The property that must hold: after a reclaim, no copy of the original
object, however delayed or retried, can ever become importable on C.

It is structural rather than argued, and the mechanism is who can obtain
the bytes:

  Available --Export--> Pending --Deliver--> Delivered --retry--> C
                           |
                           +---Reclaim---> Available

PendingExport exposes no Claim. There is no accessor, so the object to
write cannot be obtained while a reclaim is still possible. Only Deliver
produces one, and Deliver MOVES the record out of the pending domain, so
a record is either reclaimable or writable and never both. The write is
therefore only ever attempted from a committed Delivered state — the same
commit-then-apply discipline the import leg already uses.

A timeout is not the recovery mechanism. Delivery is retryable from
durable state for as long as it takes, and Redeliver returns a
byte-identical object, which is what makes C's exactly-once import treat
a duplicate arrival as the replay it is rather than a second claim.

Pending value still counts as D's in the rail equation:
imported - delivered == owned + earmarked. It has left the owner's
available balance but has not crossed, and a reclaim brings it back;
counting it as gone would make every open export read as a shortfall.

Six attacks, all refused: deliver after reclaim, redeliver after reclaim,
re-export under a reclaimed id, double reclaim, reclaim after deliver,
and the full two-chain reclaim-plus-late-delivery. Plus 500 randomly
sequenced steps asserting after each that no id is both reclaimable and
writable. 67 tests; the attack run closed at 10000 in - 660 delivered =
9323 owned + 17 earmarked.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.19
2026-08-07 16:16:55 -07:00
zeekayandHanzo Dev 22ee0495b4 dchain: the seam's unit of work is an order, not an intent
Order -> Match -> Execution -> Trade is the vocabulary; "intent" was the old
name for what an Order is. Renames the identifiers on the D-side of the C<->D
settlement seam to match precompile/dex, which pins against these by golden.

Values that live on a chain keep their bytes, because renaming one of those is
a migration and not a rename:

  - "lux.dex.native.intent.v1" and "lux.dex.native.intent.pending.v2" are hash
    INPUTS; a byte changed there moves every derived order id and the pending
    trait the precompile must agree with.
  - "seamintent:" is the on-disk escrow key prefix; renaming it orphans every
    escrow row already written.

Each now carries one line saying why the literal does not track the identifier
that names it, so the inconsistency is not "tidied" later into a fork.

TestSeamOrderWire_GoldenMatchesPrecompile and
TestSeamPendingTrait_MatchesPrecompileGolden still read the same goldens, which
is the evidence no derivation input moved.

vm.go carries the same rename in comments only; it is held back because that
file is being rewritten concurrently for the genesis record.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.18
2026-08-07 16:16:08 -07:00
zeekayandHanzo Dev 406ee2695a dexprotocol: move once, trade many — D owns what it trades
C and D each own value while it is on that chain, and a unit is spendable
on exactly one of them at a time:

  C --Export--> C_TO_D --Import--> D --Export--> D_TO_C --Import--> C

Those four transitions and no others. Never C to D directly, which would
mean a moment where value is in both places or neither.

The consequence is the point. Once imported, D is not matching against
hypothetical C collateral — it has real balances, and every order, match
and trade is a local movement. A million trades cost zero boundary
crossings. That is what makes D an exchange ledger rather than a remote
matching oracle attached to C.

So the atomic rail is narrowed to one job: move ownership exactly once.
It knows nothing about orders, executions, trades or settlement. The
per-trade cross-chain certificate, the C-parent reservation and the
accept-or-release round trip stay only for the V4 adapter, where a
contract genuinely demands synchronous C composability.

Import consumes and credits in one operation. Splitting them either
destroys value or lets a retry, a reorg, a racing relayer or a restart
import the same object twice, which is a double-spend of real money.
Refusal is loud rather than silent — a caller that imported twice has a
bug worth seeing. Export draws only on available balance, so value
committed to an open order cannot also leave the chain.

A claim names its beneficiary, which is what makes import safely
permissionless: delivering one can only credit the account it already
names. Co-location buys latency, never correctness.

Execution now carries MarketVersion and the exact token amounts D
resolved, so C verifies amounts rather than recomputing lots from
whatever the market says now — recomputing would replay a block to a
different result after a market update. Price is gone; it was Output over
Input stated twice inside a signed struct.

Partial execution is bounded by cross-multiplication,
executionOutput * orderInput >= executionInput * orderMinOutput, so there
is no division, no rounding convention, and the rule composes: if every
slice satisfies it, so does their sum.

60 tests. Conservation asserted after all 600 operations of a random run:
imported 4143, exported 2576, D owns 1567.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.17
2026-08-07 16:04:26 -07:00
zeekayandHanzo Dev ee2e2cbad3 release: retry the ghcr.io login that cost this repo two releases
v1.14.15 and v1.14.16 are both tagged and neither produced an image;
published stops at v1.14.14. Both died at the same step:

    ::error::Error response from daemon:
        Get "https://ghcr.io/v2/": denied: denied

The credential is not the story. Measured on the forge over 7 days, luxfi
ran 365 ghcr logins with the same org secret and 27 failed — 7.4% — across
23 repos and a dozen runners, and the same secret succeeded on either side
of every burst. Three readings were tested and all three are wrong:

  expired token   the same secret succeeded hours after earlier failures
  sick runner     failures spread over 12 runners; a clean one is 12 of 12
  throttling      no correlation with concurrency — 7.4% at one login per
                  minute, 10.9% at two, 0% at four and five

The only defect on our side is that nothing retries. docker/login-action
does not, and the job dies at step 3 of 6, before the build. So each
occurrence is invisible: a tag, no image, and no failed push to point at.

Five attempts over 75s, backing off. Exhausting all five is a different
failure than the one this covers and says so rather than pretending —
which is how the dead org credential was told apart from this on
2026-08-07, when it stopped working outright.

Same change as luxfi/explorer. 21 luxfi repos still carry the unretried
copy; each holds its own release.yml, so the real fix is one reusable they
all call.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 13:27:37 -07:00
zeekayandHanzo Dev 4fb53657b1 dexprotocol: quantity is conserved per order, asserted every transition
Exclusive() proves an ExecID cannot sit in two domains. It says nothing
about how much of an order has been committed, so D could certify ten
executions against one order for its full quantity — each individually
valid, correctly certified, correctly scoped — and settle whichever the C
successors happened to include. Every defect the last review turned up
was a conservation bug rather than a determinism bug, so this is tracked
rather than left as a property that happens to hold.

Per order: reserved + traded <= authorized. Reserving takes from
available, settling moves reserved to traded, releasing gives it back.
That last one is what makes a portable Order work at all — an order whose
execution lost the race is still live and matchable again, and a release
that failed to return the quantity would silently shrink every order that
ever lost one.

Conserved() recomputes both totals from the domains themselves rather
than restating the counters, so an accounting update that drifted from
the objects it summarises is caught instead of trusted. A random
400-step lifecycle asserts it plus Exclusive() after every transition:
36 reserved, 96 traded, 86 released in the recorded run.

Units are the book's lots, matching Execution.Quantity, never the order's
Input in token base units. The lot size is market metadata and stays out
of this package: a caller that knows the market converts once and passes
authorized in, and the value is checked for agreement across every
reservation so a second, different conversion cannot enlarge an order.

51 tests, up from 25 before the reshape.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.16
2026-08-07 12:15:32 -07:00
zeekayandHanzo Dev be195f4a8d dexprotocol: the Order is the authorization, and it is portable
The trader signs an Order and nothing moves. D's book matches it and
certifies an Execution; an operator carries Order + Execution + proof to
the C reactor. Only an accepted C block turns that Execution into a Trade.

CParent leaves the Order and stays on the Execution. An order is a
long-lived authorization; a reservation is scoped to one C opportunity.
Braiding them made the trader responsible for guessing which block their
order would match under, and killed the property the book depends on: an
unmatched order stays live, and one whose Execution was released can be
matched again.

Three things this shape forces, each a bug it makes unwritable:

Nonces are UNORDERED, a bitmap position rather than a counter. D matches
by price-time priority across every trader, so one trader's orders settle
out of signing order as a matter of course. A sequential nonce rejects
the low one arriving second and then blocks that trader permanently,
because the expected value can never advance past an order that may never
match.

The commitment binds NetworkID, CChainID and the reactor address, inside
the hash rather than beside it. 0x9999 runs on every EVM sharing the one
D venue; an unbound signature is one an operator replays against each in
turn, and the trader pays on all of them for the single trade they
authorized. Bound, that replay is not detected — it is unrepresentable.

Input and MinOutput are in token base units and 32 bytes wide, not the
book's lots in a uint64. They are different quantities describing the
same trade, and one type for both would let a lot count be added to a wei
balance. uint64 also caps an 18-decimal token near 18 units.

VerifiedOrder follows VerifiedExecution: unexported interface method as
the sentinel, since unexported fields alone leave Order{} constructible
anywhere. Verification stays a pure question — it does not consume the
nonce, or merely inspecting an order would burn the trader's position.
The deadline is read against the block being built, never a clock.

49 tests, up from 25.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.15
2026-08-07 12:11:32 -07:00
zeekayandHanzo Dev e714094013 dexprotocol: Reserve binds to the accepted parent — rule 1 enforced
Settle and Release already required a verified AcceptedBlock; Reserve did not,
so a reservation could still be created without any proof that its C parent was
accepted. That left the abandoned-parent case reachable through the one door
that had not been closed.

Reserve(parent AcceptedBlock, v VerifiedExecution) now enforces both halves of
rule 1: the parent must carry the verified sentinel, and the execution must be
scoped to exactly that parent. Both arguments are unforgeable outside this
package, so a reservation can be taken neither against an unauthenticated
execution nor against an unproven C parent.

25 tests passing.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.14
2026-08-07 09:15:56 -07:00
zeekayandHanzo Dev 438ad8eb9a dexprotocol: CParent means accepted C block, and non-inclusion is a real proof
THE INVARIANT

An Execution may be reserved only against an accepted C parent. The unique
accepted child of that parent permanently decides the Execution: membership
means Trade; non-membership means Release. Neither C nor D reads the other
chain's live state during consensus execution.

CPARENT ACCEPTED-ONLY

Acceptance is the finality boundary, so an accepted P is guaranteed exactly one
accepted child, and that child decides every execution reserved against P. This
removes the abandoned-parent case entirely rather than handling it: there is no
proposed parent to abandon. It also collapses two questions — "did P win?" and
"which child of P won?" — into one.

AcceptedBlock is now a struct with unexported fields, produced only by
VerifyAcceptedBlock. Unexported fields alone still leave AcceptedBlock{}
constructible from outside, so a `verified` sentinel closes that hole and every
transition requires it. Symmetric with VerifiedExecution.

Never use local C state as the proof. Even co-located, different D validators
observe C advancement at different instants, so `localC.LastAccepted()` is not a
function of the block being executed. The local C VM is delivery; the
authenticated statement is consensus input — the same rule already proven in the
other direction.

THREE RULES, NO CLOCKS

  Accepted(P)                                permits Reserve(E), E.CParent == P
  Accepted(Q), Q.Parent==P, E in Q.ExecRoot  permits Settle(E) -> Trade
  Accepted(Q), Q.Parent==P, E not in root    permits Release(E)

NON-INCLUSION IS FIRST-CLASS

Release is half the protocol, not a fallback, and it is the half that returns a
trader's liquidity. So ExecRoot is a sparse Merkle commitment over the 256-bit
ExecID keyspace where absence has the SAME proof shape as presence: the path to
the key, terminating in the empty leaf. One verifier, one code path, one bit of
difference. Leaf and node domains are separated against second-preimage
substitution, and the empty leaf is a distinct constant so absence cannot be
forged. Proofs carry a bitmap and omit empty siblings, so size tracks the
executions actually in the block, not the depth.

A useless reservation is still resolvable: if D reserves against P only after C
accepted Q, anyone can deliver Accepted(Q) plus non-inclusion and release it.
Correctness does not depend on D winning that race.

Also removes the last references to a flag day, which was never authorized —
0x9999 activated 2025-12-25 and settle_module is AlwaysOn. The wire-compatibility
test keeps its value and loses the dead concept from its name and prose.

24 tests passing in dexprotocol, dchain suite green.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.13
2026-08-07 09:14:23 -07:00
zeekayandHanzo Dev 05796eb725 fix: FIX Latest — ExecType said what OrdStatus already said
The gateway spoke 8=FIX.4.4 and reported a fill as ExecType 1/2 — the pair
FIX Latest retired precisely because it duplicated order state into the
event field. Both fields carried the same thing, so nothing on the wire
distinguished "a trade happened" from "the order is done", and a client
could read either.

Session layer is now FIXT.1.1; the application layer is FIX.5.0SP2 + EP307,
named on the Logon by DefaultApplVerID(1137) and ApplExtID(1156). Every
fill is ExecType=F (Trade) and OrdStatus alone moves Partial -> Filled.
Nothing depended on the old BeginString, so the wire moved with the
semantics instead of straddling both. This is the wire pkg/dexprotocol
already documents (150=F, 39=1|2).

A partially-filled IOC also never reached a terminal state: the last report
said Partial and nothing followed, leaving a client waiting for an end that
never came. It now closes with Canceled.

Vocabulary, one word per concept: execReport -> execution, gateway
OrderFill -> Fill, multisig ExecutionResult -> Result (it holds an error
string, never an execution).

Deletes the SDK's hand-rolled FIX encoder — it pinned a second protocol
version, omitted BodyLength entirely so the acceptor would have rejected
every frame, and sendFIXMessage returned nil without sending.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.12
2026-08-07 09:09:37 -07:00
zeekayandHanzo Dev c935e4d228 dexprotocol: the cross-chain exchange vocabulary, with the bug class unwritable
The C transaction contains an Order. D matches it and certifies an Execution —
price and quantity guaranteed and reserved. The C block carries that Execution.
EVM execution verifies it and returns the resulting BalanceDelta. When the child
of the Execution's C parent is accepted, D settles it into a Trade if that child
used it, or Releases the reservation if it did not.

This is the consensus-side half: the certified object, its canonical encoding
and domain-separated commitment, and the lifecycle.

WHY IT IS SHAPED THIS WAY

C never queries D's live state during EVM execution — that would fork C on the
first swap, since validators sit at different D heights. Instead D's statement
rides in the block and C verifies it. On replay D can be dead, the GPU gone, D a
billion blocks ahead; C has parent state, transactions, the statement, and the
certificate. That is the entire input set.

Move-typed storage, not a status field. ReservedExecution, Trade and Released
are distinct types under distinct key prefixes, and a transition MOVES the
object. Settle(Trade) and Release(Trade) do not compile — there is no reserved
execution left to hand them. This is aimed squarely at the live defect where the
intent record is written back only inside the same-asset refund branch, so the
ordinary proceeds outcome leaves it Open with its full principal and the
deadline reclaim later refunds money already spent out of other traders' pooled
reserve. That is a field someone forgot to set on one code path; here there is
no field to forget.

An unauthenticated execution cannot enter the lifecycle: VerifiedExecution is an
interface with an unexported method, so only VerifyExecution can produce one, and
Reserve takes nothing else. AcceptedBlock is the same shape, so a settle or
release can never be driven by a clock or a bare block id.

The reservation is keyed by CChain + CParent + ExecID — the parent OPPORTUNITY,
never a candidate block — so competing candidates all name one reservation and
none of them moves it.

FIX Latest externally, small internal vocabulary. ExecType (the event) and
OrdStatus (the resulting state) exist on the wire and are kept distinct per FIX;
they are never the internal source of truth. There is deliberately NO FIX state
for the provisional reservation — it is internal consensus machinery, and a
Trade is published only after C acceptance, which is what keeps the explorer's
CLOB honest.

15 tests, all passing: encoding injectivity over every field, domain separation,
verifier-supplied parent binding, terminal exclusivity, replay refusal.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.11
2026-08-07 08:57:08 -07:00
zeekayandHanzo Dev a33fab670e docs(llm): a 404 on dex_* means old method names, not a dead D-Chain
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 08:42:41 -07:00
zeekayandHanzo Dev a388e5fb08 dchain: a proposer could strand any swap for free
The drive emitted the import and the submit as a PAIR in one block, to close the
window where value sits on D with no order against it. That was one block faster
and one hole deeper: a proposer that included the import and omitted the submit
produced a perfectly valid block whose escrow was stuck `open` forever. The
import drive skips already-imported intents, so no later block would ever emit
that submit; the export drive gates on `submitted`, so nothing would ever settle
it. The taker's value sat in a keyless seam account while C's deadline reclaim
refunded them — C's books balance, but the D-side entry is stranded permanently,
and any proposer could do it to any swap, for free, undetectably.

The drive is now ONE RULE: for each intent, emit whatever leg its COMMITTED
STATUS says comes next.

    no escrow + a pending object -> TxImport
    escrow `open`                -> TxIntentSubmit
    escrow `submitted`           -> TxExport

An `open` escrow is unfinished work the next proposer picks up, exactly as a
not-yet-imported intent is. A swap takes three blocks instead of two; in
exchange no block can strand one. Self-healing beats a block of latency, and it
is the simpler rule — the pair was an optimization braided into it.

124 tests pass, 0 fail.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.10
2026-08-07 08:36:54 -07:00
zeekayandHanzo Dev 485289bb52 test(dchain): two independent nodes must agree on a seam block
The closest deterministic stand-in for the devnet gate: a second node with its
own state store, its own shared memory, and no sight of the proposer's mempool
parses the proposer's BYTES, verifies, and derives the identical root — through
the import, the cross, and the export.

Modelling shared memory as one store between the two nodes is what an earlier
draft did, and it is wrong in a way that hides the real question and invents a
fake one: the second node's Apply then collides with the first's for a Put it
would, in production, be making into its own store. Shared memory is per-NODE —
that node's local materialization of both chains' history — so the test gives
each node its own, and C's accept flushes the object into both.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 08:31:54 -07:00
zeekayandHanzo Dev 1378e366e2 dchain: one place decides what an imported intent means as an order
The seam-submit arm had grown three near-identical reject sites inline in
execute, each repeating "advance the escrow, then reject". resolveSeamOrder is
the single resolution point: escrow -> order frame, or a deterministic reject,
with the escrow advanced on every path exactly once.

That the escrow advances even on a reject is the load-bearing part, and it now
has one place to be true rather than three to stay true together.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.9
2026-08-07 08:28:41 -07:00
zeekayandHanzo Dev dc370e6f44 dchain: a rejected block put the proposer's own seam legs back in the mempool
Reject requeues b.txs wholesale, which includes the import/submit/export legs the
drive generated. Those are a pure function of committed state — the next
BuildBlock regenerates exactly the ones still warranted — so the next block would
carry each leg twice, once from the mempool and once from the drive. The replay
guards refuse the second copy, so this is duplication rather than a double spend,
but a block that carries the same import twice is a block nobody can read.

Requeue only what a client actually submitted.

Co-authored-by: Hanzo Dev <[email protected]>
v1.14.8
2026-08-07 08:26:52 -07:00