5 Commits
Author SHA1 Message Date
zeekay a0e796b1fe bridgevm: the network declares its bridge limits, not the operator
The caps decide which transfers a block may carry — admissible() refuses a
transfer over the per-transfer cap and admit() refuses one over the daily cap,
both inside block verification. Reading them from the operator's chain config
meant two validators could hold different numbers and disagree about whether a
transfer was admissible, and the first one they disagreed about is a transfer
the builder keeps proposing and everyone else keeps refusing. A cap each
operator sets for themselves is not a cap.

Limits (minConfirmations, the two spend caps, the signer bond, the set size)
move to Genesis, where the network declares them once and every validator reads
the same numbers. BridgeConfig keeps only ExternalChains — endpoints and KMS
paths, how THIS node reaches the chains it was told to bridge, which cannot
change which transfers are valid. The dead BridgeFee field goes.

MaxSigners keeps its default: it is a capacity, not a risk decision.
2026-09-07 00:06:37 -07:00
zeekay 6595c4ff2e bridgevm: an empty group is not a key, and the arms nothing can reach are gone
A CMP group with no shares sums to the identity point, which marshals to a
well-formed thirty-three bytes. Handed back as the custody key it made an
empty config look like a finished keygen: the node reported itself ready to
attest and bridge_getInfo agreed, on a chain that could verify nothing.

The rest is subtraction. A settlement record now holds only the fact that
the transfer is settled, because the block that settled it is already named
by the chain. movedToday reports what it read or why it could not, instead
of reporting zero. RemoveSigner returns what happened rather than an error
it never had. Marshal cannot fail, so Bytes has no failure to handle. The
gorilla-rpc registration was a second way to serve one API. The route table
NewHTTPHandler built was a second copy of the one CreateHandlers names. The
logger is set at Initialize, so guarding every log call against its absence
was guarding against ourselves.

How often the watcher polls is data on the watcher, as the releaser's
backoff already is.
2026-08-28 11:59:59 -07:00
zeekay a8dbf849b4 bridgevm: B-Chain boots, and what it settles it remembers
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.
2026-08-28 11:42:07 -07:00
zeekay 2dee7c2aac chains: answer for the height index that is already written
GetBlockIDAtHeight said "not implemented" on I, Z and B while returning a real
answer on K, F, Q, G, S and M. The store writes the height entry in the same
commit as the block, so on these three it was a stub in front of an index that
already existed — and the engine resolves blocks by height when it bootstraps
and when it state-syncs.

Each is now one line onto that index, which cannot name a block the chain did
not accept because the two are the same commit. A test on each proves it: a
block whose commit is refused has no height entry, and gets one when it lands.
2026-08-28 09:01:29 -07:00
zeekay 0ff6430a7b bridgevm: a transfer is released only once its block is durable
Accept credited the daily volume, completed the requests, advanced
lastAcceptedID, dropped the block from the in-flight set and handed the
transfers to the release worker — which broadcasts on an external chain — and
only then wrote the block. Every one of those survived a failed write, so a
release could be in flight for a transfer no block records.

lastAcceptedID was never written at all. The tip lived in memory, reset to
genesis on every restart, and the daily volume that bounds the bridge limit
went with it.

The block now commits through the shared store, which writes it with its height
entry and the tip in one batch, and its effects — including the handoff to the
release worker — happen only afterwards. A proposal follows the engine's
preference, which the store now holds, rather than always the accepted tip.

The bridge's own state keeps its own lock. The transfers in flight, the signer
set, the chain clients and the release plumbing are not the chain's state, and
folding them into the chain's lock would say they were. The order is the
chain's lock, then the bridge's, then the registry's, and it is written down
where the locks are.

Still in memory only, and NOT changed here because it needs its own design: the
completed-bridge registry and the daily volume. A restart still forgets both,
which re-opens the daily bridge limit.
2026-08-28 08:42:29 -07:00