Skip to main content

state — wire / transfer

Package state implements the anonymous transfer state machine used by the fintech embodiments. Import path: github.com/ShywareLLC/core/shywire/state


Invariants enforced on every block​

The state machine enforces three properties before any transfer is committed:

1. len(transferRecords) == len(participants) — count-match
2. nullifier not in participants — no double-spend
3. sender.Balance >= Amount — value conservation (plaintext scaffold)

A transfer that violates any invariant causes ExecuteTransfer to return an error and the transaction to be rejected. The block is committed with the failing transaction excluded.


State lifecycle​

CheckTx(txBytes)
→ DecodeTx + Validate() // stateless only; fast path for mempool admission

FinalizeBlock(txs)
→ for each tx:
switch tx.Type:
RegisterAsset → executeRegisterAsset
Mint → executeMint
Burn → executeBurn
Transfer → executeTransfer // two-list write + invariant checks
RegisterAccount → executeRegisterAccount
→ enforce count-match: len(L1) == len(L2)

Commit()
→ flush LevelDB writes
→ return AppHash (Merkle root of state)

Query(path, data)
→ "supply/{asset_id}" → SupplyRecord
→ "account/{commitment}" → AccountRecord (balance only; no identity)
→ "transfer/{id}" → TransferRecord (List 1 only)
→ "transfers/count" → int

executeTransfer — the core state transition​

1. DecodeTx → TransferData
2. Look up sender AccountRecord (by SenderCommitment + AssetID)
3. Check sender.Balance >= Amount → ErrorInsufficientBalance if not
4. Check nullifier not in participants → ErrorDuplicateTransfer if found
5. transfer_id = H(TransferNonce)
6. Write TransferRecord to List 1 (prefix "tx:")
7. Write ParticipantRecord to List 2 (prefix "participant:")
8. Debit sender.Balance -= Amount
9. Credit recipient.Balance += Amount
10. Assert len(L1) == len(L2) → panic if violated (should be impossible)

Steps 6 and 7 are written in a single atomic LevelDB batch. Step 10 is a post-condition assertion — if it fires, it indicates a bug in the state machine, not a malformed transaction.


List storage layout​

LevelDB key space:

"tx:{transfer_id}" → TransferRecord (List 1)
"participant:{nullifier}" → ParticipantRecord (List 2)
"account:{commitment}:{asset}" → AccountRecord
"supply:{asset_id}" → SupplyRecord
"asset:{asset_id}" → AssetRecord
"val:{pubkey_b64}" → ValidatorRecord

List 1 and List 2 are stored under separate prefixes and are never queried together by the state machine. Joining them requires an off-chain process with access to the authority or audit store — the operator audit function.


Value conservation in the production circuit​

When the Pedersen commitment circuit is complete, executeTransfer will be extended to:

1–4. (same)
5. Verify ZK proof: SenderProof proves balance >= amount using Pedersen commitments
6. transfer_id = H(TransferNonce)
7. Write TransferRecord (AmountCommitment) to List 1
8. Write ParticipantRecord (nullifier) to List 2
9. Update BalanceCommitment for sender (homomorphic subtraction)
10. Update BalanceCommitment for recipient (homomorphic addition)
11. Assert len(L1) == len(L2)

The canonical runtime never observes plaintext amounts or balances in this mode. Conservation is proven by the ZK circuit, not checked by direct comparison.


Public query surface​

The following are queryable without operator credentials:

PathReturns
/supply/{asset_id}SupplyRecord — total minted, burned, supply
/transfers/countTotal number of transfers (= len(L1))
/transfer/{transfer_id}TransferRecord — amount + asset, no identity

Account balances are not in the public query surface. The sender can query their own balance by presenting their account_commitment to the operator's admin API.