M threshold-signs a security crossing the way it signs a fungible
transfer, and B carries the attestation to the destination register.
Same leaderless ceremony, same authorisation, same group key travelling
with the result so B verifies without asking M anything. What differs is
the message, and therefore the digest.
What B does here is narrower than what it does for a fungible asset. The
fungible path releases by minting: a confirmed lock authorises N units
to an account and nothing else has to be true. Whether a SECURITY may
exist on the destination at all is the destination register's question —
is this holder verified, do their claims satisfy the token, does a
holding period follow the paper across — and the token asks it in the
block that lands the position. B never decides a security may land. It
decides the attestation is genuine and says what it claims to say.
SecurityRequest is separate from BridgeRequest rather than a flag on it.
The two are released by different calls onto the destination, and a mint
against a register nobody asked is exactly the failure this path exists
to prevent; one type that could be either leaves that difference to a
branch somebody has to remember to write.
Two fields the destination cannot look up for itself travel with the
crossing — the holder's ONCHAINID on the source register, and when they
first received there. Without the second, an arriving position anchors
its clock to now, and a holding period a crossing resets is one a
crossing washes off.
A short identity is refused rather than read as an absence: treating it
as absent lets a truncated field pass for "this holder had none". An
absent one travels as zero, which is honest.
Quantity is a uint256. A billion shares at eighteen decimals is an
ordinary size for a register and does not fit the uint64 the fungible
path carries; a truncated quantity is a position that arrives smaller
than it left, so it is refused rather than truncated.
Signer ids convert to strings at the boundary so a verifier on the far
side needs nothing of M's type graph. The crossing type itself is
aliased from internal/bridgeattest rather than copied — attest_bridge.go
keeps its own copy of BridgeTransfer, and two definitions kept in step
by hand is a way for a digest to drift across a boundary whose whole
value is that it cannot.
A plugin is its own binary; the daemon that loads it has no business being
linked into it. Twelve VMs reached into github.com/luxfi/node for four things,
and each one now programs against a home that is actually its own:
- fee admission (Policy, FlatPolicy, NoUserTxPolicy, Validate) joins the
settlement half already in chains/fee. One fee package, and the nodefee alias
that marked the split is gone.
- the cross-chain artifact types move to chains/artifacts.
- the factory the daemon registers is luxfi/vm/manager.Factory, and the version
types are luxfi/version. Both are ordinary modules the daemon reads too, so a
VM imports them directly rather than through anything of ours.
The GPU switch is deleted, not moved. It was a global set during daemon startup
that a separate process could never observe, so every plugin read its default;
FHEOptions.Backend is the switch that was really there.
go list -deps names no luxfi/node for aivm, bridgevm, dexvm, evm, fhevm,
graphvm, identityvm, keyvm, mpcvm, quantumvm, schain or zkvm, and the built
plugin binaries carry no such string. oraclevm and relayvm still do: they are
re-export shims over luxfi/oracle and luxfi/relay, which build their fee
policies upstream. Their tests keep naming the daemon's fee package because
that is the type their value has.
The three GPU suites compiled a library exporting launcher names and nothing
else, which is exactly what the identity check refuses, so eighteen tests
failed against a loader that had started doing its job. The stubs now export
lux_gpu_backend_init answering with this build's ABI version and vtable size,
compiled against the real header so an ABI bump moves them with it.
Three assertions pinned the behaviour that was removed — aivm expecting the
bare filename, mpcvm the bare leaves, bridgevm the working directory — and now
pin its absence.
TestALibraryThatOnlyExportsTheNames is new in each package: it plants the
unidentified build, watches the loader refuse it, then opens the identified
build from the same place so the refusal is the identity check rather than a
stub that never worked.
Co-authored-by: Hanzo Dev <[email protected]>
LP-134 was renumbered LP-1340 (lp-1340-lux-chain-topology); 21 citations
across 16 files still named the old number, which resolves to nothing.
Corona rests on Module-LWE, not Ring-LWE — LP-4805 says so in three places
and b17e086 already corrected one file, leaving evm/main.go and
schain/SECURITY_review.md claiming the wrong hardness assumption. The
two-family argument in LP-4900 depends on which lattice problem this is.
P3Q at 0x012205 is kind-byte signature dispatch; STARK/FRI is starkfri at
0x012220 (LP-7050). Naming P3Q for a scheme at another slot describes a
precompile that is not the one being imported.
Carries the substance of 2f22fa2, the one commit the parallel line held that
this history did not.
A GPU plugin replaces a VM's state transitions wholesale. Three of the four
seams reached one by dlsym'ing launcher names out of a library nothing had
identified, and all four would fall back to a bare filename — which makes the
dynamic loader's default search path, and in bridgevm's case the working
directory of whoever started the node, the thing that decides which code
computes consensus state.
A library exporting the five bridgevm launcher names and nothing else,
compiled from twenty lines of C that return 0:
before, LUX_GPU_PLUGIN_DIR=/tmp/rogue AutoBackend = hip
before, no env, cwd holds the .so AutoBackend = hip
after, either AutoBackend = none
The plugin header says a loader must check abi_version and vtbl_size. aivm,
bridgevm and mpcvm now do so before resolving a single launcher: a name was
the only thing being matched, and a name is not a contract.
Bare filenames and the CWD entry are gone. What is left is LUX_GPU_PLUGIN_DIR
and LUXCPP_PREFIX — places an operator named. Unset means no plugin, which is
the configuration every box without a GPU is already in.
Cherry-picked from 634df5e, whose quantumvm hunk is dropped: that seam's file
was deleted in 409d0e4 and the chain no longer loads a plugin.
Co-authored-by: Hanzo Dev <[email protected]>
chain/store.go gains ErrNotOnTip, so Accept re-checks that the block still
extends the accepted tip under the same lock that commits it, and Seed records
the tip it wrote. Open already refuses to read any error as a fresh chain;
a Seed that left no tip was the remaining way to look fresh after writing.
Carries the zkvm, identityvm and graphvm work the fixes were built on.
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181), and the health service moved onto typed
ops. Published addresses now match what a node answers on:
/v1/bc/<alias>/... -> /v1/chain/<alias>/...
/ext/bc/<alias>/... -> /v1/chain/<alias>/...
/v1/health -> /v1/health/ops/health (full report)
/v1/health/liveness -> /v1/health/ops/liveness
/v1/health/readiness -> /v1/health/ops/readiness
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.
Every adopter asked whether its block still extended the tip, released
that reading, and only then asked the store for the lock. The tip moves
in between and the commit lands anyway, writing the height index and the
tip pointer for a block on an abandoned branch -- which every peer
bootstrapping from that index is then served as canonical. The lock is
not reentrant, so the check cannot come from the caller and be held
across the commit: it comes from the store or it is not held at all.
Parent joins Height on the Block interface because a place in the
sequence takes both, and height alone is the reading that lets an old
accepted block pass. Two concurrent accepts fall to the same move, not
just the Verify-to-Accept window: the tip a block is judged against is
now read inside the critical section that commits it, so the second is
refused rather than serialised behind a reading taken before the first
landed. The store also exempts nothing, where identityvm exempted height
zero -- and its Verify skips every parent check at height zero, so a peer
could hand a live chain a second genesis carrying records of its choosing
and send the tip and the height back to the start.
mpcvm keeps its root comparison, which answers a different question --
whether applying this block reaches the root the network agreed on -- and
stays true of a node whose state moved for any reason. That a moved tip
also showed up there is a consequence of that chain refusing empty
blocks, not what the comparison is for.
Seed records the tip in that same commit, because seeding is what makes
a chain no longer fresh. Recording none left Open, for which a missing
tip is a fresh chain, reporting a seeded node fresh on every boot and
asking for the allocation again: a duplicate its own state refuses, and
a node that cannot restart until it produces a block it cannot produce.
BatchNTTForward, BatchNTTInverse and BatchPolyMul were three copies of one
loop — snapshot the session, allocate operand tensors, run a kernel, sync, read
back, copy out, and on any failure at any step close what is open and do the
operation on the CPU instead. ~45 statements each, differing in how many
polynomials an element reads, which kernel runs, and what the CPU does instead.
Three copies of a resource lifecycle are three places to forget a Close, and a
fourth kernel would have been a fourth copy.
Now one offload plus one element, and three wrappers that name their three
differences. 101 lines go, no behaviour with them.
The floors are deliberately NOT unified: forward NTT reaches the device at 64
polynomials and ring degree 8192, the other two at 4 and any degree, and the
reasoning written on the forward path applies to all three. That reads as
drift, but changing a performance threshold needs a device to measure on and
this host has none, so the numbers are carried through as parameters and the
disagreement is stated where the next reader will see it.
Also drops the count == 0 guard, which count < minBatch already subsumes for
every floor; reverting it changed no test result, which is what redundant
means.
TestUnboundLabelIsNotCounted named the boot-time binding count and then wrote
its own copy of the loop and asserted on that. It passed whatever Initialize
did, including nothing. The property is held against the VM in
TestOnlyAnEntryCarryingARealChainIdCountsAsBound, which boots a node on a table
of labels and asks it who it authorizes; what stays here is the half that is
local — a label does not parse as a chain id.
docs/ and DESIGN.md described protocol/{cggmp21,frost,corona_general,
tfhe_keygen} — four interface-only packages with no implementation and no
importer, deleted this pass — and pointed at a chains/mchain/ tree that does
not exist, which LLM.md already says never did. Prose describing an unbuilt
system reads as a contract; the reader who believes it looks for code that
isn't there.
What is true moves into LLM.md: the constants that must stay constants because
Verify enforces them, the two checks that must read consensus state rather than
the document being checked, and the boot fault that made a share-holding
validator unable to restart.
The lane that did this was killed before it could commit; the tree it left is
green and is preserved here rather than lost. Root package 51.5% at the start
of the day, 93.3% at its last checkpoint, 98.6% now, with cert, fhe, types and
cmd/plugin all passing.
Includes the fault-injection suite it was building when it stopped: a node that
cannot read its state does not start, and one that cannot enumerate its
registry does not either.
Not reviewed by me beyond build, vet, gofmt and the full suite under -count=1.
The lane's own report is owed and did not arrive, so its mutation count and its
verdict on the mpcvm/fhe duplication are unrecorded.
The VM never built its chain store, so Initialize dereferenced a nil
store and B-Chain could not start at all.
What a block settles — the transfers, and the day's running total per
destination — is now written in the same commit as the block. Both lived
in maps on the heap, so a restart forgot every transfer the chain had
released and handed back the whole day's allowance: a restart was a free
reset of the cap that bounds what this bridge can move. The counter is
cut from the block's own timestamp, so every node closes the window on
the same block, and it only ever grew before, which stopped the chain for
good once total volume passed the limit.
A request's id is now the digest of the transfer it carries, and Verify
refuses one where it is not. The id was a label the proposer chose while
the replay guard, the daily counter and the release worker all keyed off
it, so one deposit could ride in under any number of names.
Verify now resolves the parent, refuses one beneath the tip, requires the
height and the timestamp to follow it, and registers what it accepted so
a follower can verify the second block of a run. It read none of that
before. Blocks are bounded and canonically encoded, the chain id is
hashed into every block id, and assembly admits exactly what Verify
admits, so an uncarryable transfer no longer wedges the proposer.
The signer set's threshold is ⌈2n/3⌉ in integers rather than a float that
asked for unanimity at three signers and for two of one at one, the bond
it declares is enforced where a signer registers, and a replacement takes
the slot it replaces.
Deleted: the block and request MPC signature carriage, which nothing
produced and whose deserializer could not decode a canonical secp256k1
signature, so it could only ever reject; the chain-name list beside the
routing table; the confirmation depth and observation time that each node
read for itself and then wrote to the wire; a worker pool, a party id and
an engine channel nobody read.
The lane registry had two paths to a verifier. Register refuses a lane that
does not belong to the registry's owner, and its comment says that is what
enforces M/F orthogonality. RegisterLegacyAlias then mapped any id at all onto
a registered verifier, checking only that the TARGET existed and never that the
legacy id belonged to the owner -- so the alias walked around the invariant
Register exists to hold. The test demonstrated it: an arbitrary id 200, owned by
neither chain, dispatching to an M-Chain verifier.
It bought a grace window for LP-5013 T-Chain lane ids. LP-134 dissolved T-Chain
with zero remainder, so those ids cannot arrive. Nothing outside the test ever
registered an alias. Gone, with the aliases map and ClearAliases: a lane this
chain does not own now has no verifier, which is the ownership rule still
holding at dispatch.
Deleting mpcvm/runtime's two adapter interfaces emptied the package. FChainAdapter
and MChainAdapter were declared, never implemented and never called -- every
reference to either was inside its own file.
Two pieces of the new package had no caller. Pool.Holds was the read side of a
claim set that Add already enforces, so nothing ever needed to ask; it is gone,
and the tests that used it now assert the same property through Add, which is
the behaviour that matters.
chain.Closed had no caller because M-Chain still declared its own
NoUserTxPolicy. It declares chain.Closed() now — one fewer copy of a policy
five chains state identically, and the one no-user-tx chain in this pass
exercises the value that describes it.
Accept folded the operations into the key registry and the ceremony log with
one Put each, and only THEN compared the root it reached against the one the
block claims. Refusing there left every record it had already written — exactly
the disagreeing state the check exists to refuse.
That was worse than losing a block. PutKey and PutCeremony refuse a record they
already hold, so the block that failed could never be applied again: the chain
was wedged at that height, holding ceremonies no block records. PutBlock and
SetLastAccepted were each two Puts with an early return between them, so a fault
could also drop a height entry or advance the root under a stale tip.
State now writes through the chain's view, and the block, its height entry, the
tip and every record it wrote commit together. A refused block leaves nothing,
and applying it again works — which the test proves by doing it.
The staging queue becomes the shared pool: it was already a map keyed by
ceremony id plus a slice for order, rebuilt from the map when entries went, which
is what the pool derives. The store owns the tip, the height index, the blocks
in flight and the engine's preference; State keeps the registry, the ceremony log
and the root, which are M-Chain's own.
One lock order, written where the locks are: the chain's, then the VM's, then
the pool's. Verify resolves its parent before taking the VM's lock, because
holding it across a block lookup takes the two in the order Accept does not.
No existing test changed.
Across these files the comments explained a rule by recounting what happened
without it — which key was mistaken for which, which payer a full pool released,
what a verifier accepted, what a chain did with work it never announced. That
tells a reader what changed rather than what holds, and it dates: the account
stays in the file long after anyone needs it, while the rule it was meant to
convey has to be inferred back out.
They now state the rule. A circuit is judged on its own key. A witness is
exactly as long as the key's circuit says. A count describes the records it is
read from. A full pool releases what pays least. Custody membership costs stake
because an identifier can be aimed and stake cannot. Consensus builds when
WaitForEvent returns, so accepting work and reporting it are one step.
Behaviour is unchanged; the diff is comments, with one exception worth naming:
gofmt realigned a block of one-line methods in quantumvm/feegate_test.go that
was already unformatted before this commit.
Co-authored-by: Hanzo Dev <[email protected]>
Both had grown the same thing: a depth-one channel, a non-blocking send guarded
by a select with an empty default, and a WaitForEvent selecting over it and the
context. That is what vm.Latch is, so they use it. Four copies of the pattern
become none, and the comment explaining why the send is non-blocking lives with
the primitive rather than beside each caller.
Co-authored-by: Hanzo Dev <[email protected]>
Seats went to the first N validators in canonical order, and canonical order is a
lexicographic sort of node ids. A node id is a hash of the node's certificate, so
an attacker grinds certificates offline until its ids sort first -- roughly 256
hashes per leading zero byte, and a zero byte encodes as the smallest base58
digit. Validator entry is permissionless, so that bought a seat in the share set
of EVERY key the chain would ever generate, at the price of some hashing.
Under the 2-of-3 default, two such seats were a quorum. Nothing needed to be
reconstructed at that point: the holder runs the signing protocol locally with
both shares and signs whatever it likes, off-chain, with no block produced and
nothing to observe.
Seats now follow weight, which cannot be ground -- entering the set costs stake
proportional to the stake already there, the same cost as attacking consensus, so
custody stops being the cheaper target. Node id still breaks ties, but now only
between validators of equal stake, where grinding buys a position among peers you
already had to match. Selection stays a pure function of replicated state, so
every validator derives the same set without negotiating one; the ordering test
runs it repeatedly because Go randomises map iteration and a selection that leaks
it would give each validator a different set.
The default policy is 3-of-5. Two seats are deliberately not a quorum, 2K > N
still holds so the quorum is unique, and the end-to-end DKG-and-sign test now
runs the real ceremony at that policy over the gossip fabric.
Co-authored-by: Hanzo Dev <[email protected]>
RunSign and RunKeygen stayed exported through the authorization change, still
taking the chain-name string that change removed. The wrapper became unforgeable
while the thing it wraps was reachable beside it, consulting no permission, no
quota and no Caller. Lowercased; the only callers were in-package.
A permission entry whose chainId is a LABEL binds to no chain, exactly like an
empty one, because a label never equals a base58 chain id. Counting it as bound
made a stock node report five authorized chains while authorizing none -- which is
the misreading the boot warning was added to prevent, reintroduced by the warning
itself. Bound now means the chainId parses as a chain id.
Co-authored-by: Hanzo Dev <[email protected]>
Signing authority was resolved from a name the requester wrote about itself.
CrossChainRequest already receives the sender's chain id from the transport,
which authenticated it, and then passed req.RequestingChain to the permission
lookup instead -- so the proven identity sat unused in the signature while a
field the sender controlled decided what it was allowed to do. The stock table
grants B-Chain sign, keygen and reshare at 100k/day, so the claim was worth
having.
Caller carries an authenticated chain and nothing else can be substituted for
it: the only constructor takes the transport's chain id, the fields are
unexported, and the zero value grants nothing, so a path that forgets to
authenticate fails closed instead of quietly inheriting rights. Making it a type
rather than a convention is what turns "remember not to read the body" into
something the compiler checks -- it listed every call site that had been passing
a claim.
Authorization binds to the chain ID, never to the label. A label is an
operator's convenience and anyone can spell it; the id is the identity consensus
itself uses. An entry with no chainId is bound to no chain and grants nothing,
and Initialize names those entries at boot, because silence there would read as
"custody is configured" right up until the first release failed.
HTTP keygen and sign are gone rather than guarded. That transport authenticates
no chain, so there was no check to write -- a body can say it is the bridge as
easily as it can say anything else. Ceremonies are requested by chains over the
cross-chain path; the HTTP surface reads replicated state, which needs no
identity.
Co-authored-by: Hanzo Dev <[email protected]>
The pure-Go batch paths split every batch across exactly four goroutines, so a
machine with more cores than that ran the remainder idle and throughput stopped
improving once a batch outgrew four chunks. Measured on an 8-core node (6
allocatable), forward NTT:
batch before after
8 10,229/s 11,223/s
64 10,878/s 15,252/s
256 12,617/s 18,989/s
Worker count now follows GOMAXPROCS, capped by the batch (an empty chunk is a
scheduler slot spent on nothing) and floored at one (a constrained GOMAXPROCS
must not yield a zero-worker loop that skips the batch).
The path had no test of any kind: every other file in the package is
//go:build cgo, so a CGO_ENABLED=0 build compiled this file and ran nothing
against it -- while being the build a CPU-only node runs, and the only one that
cross-compiles to a cluster node. Adds a round-trip correctness test and
benchmarks at the same ring parameters as the cgo ones, including 4 and 8 to
straddle the sequential/parallel boundary.
Co-authored-by: Hanzo Dev <[email protected]>
An entitlement-gated chain needs a credential its operator cannot forge. The
collection that confers the entitlement lives on another chain's EVM, so the
node cannot read it and must not be asked to: a check a node runs against data
its own operator supplies is not a control.
So M threshold-signs the fact instead. chains/ownership is the frozen wire
format both sides share -- the claim's field layout, the domain tag, the signed
payload, and one verification entry point. It is dependency-free and
non-internal because the verifier is a node in another module that must never
link the M-Chain VM.
Authorizes answers authenticity and node-binding together. There is no exported
way to ask only one, because a caller that checked the signature and forgot the
binding would let any node activate on a peer's entitlement.
The domain registers additively, reusing ComputeAttestationPayload rather than
defining a parallel digest, so equivocation detection and quorum verification
already apply. The claim's block is the epoch, which makes two facts about one
node at one block contradictory while re-attesting later is how ownership moves.
KAT vectors freeze the bytes; a test asserts M's payload and the verifier's are
byte-identical, since nothing else forces those two functions to agree.
Co-authored-by: Hanzo Dev <[email protected]>
README.md led with "ThresholdVM is a Go library, not a chain" and LLM.md
instructed agents "do not turn this back into a chain, no new VM ID",
both describing an import graph (chains/mchain, chains/fchain) that does
not exist. While that stood, the VM declared a private thresholdvm vmID
matching nothing else in the stack and the node installed the plugin
under that dead name — M-Chain could not have started.
Rewritten to describe what is actually here: the M-Chain VM, its
canonical vmID and every declaration that must agree with it, the K-vs-t
rule and why the quorum is spelled "3-of-5", the consensus/node-private
state split, what makes a block verifiable, and what is still unproven
(no cross-process DKG yet, no resharing).
Co-authored-by: Hanzo Dev <[email protected]>
Signing used the key's whole participant set, so a 3-of-5 key signed with
five parties. That works, but it never exercises the quorum: a key that
was accidentally degree-3 would sign identically, which is precisely the
failure this VM exists to catch.
RunSign now selects K signers by ranking participants on
H(tag ‖ keyID ‖ digest ‖ party) and taking the first K. The selection is a
pure function of the task, so every node — signer or not — computes the
same quorum and the same ceremony id with no election. Ranking by digest
rather than taking the first K in canonical order spreads signing across
the committee instead of loading the same K parties for every transfer,
and stops an adversary steering which subset signs a message it does not
control. Nodes outside the quorum return ErrNotInQuorum and verify the
result like any other validator.
Verify deliberately does not pin WHICH K-subset signed. The signature
verifies under the group key or it does not, and an adversary who can
produce one already holds K shares, so constraining the subset buys no
security while leaving it open lets availability-aware reselection ship
later without a consensus change.
TestBridgeCustody_ThreeOfFive is the gate: five validators run a real
CGGMP21 DKG over the gossip fabric at degree 2, exactly three sign, two
decline, the signature verifies under the group key, a block carrying it
is verified and accepted by all five — including the two that never
touched the ceremony — and every node ends on the same state root.
Co-authored-by: Hanzo Dev <[email protected]>
M-Chain is the MPC/threshold-custody VM that replaces off-chain signer
clusters for bridge custody. This makes it one.
vmID — the one-way door. factory.go declared a private `thresholdvm`
literal that matched nothing else in the stack, and node/Dockerfile
installed the plugin under that name's CB58 while genesis declares
constants.MPCVMID. The two never met, so M-Chain could not have started.
VMID is now the shared constant, and TestVMID_IsCanonicalAndStable pins
the CB58 (qCURact1n41Fco...) with the list of every declaration that must
move together if it ever changes before launch.
k vs t. types.Ceremony carried Threshold/Total as bare uint16s: Validate
read Threshold as the signer count (enforcing threshold*2 > total) while
the VM read it as the polynomial degree and passed it straight to
cmp.Keygen. The same field meant K in one file and t in another, and the
two differ by one — the same silent off-by-one that produces a 4-of-5
wallet from a config saying 3-of-5. Both now carry a quorum.Policy, and t
is only ever obtained by calling Degree(). The majority floor survives as
types.HasUniqueQuorum, defined once and enforced on every key record.
State. The VM kept its custody keys in a map persisted only on a clean
Shutdown, with the key share excluded from the wire entirely, and reset
lastAccepted to a recomputed genesis block on every Initialize — so a
restart discarded the chain and came back unable to sign. State is now
persisted at the moment each fact becomes true, split into a replicated
c/ keyspace (key registry, ceremony log, blocks, height index, root) and
a node-private n/ keyspace (this validator's shares), and Initialize
resumes the accepted tip.
Blocks verify. A block carries its post-state root and every operation's
artifact. Verify re-checks a signature against the group key already in
the registry — a proposer cannot fabricate one without forging ECDSA — and
recomputes the root, so two validators that would diverge cannot both
accept. Keygen operations carry a proof of possession by the new group key
over its own registration; a participant additionally cross-checks the
declared degree against its own share, so one honest participant rejects a
mis-declared key.
Ceremonies are leaderless. The id is derived from the task
(key, digest, sorted signers), so every validator converges on the same
ceremony with no announce round and no coordinator, and the committee is
the chain's own validator set rather than an operator roster.
Address derivation was SHA-256 where every EVM chain uses Keccak-256, so
the published custody address did not belong to the group key. Funds sent
there would have been unspendable.
Deletes the duplicate signing paths and the in-memory session machinery:
one way to generate a key, one way to sign.
Co-authored-by: Hanzo Dev <[email protected]>
These VMs marshal Config via JSON and wire via hand-rolled/ZAP — nothing reads
the serialize tags (the reflection codec is gone). Pure dead metadata. Full
suites green (byte-identical), confirming no reader existed.
Co-authored-by: Hanzo Dev <[email protected]>
Kills node/vms/pcodecs from the M-Chain (MPC custody VM) — the LAST of the three
app-chain VMs blocking pcodecs deletion. bridgevm + zkvm already native.
Codec kill (wire.go): the legacy pcodecs LinearCodec keyed on serialize:"true"
tags; every mpcvm type carries only json: tags, so the old codec marshaled ZERO
fields — a Block round-tripped to all-zero and every block ID collided at
sha256(0x0000). This is a latent corruption bug the native wire FIXES. wire.go
owns Marshal/parse for Block (nested []*Operation as u32-length-list + concat
sub-object blob), ManagedKey (PartyIDs []party.ID as packed string list;
CreatedAt/LastUsedAt as UnixNano; Config/CMPConfig json:"-" excluded),
CrossChainMPCRequest, Operation. Block.ID_ is derived (computeID=sha256(Marshal))
so it is excluded from wire; recomputed on ParseBlock, set from DB key on getBlock.
ThresholdConfig (init.Config) + Genesis stay JSON at the boundary. Deleted
mpcvm/codec.go; swapped 9 call sites. Parse rejects trailing bytes. Round-trips
green (Operation/Block/ManagedKey/CrossChainMPCRequest incl empty-ops nil-slice).
Threshold signing (sign_distributed.go + transport/attest_bridge): CGGMP21/CMP
threshold-ECDSA ceremony across the M committee over native app-gossip; each
validator runs its own executor + per-session gossipRouter; honest signers derive
the SAME standard secp256k1 signature. B→M attestation seam is domain-bound and
replay-safe. Tests: transport-free seam (bridge_seam_test) + full 3-validator
in-process fabric driving the real Broadcast→Gossip→router.Deliver path
(bridge_transport_test).
3 of 3 chains VMs native. chains repo now has ZERO pcodecs imports.
Co-authored-by: Hanzo Dev <[email protected]>
Flip the Q-Chain fee gate from FlatPolicy (user-fee metering) to the
committee-only fee.NoUserTxPolicy sentinel. Finality-cert inclusion is
a validator obligation paid via P-Chain reward distribution; a fee
market on Q would make finality hostage to blockspace pricing — the
exact failure mode LP-0130 §6 eliminates. Consensus-internal cert
aggregation reaches txPool.AddTransaction directly and is unaffected.
Tests updated: policy is the NoUserTxPolicy sentinel and IssueTx
refuses every user tx (zero-fee, floor-fee, and 1000x floor) with
ErrChainAcceptsNoUserTxs. go test ./quantumvm/ passes; build + vet
clean.
Closes the top divergence from the 2026-07-03 LP-vs-code audit.
Co-authored-by: Hanzo Dev <[email protected]>
Under LP-0130's canonical roster, chains/thresholdvm/ hosts the M-Chain
VM (MPC threshold signing). The package name "thresholdvm" was the last
piece of T-Chain nomenclature still shipping in code — legacy from the
pre-LP-134 shared-substrate design. Renamed to mpcvm/ so the package
name matches the M-Chain letter and LP-7100 spec.
- chains/thresholdvm/ → chains/mpcvm/ (dir + inner files + package decl)
- Sed-replace thresholdvm → mpcvm across .go / .md / .yml / .json / .sh
- Fee/settlement primitive references now say mpcvm
- LLM.md canonical chain roster (LP-0130) already points at mpcvm/
F-Chain runtime pieces (fhe/, protocol/tfhe_keygen/,
runtime/f_chain_adapter.go) stay under mpcvm/ for now — they consume
the same shared threshold primitives. A follow-up extract to
chains/fhevm/ can happen once F-Chain runtime graduates.
Co-authored-by: Hanzo Dev <[email protected]>