This specification defines the deterministic PCASH state transition: how a node turns canonical Ethereum history into PCASH blocks. It covers canonical commitments, transaction wire formats, proof relations, the account-rule interface, genesis, fork rules, and, in Appendix A, the criterion for when a later protocol may claim monetary continuity with this chain. Except for Appendix A, a requirement belongs here only when differing implementations could derive different PCASH state, compute different protocol commitments, or disagree on whether a transaction or proof is valid. Appendix A instead defines when a later protocol may claim canonical monetary continuity with this chain; it imposes no V1 state transition.
Node, SDK, wallet, RPC, storage, relay, recovery, and application conventions are outside protocol semantics except where a section says otherwise. The reference implementation documents its choices for each subsystem: node, wallet and SDK, mempool and relay, publication catalog, source verification, and account rules. Design rationale is in the design notes.
How this document is organized. Section 1 explains every concept the protocol uses and how the pieces fit together; read it first. Sections 2–8 define data: encodings and primitives, configuration, notes, accounts, state, blocks, and proofs. Sections 9–15 define processing: §9 gives the per-block procedure and how a node reads Ethereum; §10–§13 admit and execute transactions of each type and define fees; §14 covers issuance and block settlement; §15 covers system payouts. Sections 16–17 list parameters and conformance vectors.
Conventions
- Requirement words. MUST, MUST NOT, SHOULD, and MAY are requirements on implementations. Formulas, layouts, tables, and check orders are normative. Explanatory prose says why a rule exists or how the pieces fit, and adds no requirement of its own. Sections marked as reference conventions (§15.1) are not consensus rules.
- Byte strings.
‖is byte concatenation. In a wire layout,name (n)is a field ofnbytes,u8,u16,u32, andu64are unsigned integers of that many bits, and every multi-byte integer is big-endian (u16beanduint64besay so explicitly).count × [ ... ]repeats a groupcounttimes. - Field elements and widths. Circuit values are elements of the BN254 scalar field (§2.2). Inside a hash input,
uintN(x)is the integerx, which MUST fit inNbits, used as a field element. The one exception is the contract address, whereuint160(...)explicitly takes the low 160 bits (§12.4).xHigh128andxLow128(also writtenx_hi128,x_lo128) are the high and low 128 bits of a 256-bit valuexsplit big-endian; the split is lossless and is never a reduction modulo the field. - Hashes.
poseidon(...)is the length-tagged Poseidon2 sponge of §2.3.keccak256is Ethereum's original Keccak-256, not NIST SHA3-256.H(...)in §15.1 also meansposeidon(...). - Formulas. Code blocks give exact formulas and layouts. Operand order in every hash is normative.
- L1 means Ethereum, the chain PCASH derives from.
- Canonical has three senses, distinguished by context. A block, transaction, or hash is canonical when it is part of the current canonical Ethereum chain or the PCASH chain derived from it. A field value is canonical when it is below
p(§2.2). An encoding is canonical when it is the unique valid representation of its value.
1. Overview
This section explains what a PCASH node computes and introduces every concept the rest of the specification uses. It is a guide, not the rule text: where it states a rule, the section it names is normative, and that section governs if the two ever differ.
1.1 What a node computes
PCASH is a private-value network built as a sovereign rollup on Ethereum. Ethereum supplies ordering and data availability: PCASH transactions are posted in ordinary Ethereum transactions, as calldata or blobs. Ethereum does not execute or validate them. There is no PCASH smart contract on Ethereum and no Ethereum-enforced validity. Instead, every PCASH node reads canonical Ethereum history and applies the rules in this document to it. Because the rules are deterministic, every correct node derives the same PCASH chain.
The chain starts at a configured Ethereum block, the anchor. Every canonical Ethereum block from the anchor onward produces exactly one PCASH block: Ethereum block anchor + N produces PCASH block N. No one proposes or signs PCASH blocks. A PCASH block is the result of executing, in Ethereum order, the PCASH transactions that the Ethereum block carries, then settling fees and issuance for the block, then committing the resulting state and history. When Ethereum reorganizes, the PCASH blocks derived from abandoned Ethereum blocks are discarded and the replacement blocks are derived from the common parent state (§9.5).
Native PCASH is the network's own money and its fee currency. It starts at zero: genesis has no notes, no accounts, and no spent nullifiers. New native PCASH is created only by issuance to successful qualifying transactions (§14), up to a gross total of 1,000,000,000 PCASH, with no calendar deadline. Fees move existing PCASH and never create supply. There is no public spendable balance and no public form of PCASH.
1.2 Concepts
Notes. Value exists only as notes. A note has an owner, an asset ID, and an amount, plus a secret known to whoever holds the note. Asset ID 0 is native PCASH. A nonzero asset ID names one instance of the standard fixed-supply fungible asset (§11.6), which anyone can create. The ledger never stores a note's contents. It stores a commitment built in three layers (§4.3): an owner commitment hides the owner behind the note secret; a note-body commitment adds the asset ID and amount; and an output commitment adds the note's position in the output tree. The data needed to recompute a commitment is the note's opening. Receiving a note requires no registration and no consent from the recipient.
The output tree. Every note ever created is appended, as its output commitment, to one append-only Merkle tree keyed by a sequential outputIndex (§6.2). Spent notes are never removed from it.
Nullifiers. Spending a note publishes its nullifier, a value derived from the note's output commitment, its owner's note key, and its secret (§4.3). Every valid spend of the same note produces the same nullifier, and nothing public links the nullifier to the note. Nodes keep the set of all nullifiers ever published (§6.4); a transaction that presents one already in the set fails. The same set also holds three other kinds of one-time markers: dummy nullifiers for padding slots, application markers that account rules use for replay protection, and asset-issuance markers that make each asset's issuance happen once (§11.3).
Accounts. An account is the control point for spending. It is identified by a 160-bit address and stored as one AccountState row in the account tree (§5, §6.3). The row commits the account's address, its nullifier-key commitment (a commitment to the account's note key root, nkRoot, normally secret and fixed forever at creation), its rule set, its kind, a revision counter, and an optional receive key that senders can use to encrypt note openings to it. There are two kinds:
- A user account (
USER, kind0) belongs to an Ethereum address. The ECDSA key for that address authorizes its creation (§12.2), because no rules exist before it. After creation, only the account's installed rules can authorize its spending and its updates (§12.3). There is one user account per address. - A contract account (
CONTRACT, kind1) has no key. Its address is derived from its contents, its rules are fixed at creation, and it can never be updated (§12.4). A user account sponsors its creation. Afterwards a contract can hold native and asset notes, pay native fees, and pay out directly, without any user account initiating its actions.
A note's owner is an account address. Spending existing value requires authorization from the owning account's installed rules, so it requires a registered account. One case needs no registered account: a transaction whose inputs are all padding spends nothing and exercises no account's authority (§11.4).
Actions. An action is the complete content that one transaction asks account rules to authorize: for type 0x00, its inputs, outputs, fee, issuance destination, and time bounds; for an account update or a contract creation, the account change together with its fee. Every admitted transaction except a user-account creation carries exactly one action, so this document counts qualifying actions and qualifying transactions interchangeably.
Account rules and applications. An account's rule set is a sparse Merkle tree of up to 256 entries (§5.2). An entry commits the hash of one circuit's verification key, called an application, plus a configuration commitment that the application interprets. A user account's entries are called policies; a contract account's entries are called functions. An application decides whether to authorize an action. It returns five values to the protocol, the permanent account-rule statement (§8.2): its configuration commitment, a commitment to the exact action it approves, the recent block it read context from, the account it acts for, and the chain ID. PCASH attaches no meaning to an entry beyond that statement: there are no capability flags. Anyone can write an application, and a weak one endangers only the accounts that install it.
Proofs. Every transaction type except user-account creation carries one CHONK zero-knowledge proof (§8.1). The proof folds a stack of circuits: one or more applications, then a protocol kernel that enforces the ledger rules (note membership and ownership, nullifier derivation, output construction, value conservation, asset rules) and checks that the applications approved the exact action, then a finalizer that accepts only protocol kernels, then a wire circuit that exposes the transaction's public inputs. Nodes verify only the final proof, against a wire verification key pinned for the transaction type (and, for type 0x00, for its profile). A type 0x00 proof uses one of three private kernels: the direct kernel, where one account owns every real input; the native fee-payer kernel, where a user account pays the native fee of a contract account's asset action; and the issuance kernel, which creates a new asset (§11.5). Types 0x02 and 0x03 each have their own kernel. CHONK is the client-side proof-folding scheme of the Barretenberg proving system, and Mega and MegaZK are its circuit and verification-key formats. A node verifies proofs by calling the pinned Barretenberg release (§8.1, §16); it does not implement the scheme itself. Which account acted, which rule approved, which notes were spent, and what amounts moved stay private.
Transactions. A transaction is one byte string, txBytes, with a common envelope naming its type and its PCASH chain ID (§10.1). There are four types:
0x00, a private-value transaction (§11): spends and creates notes of native PCASH, of one private asset, or both. It uses one of two fixed profiles: S, with 4 input slots and 5 output slots, or L, with 32 of each.0x01, user-account creation (§12.2): registers a user account. It carries no proof and pays no PCASH fee.0x02, contract-account creation (§12.4): publishes a contract account, sponsored and paid for by a user account.0x03, user-account update (§12.3): replaces a user account's rule set or receive key, paid for from the account's own notes.
Types 0x00, 0x02, and 0x03 all contain the same private-value section (§10.3): the proof, its public inputs, and one fixed-size data item per output. Public inputs include the nullifier of every input slot and the note-body commitment of every output slot, the recent block the proof used, the open fee, the issuance destination, and the time window. Every slot is always filled: unused slots hold padding (class DUMMY), which is indistinguishable from real slots from outside the proof, so the public shape reveals only the profile.
Recent block. Every proof names one recentPcashBlockHash. A node accepts the transaction only while that block is canonical and at most RECENT_PCASH_BLOCK_WINDOW_SECONDS (one hour) older than the block being derived (§10.6). Inside the proof, the circuits open whatever state they need beneath that single hash: the output tree, the account tree, Ethereum history, or earlier PCASH blocks (§8.4). Nothing about which parts were opened is public.
Batches, posters, and relaying. An Ethereum transaction sent to the configured INBOX_ADDRESS is an Inbox transaction. Its carrier, either its calldata or the payload decoded from its blobs, holds one batch: a short header followed by a list of items, each item a candidate txBytes (§9). Anyone may post a batch; there is no sequencer. The batch header names the poster's poster owner commitment, the recipient of open fees and open-route issuance, which is not the Ethereum address that sent the carrier.
Admission, outcomes, and fatal errors. An item is admitted as a transaction only if its bytes decode completely as a transaction type active at the Ethereum block's timestamp and its envelope names this chain (§10.2). Non-admitted items are skipped and produce no result. Every admitted transaction executes in order against the state left by every earlier successful transaction, and receives exactly one outcome (§10.5): Succeeded, or the first failed check in a fixed order. A failed transaction changes no state, but its exact bytes and outcome are part of the block (§7.3, §7.8). When a node cannot evaluate something at all, such as a missing blob or a verifier that cannot run, that is a fatal derivation error: derivation halts at that Ethereum block and retries, and never turns the problem into an outcome.
Fees and issuance destinations. Every private-value section declares an openFeeAmount, existing native PCASH paid publicly to the poster of the batch that successfully includes it, and an issuanceOwnerCommitment, where any issuance goes (§13). A nonzero issuance commitment names a one-time destination fixed by the proof; zero sends issuance to the successful poster. A positive open fee and a nonzero issuance commitment are mutually exclusive. A wallet can also pay a chosen relayer privately, as an ordinary encrypted output.
Issuance and settlement. After all transactions in a block have executed, the node counts the successful qualifying transactions (every successful type 0x00 transaction), computes one issuance amount rewardEach that each of them earns (§14.2), and appends system payout notes: first designated issuance in transaction order, then one payout per poster combining that poster's open fees and open-route issuance. System payouts are ordinary native notes with public amounts and hidden owners (§15). Because settlement appends these notes after the block's transactions, each transaction reserves room in the output tree for the payout notes it could cause, and fails with OutputCapacity if that room is not available (§14.3).
State, blocks, and history. After each block, the state is summarized by a StateRoot (§6.5) committing the output tree, the account tree, the nullifier set, gross issuance, and the issuance controller's lag. The block header (§7.5) commits the parent block, number, timestamp, the block's transaction results, the StateRoot, a tree of every Ethereum block since the anchor with its event logs (§7.2), and a tree of every earlier PCASH block (§7.6). The block hash is the root of the header tree.
1.3 The derivation loop
The normative block procedure is at the start of §9. This outline shows the whole of derivation at a glance, with the section that defines each step.
parent = empty genesis state and no parent block (§7.7)
for N = 0, 1, 2, ...:
B = canonical Ethereum block GENESIS_L1_BLOCK_NUMBER + N
rules = fork rules active at B.timestamp (§3.2)
monetary inputs from the parent state and B.timestamp (§14.1)
for each top-level Ethereum transaction in B, in index order:
if it is not sent to INBOX_ADDRESS: continue (§9.1)
candidate batch = its calldata, or its decoded blobs (§9.1–9.2)
(unavailable blobs are a fatal error; a malformed carrier is ignored)
frame the batch into items, or ignore it whole (§9.3)
for each item, in order:
if the item is not admitted: record why; continue (§10.2)
outcome = first failing check, else Succeeded (§10.5, §11–§12)
if Succeeded: apply its effects to the working state
record TransactionResult and keep the exact bytes (§7.3, §7.8)
settle the block: rewardEach, system payouts, issuance, lag (§14.2–14.3)
append Ethereum block leaf N; build StateRoot and header (§6.5, §7.2, §7.5)
commit the block atomically, then append history leaf N (§14.4, §7.6)
1.4 Where terms are defined
| Term | Defined in |
|---|---|
anchor, GENESIS_L1_BLOCK_NUMBER, CHAIN_ID, INBOX_ADDRESS |
§3.1 |
| fork, activation timestamp | §3.2 |
ownerId, nkRoot, note key noteNk |
§4.1–4.2 |
| owner, note-body, and output commitments; nullifier | §4.3 |
AccountState, account kinds, revision, receive key |
§5.1 |
| account-rule entry, rule set, policy, function | §5.2 |
authorizationAccountId |
§5.3 |
| output tree, account tree, nullifier set, StateRoot | §6 |
| Ethereum history tree, normalized logs | §7.2 |
TransactionResult, contentHash, transactionId |
§7.3–7.4 |
| block header, block hash, history tree, genesis | §7.5–7.7 |
| CHONK proof, application, kernel, finalizer, wire circuit | §8.1 |
| account-rule statement, action commitment | §8.2–8.3 |
| Inbox transaction, carrier, batch, poster owner commitment | §9 |
| envelope, admission, private-value section, outcome | §10 |
| profile, slot class, transaction commitment | §11.1–11.3 |
| asset ID, standard fungible asset | §11.6 |
| open fee, issuance destination, private relay payment | §13 |
baseline, offer, allowance, lag, rewardEach |
§14 |
| system payout note | §15 |
2. Encodings and Primitives
2.1 Integer encodings
Multi-byte integers in PCASH wire formats are unsigned and big-endian, except for the signed monetary lag of §14 and §6.5. Field elements are encoded as 32-byte big-endian integers. Ethereum and PCASH account addresses are 20 bytes, widened to uint160 inside field arithmetic.
Value ranges:
CHAIN_IDis a PCASH network identifier< 2^32.L1_CHAIN_IDis the EIP-155 identifier of the Ethereum chain that carries PCASH data.- A PCASH block number is
< 2^64. - Every note amount and every native-PCASH fee MUST be
< 2^128. Asset input and output sums, burn amounts, and an asset's original issuance MUST be< 2^131. - Gross native issuance, Q128 rates, and the signed Q128 lag use the widths prescribed by §14. Monetary arithmetic never reduces values modulo the proof field.
- Native amounts use 18 decimal places: one PCASH is
10^18atomic units.
2.2 Field
All circuit arithmetic is over the BN254 scalar field:
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
= 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001
Every public input interpreted as a field element MUST satisfy x < p. A value < p is called canonical.
2.3 Poseidon2 hash
PCASH uses Poseidon2 over the BN254 scalar field for its circuit-friendly commitments. (The nullifier set, the transaction-results root, output-data hashes, and receive-key digests use Keccak-256 instead, as defined where they appear.) Parameters: state width t = 4, rate 3, capacity 1, S-box x^5, R_F = 8 full rounds, R_P = 56 partial rounds, and matrices and round constants exactly as in the PCASH V1 source asset poseidon2_bn254_t4_rf8_rp56.json. In that asset, M_E = externalMatrix, D_I = internalDiagonal, and RC = roundConstants; every asset value is interpreted as a field element.
For a state s of four field elements, define extern(s) = M_E · s. Define intern(s) by S = s[0] + s[1] + s[2] + s[3] and intern(s)[i] = S + D_I[i] × s[i]. The permutation is:
- Set
state = extern(state). - For full rounds
r = 0..3, addRC[4r + i]to eachstate[i], applyx^5to every element, then applyextern. - For partial rounds
r = 0..55, addRC[16 + r]tostate[0], applyx^5only tostate[0], then applyintern. - For full rounds
r = 0..3, addRC[72 + 4r + i]to eachstate[i], applyx^5to every element, then applyextern.
The first full-round block therefore consumes RC[0..15], the partial rounds consume RC[16..71], and the second full-round block consumes RC[72..87].
Every poseidon(...) in this specification is the following length-tagged sponge:
poseidon(x_1, ..., x_N) = Poseidon2_sponge(x_1, ..., x_N)
Poseidon2_sponge: let N be the number of inputs. Initialize the 4-element state to [0, 0, 0, N << 64]. If N = 0, apply one permutation and return state element 0. Otherwise, process the inputs in chunks of 3, zero-padding the final chunk: add each chunk element into state[0..2] mod p, then apply one permutation. Return state element 0. There is no additional final permutation after the last absorbed chunk. Implementations MUST use this length-tagged sponge; the bare permutation with capacity 0 produces different values. Known-answer values and worked multi-chunk cases are in vectors/poseidon2_vectors.json.
2.4 Merkle trees
Most PCASH commitments are binary Merkle trees whose internal nodes are poseidon(left, right). Empty leaves are 0; EMPTY[h + 1] = poseidon(EMPTY[h], EMPTY[h]); the empty root of a depth-D tree is EMPTY[D].
A membership proof for a depth-D tree is D sibling nodes, listed from the leaf level upward. At height h, bit h of the tree key (least significant bit first) selects the side: bit 0 means the current node is the left child.
cur = leaf
idx = key
for h in 0..D-1:
if (idx & 1) == 0: cur = poseidon(cur, siblings[h])
else: cur = poseidon(siblings[h], cur)
idx = idx >> 1
root = cur
The circuit-facing trees are listed below; each is defined in the section cited. The state and header trees absorb a domain tag at every node instead of using this plain node function. The nullifier set (§6.4) and the transaction-results tree (§7.3) are Keccak trees and are defined separately.
- Output-commitment tree (§6.2): depth 40, append-only, keyed by
outputIndex(uint64 < 2^40, sequential from0). It contains indexed commitments to ordinary note bodies, including system payouts and padding bodies. - Address Account State Tree (§6.3): depth 40, mutable, keyed by
accountIndex(uint64 < 2^40, assigned sequentially from0when an address's account is first created). - State commitment tree (§6.5): depth
STATE_TREE_DEPTH = 4, keyed by protocol-assigned integers0–15; every internal node absorbsSTATE_COMMITMENT_DOMAIN. - Block-header tree (§7.5): depth
BLOCK_HEADER_TREE_DEPTH = 4, keyed by protocol-assigned integers0–15; every internal node absorbsBLOCK_HEADER_DOMAIN. - Ethereum blockhash tree (§7.2): depth
ETHEREUM_BLOCK_TREE_DEPTH = 40, append-only, keyed byblockNumber − anchorNumber. - PCASH history tree (§7.6): depth
HISTORY_TREE_DEPTH = 40, append-only, keyed by PCASH block number, committing only completed prior blocks. - Account-rule-set tree (§5.2), a user account's policy set or a contract account's function set: depth
ACCOUNT_RULE_SET_DEPTH = 8, sparse. Slot assignment is not protocol-constrained. Empty rootEMPTY[8].
2.5 Domain tags
Most Poseidon commitments take a domain tag as their first operand, so that a value computed for one purpose cannot be reinterpreted as another. Each tag is derived from a context string:
DOMAIN = uint256(keccak256("pcash.v1." || context_string)) mod p
The prefix is the literal nine-byte ASCII string pcash.v1., including its trailing dot. Interpret the digest as a big-endian unsigned integer before reducing it modulo p. assets/gen_domain_tags.mjs is the deterministic generator, and assets/domain_tags.json is the generated source asset consumed across implementations.
| Constant | Context string |
|---|---|
NULLIFIER_KEY_COMMITMENT_DOMAIN |
nullifier_key_commitment |
NOTE_NULLIFIER_KEY_DOMAIN |
note_nullifier_key |
ADDRESS_OWNER_DOMAIN |
address_owner |
DUMMY_OWNER_DOMAIN |
dummy_owner |
PAYOUT_SEED_DOMAIN |
payout_seed |
PAYOUT_TX_CONTEXT_DOMAIN |
payout_tx_context |
PAYOUT_NOTE_SECRET_DOMAIN |
payout_note_secret |
PAYOUT_BATCH_CONTEXT_DOMAIN |
payout_batch_context |
OWNER_COMMITMENT_DOMAIN |
owner_commitment |
NOTE_BODY_COMMITMENT_DOMAIN |
note_body_commitment |
OUTPUT_COMMITMENT_DOMAIN |
output_commitment |
NULLIFIER_DOMAIN |
nullifier |
DUMMY_NULLIFIER_DOMAIN |
dummy_nullifier |
APPLICATION_NULLIFIER_DOMAIN |
application_nullifier |
IMPLICIT_NOTE_SECRET_SEED_DOMAIN |
implicit_note_secret_seed |
TRANSACT_NOTE_SECRET_DOMAIN |
transact_note_secret |
TX_COMMITMENT_GLOBAL_DOMAIN |
tx_commitment_global |
TX_COMMITMENT_INPUT_DOMAIN |
tx_commitment_input |
TX_COMMITMENT_OUTPUT_DOMAIN |
tx_commitment_output |
TX_COMMITMENT_DOMAIN |
tx_commitment |
ASSET_ID_DOMAIN |
asset_id |
STANDARD_FUNGIBLE_LAW_ID |
standard_fungible_law |
STANDARD_FUNGIBLE_CONFIGURATION_DOMAIN |
standard_fungible_configuration |
FIXED_ISSUANCE_SECRET_COMMITMENT_DOMAIN |
fixed_issuance_secret_commitment |
ASSET_ISSUANCE_REPLAY_DOMAIN |
asset_issuance_replay |
ASSET_ISSUANCE_NULLIFIER_DOMAIN |
asset_issuance_nullifier |
PRIMARY_FEE_PAYER_ACTION_DOMAIN |
primary_fee_payer_action |
NATIVE_FEE_ACTION_DOMAIN |
native_fee_action |
CONTRACT_FEE_SPONSORSHIP_ACTION_DOMAIN |
contract_fee_sponsorship_action |
STATE_COMMITMENT_DOMAIN |
state_commitment |
BLOCK_HEADER_DOMAIN |
block_header |
GENESIS_EXTRA_DATA_DOMAIN |
genesis_extra_data |
ETHEREUM_BLOCK_LOGS_DOMAIN |
ethereum_block_logs |
ETHEREUM_LOG_DOMAIN |
ethereum_log |
ETHEREUM_LOG_TOPICS_DOMAIN |
ethereum_log_topics |
ETHEREUM_LOG_DATA_DOMAIN |
ethereum_log_data |
HISTORY_LEAF_DOMAIN |
history_leaf |
AUTHORIZATION_ACCOUNT_DOMAIN |
authorization_account |
ACCOUNT_UPDATE_AUTHORIZATION_ACTION_DOMAIN |
account_update_authorization_action |
ACCOUNT_UPDATE_FUNDED_ACTION_DOMAIN |
account_update_funded_action |
ACCOUNT_STATE_COMMITMENT_DOMAIN |
account_state_commitment |
ACCOUNT_RULE_ENTRY_DOMAIN |
account_rule_entry |
CONTRACT_ADDRESS_DOMAIN |
contract_address |
Keccak-based commitments (the nullifier set, the transaction-results root, and transaction IDs) use their own UTF-8 labels, given where they are defined.
3. Network Configuration and Forks
3.1 Configuration and chain identity
A PCASH network is fixed by its derivation configuration:
CHAIN_ID: the PCASH network identifier. Different chain instances and genesis anchors MUST use distinct nonzeroCHAIN_IDvalues below2^32.L1_CHAIN_ID: the EIP-155 chain ID of the Ethereum chain that carries the data.INBOX_ADDRESS: the Ethereum address whose incoming transactions carry PCASH batches (§9.1).GENESIS_L1_BLOCK_NUMBERandGENESIS_L1_BLOCK_HASH: the anchor, the Ethereum block that produces PCASH block0.
The anchor is elapsed issuance time zero and produces block 0; there is no separate, delayed monetary activation. The production launch identity is fixed before genesis; local and test identities are assigned separately. The public chain identity is the pair (chainId, genesisHash), where genesisHash is the ordinary block hash of block 0 (§7.7).
3.2 Forks
A fork is a human-readable name and an Ethereum header activation timestamp compiled into a software release. Aster is the launch rule set and is active from genesis. The compiled fork schedule is empty: no later fork is scheduled. A future release may add one. Fork names and timestamps are not hashed into genesisHash, StateRoot, or a separate upgrade identity.
For an Ethereum block with header timestamp t, derivation applies the rules of the latest compiled fork whose activation timestamp is <= t. Activation is by timestamp rather than height so that the scheduled wall-clock boundary is predictable. Historical replay uses each block's actual timestamp, so a reorganization reselects fork rules from the same deterministic input used during initial derivation.
At each activation timestamp, the fork definition fixes:
- the complete accepted batch-version set, as ascending unique bytes (§9.3);
- the transaction-type and verification-key mapping (§10.1);
- whether the fork declares a monetary trust reset (Appendix A).
Each assigned transaction type permanently fixes its wire payload shape, verifier, and type-specific interpretation, and historical entries always decode under that meaning. A fork may activate or retire a type for new blocks. Each assigned batch-version byte likewise permanently fixes the framing parser and structural bounds used to recover an ordered list of typed transactions; a different framing rule requires a different batch version. Historical derivation selects the exact rules in force at each Ethereum block's timestamp.
4. Notes and Nullifiers
A note is a unit of private value. The ledger stores only commitments to notes; whoever holds a note's opening can prove things about it without revealing it. This section defines how notes name their owners, how they are committed, and how spending them produces nullifiers.
4.1 Owners
A note's owner is a 160-bit PCASH account address. Every note binds an ownerId derived from that address:
ownerId = poseidon(ADDRESS_OWNER_DOMAIN, uint160(address))
For a user account, address is the Ethereum-style secp256k1 address whose key authorizes the account's creation (§12.2); after that, the account's installed applications authorize its private-value transactions and updates (§8.2). For a contract account, address is derived from the contract's immutable contents (§12.4) and has no corresponding key. Both kinds map to the same kind of ownerId.
Padding output slots use a fixed placeholder owner that no account controls:
DUMMY_OWNER_ID = poseidon(DUMMY_OWNER_DOMAIN, 0xdead)
4.2 Nullifier keys
Each account binds one nonzero BN254 scalar, its nullifier-key root nkRoot, through a public commitment stored in its account state (§5.1). nkRoot is secret unless a contract chooses to publish it. The note key used in note nullifiers is derived from it:
nullifierKeyCommitment = poseidon(NULLIFIER_KEY_COMMITMENT_DOMAIN, nkRoot) // published in every account
noteNk = poseidon(NOTE_NULLIFIER_KEY_DOMAIN, nkRoot) // note nullifiers
Whoever spends the account's notes knows nkRoot. A contract account may also publish its nkRoot when its immutable publishesNkRoot flag is set (§12.4). nullifierKeyCommitment is immutable once written: account creation supplies it and no update can change it (§12.3).
Disclosing nkRoot or noteNk reveals nullifier material but does not by itself authorize a spend. A real note's nullifier also requires the note's secret, and every transfer still requires a valid authorization proof.
4.3 Note commitments and nullifiers
A note's opening is its owner (ownerId), a nonzero secret noteSecret, its assetId, and its amount. Its commitment is built in three layers. The owner commitment hides the owner behind the secret. The body commitment adds what the note holds. The output commitment adds where the note sits in the output tree, which the node assigns when the transaction succeeds; separating the body from its position lets a wallet prove a transaction before knowing where its outputs will land.
ownerCommitment = poseidon(OWNER_COMMITMENT_DOMAIN, ownerId, noteSecret)
noteBodyCommitment = poseidon(NOTE_BODY_COMMITMENT_DOMAIN, ownerCommitment, assetId, amount)
outputCommitment = poseidon(OUTPUT_COMMITMENT_DOMAIN, noteBodyCommitment, outputIndex)
nullifier = poseidon(NULLIFIER_DOMAIN, outputCommitment, noteNk, noteSecret)
dummyNullifier = poseidon(DUMMY_NULLIFIER_DOMAIN, dummyInputSeed, inputIndex)
applicationNullifier = poseidon(
APPLICATION_NULLIFIER_DOMAIN,
authorizationAccountId, selectedEntryCommitment, replayId)
The operands are: outputIndex, the note's position in the output tree; inputIndex, the zero-based input slot index; dummyInputSeed, a private nonzero seed chosen once per action; selectedEntryCommitment, the accountRuleEntryCommitment (§5.2) of the rule entry selected to authorize the action; and replayId, a value defined by that rule (§11.3).
nullifier is the spend marker of a real note: it binds the exact output (and so its position), the owner's note key, and the note secret, so it is the same for every valid spend of that note and different for every other note. dummyNullifier and applicationNullifier are the markers of padding input slots and of account-rule replay protection; §11.3 defines when each is used. All three, together with asset-issuance markers (§11.6), share one spent set (§6.4).
assetId = 0 identifies native PCASH. A nonzero assetId identifies one instance of the standard fungible asset (§11.6).
Receiving native or asset notes requires neither registration nor the recipient's consent: a note's owner commitment needs only the owner's address, and nothing checks that the address has an account.
4.4 Real notes and padding
- Every real input's
noteSecretis nonzero. - A padding (dummy) input slot has no input commitment and no note secret. The protocol kernel derives its nonzero indexed nullifier from one private nonzero
dummyInputSeedper action. Because the transaction commitment (§11.3) includes every input slot's class, commitment, and nullifier, changingdummyInputSeedproduces a different authorized transaction. - The note body has exactly the three semantic operands shown: owner commitment, asset ID, and amount. There is no note-state operand. Applications may commit opaque private material inside
noteSecret, but consensus exposes no standardized, independently openable state field and no state-transition interface for it. - For every real output, the protocol kernel receives an opaque nonzero
ownerCommitmentand constructs the body under the selected asset and amount. Consensus does not require the transaction's prover to know the opening of a real output's owner commitment. Recipient acceptance, delivery, and derivation of output secrets are application and wallet policies. - A padding output has a private nonzero
dummySecretand the canonical owner commitmentownerCommitment = poseidon(OWNER_COMMITMENT_DOMAIN, DUMMY_OWNER_ID, dummySecret). - A native-only action has one native input owner. An asset-bearing action has one input owner across every real native and asset input, except on the native fee-payer path (§11.7).
5. Accounts
An account is the durable control point for spending. Notes name their owner by address; the account row stored under that address says which rules may authorize spending and what the account's permanent key commitment is. Accounts are created by transaction types 0x01 and 0x02 and updated by type 0x03 (§12).
5.1 Account state
Each account is one AccountState row, committed as one leaf of the account tree (§6.3):
accountStateCommitment = poseidon(ACCOUNT_STATE_COMMITMENT_DOMAIN,
uint160(address),
nullifierKeyCommitment,
accountRuleSetCommitment,
accountKind,
accountRevision,
receiveKeyPresent,
receiveKeyHashHigh128,
receiveKeyHashLow128)
This is the only account state commitment. It commits the account's immutable identity, its spending rules, its kind, its revision, and its receive-key state. The complete row and its leaf always change together as one transition. The fields are:
address: the account's 160-bit address.nullifierKeyCommitment: the commitment to the account'snkRoot(§4.2). It commits the account's key hierarchy and is unrelated to the root of the global spent-nullifier set (state keys6and7, §6.5). Immutable: creation supplies it, and every update derives the existing value from live state.accountRuleSetCommitment: the root of the account's rule set (§5.2), orEMPTY[8]. A user account supplies it as its mutablepolicySetCommitment; a contract account supplies it as its immutablefunctionSetCommitment.accountKind:0for a user account,1for a contract account. Every reconstruction of an account state commitment enforces this Boolean range, and each relation separately restricts which kind it accepts.accountRevision: a nonzerou32. Consensus sets it to1at creation and increments it exactly once on each accepted user-account update. It is never serialized by creation.receiveKeyPresent(also writtenpresent): Boolean.receiveKeyHashHigh128,receiveKeyHashLow128: the exact big-endian high and low 128-bit halves ofreceiveKeyHash, never a field reduction.
A receive key is an ML-KEM-768 public key of exactly ML_KEM768_PUBLIC_KEY_LEN = 1,184 bytes that senders may use to encrypt note openings to the account. State stores only its digest:
receiveKeyHash = keccak256(mlKem768PublicKey)
If present = 0, both digest limbs are zero and the account has no receive key. If present = 1, the digest is the Keccak hash of the exact key bytes introduced by creation or by the latest update REPLACE operation, and a later update may retain it with KEEP (§12.3); either limb may numerically be zero. The raw key is not part of intrinsic state. Raw keys remain recoverable from their creation or latest REPLACE body in Ethereum data; a node may store them beside intrinsic state.
A user account (accountKind == 0) is created by type 0x01 and updated by type 0x03; there is at most one per Ethereum address. Its creation is authorized by that address's ECDSA key; afterwards it is governed only by its installed rule set. A contract account (accountKind == 1) is created by type 0x02, is keyless, derives its address from its contents, and is never updated. A contract's immutability rests on its leaf commitment together with the write rules, not on the collision resistance of the address derivation.
5.2 Account-rule entries and rule sets
An account's rule set is a sparse depth-8 Poseidon tree (§2.4), so it holds at most 256 entries. Each entry names one authorization circuit, called an application, by the hash of its verification key, together with configuration that the application interprets:
accountRuleEntryCommitment = poseidon(ACCOUNT_RULE_ENTRY_DOMAIN, implementationVkHash, configurationCommitment, commitmentBlinder)
implementationVkHashis the canonical Poseidon2 hash of the application's 163-field Mega verification key (§8.1). It may commit to any Mega circuit that returns the permanent five-field account-rule statement (§8.2). The verification key itself is a private proving witness and is never published.configurationCommitmentis opaque to the protocol. The selected application interprets it: it might commit to a signer, a threshold, a passkey, a recovery condition, or anything else.commitmentBlinderseparates otherwise identical entries in a user account, whose entries are never published. A wallet SHOULD draw it unpredictably, so that an installed policy cannot be recognized from a known template. A contract function uses the canonical blinder0(§12.4), because a contract's entries are published in full in Ethereum data and a published blinder hides nothing.
The entry contains no capability declaration: nothing in it says what kinds of action the application approves. The protocol kernel authenticates the account and the exact action commitment; the application decides which action shapes and conditions it accepts. A narrow application MUST reconstruct enough of the domain-separated action to fail closed for every shape it does not support. A deliberately universal application may treat the action commitment as opaque.
The two account kinds expose the same entries under different names:
| Account kind | An account rule is exposed as | Selected by |
|---|---|---|
| User account (§12.2) | a policy | its slot → the entry index |
| Contract account (§12.4) | a function | its selector → the entry index |
Duplicate entries add no power. To revoke a user policy, the account must commit a new rule set that excludes every index containing it.
5.3 Stable account identity
A proof ties the notes it spends to the account that authorizes them through one derived identity. For a selected account:
ownerId = poseidon(ADDRESS_OWNER_DOMAIN, uint160(address))
nullifierKeyCommitment = poseidon(NULLIFIER_KEY_COMMITMENT_DOMAIN, nkRoot)
authorizationAccountId = poseidon(AUTHORIZATION_ACCOUNT_DOMAIN, ownerId, nullifierKeyCommitment)
authorizationAccountId uses the account's public nullifierKeyCommitment. It does not change when the account's rule set, receive key, or revision changes. It is distinct from any signing key inside an application, so an account can delegate authority or rotate signers without redirecting its change outputs.
6. State
6.1 Components
PCASH state is what transactions and block settlement change. The intrinsic V1 transition state consists of:
- the append-only output-commitment tree and its count (§6.2);
- gross native issuance and the signed post-settlement Q128 issuance lag (§14);
- the mutable Address Account State Tree and its count, with each account address assigned one sequential index and one complete current
AccountStaterow (§6.3); - the spent-nullifier sparse set (§6.4).
Canonical PCASH headers, exact L1Origin records (§7.1), the Ethereum blockhash tree, and the PCASH history tree are derived chain data, not intrinsic StateRoot components (§7). Raw transaction bodies and output data are likewise not StateRoot components.
6.2 Output tree
The output tree (depth 40, §2.4) holds the output commitment of every note ever created, at sequential outputIndex values starting from 0. Outputs are appended as follows:
- Every successful private-value section appends its selected profile's
Oordinary output commitments, one per output slot, including padding. - After the block's ordinary outputs, positive system payouts append additional ordinary native notes: designated issuance in canonical transaction order, then poster payouts in first-positive-contribution order (§14.3).
- User-account creation appends no output. Contract creation and user-account update each carry a five-output fee section and may also produce an open-fee payout note.
A transaction fails with OutputCapacity if its outputs, the payout notes it could cause, and the payout notes reserved by earlier successful transactions in the block could together exceed 2^40 outputs (§14.3).
6.3 Account tree
The Address Account State Tree (depth 40, §2.4) holds one accountStateCommitment (§5.1) per account at its accountIndex. The first account created receives index 0, and each later creation receives the next index. Creation (types 0x01 and 0x02) fails with AccountCapacity unless accountCount + pendingNewAccounts + 1 <= 2^40, where accountCount is the count at the start of the block and pendingNewAccounts is the number of accounts created earlier in the block. Nodes also keep an address-to-index mapping and each account's complete current row; the row and its leaf are always written together.
6.4 Nullifier set
The spent-nullifier set is a depth-256 binary Keccak sparse tree keyed directly by the exact nonzero canonical 32-byte BN254 encoding n of each spent nullifier. The key is never reduced or rehashed into a namespace:
EMPTY[0] = keccak256(utf8("pcash.nullifier.empty.v1"))
EMPTY[h + 1] = keccak256(utf8("pcash.nullifier.node.v1") || EMPTY[h] || EMPTY[h])
spentLeaf(n) = keccak256(utf8("pcash.nullifier.leaf.v1") || n)
node(l, r) = keccak256(utf8("pcash.nullifier.node.v1") || l || r)
Sibling arrays run from the leaf upward. Level h uses bit h of n, interpreted as a big-endian unsigned integer, so leaf-up traversal consumes the least-significant bits first. As in §2.4, bit 0 means the current node is the left child: node(current, sibling); bit 1 gives node(sibling, current). The empty root is EMPTY[256]. Note, dummy, asset-issuance, and application nullifiers all share this set. The set has no sequential append position, so there is no nullifier count.
6.5 StateRoot
After every completed transition, the state is summarized by a fixed depth-4 Poseidon tree whose leaves are the state components:
| Key | Value |
|---|---|
| 0 | outputRoot |
| 1 | outputCount |
| 2 | issuedSoFar |
| 3 | lagHigh128 |
| 4 | accountStateRoot |
| 5 | accountCount |
| 6 | nullifierRootHigh128 |
| 7 | nullifierRootLow128 |
| 8 | lagLow128 |
| 9–15 | 0, unassigned |
- Output and account counts are in
[0, 2^40]; occupied indices are below the count and fituint40. Counts are committed because a Merkle root authenticates contents but not the next free append position. - The lag limbs are the big-endian high and low halves of the canonical signed 256-bit two's-complement encoding of
lagQ(§14.1). - The nullifier root is split losslessly into big-endian 128-bit limbs.
stateNode(left, right) = poseidon(STATE_COMMITMENT_DOMAIN, left, right)
StateRoot = root(stateLeaves, depth = 4, node = stateNode)
StateRoot deliberately excludes Ethereum provenance, PCASH history, source configuration, the header timestamp, raw material, and lookup indexes. Keys 9–15 are literal zero leaves when constructing this root. They preserve the typed opening formula used by immutable circuits; no current circuit may treat an unassigned leaf's zero value as a protocol fact.
7. Blocks and History
A PCASH block records what one Ethereum block did to PCASH: the outcome of every admitted transaction, the resulting state, and authenticated history. Its hash is the root of a small typed header tree, so a circuit can open any one header fact without the others.
7.1 One PCASH block per Ethereum block
Derivation produces one canonical PCASH block for every canonical Ethereum block from the anchor onward. The anchor produces PCASH block 0, and each later block has number parent.number + 1. PCASH block N therefore corresponds to Ethereum block GENESIS_L1_BLOCK_NUMBER + N. No earlier Ethereum block produces a PCASH block. A PCASH block's timestamp is its Ethereum block's header timestamp.
Each block has an associated L1Origin = (blockNumber, blockHash, blockTimestamp, ethereumLogsRoot, ethereumLogCount), the exact preimage of its Ethereum history leaf (§7.2). L1Origin records which Ethereum input produced the block as derivation provenance. It is not a header field and does not enter the block hash. Explanatory failure operands and diagnostic exclusion strings are not protocol inputs either.
7.2 Ethereum history tree and normalized logs
Every PCASH block commits a record of every canonical Ethereum block since the anchor, including each block's event logs, so that account rules can prove Ethereum facts (§8.4).
Derivation maintains a depth-ETHEREUM_BLOCK_TREE_DEPTH = 40 append-only Poseidon Merkle tree over canonical Ethereum blocks. Leaf i commits Ethereum block anchorNumber + i, where the configured anchor is leaf 0:
ethereumBlockLeaf_i = poseidon(
ETHEREUM_BLOCK_LOGS_DOMAIN,
uint64(blockNumber),
blockHashHigh128,
blockHashLow128,
uint64(blockTimestamp),
logsRoot,
uint64(logCount))
PCASH block N appends Ethereum leaf N before its header is constructed, so ethereumBlockhashRoot_N contains indices 0..N.
Normalized logs. For each canonical Ethereum block and its ordered execution receipts, derivation orders every receipt log by transaction index and then by receipt-local log index, and commits the logs in a depth-64 Poseidon tree. How an implementation acquires canonical Ethereum blocks and execution results is outside the protocol. Each log is normalized as follows:
- Its topics are padded to four 32-byte values and each is split losslessly into big-endian 128-bit high and low limbs, eight limbs in total.
- Its data is split into consecutive 16-byte big-endian chunks. The final short chunk is interpreted as its big-endian integer; the separately committed byte length distinguishes encodings that differ only in leading zeros.
topicsHash = poseidon(ETHEREUM_LOG_TOPICS_DOMAIN, uint3(topicCount), topic0High128, topic0Low128, ..., topic3High128, topic3Low128)
dataHash_0 = poseidon(ETHEREUM_LOG_DATA_DOMAIN, uint64(dataByteLength))
dataHash_(j+1) = poseidon(dataHash_j, dataChunk_j)
logLeaf = poseidon(ETHEREUM_LOG_DOMAIN, uint64(transactionIndex), uint64(receiptLocalLogIndex), uint160(emitter), topicsHash, uint64(dataByteLength), dataHash_dataChunkCount)
logsRoot = merkleRoot({ logLeaf_j at index j in canonical log order }, depth = 64)
EMPTY_LOGS_ROOT = EMPTY[64] under the ordinary poseidon(left, right) node function.
Each Ethereum leaf binds the exact Ethereum block identity, timestamp, normalized-log root, and log count.
Note, how applications use the tree. An application that authenticates an Ethereum event opens key 6 of a recent PCASH header, then the exact Ethereum leaf beneath that root, then the exact log beneath logsRoot, and then interprets the emitter, topics, and data according to its own semantics. Unrelated logs change only the block's log root and the selected path; they do not enlarge the application circuit. A proof-serving index need retain only log coordinates, leaf indices and hashes, and shared tree nodes; the caller supplies the public raw log and recomputes its leaf against the returned opening. From the authenticated Ethereum block hash, a generic application may instead use raw header, receipt, state, storage, transaction, timestamp, or PREVRANDAO proofs.
The commitment is derived state and requires no PCASH-owned Ethereum contract. Canonical Ethereum history and the derivation rules determine the tree; circuits need no separate source or anchor opening. PCASH does not reproduce Ethereum's receipts root in its header; the proof-friendly normalized-log root inside each Ethereum leaf takes its place.
7.3 Transaction results
Every admitted transaction (§10.2) contributes one result to its block:
TransactionResult = (l1BlockNumber, l1BlockHash, l1TransactionIndex, batchItemIndex, contentHash, outcomeCode)
where contentHash = keccak256(txBytes) (§7.4) and outcomeCode is the transaction's outcome (§10.5). Results are ordered by Ethereum transaction index and then by batch item index. A result's dense zero-based position among the block's results is its blockTransactionIndex. By contrast, batchItemIndex is the item's original zero-based position in its carrier and may have gaps where items were not admitted.
Succeeded means that the transaction's state transition applied. Any deterministic execution failure records the first outcome selected by the precedence rules of §10.5 and leaves state unchanged for that transaction. Two nodes that assign different outcomes disagree on the canonical block even if they compute the same stateRoot.
The results are committed by transactionResultsRoot, a Keccak construction. W64 and W16 encode the stated unsigned integer in big-endian order and left-pad it to 32 bytes, rejecting values outside the stated width.
TRANSACTION_RESULTS_ROOT_DOMAIN = keccak256(utf8("pcash.block.transactionresults.v1"))
TRANSACTION_RESULT_LEAF_DOMAIN = keccak256(utf8("pcash.block.transactionresult.v1"))
TRANSACTION_RESULT_NODE_DOMAIN = keccak256(utf8("pcash.block.transactionresultnode.v1"))
transactionResultLeaf_i = keccak256(
bytes32(TRANSACTION_RESULT_LEAF_DOMAIN) || W64(i) ||
W64(l1BlockNumber_i) || bytes32(l1BlockHash_i) ||
W64(l1TransactionIndex_i) || W64(batchItemIndex_i) ||
bytes32(contentHash_i) || W16(outcomeCode_i))
transactionResultNode(left, right) = keccak256(bytes32(TRANSACTION_RESULT_NODE_DOMAIN) || bytes32(left) || bytes32(right))
merkleRoot([]) = bytes32(0)
transactionResultsRoot = keccak256(bytes32(TRANSACTION_RESULTS_ROOT_DOMAIN) || W64(count) || bytes32(merkleRoot))
i is blockTransactionIndex. The binary tree pairs adjacent nodes left to right, and an unpaired final node is promoted unchanged. The indexed leaves and the outer count bind order and shape.
7.4 Transaction identity
A transaction has two identifiers, for two different questions.
contentHash = keccak256(txBytes) names one exact serialization; before inclusion it is the only identifier a transaction has. It does not name a logical payment or intent: many valid proof byte strings can encode one statement, and re-proving the same action produces different bytes. Conflicts between transactions are determined by the nullifiers they consume, not by content hashes.
transactionId names one exact canonical inclusion:
TRANSACTION_ID_DOMAIN = keccak256(utf8("pcash.transaction.id.v1"))
transactionId = keccak256(
bytes32(TRANSACTION_ID_DOMAIN) ||
bytes32(l1BlockHash) ||
uint64be(l1TransactionIndex) ||
uint64be(batchItemIndex) ||
bytes32(contentHash))
The integer widths, big-endian encoding, concatenation order, UTF-8 label bytes, and Keccak-256 are normative. transactionId is derived from committed fields and is not committed again. Identical bytes included at different Ethereum coordinates have distinct transaction IDs, and an Ethereum reorganization that changes the block hash invalidates the old ID. Do not add CHAIN_ID, genesisHash, or the PCASH block hash separately: the chain ID is already inside contentHash, because it is serialized in the transaction envelope (§10.1).
7.5 Block header and block hash
The header is a fixed depth-4 Poseidon tree with empty leaf 0:
| Key | Value |
|---|---|
| 0 | parentHash |
| 1 | number |
| 2 | timestamp |
| 3 | transactionResultsRootHigh128 |
| 4 | transactionResultsRootLow128 |
| 5 | stateRoot |
| 6 | ethereumBlockhashRoot |
| 7 | historyRoot |
| 8 | extraData |
| 9–15 | 0, unassigned |
parentHash, stateRoot (§6.5), ethereumBlockhashRoot (§7.2), historyRoot (§7.6), and extraData (§7.7) are canonical BN254 field elements. number and timestamp fit uint64. The transaction-results-root limbs are a lossless big-endian split of transactionResultsRoot (§7.3). Keys 9–15 are literal zero leaves when constructing this root. They preserve the typed opening formula used by immutable circuits; no current circuit may treat an unassigned leaf's zero value as a protocol fact.
headerNode(left, right) = poseidon(BLOCK_HEADER_DOMAIN, left, right)
blockHash = root(headerLeaves, depth = 4, node = headerNode)
Raw L1Origin, fork name, software version, and batch version are not header fields.
7.6 PCASH history tree
For block N, historyRoot_N commits exactly the completed PCASH blocks 0..N-1:
historyLeaf_i = poseidon(HISTORY_LEAF_DOMAIN, uint40(i), blockHash_i)
historyRoot_N = merkleRoot({ historyLeaf_i at index i | 0 <= i < N }, depth = 40)
Block 0 therefore commits EMPTY[40]. After block N, historyLeaf_N is appended to the accumulator used by block N + 1. A rule may use authenticated historical state as facts or as authorization; history authentication itself imposes no freshness or revocation semantics. The history root authenticates old headers but does not imply the availability of every mutable historical leaf witness. Witness availability is an implementation and service property; see the node reference.
7.7 Genesis
Initial state has empty output, account, and nullifier trees, zero gross issuance, and the initialized signed issuance lag (§14). The anchor Ethereum block has zero elapsed issuance time and produces ordinary PCASH block 0: its admitted transactions execute, its Ethereum history leaf is appended, and its completed state is committed.
Block 0 commits the network configuration (§3.1) in its extraData:
extraData_0 = poseidon(
GENESIS_EXTRA_DATA_DOMAIN,
uint32(CHAIN_ID), uint64(L1_CHAIN_ID), uint160(INBOX_ADDRESS),
uint64(GENESIS_L1_BLOCK_NUMBER),
GENESIS_L1_BLOCK_HASH_hi128, GENESIS_L1_BLOCK_HASH_lo128)
extraData_N = 0 for N > 0
Block 0 has parentHash = 0 and extraData = extraData_0, and genesisHash = blockHash_0. Later blocks have extraData = 0; the parent chain carries the genesis identity.
7.8 Block body and delivery manifests
Body. The canonical block body retains the exact txBytes of every admitted transaction, successful or failed, aligned one-to-one with the block's TransactionResult values by blockTransactionIndex. A full block response supplies those bytes; a compact response may omit them. The body is authenticated by requiring keccak256(txBytes_i) = contentHash_i for every result. Transaction bodies are canonical PCASH history, not StateRoot leaves. A node can rebuild admitted-transaction projections by scanning PCASH blocks and bodies, without reconstructing an Ethereum batch carrier.
Delivery manifests (node requirement, not consensus: manifests are not committed by any root, and a different manifest layout does not fork the chain). Every canonical block retains a compact Ethereum delivery manifest for each Inbox posting processed in its Ethereum block. A manifest contains:
- the Ethereum block and transaction coordinates, transaction hash, and sender;
- the carrier type, carrier hash and length, and ordered blob versioned hashes;
- the batch-level exclusion reason, if any;
- each established item boundary, with its content hash, optional inclusion transaction ID, and optional exclusion reason;
- for every validly framed batch, additionally the poster owner commitment, the explicit poster context (§9.3), and
itemsHash: the Keccak-256 of the batch bytes after the 65-byte version, poster commitment, and poster context, that iscount (u16)followed by each item'slen (u32) ‖ txBytesin order, exactly as framed, including non-admitted items.
The ordered manifests cover every Inbox transaction in the Ethereum block exactly once. Admitted manifest items are in bijection with the block's TransactionResult values: every result has exactly one matching admitted item, no admitted item lacks a result, and no excluded item occupies a result coordinate. A manifest need not retain raw non-admitted bytes once this recovery context is stored. Admitted items refer to the canonical PCASH transaction bodies; original rejected or unparseable bytes remain retrievable only from the identified Ethereum calldata or beacon blob sidecars.
Explanatory exclusion reasons are not committed in the PCASH block header. The manifest is nevertheless mandatory core node history: it is stored and rewound with its canonical block and MUST NOT be reconstructed from a synthetic carrier.
8. Proofs and Account Rules
Every transaction type except user-account creation carries one zero-knowledge proof. This section defines the proof system, the interface between an account's authorization application and the protocol, and what a proof may read.
8.1 CHONK proofs and verification keys
All client proofs are zero-knowledge CHONK proofs over BN254, produced and verified by the V1-pinned Barretenberg build. One proof folds an ordered stack of Mega circuits:
- one or more private applications, each an account's selected authorization rule (or, for asset issuance, the standard issuance application, §11.6);
- one protocol kernel, which enforces the transaction's ledger rules and links the applications;
- one family finalizer, which accepts only release-pinned kernel verification keys;
- one wire circuit, which alone exposes the transaction type's ordered public inputs. Under the pinned CHONK implementation, the wire circuit uses MegaZK.
The expected external BN254 G2 and Grumpkin G1 CRS bytes are part of the compiled V1 profile and are checked wherever those runtime files are consumed. There are no per-circuit setup ceremonies; soundness depends on the authenticated universal CRS and the pinned proof-system implementation.
Verification keys. Every application, kernel, and finalizer uses the canonical 163-field Mega verification key (VK). Its persistent implementation identity is Barretenberg's canonical Mega VK hash, poseidon(vk_0, ..., vk_162). An account-rule entry commits that exact hash (§5.2). The protocol kernel receives the selected application's VK privately, authenticates its hash through the installed entry, and links the preceding application through CHONK's databus. Protocol finalizers accept only release-pinned kernel VK hashes. The final 115-field MegaZK wire VK is fixed by the active transaction-type rule and is the only VK used in consensus verification.
Verification. The compressed CHONK proof is carried in txBytes. A proof is valid exactly when the pinned verifier accepts it:
- Reconstruct the transaction's ordered public inputs (§11.2, §12.3, §12.4), each as a 32-byte big-endian word. A word
>= phas already failed asPublicInputNotInField. - Decompress the proof with the pinned Barretenberg release (
ChonkDecompressProof). - Require the first
kelements of the decompressed proof'shiding_oink_prooffield, wherekis the number of public inputs, to equal the reconstructed words byte for byte. - Verify the decompressed proof against the type's wire VK (
ChonkVerify).
A failed decompression, a public-input mismatch, or a verifier rejection is ProofRejected (§10.5). Every valid proof of a type has that type's pinned length (§16), so a proof of any other length is never valid; no separate length check is needed. The wire VKs are the release circuit bundle's vks/<name>.chonk.vk files (§16). The kernel and finalizer VK hashes that the circuits accept are compiled into the circuits themselves (circuits/libraries/protocol/src/generated/protocol_stack_hashes.nr); a node never checks them directly, because the wire VK fixes the finalizer and the finalizer fixes the kernels.
8.2 The account-rule statement
Every protocol kernel that authorizes value or account changes (the direct, fee-payer, authorized-issuance, user-account-update, and contract-creation kernels) links private account-rule applications. An application defines the condition under which its entry authorizes the exact action and account contribution that the kernel supplies.
Permanent account-rule statement. Every account-rule application receives and returns exactly these five values, as private CHONK databus values:
[configurationCommitment, actionCommitment, recentPcashBlockHash, authorizationAccountId, pcashChainId]
configurationCommitmentis an opaque, application-owned value committed by the selected entry (§5.2). PCASH defines neither its derivation nor its meaning.actionCommitmentbinds the complete action being authorized. §8.3 lists the action commitment each kernel supplies.recentPcashBlockHashprovides authenticated application context (§8.4).authorizationAccountIdbinds the stable identity derived from the supplied account fields (§5.3). The direct, fee-payer, issuance, and contract-creation kernels prove that the account exists under the recent block's account-state root, except on the direct path when every input isDUMMY(§11.4). The user-account-update kernel instead binds the account's current state commitment into the public action commitment, which the node recomputes from the live row (§12.3).pcashChainIdis authenticated ambient context, analogous to the EVM'sblock.chainid.
The application defines its complete authorization relation and must constrain whatever action fields, network, signer, threshold, or external facts its policy intends. A narrow application MUST privately open and recompute the action formulas it accepts. An application that approves any nonzero action commitment for its authenticated account is universal by construction. A rule may sign the raw action and account identity or a semantic projection of them, but its circuit must bind that authorization to the authenticated account and the exact action commitment it returns.
All five values stay private. Consensus learns that the folded proof is valid, not which policy ran, which configuration it opened, or what secret satisfied it.
8.3 Action commitments
The actionCommitment a kernel supplies depends on what is being authorized. Distinct action domains keep an approval for one role from becoming authority for another.
| Action | actionCommitment |
Defined in |
|---|---|---|
| Direct private-value transaction | the complete transactionCommitment, including private note inputs and assetBurnAmount |
§11.3–11.4 |
| Asset creation by a registered account | the same transactionCommitment |
§11.6 |
| Primary account in a fee-payer action | primaryFeePayerActionCommitment |
§11.7 |
| Fee payer sponsoring a contract's action, or sponsoring a contract's creation | contractFeeSponsorshipActionCommitment, which additionally binds the contract address |
§11.7, §12.4 |
| User-account update and its fee | accountUpdateFundedActionCommitment |
§12.3 |
A rule written for one of the role-separated or update actions is not authority over another: the separation stops a sponsorship from being replayed on a direct path or against another contract.
8.4 What a proof can open
Every proof-bearing transaction names one recentPcashBlockHash. Its execution succeeds only while that hash identifies a canonical block in the recent-block window (§10.6); block N first becomes usable in block N + 1. The hash authenticates the complete depth-4 header (§7.5), and a circuit opens only the header and state facts it consumes. A rule that does not consume a fact does not pay for its path.
- Current account-rule selection always proves
recentPcashBlockHash -> stateRoot -> accountStateRoot, then opens the selected account and rule entry. - A private-value transaction proves its required output and account memberships against that one completed state snapshot. There are no independently selected output, account, or state-root windows. Direct actions whose inputs are all padding retain the block and state-root bindings but do not require account membership (§11.4).
- The header's
numberandtimestampopen directly beneath the header. Intrinsic roots open throughstateRoot. Ethereum facts open throughethereumBlockhashRoot(§7.2), and earlier PCASH blocks throughhistoryRoot(§7.6).
The permanent account-rule statement carries recentPcashBlockHash and pcashChainId directly (§8.2), so applications read context through the same anchor. A rule may prove older PCASH or Ethereum facts through the recent header's append-only roots; authenticating them does not make them current.
8.5 Policies and functions
Every authorized action uses the same entry mechanism: the protocol kernel proves the entry's membership under the exact committed Mega VK hash and links the preceding application through CHONK, all privately. User accounts expose entries as policies whose slot maps to an entry index; contract accounts expose entries as callable functions whose selector maps to an entry index (§5.2).
Rules are permissionless. Anyone may write and install any application. A weak or trivial application cannot bypass the protocol kernel, which enforces the ledger, asset, or account-update invariants outside the application. Account membership authenticates installed authority except in the direct all-padding case (§11.4), which spends no existing value. Permissionless rules may intentionally use broader reusable approvals. Installed applications may be arbitrarily inexpensive; the protocol does not impose the reference wallet's ECDSA cost on every action. All-padding actions still execute the ordinary recursive application and direct stack, whether or not the supplied account exists.
PCASH imposes no configuration schema, salt requirement, signing convention, or replay formula on an account rule. The reference implementation's optional examples and rendering profile are described in the account rules reference.
9. Deriving Blocks from Ethereum
This section gives the procedure that turns each Ethereum block into a PCASH block, then defines how a node finds PCASH data in an Ethereum block and splits it into candidate transactions. PCASH consumes only the few Ethereum facts listed in §9.6 and never executes the EVM.
Block procedure. For each canonical Ethereum block B from the anchor onward, a node derives PCASH block N = B.number - GENESIS_L1_BLOCK_NUMBER as follows:
- Let
t = B.timestamp, which becomes the PCASH block's timestamp (§7.1). ForN > 0, requiret >= parentTimestamp; a regression halts derivation. - Select the fork rules active at
t(§3.2). - Compute the block quote from the parent state and elapsed time (§14.1). Initialize the capacity reservation state (§14.3) and the count of accounts created in this block (§6.3).
- For each top-level transaction of
B, in index order, that is an Inbox transaction (§9.1), obtain its candidate batch (§9.1–9.2) and frame it (§9.3). Then, for each framed item in order:- Decide admission (§10.2). A non-admitted item is recorded in the delivery manifest (§7.8) and skipped.
- Run the admitted transaction's checks in its type's order (§10.5, §11–§12), against the state left by every earlier successful transaction.
- If every check passes, apply its effects. In every case, record its
TransactionResultand exact bytes (§7.3, §7.8).
- Settle the block: compute
rewardEach, append the system payouts, and set the new gross issuance and lag (§14.2–14.3). - Append Ethereum history leaf
N(§7.2), computeStateRoot(§6.5), and build the header and block hash (§7.5), whosehistoryRootcovers blocks0..N-1(§7.6). - Commit the block atomically (§14.4). Block
N's hash then becomes selectable as a recent block (§10.6), and history leafNis appended for blockN + 1.
If required input or evaluation machinery is unavailable at any step, derivation halts at B and retries (§9.2, §10.5); it never records a guess. Reorganizations re-run this procedure from the common ancestor (§9.5).
9.1 Inbox and carriers
INBOX_ADDRESS is a routing tag, not a smart contract. A top-level Ethereum transaction with to == INBOX_ADDRESS is an Inbox transaction. It selects exactly one batch carrier, its calldata or its blobs, and the carrier yields one candidate batch:
- If its
blobVersionedHasheslist is empty, its complete calldata is the candidate batch bytes. - If
blobVersionedHashesis nonempty, its calldata MUST be empty, the hash count MUST be at mostMAX_BATCH_BLOBS, every hash MUST use KZG version0x01, and the ordered blobs MUST be available and match those hashes. Their canonical decoding (§9.2) is the candidate batch bytes. - A transaction with both nonempty calldata and blob hashes, an unsupported blob-hash version, or more than
MAX_BATCH_BLOBSblob hashes is ignored in full. No blob retrieval is attempted for such a transaction.
Internal calls made by Ethereum smart contracts are not recognized: only top-level transactions are Inbox transactions. An Inbox transaction's Ethereum value, receipt status, and any code at INBOX_ADDRESS are ignored.
MAX_BATCH_BLOBS = 6, matching Ethereum's Fusaka maximum for one blob transaction. MAX_BATCH_BYTES = (MAX_BATCH_BLOBS × 4,096 − 1) × 31 = 761,825: one field element is reserved for the length header and every other field element carries 31 bytes. The candidate batch MUST contain at most MAX_BATCH_BYTES, whether it arrived as calldata or as blobs.
9.2 Blob encoding
A blob is 4,096 consecutive 32-byte field elements. A blob carrier encodes the candidate batch bytes as follows:
- The first byte of every field element is zero.
- The first field element stores the candidate-batch byte length as an eight-byte big-endian integer in bytes 1 through 8; the rest of that field element is zero.
- Starting at the next field element, each field element stores up to 31 consecutive candidate-batch bytes in bytes 1 through 31.
- The final partial field element and all remaining field elements are zero.
- The transaction MUST carry the minimum number of blobs needed for the declared nonzero length, the declared length MUST be at most
MAX_BATCH_BYTES, and exactly one value is encoded.
Any other representation is noncanonical, and the transaction is ignored in full.
Availability versus validity. An implementation verifies untrusted blob bytes by recomputing each EIP-4844 KZG commitment and its versioned hash and matching the transaction's ordered blobVersionedHashes. Failure to obtain every blob with matching content is an availability failure: derivation halts at that Ethereum block and retries, rather than treating the transaction as invalid. Once all hashes have been verified, a noncanonical blob encoding or a batch-level parse failure is deterministic malformed input, and the transaction is ignored in full.
9.3 Batch framing
The candidate batch bytes parse as:
batchVersion (1, = 0x01) ‖ posterOwnerCommitment (32) ‖ posterContext (32) ‖ count (u16) ‖ count × [len (u32) ‖ txBytes]
batchVersionselects only the encoding that recovers the orderedtxBytesitems. It never selects the Ethereum carrier, transaction parsing, verification keys, or execution semantics. Version0x01is the only defined encoding.- The 67-byte header carries a canonical, nonzero field element
posterOwnerCommitmentand a canonical field elementposterContext, even when no item pays an open fee or earns issuance. The poster commitment names the recipient of every open fee and any open-route issuance in the batch (§13); it is not the Ethereum sender address. The context is chosen before posting and imposes no inclusion-height or freshness constraint. The reference payout-recovery convention uses it (§15.1). countitems follow, each au32length and then that many bytes.
Ignoring a whole batch. The batch is ignored in full, producing no transactions and no results, when any of the following holds, because its claimed framing cannot be trusted:
- the candidate batch is longer than
MAX_BATCH_BYTES; batchVersionis not a batch version accepted at the block's timestamp (§3.2);posterOwnerCommitmentis zero or not canonical, orposterContextis not canonical;countexceedsMAX_BATCH_ENTRIES;- the bytes end before the header,
count, or any item'su32length is complete, or a declared item length, at any position, runs past the end of the bytes; - bytes remain after the last declared item.
Conditions that ignore an Inbox transaction before any batch exists are in §9.1–9.2.
Oversized items. If a declared item length is fully present but exceeds MAX_TX_BYTES, framing consumes exactly those bytes without allocating that declared size, the item is not admitted (EntryTooLarge), and parsing continues at the next item.
MAX_TX_BYTES = 131,072 is a fixed parsing bound. Exact per-type structural decoding (§10.2) enforces every transaction's wire shape; an item between its real maximum size and this bound fails structural decoding and is not admitted.
There is no batch atomicity: each item is admitted and executed on its own.
9.4 Ordering
Transactions are ordered by Ethereum block number, then Ethereum transaction index, then batch item index. batchItemIndex is the item's original zero-based position in its carrier and may have gaps among admitted transactions. blockTransactionIndex is a separate dense zero-based ordinal over the admitted transactions of one PCASH block (§7.3).
Each transaction executes at its position against the state produced by all prior successful transactions. When transactions conflict, the first one to succeed changes state, and each later conflicting transaction fails with its first applicable outcome. An earlier transaction that fails its own checks consumes nothing, so it cannot block a later one.
9.5 Reorganizations and finality
Derivation is a pure function of canonical Ethereum history. When Ethereum reorganizes, the node removes the orphaned PCASH suffix and derives the replacement suffix from the common parent state, with the ordinary derivation rules. The required result is exactly the state a fresh node would derive from genesis over the new canonical history. Ethereum finality does not change transaction or block validity.
A reorganization also removes abandoned PCASH block hashes from the recent-block window immediately (§10.6).
9.6 Unsupported Ethereum data
PCASH consumes only: each block's number, hash, and timestamp; each top-level transaction's index, recipient, sender, calldata, and blob versioned hashes; resolved blob bytes; and each block's receipt logs with their transaction and receipt-local indices, for the Ethereum history tree (§7.2). If an implementation cannot decode a block, transaction, or receipt into those required values, or cannot resolve a structurally valid blob carrier, derivation halts rather than skipping data and risking a different result. Unrecognized blob-hash versions are deterministically invalid carriers as defined in §9.1; they do not acquire meaning retroactively. V1 does not attempt to predict or encode future Ethereum fork activations.
V1 has no per-block proof-verification budget.
10. Transactions
This section defines what every transaction has in common: its envelope, when an item counts as a transaction at all, the shared private-value section, and how execution selects an outcome. Sections 11 and 12 define each type's specific rules.
10.1 Envelope and types
Every transaction has one common envelope:
txBytes = txType (u8) ‖ pcashChainId (u32be) ‖ txPayload
An assigned txType permanently fixes its payload grammar and type-specific interpretation; private-value profiles select exact capacities and verifier keys within that grammar. An assigned type MUST NOT be reinterpreted or reused. A fork MAY activate or retire a type for new blocks without changing how historical bytes decode (§3.2).
pcashChainId occurs exactly once in txBytes, at the fixed envelope offset. Proof and signature statements consume that envelope value rather than an independently configured one: position 0 of the private-value and account-update public inputs, and the numeric pcashChainId of the account-creation EIP-712 message (§12.1), both use it. Proof and signature statements use this conversion, whose result is below p by construction:
chainIdWord = bytes28(0) || uint32be(pcashChainId)
The four assigned types are:
txType |
Transaction | Carries a proof | Defined in |
|---|---|---|---|
0x00 |
private-value transaction | yes | §11 |
0x01 |
user-account creation | no | §12.2 |
0x02 |
contract-account creation | yes | §12.4 |
0x03 |
user-account update | yes | §12.3 |
10.2 Admission and structural decoding
An item is admitted as a transaction if and only if its complete bytes structurally decode under a txType active at the executing Ethereum block's timestamp, and its envelope pcashChainId == CHAIN_ID. Admission happens before signature recovery, proof verification, or any state access, so every node decides it identically.
An item that is not admitted is one of the following non-admission classes. They are not outcome codes: a non-admitted item produces no transaction, no transaction result, and no transaction ID, only a gap in batchItemIndex and a manifest entry (§7.8).
| Class | Meaning |
|---|---|
EntryTooLarge |
the declared item length exceeds MAX_TX_BYTES (§9.3) |
Empty |
the item has no bytes |
UnknownKind |
the type byte is unassigned or not active at the block's timestamp |
UnknownPrivateValueShape |
a type 0x00 profile selector other than 0 or 1 (§10.4) |
UnknownAuthorizationMode |
a user-account creation authorization mode other than 0 or 1 (§12.2) |
Truncated |
the bytes end before the structure does, including a declared length beyond the remaining bytes |
TrailingBytes |
bytes remain after the structure ends |
Overflow |
a declared length overflows the decoder's offset arithmetic |
WrongChainId |
the envelope chain ID is not CHAIN_ID |
A correct envelope chain ID paired with a signature or proof produced for another domain is admitted and then fails execution. A wrong envelope chain ID is non-admitted without invoking recovery or verification. Carriers and batches rejected before trustworthy item boundaries are framed likewise produce no transaction results.
Structural decoding determines exact field boundaries and complete byte consumption without applying value semantics. Only three discriminants determine structure: txType, the type 0x00 profile selector shapeId, and the user-account-creation authorizationMode. No other semantic discriminant may determine the boundary of later fields. Parsers therefore preserve every other raw discriminant (receive-key modes, publishesNkRoot, yParity), declared lengths, and 32-byte words until ordered execution validation. They MUST NOT require an invalid raw Boolean or enum value to fit a semantic type merely to finish decoding; such values are execution failures of the admitted transaction.
Receive-key fields. All three receive-key sites (user creation, user update, contract creation) encode receiveKeyMode (u8) ‖ receiveKeyLength (u16be) ‖ receiveKeyBytes[receiveKeyLength]. Structural decoding always consumes the declared bytes. User-account and contract-account creation define ABSENT = 0 with length 0 and PRESENT = 1 with length ML_KEM768_PUBLIC_KEY_LEN. User-account update defines KEEP = 0 and CLEAR = 1, each with length 0, and REPLACE = 2 with length ML_KEM768_PUBLIC_KEY_LEN. Any other mode and length combination is an admitted execution failure. A contract's publishesNkRoot flag does not select structure, because the nullifier-key material that follows it is always 32 bytes.
10.3 The private-value section
One structure, the private-value section, carries every relation that spends notes or pays a fee: the payload of a type 0x00 transaction after its profile byte, and the fee payment appended to types 0x02 and 0x03. With I input slots and O output slots:
privateValueSection = (I + O + 5) × verifierInput (32) ‖ proofLen (u32) ‖ proof ‖ O × { outputDataLen_i (u16) ‖ outputData_i }
The serialized words are the private-value verifier inputs (§11.2) other than those derivation supplies itself: position
0(the chain ID, taken from the envelope) and theOoutput-data hashes. Account-action sections always use the S profile, without a profile byte.Derivation computes
outputDataHash_i = uint256(keccak256(outputData_i)) mod pfrom the exact posted bytes.Length prefixes only delimit structure. A declared length beyond the remaining bytes is structural
Truncated.Every private-value section carries exactly
Ooutput-data items. After structural decoding, execution checks them in two passes:- For each
iin index order, ifoutputDataLen_i ≠ 1,750, the outcome isInvalidOutputDataSize. - For each
iin index order, ifoutputData_i[0] ≠ 0x01, the outcome isInvalidOutputDataVersion.
Nothing else about the content is checked. Genuine encrypted envelopes and compliant randomized filler are equally valid; zero-byte counts, entropy, ciphertext validity, and carrier cost do not affect acceptance. (Note: the reference envelope is
1 + 1088 + 12 + 633 + 16bytes.)- For each
A valid proof always has the carrying type's pinned length (§16), so a proof of any other length fails verification (
ProofRejected, §8.1).
10.4 Type payloads
txType 0x00, private-value transaction:shapeId (u8) ‖ privateValueSection. The structural selector is0for profile S or1for profile L (§11.1); any other selector is non-admitted asUnknownPrivateValueShape. No other top-level payment type is assigned.txType 0x01, user-account creation: the authorization mode, the user-account creation payload, a recent block hash, a deadline, and an ECDSA signature only for signed authorization (§12.2). It has no proof and no fee. It is authorized by the authenticated top-level Ethereum sender or by node-sideecrecover, followed by shared creation-only state checks. Consensus derivesaccountRevision = 1.txType 0x02, contract-account creation: the unsigned contract body (§12.4), then a private-value section paid by a sponsoring user account. The proof has twenty-one public inputs: the section's twenty, then the contract address consensus derives from the body.openFeeAmount = 0with no private relay payment is valid and may spend nothing: every input and output slot isDUMMY, so the section authorizes the publication alone.txType 0x03, user-account update: the proposed policy commitment and receive-key operation, the asserted update action commitment, and a private-value section that pays the fee from the updated account's own native notes (§12.3). The section'srecentPcashBlockHashandvalidUntilTimestampare the update's recent block and deadline. Consensus derives the immutablenullifierKeyCommitment, the next revision, and the resulting account state row from live state. The proof has twenty-one public inputs: the section's twenty, then the update action commitment.
10.5 Execution and outcomes
Once admitted, a transaction executes its type's checks in a fixed order. The first failing check determines the outcomeCode; if every check passes, the outcome is Succeeded. An implementation may parallelize internal computation only if it always selects the same first failure. These orders are consensus.
Private value (§11):
InvalidOutputDataSize
InvalidOutputDataVersion
PublicInputNotInField
RecentPcashBlockNotInWindow
Expired
NotYetValid
ZeroNullifier
DuplicateInputNullifier
NullifierAlreadySpent
OutputCapacity
ZeroOutputCommitment
ProofRejected
User-account creation (§12.2):
InvalidReceiveKeyPresence
InvalidAccountSender (direct mode)
InvalidYParity (signed mode)
InvalidAccountSignature (signed mode)
NonCanonicalAccountField
ZeroAccountStateCommitment
AccountAlreadyExists
RecentPcashBlockNotInWindow
Expired
AccountCreationDeadlineOutsideWindow
AccountCapacity
Contract-account creation (§12.4):
InvalidOutputDataSize
InvalidOutputDataVersion
PublicInputNotInField
InvalidPublishesNkRootFlag
InvalidReceiveKeyPresence
NonCanonicalContractField
InvalidContractNkRoot
EmptyContractFunctionSet
SelectorsNotStrictlyIncreasing
ZeroContractFunctionCommitment
ZeroContractAccountCommitment
ContractAddressExists
AccountCapacity
RecentPcashBlockNotInWindow
Expired
NotYetValid
ZeroNullifier
DuplicateInputNullifier
NullifierAlreadySpent
OutputCapacity
ZeroOutputCommitment
ProofRejected
User-account update (§12.3):
InvalidOutputDataSize
InvalidOutputDataVersion
PublicInputNotInField
InvalidReceiveKeyOperation
NonCanonicalAccountField
RecentPcashBlockNotInWindow
Expired
NotYetValid
AccountNotFound
WrongAccountKind
AccountRevisionExhausted
ClearWithoutReceiveKey
ReplaceWithIdenticalReceiveKey
ZeroAccountStateCommitment
ActionCommitmentMismatch
ZeroNullifier
DuplicateInputNullifier
NullifierAlreadySpent
OutputCapacity
ZeroOutputCommitment
ProofRejected
Rules that apply across these lists:
- Update fields.
PublicInputNotInFieldon an update covers the section's words and the appended action commitment.NonCanonicalAccountFieldcovers only the proposed policy set commitment. - Fee-bearing account actions. The section checks of an account action are the private-value checks of §11.2 applied to the same words. A fee-bearing account action records its fee, marks its four nullifiers, and appends its five output commitments exactly as a private-value transaction does.
- Nullifier checks. Nullifier value errors precede the state-dependent spent check, with
ZeroNullifierbeforeDuplicateInputNullifier. When several operands fail one category, report the lowest wire slot. - Contract validation order. Contract function-set validation precedes the derived account-commitment, address-collision, and capacity checks.
Failures versus fatal errors. A clean verifier rejection, including malformed proof encoding, a wrong proof length, or a public-input mismatch, is ProofRejected. A verifier crash, timeout, missing verification key, invalid verifier response, or any other inability to obtain the pinned verifier's verdict is a fatal derivation error. A false state predicate is an included failed transaction; a state read, I/O, corruption, or other infrastructure error is fatal. Fatal derivation errors halt derivation at the Ethereum block and never become transaction outcomes.
Atomicity. All checks and state changes for one transaction are atomic. Only Succeeded marks nullifiers, records fees, updates accounts, or appends outputs.
Outcome codes. The stable uint16 outcomeCode values are:
| Code | Outcome |
|---|---|
| 0 | Succeeded |
| 1 | InvalidOutputDataSize |
| 2 | InvalidReceiveKeyPresence |
| 3 | InvalidReceiveKeyOperation |
| 4 | InvalidYParity |
| 5 | InvalidPublishesNkRootFlag |
| 6 | PublicInputNotInField |
| 7 | ZeroOutputCommitment |
| 8 | RecentPcashBlockNotInWindow |
| 9 | Expired |
| 10 | NotYetValid |
| 11 | ZeroNullifier |
| 12 | DuplicateInputNullifier |
| 13 | NullifierAlreadySpent |
| 14 | ProofRejected |
| 15 | OutputCapacity |
| 16 | AccountCapacity |
| 17 | InvalidAccountSignature |
| 18 | AccountNotFound |
| 19 | AccountAlreadyExists |
| 20 | AccountRevisionExhausted |
| 21 | WrongAccountKind |
| 22 | ClearWithoutReceiveKey |
| 23 | ReplaceWithIdenticalReceiveKey |
| 24 | ActionCommitmentMismatch |
| 25 | NonCanonicalAccountField |
| 26 | ZeroAccountStateCommitment |
| 27 | AccountCreationDeadlineOutsideWindow |
| 28 | ContractAddressExists |
| 29 | NonCanonicalContractField |
| 30 | ZeroContractAccountCommitment |
| 31 | InvalidContractNkRoot |
| 32 | EmptyContractFunctionSet |
| 33 | SelectorsNotStrictlyIncreasing |
| 34 | ZeroContractFunctionCommitment |
| 35 | InvalidAccountSender |
| 36 | InvalidOutputDataVersion |
The integers are explicit protocol values, not positions inferred from a language-level array. Unknown numeric values are unsupported and MUST NOT be shifted, aliased, or reinterpreted.
10.6 Recent block window and time bounds
The recent block. Every proof-selected protocol-state snapshot is identified by one public recentPcashBlockHash. Let currentTimestamp be the timestamp of the block being derived. The hash passes the window check if and only if it is nonzero and is the hash of an already committed block B of the current canonical PCASH chain with
0 <= currentTimestamp - B.timestamp <= RECENT_PCASH_BLOCK_WINDOW_SECONDS
The block being derived is not yet committed, so block N first becomes usable in block N + 1, and values produced by earlier transactions in the same block cannot be selected. The proof privately opens the state it consumes beneath this block (§8.4).
A block hash passes only while it belongs to the current parent-linked header chain and remains within the window. An orphaned block hash fails immediately rather than remaining usable until its former age limit: after a reorganization removes a block, an admitted transaction naming it fails with RecentPcashBlockNotInWindow and must be rebuilt to succeed.
Time bounds. notBeforeTimestamp and validUntilTimestamp bound when the authorized action may execute. validUntilTimestamp is a pure deadline and does not extend the lifetime of the referenced block.
Authorization lifetime versus proof lifetime. Every proof is separately bound to its exact recentPcashBlockHash and can succeed only while that snapshot passes the window check. If the selected application's authorization condition does not itself commit to the snapshot, the same unchanged action may be re-proved against a newer qualifying snapshot without being reauthorized. Changing any action field produces a different action and requires authorization for that action.
11. Private-Value Transactions (txType 0x00)
A private-value transaction spends notes and creates notes. It moves native PCASH, one standard non-native asset, or both, in one shared relation. Every private-value transaction has the same public shape for its profile: a fixed number of input slots, each publishing one nullifier, and a fixed number of output slots, each publishing one note-body commitment and one data item. The proof shows privately that the real slots obey the ledger rules and that the owning account's rule approved the exact transaction.
This section first defines what nodes see and check (§11.1–11.2), then the relation the proof establishes (§11.3–11.8).
11.1 Profiles
Exactly two public profiles select compiled instances of one shared relation:
| Profile | shapeId | Inputs I | Outputs O | Public fields | Wire words | Compressed proof bytes |
|---|---|---|---|---|---|---|
| S | 0 | 4 | 5 | 20 | 14 | 23,840 |
| L | 1 | 32 | 32 | 102 | 69 | 26,464 |
- Dimensions are compiled from
assets/private_value_profiles.json, never inferred from vector lengths or configured by a client. - Each profile selects its own exact wire VK.
- Real occupancy is private: which slots carry value is not public, within the limits each kernel sets (§11.4–11.7), and minimally occupied L actions are valid.
- S and L share notes, the output tree, and the spent set. A note keeps its creation secret when it is spent through the other profile.
With exact output-data sizes, txBytes = 170 + 32*I + 1784*O + proofBytes. An S transaction occupies 33,058 bytes and an L transaction 84,746 bytes. One L transaction plus the 67-byte batch header and 4-byte item framing occupies 84,817 bytes, within the 126,945-byte capacity of one blob. Both carriers admit both profiles; blob-first publication and native-only L proving are product support policies, not validity rules.
11.2 Public inputs and validation
Public inputs. For the selected profile, the proof's public inputs are, in this fixed order:
0 chainId
1 recentPcashBlockHash
2 .. 1+I nullifiers
2+I .. 1+I+O outputBodyCommitments
2+I+O .. 1+I+2O outputDataHashes
2+I+2O openFeeAmount
3+I+2O issuanceOwnerCommitment (0 = open route)
4+I+2O notBeforeTimestamp (< 2^64; 0 = unconstrained)
5+I+2O validUntilTimestamp (< 2^64, nonzero)
There are I + 2*O + 6 public inputs. The wire serializes the I + O + 5 non-derived fields in that order, omitting the chain ID and every output-data hash (§10.3). Derivation supplies the envelope chain ID and hashes all O exact output-data items, including late L slots.
The fields mean:
nullifiers: one per input slot. A real input publishes its note's nullifier; a padding input or marker slot publishes its marker (§11.3).outputBodyCommitments: one note-body commitment per output slot, including padding. Every body appends one ordinary output commitment.outputDataHashes: the hash of each output's exact data item.openFeeAmountandissuanceOwnerCommitment: the public fee and the issuance destination (§13).notBeforeTimestampandvalidUntilTimestamp: the time window (§10.6).
assetId, slot classes, the selected kernel path, application VKs, owner accounts, funding composition, and amounts are not public inputs.
Rules the proof enforces. The following hold for every valid proof, and nodes do not check them separately: notBeforeTimestamp < 2^64; validUntilTimestamp < 2^64 and nonzero; openFeeAmount < 2^128; a nonzero issuanceOwnerCommitment requires openFeeAmount = 0; and in the fee sections of types 0x02 and 0x03, issuanceOwnerCommitment = 0. A transaction that violates one of them fails with ProofRejected, unless an earlier check fails first: for example, a zero validUntilTimestamp fails as Expired, and an open fee large enough to overflow the payout reservation fails as OutputCapacity (§14.3). The selected relation privately opens the state roots it needs through recentPcashBlockHash -> stateRoot, and nested rules may open other authenticated facts they consume (§8.4).
Validation. Nodes apply the complete private-value precedence of §10.5. In detail:
- Check output-data sizes, then versions (§10.3).
- Require every reconstructed public input to be canonical (
PublicInputNotInField). - Require
recentPcashBlockHashwithin the recent-block window (§10.6). - Require
currentTimestamp <= validUntilTimestamp(Expired). - Require
notBeforeTimestamp <= currentTimestamp(NotYetValid).notBeforeTimestamp == 0is unconstrained below, and there is no maximum lookahead. - Require the nullifiers, in this order: no zero value, pairwise distinct, then globally unseen. Report the lowest failing input slot.
- Require the capacity reservations of §14.3 (
OutputCapacity). - Compute the
Ofinal output commitments below and require each to be nonzero, in output-slot order (ZeroOutputCommitment). - Verify the proof against the selected profile's pinned wire VK (
ProofRejected).
Effects. Only if every check succeeds, atomically:
Insert all
Inullifiers into the spent set (§6.4).Append the
Ooutput commitments atoutputIndex0 = nextOutputIndex, for0 <= i < O:outputCommitment_i = poseidon(OUTPUT_COMMITMENT_DOMAIN, outputBodyCommitment_i, outputIndex0 + i)Update the capacity reservations (§14.3).
Record the settlement obligations
(openFeeAmount, issuanceOwnerCommitment, posterOwnerCommitment)(§13), and count the transaction as a qualifying action (§14).
11.3 Slot classes, markers, and the transaction commitment
Slot classes. Every slot has a private class. Classes 0..2 are shared by inputs and outputs and name the value the slot holds: DUMMY = 0 (padding), NATIVE = 1, ASSET = 2. ASSET_ISSUANCE = 3 and APPLICATION = 4 are input-only marker classes: they carry no note, and exist only to publish a one-time marker in the spent set. The classes are private transaction-commitment fields.
An input slot that holds no note still publishes a nonzero nullifier. The three kinds of non-note input differ in who defines their nullifier and what it is scoped to:
| Class | Nullifier defined by | Scoped to |
|---|---|---|
DUMMY |
the protocol kernel, from one fresh per-action seed | nothing |
ASSET_ISSUANCE |
the fixed issuance relation (§11.6) | the asset |
APPLICATION |
the selected account rule (§8.2) | (account, entry) |
Slot requirements:
- A dummy, issuance, or application input has zero input commitment. A native or asset input's input commitment is the spent note's nonzero output commitment (§4.3), which includes its output index.
- A native or asset output has a positive 128-bit amount. A dummy output has asset ID zero and amount zero.
- Every real note output carries a nonzero opaque owner commitment, which must differ from the transaction's
issuanceOwnerCommitment. Every dummy output carries the canonical dummy-owner commitment derived from a private nonzero secret (§4.4). - Every output body commitment and every input nullifier is nonzero, including padding.
Transaction commitment. The transaction commitment binds the complete action: the profile, the fee, issuance destination, time bounds, private burn amount, and every slot's index, class, commitment, and nullifier or data hash. It is what an account rule approves (§8.3).
globalSlot = poseidon(
TX_COMMITMENT_GLOBAL_DOMAIN,
openFeeAmount, issuanceOwnerCommitment, notBeforeTimestamp, validUntilTimestamp,
assetBurnAmount)
inputSlotDigest_i = poseidon(
TX_COMMITMENT_INPUT_DOMAIN,
i, inputSlotClass_i, inputCommitment_i, nullifier_i)
outputSlotDigest_i = poseidon(
TX_COMMITMENT_OUTPUT_DOMAIN,
i, outputSlotClass_i, outputBodyCommitment_i, outputDataHash_i)
transactionCommitment = poseidon(
TX_COMMITMENT_DOMAIN,
shapeId, N_IN, N_OUT,
globalSlot,
inputSlotDigest_0, ..., inputSlotDigest_(N_IN-1),
outputSlotDigest_0, ..., outputSlotDigest_(N_OUT-1))
The commitment has no reserved global operand and no public-application operand. It binds the selected profile, the complete ordered action, and the private assetBurnAmount, without binding transient tree roots, which is why an unchanged action can be re-proved against a newer recent block (§10.6). Binding each slot's index and class prevents moving an output or reinterpreting a slot without changing the authorized action. Require 0 <= assetBurnAmount < 2^131.
Selecting the relation. The selected protocol kernel derives hasAsset in both directions: it is true if and only if any input is ASSET or ASSET_ISSUANCE, or any output is ASSET. An APPLICATION marker carries no asset value and does not select the asset path.
- If
hasAssetis false,assetIdandassetBurnAmountare zero. - If
hasAssetis true,assetIdis nonzero, and:- one or more
ASSETinputs with noASSET_ISSUANCEmarker take the direct relation (§11.4) or the two-account fee-payer relation (§11.7); - no
ASSETinput, exactly oneASSET_ISSUANCEmarker, noAPPLICATIONinput, and at least oneASSEToutput take an issuance kernel (§11.6) and requireassetBurnAmount = 0; - every other asset shape fails.
- one or more
The slot grammar determines the relation in every case but one. Slot classes name namespaces, not owners, so they cannot express whether a second account funds the fee. A direct action and a fee-payer action therefore use different private protocol kernels, both accepted by the same private-value finalizer. The fee-payer kernel requires its two authenticated accounts to differ, so a one-account action cannot gratuitously take that path.
Padding inputs. The protocol kernel derives every dummy input's nullifier as poseidon(DUMMY_NULLIFIER_DOMAIN, dummyInputSeed, inputIndex) from one private nonzero seed. A dummy input is shape noise, not application replay protection: its value is slot-indexed and fresh per action, which is the exact opposite of what a marker requires.
Application markers. An account rule that must allow some fact to be used only once, such as a claim against an escrow, consumes an APPLICATION slot whose nullifier is:
applicationNullifier = poseidon(
APPLICATION_NULLIFIER_DOMAIN,
authorizationAccountId, selectedEntryCommitment, replayId)
The authenticated account and the exact selected account-rule entry supply the scope. replayId is defined entirely by that rule and is never interpreted by any fixed relation. Scoping to the entry rather than to the account alone means a sibling entry cannot consume another entry's markers. Because markers are scoped to one entry, two installed entries never share replay state. An application whose S and L variants must share replay state MUST therefore be installed as one entry whose VK accepts both profiles; its S opening excludes the canonically zero trailing capacity. Separately installed note-funded entries need no shared replay state, because the real-note nullifiers they consume already prevent double spending.
The operand list is closed and normative. It binds no slot index, no input note commitment, no output, no fee, no timestamp, no recent root, no chain ID, and no proof randomness. If it bound any of them, a party could reshape the transaction (spend different inventory, move the marker to another slot, change the fee) and derive a fresh marker for the same fact. Independence from slot and transaction shape is what makes the value one-shot at all. No PCASH nullifier binds the chain ID. A marker that must be unguessable, or must differ across chains, carries a secret inside replayId, as noteSecret and the issuance secret do elsewhere.
The direct kernel (§11.4) and the fee-payer kernel (§11.7) enforce markers by authenticating both the account and the selected entry against the same transaction; the fee-payer kernel scopes every marker to its primary account. Each kernel:
- takes a private
replayIdper slot, requires it to be zero in every non-APPLICATIONslot, and requires eachAPPLICATIONslot's nullifier to equal the value above; - relies on the selected application to derive its own canonical
replayIdindependently and assert the same equality against the same nullifier, so the two agree only if the replay identities are equal. The application receivesselectedEntryCommitmentprivately and does not re-derive its own entry, which would be a self-reference with no fixed point.
Standard issuance rejects APPLICATION inputs. There is no protocol cap on the number of APPLICATION slots; an application caps itself.
Output construction. The kernel constructs all O output bodies from the owner commitments, the slot-derived asset ID, and the amounts. For ASSET outputs it uses the action's nonzero asset ID; for NATIVE and DUMMY outputs it uses zero. It does not open a real output's owner commitment. For each dummy slot i, it requires a private dummySecret_i != 0, derives ownerCommitment_i = poseidon(OWNER_COMMITMENT_DOMAIN, DUMMY_OWNER_ID, dummySecret_i), and rejects any other owner commitment.
11.4 The direct path
The direct path is the ordinary one-account spend: one account owns every real input, and one of its installed rules authorizes the complete transaction. It handles native payments, asset transfers and burns, and actions that spend nothing.
A direct action contains no ASSET_ISSUANCE input and either at least one real note input or only DUMMY inputs: the kernel requires realInputCount != 0 OR every input slot is DUMMY. It MAY contain NATIVE inputs, ASSET inputs, or both, and it MAY contain APPLICATION markers, whose nullifiers the direct protocol kernel enforces (§11.3). A marker is not a dummy, so a marker without a real input fails.
With real inputs. All real inputs belong to one authenticated USER or CONTRACT account. A NATIVE input uses assetId = 0, and an ASSET input uses the action's nonzero asset ID. A native-only action additionally has no ASSET output and assetBurnAmount = 0. The kernel:
- proves every real input's note membership and canonical nullifier;
- constructs native, asset, and dummy output bodies;
- enforces native conservation including the fee, and asset conservation including the burn (§11.6);
- authenticates one application entry against the supplied rule-set commitment by its exact Mega VK hash;
- links that application's private return statement
[configurationCommitment, transactionCommitment, recentPcashBlockHash, authorizationAccountId, pcashChainId].
The account ID is derived from the supplied account fields (§5.3); historical account membership authenticates those fields whenever any input is not DUMMY.
With only padding inputs. An action whose inputs are all DUMMY spends nothing. For such an action:
- it requires no registered account, because the kernel does not prove that the supplied account exists;
- the output rules of §11.3 and both conservation equations force
openFeeAmount = 0,assetBurnAmount = 0,assetId = 0, and onlyDUMMYoutputs; - it is still a qualifying action if it succeeds (§14), and its issuance uses the ordinary destination rules (§13).
The kernel still opens the supplied account and rule set, binds the exact application VK and transaction statement, and recursively verifies the application. Account and key well-formedness, the account-leaf and Merkle computations, and rule membership all remain mandatory. Only one check is conditional:
allInputsDummy OR reconstructedAccountRoot == historicalAccountRoot
allInputsDummy is derived in constrained code from every private input class being exactly DUMMY; APPLICATION and ASSET_ISSUANCE never qualify. The supplied account fields and rule set on this path therefore do not represent authenticated installed-account authority.
Dummy nullifiers remain globally single-spend (§10.5), so an accepted proof cannot earn issuance twice. Registration itself earns no issuance. A registration and an all-padding action may be published in one batch with independent outcomes; the action is valid regardless of the registration's position or success, including when no registration is published. Spending a recovered reward later requires normal historical account and note membership.
Both cases use the same direct kernel and VK within their profile, followed by the ordinary finalizer and wire. The fee-payer, issuance, and contract-creation kernels require unconditional account membership; the account-update kernel relies on the node's check of the live row (§12.3). The folded stack is selected application → direct protocol kernel → private-value finalizer → private-value wire. A zero-fee action remains valid when ordinary conservation holds.
11.5 Protocol kernels
The protocol kernels own ledger structure: exact slot classification, note-body construction, dummy padding, the transaction commitment, timestamp and amount bounds, note membership, namespace isolation, conservation, burn, and fees. The standard issuance application owns asset identity and one-shot supply (§11.6). An asset creator supplies no circuit or executable policy.
A type 0x00 proof privately selects one of three kernels, each compiled for S and L:
- the direct kernel (§11.4): one account owns every real input;
- the native fee-payer kernel (§11.7): a contract account's asset action with a distinct user account paying the native fee;
- the authorized issuance kernel with optional native funding (§11.6): creates a new asset's supply.
The private-value finalizer accepts only these three kernel VKs, and all three converge on the same public wire. Nothing public distinguishes which kernel ran.
Every kernel that spends notes authenticates the relevant owner account, derives its authorizationAccountId, authenticates each selected installed entry, and links each application. For every real input slot it proves exact note membership, owner binding, amount range, asset namespace, and canonical nullifier. Application identity, account identity, note secrets, nullifier-key material, account openings, and amounts remain private inside the folded proof.
Real-input witnesses cover every ASSET and NATIVE slot exactly once; the protocol kernel separately owns marker and dummy slots. One transaction may contain only one nonzero asset ID. All positive fees are funded by native notes. Contract-owned native inputs remain on the direct path; the fee-payer kernel does not reinterpret them as a user contribution. Zero-fee native and asset actions remain consensus-valid.
assetBurnAmount is a private transaction field: it changes the transaction commitment and is therefore available to any installed application that opens the transaction, but it is not a public proof input.
11.6 Standard fungible assets
Every nonzero asset is an instance of one semantic protocol law, the standard fungible asset: a fixed supply issued once, then only transferred, split, merged, or burned. An asset's identity is a commitment to its immutable configuration:
issuanceSecretCommitment = poseidon(
FIXED_ISSUANCE_SECRET_COMMITMENT_DOMAIN, issuanceSecret)
standardFungibleConfigurationCommitment = poseidon(
STANDARD_FUNGIBLE_CONFIGURATION_DOMAIN,
instanceSalt, metadataCommitment, initialSupply,
issuanceSecretCommitment)
assetId = poseidon(
ASSET_ID_DOMAIN,
STANDARD_FUNGIBLE_LAW_ID,
standardFungibleConfigurationCommitment)
instanceSalt,metadataCommitment,issuanceSecret,issuanceSecretCommitment,standardFungibleConfigurationCommitment, andassetIdMUST be nonzero.0 < initialSupply <= 5 × (2^128 - 1), and the selected issuance shape is valid only when its asset output slots can represent that supply.STANDARD_FUNGIBLE_LAW_IDidentifies the transfer, burn, and fixed one-shot issuance semantics of this section; it is not a compiled VK hash. The network separately pins the implementing VKs.- There is no asset registry, transition VK, executable asset configuration, mutable supply object, public supply counter, or public asset-ID field.
Asset definition. The canonical definition opening of one standard asset is standardFungibleAssetDefinition = (instanceSalt, metadataCommitment, initialSupply, issuanceSecretCommitment). Together with STANDARD_FUNGIBLE_LAW_ID it recomputes the configuration commitment and assetId. It contains the issuance-secret commitment rather than the secret, and grants no authority to issue or spend. Wallets and applications MAY retain it privately to authenticate metadata, prove the immutable original supply, or establish lineage to a successor asset under Appendix A. Consensus never requires it to be public, and a successor application MAY identify a source asset by assetId alone.
Transfer and burn. A nonzero asset is ordinary notes under its own namespace with a second conservation equation, so transfer and burn take the direct path (§11.4) alongside native PCASH: the direct protocol kernel authenticates the account, opens every real input in either namespace, links the selected application, and enforces both conservation equations itself. There is no separate transfer relation, and no configuration opening is needed on transfer. The direct relation requires one to I asset inputs when the action carries an asset, no ASSET_ISSUANCE marker, and zero to O asset outputs. It range-checks each note amount to 128 bits and enforces:
sum(asset inputs) = sum(asset outputs) + transaction.assetBurnAmount
sum(native inputs) = sum(native outputs) + openFeeAmount
Native conservation intermediates are bounded by the compiled slot capacities: for L, each sum of notes is below 2^133 and an output sum plus the fee is below 2^134. Asset sums and burns keep the independent < 2^131 semantic bound. All these quantities are strictly below the field modulus, so both equalities have their integer meanings. assetBurnAmount is not a separate relation witness: the kernel opens the value already bound by transactionCommitment.
The direct relation proves that every real asset and native input belongs to its one authenticated account. An uncovered real input, or a note witness attached to a slot that holds no note, fails. Inputs owned by different accounts fail on the direct path and are admitted only by §11.7, which authenticates a second account for the single purpose of funding the fee. One account owns every real input to an asset-bearing action on the direct path, and one selected installed rule authorizes its complete contribution against the exact transaction commitment. The rule may enforce commercial terms such as price, required PCASH recipients, royalties, redemption conditions, schedules, reserve proofs, or successor custody.
Issuance. Fixed issuance is a distinct application and kernel, because it derives the asset identity from a committed configuration, fixes the initial supply, and consumes a one-shot marker, none of which an ordinary spend does:
issuance with optional native funding:
standard issuance application → installed account authorization
→ issuance kernel → private-value finalizer → private-value wire
The standard issuance application VK is protocol-pinned. The application returns five private fields, with field 3 reserved as canonical zero:
standard issuance:
[assetId, transactionCommitment,
recentPcashBlockHash, 0, pcashChainId]
The standard issuance application requires no asset input, no APPLICATION marker, exactly one ASSET_ISSUANCE marker, one to O asset outputs, and assetBurnAmount = 0. It:
- opens the complete standard configuration and proves
issuanceSecretCommitment; - recomputes
assetId; - derives its replay identity
replayId = poseidon(ASSET_ISSUANCE_REPLAY_DOMAIN, issuanceSecret)and publishesposeidon(ASSET_ISSUANCE_NULLIFIER_DOMAIN, assetId, replayId)in the marker slot; - requires the sum of asset outputs to equal
initialSupplyexactly.
The marker has the same construction as an application marker (scope operands, then a replay identity), except that its scope is the asset, because the asset's replay scope must be independent of the authorizing account and installed entry. The secret MUST be a nonzero high-entropy field element. It both authorizes the initial allocation and blinds the public marker. Every reshaped issuance of the same asset derives the same final nullifier, so the global spent set accepts at most one. Ordinary transfer, split, merge, and burn use real-note nullifiers and no issuance marker. No later mint path exists.
Every issuance additionally folds an installed authorization application from an existing USER or CONTRACT account, whose account and exact rule VK are authenticated against the selected recent state. That application authorizes the asset's creation and its issuance route, not merely a fee. The one issuance kernel accepts optional native note funding owned by this same account and enforces sum(native inputs) = sum(native outputs) + openFeeAmount. With no native inputs, positive native outputs and positive fees are impossible.
11.7 Asset transfer with a native fee payer
On the direct path every real input belongs to the single authorizing account, so an action spending a contract's asset inventory must also be funded by the contract, not by the party who initiated it. This relation admits a second account in exactly one role: a user account supplies native PCASH for the open fee, or for one exact private relay output, of a named contract account's asset action, and receives its own native change.
The relation:
- authenticates two accounts and their selected installed rules, and requires the primary account's kind to be
CONTRACT, the fee payer's kind to beUSER, and theirauthorizationAccountIds to differ; - partitions inputs by slot class:
ASSETinputs and everyAPPLICATIONmarker belong to the primary account, andNATIVEinputs belong to the fee payer. At least oneNATIVEinput is required; - enforces
sum(asset inputs) = sum(asset outputs) + assetBurnAmountandsum(native inputs) = openFeeAmount + privateRelayAmount + nativeChangeAmount; - permits at most one ordinary private native relay output and one native change output. Change must open to
ownerCommitment(feePayerOwnerId, changeSecret)for a private nonzero secret. The installed sponsorship rule signs the exact relay amount and owner commitment; - admits no other native payment, no asset contribution from the fee payer, and no native input owned by the primary account. Payouts of the asset to third parties remain governed by the primary contract account's rule. An
ASSET_ISSUANCEmarker is rejected.
The fee payer contributes no asset, owns no marker, and can lose at most openFeeAmount + privateRelayAmount. Exchange and every other multi-party settlement use application contracts and sequential actions rather than multi-owner type 0x00 proofs, because those require parties to contribute value to each other rather than only to the open fee and selected relay payment.
The two installed applications return their role-separated five-field statements privately. The fee-payer kernel authenticates both exact Mega VK hashes, links both applications, enforces the complete type 0x00 relation, and returns the same I + 2*O + 6-field statement for the selected profile as the direct and issuance kernels. Nothing about the second account is public: no transaction type, mode bit, public input, or slot distinguishes a fee-paid action from a direct one.
Role-separated action commitments. The two rules are verified against domain-separated actions:
primaryFeePayerActionCommitment = poseidon(
PRIMARY_FEE_PAYER_ACTION_DOMAIN, transactionCommitment)
contractFeeSponsorshipActionCommitment = poseidon(
CONTRACT_FEE_SPONSORSHIP_ACTION_DOMAIN,
transactionCommitment,
primaryContractAddress)
The primary application returns [primaryConfigurationCommitment, primaryFeePayerActionCommitment, recentPcashBlockHash, primaryAuthorizationAccountId, pcashChainId]. The sponsor returns the corresponding contract-fee action, whose authenticated address operand must equal the contract named in its authorization.
The separation is necessary. A primary rule might omit native constraints because this relation controls all native value. Replayed through the direct path, where the primary account itself owns the native inputs, that rule would permit exactly the redirection this relation forbids. Separate domains make the two authorizations non-interchangeable: a direct authorization cannot become a primary one, a primary authorization cannot spend the sponsor's notes, and sponsorship is never authority over application state or over another contract account.
11.8 Nonzero-asset provenance invariant
Every reachable nonzero-asset note descends from exactly one valid standard issuance. The rules above prove this by induction:
- Genesis and every canonical reconstruction begin with no nonzero-asset note.
- Standard issuance is the only relation that may create a nonzero-asset output without consuming a nonzero-asset input, and it authenticates the complete configuration and derived asset ID.
- Every direct or sponsored transfer with a nonzero asset requires at least one real asset input and constructs every asset output using exactly that input's asset ID.
- Account creation, account update, and every other non-issuance relation cannot create nonzero-asset value without consuming it.
Checkpoint restore, restart, reorg replay, and fresh reconstruction MUST reproduce outputs through canonical transaction execution; they MUST NOT admit independent output-tree contents that bypass this invariant. Conformance testing MUST attempt nonzero-asset creation through every non-asset relation and require it to be rejected.
The standard law fixes fungible transfer and burn semantics. The issuer has no later mint, freeze, pause, clawback, blacklist, transfer-tax, receiver-hook, or administrative-spend path. Controlled distribution is ordinary contract custody: initial issuance may create notes owned by a contract, and later movement of those notes must satisfy one installed rule capable of authorizing the contract's complete contribution. Contract conditions apply only while the contract owns the notes.
12. Account Transactions
Three transaction types create and change accounts (§5). A user account is created by its Ethereum key (0x01) and afterwards changed only by an update its own rules authorize (0x03). A contract account is created once (0x02), sponsored by a user account, and never changes. Updates and contract creations pay their fees through an ordinary private-value section (§10.3).
12.1 Protocol-defined EIP-712 message
Signed user-account creation uses EIP-712 with domain:
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
name = "PCASH" version = "1" chainId = L1_CHAIN_ID verifyingContract = INBOX_ADDRESS
The exact primary type is PcashUserAccountCreation(uint256 pcashChainId,address account,uint256 nullifierKeyCommitment,uint256 policySetCommitment,bytes32 receiveKeyHash,bytes32 recentPcashBlockHash,uint256 validUntilTimestamp). ECDSA is over secp256k1 with digest keccak256(0x1901 || domainSeparator || hashStruct(message)). The signature MUST have 0 < r < n, 0 < s <= n/2, and y-parity 0 or 1. Node-side ecrecover MUST return the exact account address.
This is the only message the protocol itself signs over. Private-value and administration ECDSA messages are relations implemented by optional account-rule circuits (§8.2), not ambient protocol authority.
12.2 User-account creation (txType 0x01)
Creation registers a user account for an Ethereum address. It carries no proof, pays no PCASH fee, and earns no issuance. Because no rule exists before the account does, creation is authorized by the address's Ethereum key, in one of two modes.
Payload.
authorizationMode (u8) ‖ address (20) ‖ nullifierKeyCommitment (32) ‖ policySetCommitment (32) ‖
receiveKeyMode (u8) ‖ receiveKeyLength (u16be) ‖ receiveKeyBytes[receiveKeyLength] ‖ recentPcashBlockHash (32) ‖
validUntilTimestamp (u64) ‖ authorizationBytes
Direct mode (authorizationMode = 0) has empty authorizationBytes. The serialized account address MUST equal the authenticated sender of the top-level Ethereum transaction carrying this batch item; a mismatch is InvalidAccountSender. This applies equally to calldata and blob carriers. No sender claimed inside PCASH bytes or in a submission RPC request supplies this authority. The Ethereum transaction authenticates the complete carrier, including the account fields, PCASH chain ID, recent block, and deadline.
Signed mode (authorizationMode = 1) requires exactly r (32) ‖ s (32) ‖ yParity (1) as authorizationBytes. Any Ethereum sender may carry it. The signature is over §12.1's exact domain and primary type, with pcashChainId decoded from the common envelope, nullifierKeyCommitment from the account body, and receiveKeyHash = keccak256(receiveKeyBytes) for PRESENT, else bytes32(0).
Other modes are structurally undecodable (UnknownAuthorizationMode); missing or extra authorization bytes are Truncated or TrailingBytes. A direct entry never falls back to signature recovery, and a signed entry never falls back to Ethereum sender authorization.
Checks, in the user-account-creation precedence of §10.5:
- The receive-key encoding is
ABSENT = 0with length0orPRESENT = 1with lengthML_KEM768_PUBLIC_KEY_LEN = 1,184; any other combination isInvalidReceiveKeyPresence. - Direct mode: the address equals the Ethereum sender (
InvalidAccountSender). Signed mode: the parity byte is0or1(InvalidYParity), then the signature is valid and recovers the serialized address (InvalidAccountSignature, covering invalid scalar ranges, highs, failed recovery, and recovery to another address). nullifierKeyCommitment < pand nonzero, andpolicySetCommitment < p(NonCanonicalAccountField).- The resulting user
accountStateCommitmentis nonzero (ZeroAccountStateCommitment). - No account exists at the address (
AccountAlreadyExists). - The recent block is canonical and in the recent window (
RecentPcashBlockNotInWindow). - The candidate block's timestamp does not exceed
validUntilTimestamp(Expired). validUntilTimestamp <= selectedBlockTimestamp + RECENT_PCASH_BLOCK_WINDOW_SECONDS(AccountCreationDeadlineOutsideWindow). The selected block is historical, so onceExpiredpasses,selectedBlockTimestamp <= validUntilTimestampis implied and is not a separate outcome.- Account capacity (
AccountCapacity).
Effects. Consensus derives accountRevision = 1; it is neither serialized nor signed. On success, assign the next 40-bit accountIndex and atomically write the complete account state row, the account state commitment, and the address-to-index mapping. Raw receive-key publication material is not part of intrinsic state.
Creation cannot update an existing account. Neither authorization mode grants any authority after creation: later actions must satisfy the installed policy set.
12.3 User-account update (txType 0x03)
An update may replace a user account's policy set, its receive key, or both; an update that changes neither is valid and only increments the revision. It is authorized by one of the account's current policies and pays its own fee from the account's notes, so any relayer can include it. The account's address is public in the payload.
Payload. The proposed mutable account fields:
address (20) ‖ newPolicySetCommitment (32) ‖
receiveKeyOperation (u8) ‖ receiveKeyLength (u16be) ‖ receiveKeyBytes[receiveKeyLength]
followed by accountUpdateActionCommitment (32) ‖ privateValueSection.
Receive-key operations. The valid encodings are KEEP = 0 with length 0, CLEAR = 1 with length 0, and REPLACE = 2 with length ML_KEM768_PUBLIC_KEY_LEN = 1,184. Any other combination is InvalidReceiveKeyOperation.
KEEPcopies the current presence flag and digest.CLEARrequires the current receive-key state to be present and produces an absent state with zero digest; otherwise it fails withClearWithoutReceiveKey.REPLACEderives the exact Keccak-256 digest of its key and requires it to differ from the current digest; otherwise it fails withReplaceWithIdenticalReceiveKey.
Consequently, a transition that leaves the receive-key state unchanged has exactly one canonical encoding: KEEP.
Fee section. The private-value section is the update's fee payment (§13):
- every input slot is a
NATIVEnote owned by the updated account, orDUMMY; - every output slot is
DUMMYexcept an optional ordinary privateNATIVErelay output and at most oneNATIVEchange note returning to the account's owner identity; assetBurnAmount = 0, andsum(native inputs) = openFeeAmount + privateRelayAmount + changeAmount.
A zero fee with every slot dummy is valid. The proof's ordered public inputs are the twenty private-value inputs of §11.2 (profile S), followed by accountUpdateActionCommitment at position 20. pcashChainId is the zero-extended common-envelope value, and the recent block hash and validUntilTimestamp are the section's own words. The proof verifies only against the protocol-pinned user_account_update VK.
The action commitment. At the transaction's exact sequential processing position, consensus reads the immutable nullifierKeyCommitment and currentAccountRevision from the live row, derives newAccountRevision = currentAccountRevision + 1 without overflow, and derives the live current and proposed leaves:
currentAccountStateCommitment = accountStateCommitment(address, nullifierKeyCommitment, currentPolicySetCommitment, USER,
currentAccountRevision, currentReceiveKeyPresent,
currentReceiveKeyHashHigh128, currentReceiveKeyHashLow128)
newAccountStateCommitment = accountStateCommitment(address, nullifierKeyCommitment, newPolicySetCommitment, USER,
newAccountRevision, newReceiveKeyPresent,
newReceiveKeyHashHigh128, newReceiveKeyHashLow128)
accountUpdateActionCommitment = poseidon(
ACCOUNT_UPDATE_AUTHORIZATION_ACTION_DOMAIN,
uint160(address),
accountIndex,
currentAccountStateCommitment,
newAccountStateCommitment,
validUntilTimestamp)
The installed rule authorizes the transition and its fee together, through the funded action:
accountUpdateFundedActionCommitment = poseidon(
ACCOUNT_UPDATE_FUNDED_ACTION_DOMAIN,
accountUpdateActionCommitment,
transactionCommitment)
where transactionCommitment is the private-value commitment (§11.3) of the section's transaction. The public action commitment binds the transition alone, so consensus can recompute it from live state. The funded action binds it to the exact fee transaction, so one authorization cannot be reattached to different notes, a different fee, or a different change note.
The update kernel. The account-update kernel:
- takes the account's
nkRootand derivesnullifierKeyCommitmentfrom it; - privately reconstructs both account leaves, and requires
newAccountRevision == currentAccountRevision + 1without overflow andtransaction.validUntilTimestampequal to the action's deadline; - opens the recent block's state root and output root;
- proves every
NATIVEinput's membership and canonical nullifier under the account's note key, and enforces the change and conservation rules above; - proves a nonzero four-field rule entry against
currentPolicySetCommitment, and requires the canonical Mega VK hash to equal that entry'simplementationVkHash; - derives
authorizationAccountId(§5.3), links the preceding application, and requires its private return statement to equal[configurationCommitment, accountUpdateFundedActionCommitment, recentPcashBlockHash, authorizationAccountId, pcashChainId].
The change note's secret is a free kernel witness; the authorizing rule binds whichever opening it approved.
Execution. Execution MUST validate the receive-key operation against the live current receive-key state, derive the resulting account state row, and reconstruct the action commitment from the exact live row and the exact proposed policy and receive-key operation, including earlier successful transactions in the same block, then require equality with the serialized proof input. The action commitment binds the complete resulting account transition rather than the operation byte; the CLEAR and REPLACE preconditions make the operation encoding canonical.
Checks, in the account-update precedence of §10.5:
- Output-data sizes, then versions (
InvalidOutputDataSize,InvalidOutputDataVersion, §10.3). - Every section word and the appended
accountUpdateActionCommitmentis canonical (PublicInputNotInField). - The receive-key operation's mode and length are a valid combination (
InvalidReceiveKeyOperation). newPolicySetCommitment < p(NonCanonicalAccountField).- The recent block is in the window (
RecentPcashBlockNotInWindow), then the deadline has not passed (Expired), thennotBeforeTimestamphas been reached (NotYetValid) (§10.6). - An account exists at the address (
AccountNotFound), and it is a user account (WrongAccountKind). currentAccountRevision + 1fits inu32(AccountRevisionExhausted).- The receive-key operation's state precondition holds (
ClearWithoutReceiveKey, thenReplaceWithIdenticalReceiveKey). - The new account state commitment is nonzero (
ZeroAccountStateCommitment). - The reconstructed
accountUpdateActionCommitmentequals the serialized one (ActionCommitmentMismatch). - The section's nullifier, capacity, and output-commitment checks, as in §11.2 steps 6–8, then proof verification against the
user_account_updateVK (ProofRejected).
Effects. On success, atomically mark the nullifiers, record the fee, append the five output commitments, and replace the canonical row and account state commitment. (The failure-isolated derived index records absence, a new key publication, or a reference to the prior publication.) On failure the transaction has no effect.
Validation MUST NOT reject duplicate nullifierKeyCommitment values across addresses.
12.4 Contract-account creation (txType 0x02)
A contract account is a keyless account whose kind is committed in its account state (accountKind == 1), whose address derives from its contents, and which consensus never updates. It exposes an immutable set of functions, each one account-rule entry (§5.2). Calling a function means folding that entry's application into the active protocol kernel, which authenticates the exact action and the account's contribution. There is no node-executed contract code and no contract storage. Notes owned by a contract are spendable only through one of its committed functions.
Contract creation is unsigned, and the contract's full contents are published in Ethereum data. A contract has nothing to spend before it exists, so a user account sponsors its creation through an appended private-value section: the sponsor authorizes the publication and pays its fee from its own native notes, so any relayer can include it. An openFeeAmount of zero with no private relay payment spends nothing and leaves inclusion to a self-publisher or a selected publisher willing to carry it.
Body.
publishesNkRoot (u8) ‖ nullifierKeyMaterial (32) ‖
receiveKeyMode (u8) ‖ receiveKeyLength (u16be) ‖ receiveKeyBytes[receiveKeyLength] ‖
functionCount (u8) ‖
functionCount × { selector (u8) ‖ implementationVkHash (32) ‖ configurationCommitment (32) }
The valid receive-key encodings are ABSENT = 0 with length 0 and PRESENT = 1 with length ML_KEM768_PUBLIC_KEY_LEN = 1,184; any other combination is InvalidReceiveKeyPresence.
Nullifier-key publication. The body carries exactly one representation of the account's nullifier-key material, selected by publishesNkRoot after structural decoding:
publishesNkRoot = 0: nullifierKeyMaterial = nullifierKeyCommitment
publishesNkRoot = 1: nullifierKeyMaterial = nkRoot
nullifierKeyCommitment = poseidon(NULLIFIER_KEY_COMMITMENT_DOMAIN, nkRoot)
- A private contract (
publishesNkRoot = 0) carriesnullifierKeyCommitmentand does not revealnkRoot. - A published-root contract (
publishesNkRoot = 1) carries a nonzeronkRoot, and the commitment is derived from it. Publishing the root lets any party that knows one of the contract's note openings derive that note's nullifier, so parties other than the creator can spend the contract's notes whenever one of its functions authorizes it.
A contract body never carries both, so it cannot encode an nkRoot and a nullifierKeyCommitment that disagree. publishesNkRoot is an immutable part of the contract's identity because it is encoded in the body and in the contract-address preimage. It is not a separate account-state-commitment operand; the address already binds it.
A function entry is exactly 65 bytes in the order shown. At the maximum functionCount of 255, the largest contract payload is 17,796 bytes, and the complete transaction size is bounded by its release-pinned proof and five fixed-size output-data items.
Function set. functionSetCommitment is derived from the complete canonical function list rather than serialized beside it:
contractFunctionCommitment_i = poseidon(ACCOUNT_RULE_ENTRY_DOMAIN, implementationVkHash_i, configurationCommitment_i, 0)
functionSetCommitment = depth-8 sparse root over (selector_i -> contractFunctionCommitment_i)
A contract function is an ordinary account-rule entry (§5.2) whose commitmentBlinder is the canonical 0: a contract's entries are published in full, so a published per-function blinder would hide nothing, and distinct deployments already differ through nullifierKeyCommitment, which the address preimage binds. The selector is the entry's position in the depth-8 rule-set tree, not a hash-derived selector. It remains private when the function is called, because the function opening and application VK are kernel witnesses.
Address derivation.
receiveKeyHashField = uint256(keccak256(mlKem768PublicKey)) mod p // 0 when hasReceiveKey == 0
contractPreimage = poseidon(CONTRACT_ADDRESS_DOMAIN, nullifierKeyCommitment, functionSetCommitment, receiveKeyHashField, publishesNkRoot)
address = uint160(contractPreimage) // low 160 bits of the canonical integer
A contract address therefore binds nullifierKeyCommitment, the derived functionSetCommitment, the receive-key hash, and publishesNkRoot. Two creations with the same complete identity derive the same address, and the second is rejected as an existing account. An application that needs a distinct instance must use a distinct nkRoot or a distinct committed function configuration. An ECDSA key that happens to correspond to a contract address authorizes nothing, because every consumed note authenticates an installed account rule (§11.5) and contract accounts reject all writes.
Sponsorship. The appended section is the sponsor's fee payment (§13):
- every input slot is a
NATIVEnote owned by the sponsor, orDUMMY; - every output slot is
DUMMYexcept an optional ordinary privateNATIVErelay output and at most oneNATIVEchange note returning to the sponsor's owner identity; assetBurnAmount = 0, andsum(native inputs) = openFeeAmount + privateRelayAmount + changeAmount.
The contract-creation kernel authenticates one sponsor account of kind USER under the recent block's account-state root together with its selected installed rule, opens the recent block's output root, proves every native input and the change rule above, links the sponsor's application, and requires its private return statement to equal [configurationCommitment, poseidon(CONTRACT_FEE_SPONSORSHIP_ACTION_DOMAIN, transactionCommitment, contractAddress), recentPcashBlockHash, sponsorAuthorizationAccountId, pcashChainId]. This is the same sponsorship action as §11.7, with the contract being created as the named contract, so one installed sponsorship rule serves both. Reusing it is sound: a native-only transaction cannot satisfy the asset fee-payer kernel, which requires an asset input, and the sponsor's nullifiers are single-spend.
contractAddress is public input position 20. Consensus derives it from the body exactly as in the address derivation above, so a proof for one body cannot fund another. The section is not part of the address preimage: two parties may race to publish the same contract identity under different sponsorships, and exactly one succeeds.
Checks, in the contract-creation precedence of §10.5 after the section's output-data and field checks:
publishesNkRootis0or1(InvalidPublishesNkRootFlag), then the receive-key mode and length are a valid creation combination (InvalidReceiveKeyPresence).nullifierKeyMaterial < pand nonzero; then, for each function in body order,implementationVkHash_i < pandconfigurationCommitment_i < p(NonCanonicalContractField). Zero is allowed for a function's VK hash and configuration commitment.- Derive
nullifierKeyCommitmentfromnullifierKeyMaterial: forpublishesNkRoot == 0the material is the commitment itself; forpublishesNkRoot == 1it isnkRootand the commitment is derived. The resultingnullifierKeyCommitmentmust be nonzero and not equal toposeidon(NULLIFIER_KEY_COMMITMENT_DOMAIN, 0)(InvalidContractNkRoot). - The function set is nonempty (
EmptyContractFunctionSet), then selectors are strictly increasing (SelectorsNotStrictlyIncreasing), then eachcontractFunctionCommitmentis nonzero (ZeroContractFunctionCommitment), all in function index order. A zero function commitment would be indistinguishable from an absent sparse-tree leaf and could never be called. - Derive
functionSetCommitmentfrom the complete canonical list; each function commitment is an account-rule entry commitment and its selector is its entry index. The full function opening is consensus-verified and present in Ethereum data. - Derive
addressfrom the canonical operands and compute the account state commitment, and require it to be nonzero (ZeroContractAccountCommitment); then require no existing account at that address (ContractAddressExists); then require capacity (AccountCapacity). - Apply the section checks of §10.5 in order, and verify the proof against the protocol-pinned
contract_creationwire VK with the derived address as its last public input.
Effects. On success, atomically write the complete kind-1, revision-1 account state row and its leaf at the new accountIndex, together with the section's nullifiers, fee, and output commitments. The raw body and receive key remain in Ethereum data, not in intrinsic state.
13. Fees, Relay Payments, and Issuance Destinations
A transaction reaches Ethereum only if someone posts it and pays the Ethereum gas. PCASH gives transactions two ways to pay whoever does that, and, because successful type 0x00 transactions earn issuance, a way to say where that issuance goes.
Every private-value transaction, user-account update, and contract creation declares (openFeeAmount, issuanceOwnerCommitment) in its proof-bound private-value section.
Open fee. openFeeAmount is existing native PCASH offered publicly to the poster of the batch that successfully includes the action, identified by the batch's posterOwnerCommitment (§9.3). Native conservation is sum(native inputs) = sum(ordinary native outputs) + openFeeAmount. Issuance is created only at settlement, after the transaction, so it can never fund the same action's inputs or outputs.
Private relay payment. A wallet may also, or instead, pay a selected relayer with an ordinary encrypted native output, which is included in the ordinary-output sum above. It has no public fee classification. The proof requires every real output's owner commitment, including a relay payment's, to differ from issuanceOwnerCommitment (§11.3); by wallet convention the relay note's owner commitment is also fresh and unlinkable to it (§15.1).
Issuance destination. For a qualifying type 0x00 action:
issuanceOwnerCommitment = 0sends issuance to the successful poster (the open route);- a nonzero value sends issuance to that exact one-time owner commitment (the designated route). The destination is proof-bound, so copying the transaction into another batch cannot redirect it.
A nonzero issuance commitment requires openFeeAmount = 0, and a positive open fee requires a zero issuance commitment. openFeeAmount = 0 with a zero issuance commitment is valid and leaves issuance open to the poster. User-account updates and contract creations earn no issuance and require issuanceOwnerCommitment = 0. The proof enforces these rules (§11.2); nodes do not check them separately. User-account creation has neither a mandatory nor an optional PCASH fee.
The open fee and issuance are distinct accounting components even when settlement combines them in one native note (§14.3). A selected relayer is paid from an ordinary output and may separately receive issuance through the designated commitment on an eligible action. Self-publication uses a zero open fee and no relay-payment output, and an eligible action may designate the wallet's own one-time issuance commitment.
Who pays. A user-account update spends the updated account's own notes (§12.3). Contract creation uses a registered user-account sponsor (§12.4). Subsequent contract actions may pay from contract-owned native notes without any user initiator. The two-account sponsorship relation (§11.7) bounds the sponsor's contribution to the open fee, one exact ordinary relay output, and the sponsor's change, without granting authority over the primary account's value.
14. Native Issuance and Block Settlement
Native PCASH is created only when a block is settled, as the issuance component of system payout notes (§14.3). Issuance follows a declining supply curve. A controller raises or lowers the per-transaction offer according to whether issuance is behind or ahead of that curve, and each block splits its issuance equally among its successful qualifying transactions. The per-block computation has three stages: a block quote computed from the parent state before any transaction executes (§14.1), a capacity reservation as each transaction executes (§14.3), and settlement after the last transaction (§14.2–14.3).
Constants. In this section, S is the supply cap, I is gross issuance, and p(·) is reference progress; they are unrelated to profile S, the input count I, and the field modulus p. Let:
S = 1,000,000,000 × 10^18atomic units, the gross supply cap;T = 365 × 86,400seconds, the curve's reference duration;tau = 12seconds, the reference interval between qualifying actions;H = 2 × 86,400seconds, the controller's response half-life;M = 4, the maximum internal multiplier;k = 3, the maximum issuance speed: a block's allowance isktimes the baseline;Q = 2^128, the fixed-point scale.
The initial multiplier is provisionally 1/16, represented by initial lag -8 days × Q (the initial state's lagQ, §7.7). The maximum remembered positive lag is LQ = 4 days × Q. There is no registration grant, issuance balance, entitlement tree, separate native spending representation, calendar deadline, or saved allowance from an empty or underfilled block.
Qualification. Only successful type 0x00 actions qualify, once per action, in either profile. Qualification counts actions, not participants: publicly callable contracts may authorize repeated qualifying actions. Registrations, updates, contract creations, folded applications, failed actions, and system notes do not qualify. A qualifying action's issuance goes to its issuance destination (§13), independently of any ordinary private relay output, and the action remains valid when its actual issuance is zero.
14.1 The block quote
The block quote is computed once per block, before any of its transactions execute, from the parent state (I = issuedSoFar and lagQ, §6.5) and the block's elapsed time. For block 0, elapsed time is zero. For each later block, elapsed = timestamp - parentTimestamp; a timestamp regression is invalid and halts derivation.
Exhausted supply. If I = S, issuance has ended. The quote is all zero: baseline, offer, allowance, and oneActionMax are 0, rewardEach will be 0, and the next state keeps issuedSoFar = S and the parent's lagQ unchanged. Elapsed time is not added to the lag and no exponent is evaluated. The rest of §14.1 and §14.2 does not apply. Exhaustion is declared only at I = S.
Supply curve. Otherwise I < S. The equivalent reference progress is p(I) = T × (1 - sqrt(1 - I/S)), and the corresponding ideal curve is I(p) = S × (2p/T - (p/T)^2). T shapes the declining baseline. It is not an expiry, nor a guarantee that issuance completes in one year. Consensus evaluates the curve with exact integer intermediates and Q128 outputs:
q = isqrt(floor((S - I) × Q² / S))
progressQ = T × (Q - q)
baselineQ = floor(2 × S × tau × q / T)
allowanceQ = k × baselineQ
At I = S, q = 0 and progressQ = T × Q. The square-root floor rounds the baseline down and progress up. The same progressQ function is used before and after settlement, so progress differences telescope.
Lag. lagQ measures, in Q128 seconds, how far issuance is behind (positive) or ahead of (negative) the reference pace. The parent state stores the signed post-settlement lagQ. Before computing the offer, set:
offerLagQ = min(LQ, lagQ + elapsed × Q)
This clamp discards excess positive delay from authenticated state. There is no negative-lag clamp. A missed Ethereum slot advances lag by its elapsed seconds but does not multiply the block allowance.
Offer. If offerLagQ < -128 × H × Q, set multiplierQ = 0. This cutoff is applied before the exponent is quantized. Otherwise evaluate, in exact integers:
expQ16 = trunc(offerLagQ × 65536 / (H × Q)) # toward zero
shift = floor(expQ16 / 65536)
frac = expQ16 - shift × 65536 # 0 <= frac < 65536
factorQ16 = ((195766423245049 × frac + 971821376 × frac² + 5127 × frac³ + 2^47) >> 48) + 65536
scaled = shift < 0 ? (Q × factorQ16) >> -shift : (Q × factorQ16) << shift
multiplierQ = min(M × Q, scaled >> 16)
offerQ = floor(baselineQ × multiplierQ / Q)
multiplierQ approximates Q × 2^(offerLagQ / (H × Q)). The exponent is quantized to 1/65536 of a half-life (2.63671875 seconds) by truncation toward zero, and factorQ16 approximates 65536 × 2^(frac / 65536) with the cubic of Bitcoin Cash's aserti3-2d difficulty algorithm, whose coefficients, rounding constant, and operation order are unchanged. Whole half-lives are exact: offerLagQ = j × H × Q gives multiplierQ = 2^j × Q for -128 ≤ j ≤ 2, so the initial lag gives exactly Q / 16. Against 2^(offerLagQ / (H × Q)) the relative error before the final floor is below 0.013%. The cutoff and the LQ clamp keep -128 ≤ shift ≤ 2, and the right shifts floor nonnegative values. The polynomial exceeds the signed 64-bit range before its shift; no operation may wrap.
At the underflow boundary, offerLagQ = -128 × H × Q evaluates normally to multiplierQ = 1. A zero encoded offer is not terminal: elapsed time can raise the lag and restore a positive offer.
One-action maximum. The most any single action could earn in this block, if it were the only qualifying action, is:
oneActionMax = min(floor(offerQ / Q), floor(allowanceQ / Q), S - I)
Issuance is active in the block when oneActionMax > 0. This predicate decides capacity reservations (§14.3) and does not depend on how many actions eventually qualify.
14.2 Complete-block equal settlement
After all of the block's transactions have executed, let n be the count of successful qualifying actions. The count does not change the quote. For n > 0, every qualifying action receives the same integer-atomic issuance:
rewardEach = min(floor(offerQ / Q), floor(allowanceQ / (n × Q)), floor((S - I) / n))
nextIssued = I + n × rewardEach
nextLagQ = offerLagQ - (progressQ(nextIssued) - progressQ(I))
For n = 0, issuance is zero and nextLagQ = offerLagQ. The block's post-settlement state is issuedSoFar = nextIssued and lagQ = nextLagQ, except in the exhausted case of §14.1.
The following are consequences of these formulas, not additional rules:
- Division remainders stay unissued. There is no transaction-order remainder recipient, final sweep, or allowance carry-over.
- If fewer than
natomic units remain,rewardEach = 0and the entire block pays zero; a later block with fewer qualifying actions may consume the remainder. Such a block is not exhausted, so its lag still advances by elapsed time. - With several qualifying actions, the allowance and the remaining supply are divided equally, so actual issuance can be below both the internal offer and
oneActionMax. Sparse or uneconomic participation can extend distribution indefinitely. - Fees and burns do not change gross issuance, including fees that settle in the same note as issuance. Spent, unspent, lost, and burned issuance all remain included in it.
14.3 Capacity reservation and payout order
Settlement appends payout notes after all of the block's ordinary outputs, and it must never fail for lack of output-tree space. So each transaction with a private-value section reserves, before it may succeed, room for every payout note it could cause.
Reservation. Let MAX_NOTE = 2^128 - 1 and noteCount(A) = floor(A / MAX_NOTE) + (A mod MAX_NOTE != 0), the number of notes a payout of A needs. At the start of each block:
nextOutputIndex = parent outputCount
pendingPayoutSlots = 0
posterTotal = {} // accepted open-fee total per posterOwnerCommitment, across all batches
For each transaction with a private-value section (types 0x00, 0x02, and 0x03), at the OutputCapacity step of its precedence (§10.5), with O its output count, f its openFeeAmount, and C the posterOwnerCommitment of its batch:
feeSlots = noteCount(posterTotal[C] + f) - noteCount(posterTotal[C])
issSlot = 1 if the transaction is type 0x00 and issuance is active (§14.1), else 0
require nextOutputIndex + O + pendingPayoutSlots + feeSlots + issSlot <= 2^40
// all arithmetic is exact; otherwise the outcome is OutputCapacity
A zero-fee type 0x00 transaction still reserves issSlot; zero-fee types 0x02 and 0x03 reserve nothing beyond their outputs. The reservation state changes only if the transaction succeeds:
pendingPayoutSlots += feeSlots + issSlot
posterTotal[C] += f
nextOutputIndex += O // the transaction's ordinary outputs are appended now
A transaction that fails for capacity is not reconsidered, even when settlement ends up using fewer notes than were reserved.
Settlement. After the block's transactions have executed, compute rewardEach (§14.2) once and append the system payouts, in this order:
// 1. Designated issuance, one payout per action, in transaction order.
for each successful type 0x00 transaction t, in block order:
if t.issuanceOwnerCommitment != 0:
appendPayout(t.issuanceOwnerCommitment, openFee = 0, issuance = rewardEach)
// 2. Poster groups: open fees, plus open-route issuance, per poster commitment.
for each successful transaction t with a private-value section, in block order:
C = posterOwnerCommitment of t's batch
group[C].openFee += t.openFeeAmount
if t is type 0x00 and t.issuanceOwnerCommitment == 0:
group[C].issuance += rewardEach
// a group's position is the first t that adds a positive amount to it
for each group (C, openFee, issuance), in position order:
appendPayout(C, openFee, issuance)
appendPayout(C, openFee, issuance):
A = openFee + issuance
if A == 0: append nothing
append floor(A / MAX_NOTE) notes of amount MAX_NOTE, then one note of A mod MAX_NOTE if nonzero,
each an ordinary native note owned by C (§15), at nextOutputIndex, with split indices from 0
The open fee transfers existing PCASH; issuance creates new PCASH. Matching poster commitments aggregate across every batch in the block; distinct commitments and distinct PCASH blocks never aggregate. Each payout record keeps separate open-fee and issuance components even when one physical note combines them (§15.2). Gross issuance advances by exactly n × rewardEach. System notes have no transaction result of their own and earn no issuance.
14.4 Atomic commit
Commit output leaves, nullifiers, account changes, gross issuance, post-settlement lag, ordered payouts, and canonical block history atomically. A verifier or infrastructure failure aborts derivation rather than omitting payouts or changing the accepted set. New system notes and newly registered accounts become usable only through a later completed authenticated boundary (§10.6).
15. System Payout Notes and Recovery
A system payout note is the note settlement creates for a designated issuance or a poster group (§14.3). For every positive system payout amount A, canonical nonzero owner commitment C, and assigned output index j, construct:
body = poseidon(NOTE_BODY_COMMITMENT_DOMAIN, C, 0, A)
output = poseidon(OUTPUT_COMMITMENT_DOMAIN, body, j)
C is the action's issuanceOwnerCommitment for a designated payout, or the batch's posterOwnerCommitment for a poster payout. The note is spent with the ordinary rules: owner opening, account authorization, membership, amount bounds, and note nullifier. There is no system-note flag and no separate spending relation. The amount and system origin are public; the owner is hidden behind its secret-opening commitment. Distinct output indices distinguish otherwise identical payouts. Each designated issuance produces its own payout. Poster payouts aggregate per poster commitment across the complete block (§14.3), and one note may combine an open fee with issuance.
A payout note carries no encrypted output data, so its owner must be able to find it and reconstruct its opening another way. §15.1 gives the reference wallet's conventions for doing that from a seed; §15.2 gives the records a node keeps so that such recovery works.
15.1 Recoverable ownership
This subsection defines reference conventions, not consensus rules. Conformance vectors cover them (§17) so that wallets interoperate. H(...) is poseidon(...).
Designated issuance. The reference wallet derives a designated issuance destination with these domain-separated derivations, where ownerId and nkRoot belong to the recipient:
payoutSeed = H(PAYOUT_SEED_DOMAIN, noteNk(nkRoot))
txContext = H(PAYOUT_TX_CONTEXT_DOMAIN, chainId, shapeId, N_IN, N_OUT, nullifier_0, ..., nullifier_(N_IN-1))
issuanceSecret = H(PAYOUT_NOTE_SECRET_DOMAIN, payoutSeed, ISSUANCE_ROLE=3, txContext)
issuanceOwnerCommitment = H(OWNER_COMMITMENT_DOMAIN, ownerId, issuanceSecret)
The transaction context includes the compiled shape and the complete ordered nullifier list. It excludes proof bytes, the final transaction hash, the amount, outputs, the block, and the output index. Re-proving against another recent state therefore preserves recovery as long as the profile and nullifiers stay fixed. Every successful transaction consumes all its input nullifiers, including dummy nullifiers, so the same nullifiers cannot succeed again. Whether one authorization permits multiple distinct transactions is determined by the installed account rule.
Output secrets. Reference implicit output secrets bind the same compiled dimensions and every ordered nullifier:
noteSecretSeed = H(IMPLICIT_NOTE_SECRET_SEED_DOMAIN, noteNk(nkRoot))
noteSecret_i = H(TRANSACT_NOTE_SECRET_DOMAIN, noteSecretSeed, shapeId, N_IN, N_OUT,
nullifier_0, ..., nullifier_(N_IN-1), i, outputEntropy_i)
A note keeps this creation secret when it is spent through another profile; spending does not rederive it using the consuming transaction's profile.
Selected relayers. On a designated route, the receiving wallet derives issuanceSecret from its own payout seed and the transaction context. After the nullifiers are fixed, a selected relayer supplies an authenticated quote containing: that one-time issuance opening; a separate authenticated private receive destination for an ordinary native fee note; the exact private fee amount; the chain and transaction context; and an expiry. The ordinary fee note uses an independent fresh owner commitment and must never reuse the public issuanceOwnerCommitment. The wallet's semantic approval binds the exact private fee note, issuance commitment, transaction context, expiry, and funding constraints. The relayer decrypts its output envelope and recomputes the proof-bound ordinary note commitment before publishing. A different issuance secret for the same identity is a different commitment and requires new approval; changing the nullifiers requires a new quote. Quote authentication happens in the wallet, without a second in-circuit recipient-signature system.
Open route. For the open route, the poster derives:
posterContext = H(PAYOUT_BATCH_CONTEXT_DOMAIN, chainId, selectedHeadNumber)
posterSecret = H(PAYOUT_NOTE_SECRET_DOMAIN, payoutSeed, POSTER_ROLE=2, posterContext)
posterOwnerCommitment = H(OWNER_COMMITMENT_DOMAIN, ownerId, posterSecret)
The reference poster selects its known completed PCASH head number (uint64) before submission and carries the resulting context explicitly in every batch prepared at that head (§9.3). It may reuse a selected context across submissions. Neither the selected head nor the context has to match the eventual inclusion block; delayed submissions remain valid and recoverable. A shared commitment makes its batches publicly linkable, including across different inclusion blocks. The node cannot infer common ownership of different commitments. The exact itemsHash (§7.8) remains delivery provenance, independently of recovery.
Copying a transaction preserves a designated destination, which is proof-bound. An open destination follows the poster of the successful inclusion: republishing an open-route proof in a different batch that succeeds first redirects both its fee and its issuance.
These are application recovery conventions, not a universal secret derivation for contract keys. A contract rule that designates its issuance route supplies an independent secret or delivery convention; published contract nkRoot values cannot supply secret recovery material. Multiple authorizers approve one shared route according to their roles, and a distinct fee sponsor need not own the issuance.
15.2 Canonical recovery records
This subsection is a node requirement, not consensus: recovery records are not committed by any root. The node retains designated transaction contexts, explicit batch poster contexts, and ordered payout records, produced by the same settlement pass that creates the payouts. They are discovery material, not another mint authority. Each payout record carries:
- its kind,
designatedIssuanceorposterPayout; - its note amount, with separate open-fee and issuance components;
- its owner commitment, output index, and split index within its payout;
- its recovery contexts.
A designated record identifies its source transaction by Ethereum transaction index and batch item index, and retains that transaction's single context. A poster record identifies its contributing Ethereum transaction indices in encounter order and has no source transaction ID; it retains the distinct contexts of all contributing batches in encounter order, including batches whose only contribution is issuance at zero fee. When a payout splits, each note's open-fee component consumes the payout's remaining open fee before any issuance, so the components across its notes sum to the payout's open fee and issuance. Individual successful fee contributions remain available through batch manifests and transaction records. Untrusted posters can copy a commitment with an unrelated context, so recovery tries every retained context rather than trusting the first.
Canonical blocks, checkpoints, export and import, and suffix replacement preserve these records, or sufficient history to rebuild them. A node advertising seed restoration MUST NOT silently prune them; state roots alone do not suffice.
A fresh wallet scans paginated node records, derives candidate commitments from its seed, reconstructs matching ordinary notes, and obtains canonical membership and spentness evidence at a coherent completed boundary. It requires no saved transaction hashes, quote list, plaintext backup, Ethereum connection, or historical blob retrieval. An explicitly lagging index is not an authoritative zero balance. Orphaned discoveries are replaced on reorganization. Recovered notes and notes delivered by encryption share one native inventory. Consolidation uses ordinary private-value transfers of a supported profile when necessary to fit a requested spend, or when explicitly requested by an operator.
16. Parameters
Compiled PCASH V1 rules:
| Parameter | Value |
|---|---|
RECENT_PCASH_BLOCK_WINDOW_SECONDS |
3,600 |
OUTPUT_TREE_DEPTH / ACCOUNT_TREE_DEPTH |
40 |
NULLIFIER_TREE_DEPTH |
256 (Keccak sparse set) |
ETHEREUM_BLOCK_TREE_DEPTH / HISTORY_TREE_DEPTH |
40 |
STATE_TREE_DEPTH / BLOCK_HEADER_TREE_DEPTH |
4 |
ACCOUNT_RULE_SET_DEPTH |
8 |
| State keys | 0/1 output root/count, 2 gross issued, 3/8 signed Q128 lag limbs, 4 account state root, 5 account count, 6/7 nullifier-root limbs, 9–15 unassigned zero leaves |
| Block-header keys | 0 parent hash, 1 number, 2 timestamp, 3/4 transaction-results-root limbs, 5 state root, 6 Ethereum blockhash root, 7 PCASH history root, 8 extra data, 9–15 unassigned zero leaves |
AMOUNT_BITS (all notes and native fees) / asset aggregate bits |
128 / 131 |
| Output data size/version | Exactly 1,750 bytes / 0x01 |
ML_KEM768_PUBLIC_KEY_LEN |
1,184 |
MAX_TX_BYTES |
131,072 (fixed parsing bound, §9.3) |
MAX_BATCH_ENTRIES |
1,024 |
MAX_BATCH_BLOBS |
6 (maximum blobs in one Inbox transaction, §9.1) |
MAX_BATCH_BYTES |
761,825 (six-blob canonical payload capacity, §9.1) |
| Batch versions | 0x01 (nonzero poster owner commitment, count, and length-prefixed typed transactions) |
| Transaction types | private-value 0x00, user-account creation 0x01, contract-account creation 0x02, user-account update 0x03 |
| Aster accepted transaction types | 0x00, 0x01, 0x02, 0x03 |
| Private-value S / L / user-account-update / contract-creation public inputs | 20 / 102 / 21 / 21 |
| Serialized verifier-input fields in a private-value section | S: 14; L: 69; the chain ID comes from the common envelope and output-data hashes are derived from output data |
| Private application return fields | 5 |
| Private-value commitment and wire/effect arity | S: 4 input slots, 5 output slots; L: 32 input slots, 32 output slots |
| Account kinds | user 0, contract 1 |
| Protocol-pinned final wire VKs | private_value_wire, private_value_large_wire, user_account_update_wire, contract_creation_wire |
| Wire VK files | vks/<name>.chonk.vk in the release circuit bundle; the bundle manifest records each file's SHA-256, and sdk/src/constants/proverArtifacts.ts carries the current digests |
| Type-0 protocol kernels | Direct, native fee-payer, and asset issuance, each compiled for S and L |
| Account-action protocol kernels | user_account_update, contract_creation_with_native_fee_payer |
| Compressed proof bytes: private value S / L / account update / contract creation | 23,840 / 26,464 / 23,872 / 23,872 |
| Barretenberg release | 5.2.0 |
| BN254 G2 CRS SHA-256 | 0x01797bfc4de5a96f0e516a9ea4537d18786dc30cb991aca4274c95822b69c32f |
| Grumpkin G1 CRS SHA-256 | 0x8df01ac0f564db0b52857d37b363b8f8042cd1d886f8d8fd2677375a6290e870 |
| Launch identity | Selected and pinned by launch preparation, including the anchor |
17. Conformance Vectors
The V1 protocol conformance-vector package contains these JSON files under vectors/:
poseidon2_vectors.json: the field modulus, hash parameters, the empty-input case, and single- and multi-chunk known answers for §2.3.merkle_vectors.json: the empty ladder and roots, plus worked least-significant-bit-first membership examples for §2.4.monetary.json: exact curve, lag, and settlement transitions, exponent quantization boundaries, rounding boundaries, saturation, terminal state, and dust (§14).protocol_vectors.json: chain-neutral note and dummy nullifiers; permanent transaction slot digests and the transaction commitment; standard asset identity, issuance, and transfer; stable authorization account IDs; the account-update action and its complete unified account-state-commitment operands; reference output-note-secret derivation; designated-issuance and poster recovery; account state roots; the nullifier tree; account-rule entries; genesisextraData; Ethereum and PCASH history leaves and trees; transaction-result leaves and roots; exact-inclusion transaction IDs; typed StateRoot and header openings; the block hash; and contract-address derivation.
JSON is only a transport and display container; no commitment consumes JSON bytes. genesisHash is derived from canonical block 0 (§7.7) and binds its extraData. node assets/gen_domain_tags.mjs --check and node scripts/gen_vectors.mjs --check independently reconstruct the generated constants and protocol vectors. Example-rule and reference-implementation vectors are documented with their respective components and are not protocol conformance vectors.
Appendix A. Monetary Trust Resets
This appendix is a cross-version continuity criterion, not an Aster state transition. It defines when a later protocol may claim canonical monetary continuity with this chain after it stops trusting a predecessor proof system. V1 nodes evaluate nothing in it.
A fork declares a monetary trust reset when it no longer treats predecessor proofs as sufficient evidence that private value was created, conserved, spent, or authorized correctly. A fork without a reset carries value forward under its ordinary rules; a change of format, verifier, implementation, or parameter does not by itself require one. Let H be the final block of the distrusted generation. Blocks after H are re-derived under successor rules, so orphaned notes, payouts, and account effects are excluded. Account actions replay only when their authorization and recent-state requirements hold under successor rules.
Establishing predecessor value. A successor that recognizes predecessor value as native PCASH MUST independently establish, under the source generation's fixed historical interpretation:
- the commitment opening;
- authenticated membership in the applicable historical state;
- the asset identity and amount;
- ownership and a successor-recognized authorization relation;
- the canonical nullifier;
- that the nullifier was consumed neither at the recovery basis nor by an earlier successor claim.
The fork selects one recovery basis B <= H for native PCASH. Terminal recovery uses B = H. Snapshot recovery uses an earlier B at which native state is still trusted, admits only outputs present and unspent at B, and excludes every later native output and every descendant of a claimed note. Either way, the eligible set is one set and its claims are mutually exclusive. No predecessor proof is sufficient merely because it verified under its historical verifier. Historical transaction bytes, commitment and nullifier formulas, account states, and asset identifiers keep exactly their original meanings; a successor MUST NOT reinterpret a historical value to make it admissible. Value controlled by an authorization relation the successor cannot establish MAY remain unavailable.
Native PCASH. A successor claiming canonical native continuity MUST bound the aggregate predecessor-native value it admits:
nativeIssuanceEnvelope_H = S
legacyNativeDebit <= nativeIssuanceEnvelope_H
The gross cap S of §14 is the conservative proof-independent upper bound. The authenticated issued counter records system issuance, but it does not prove that a distrusted private-value circuit never counterfeited notes. A tighter bound on inherited value requires a separate proof-independent justification. Fees do not enlarge the envelope and burns do not reduce it. All recognized predecessor-native notes compete for the same capacity. Capacities from different generations MUST NOT be added: each later reset establishes one fresh envelope through its boundary. Each accepted claim debits at least the net successor-native value it makes spendable, including any successor fee funded by inherited value.
A release MAY temporarily quarantine predecessor-native value, but MUST NOT describe canonical native continuity as completed unless it provides a deterministic, permissionless, trustless predecessor-native claim path. That path MAY reveal the amount charged to the capacity; it MUST NOT require disclosure of the source commitment, source account, claimant, recipient, or unrelated balances. Admitted value is ordinary native PCASH: its later validity MUST NOT depend on which predecessor claim funded it, and it is not subject to a provenance clawback. The envelope provides containment, not restitution. Indistinguishable counterfeit claims may exhaust it before legitimate claims. Incident-specific rules MAY prioritize claims established by independently trustworthy facts, such as membership below a defect boundary, but every admitted amount still counts against the capacity.
Standard non-native assets. A nonzero assetId is an immutable identity. A successor MUST NOT assign it different semantics, supply, metadata, or ownership history, and MUST NOT recognize predecessor value under an existing nonzero assetId; such notes MAY remain quarantined under their original identifier, and migration to a fresh asset is the only continuation path. Any party MAY create a fresh standard asset and issue a bounded inventory of it to an immutable contract account whose installed application releases that inventory against predecessor-note claims. That application defines its own eligibility policy, including a basis earlier than H and the exclusion of later outputs and their descendants. Its fixed supply bounds its payouts under any policy, it requires no reset, and it MAY exist within a generation. Each predecessor claim MUST be accepted at most once per such account, and recognizing it grants the predecessor asset no authority over the successor asset. Consensus does not choose which candidate, if any, carries a predecessor asset's identity.
V1 side. V1 needs no generation field, inherited-value counter, asset registry, migration transaction, or state leaf to satisfy this criterion. Indexed note commitments and canonical nullifiers (§4.3), typed terminal state (§6.5), exact retained transaction bytes (§7.8), permanent transaction-type meanings (§3.2), the gross issuance cap (§14), and the immutable standard-asset definition (§11.6) supply every predecessor fact above without accepting a predecessor proof.
References
- EIP-2: low-s signature rule.
- EIP-712: typed-data signing.
- EIP-1559: base fee and burn semantics.
- EIP-4844: blob transactions and KZG commitments.
- Ethereum Execution APIs: Ethereum JSON-RPC and block tags.

