PR #1438proposed changes to·draft-ijaack-zsv

Add ZIP draft: Zcash Stablecoin Vaults

status → Draftby iJaack · updated 2026-11-05zips/draft-ijaack-zsv.md @ 69f2175f41 · upstream 4699a8d518view on GitHub ↗

Unofficial preview — the proposed text below is rendered from the pull request head and is not part of the canonical ZIP set.

No ZIP number or network upgrade has been assigned. Deployment allocations and outstanding review are listed below.

Terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD" and "MAY" in this document are to be interpreted as described in BCP 14 1 when, and only when, they appear in all capitals. Conformance requirements appear in the Specification section.

  • ZSV means Zcash Stablecoin Vaults.
  • zUSD is the provisional name of the single dollar-targeting stable asset defined here.
  • Principal is debt attached to an active vault.
  • Bad debt is stablecoin liability remaining after a vault closes without burning all its principal.
  • Stable supply includes all unspent zUSD, including coins whose spending keys have been lost.
  • A report certificate is one price report with the required matching reporter signatures.
  • A current vault reference identifies the transaction that created its latest state.

Abstract

This proposal adds native ZEC vaults and one protocol-defined stablecoin to Zcash. Borrowing against sufficient collateral creates zUSD; repayment and liquidation destroy existing zUSD and reduce the corresponding liability. Zcash consensus enforces ownership, collateral release, issuance, burns and independent conservation of both assets.

The protocol provides no arbitrary asset registration, issuer authority, governance token or general execution environment. It uses a dedicated transaction format, a separate ZEC vault value pool and public zUSD outputs. Existing ZEC transaction formats remain available under their applicable consensus rules. Public vaults and payments expose balances, amounts and financial relationships; shielded zUSD requires a subsequent specified and reviewed protocol.

A fixed reporter quorum supplies ZEC/USD observations. Consensus verifies their authentication and deterministic validity conditions, but cannot establish their economic truth. Full liquidation records any uncovered principal as bad debt. Further borrowing stops while bad debt remains; holders can retire that debt by burning existing zUSD without compensation. No dollar-redemption claim or guaranteed market peg is created.

The proposal is independent of a Dev Fund allocation or reserve. Those mechanisms may be considered separately. Research code and economic tests support selected design choices; native format, authorization and deployment rules require implementation and independent review.

Motivation

ZEC holders may want dollar-denominated liquidity while retaining exposure to their remaining collateral. A native vault would let them borrow under rules checked by Zcash nodes, accepting liquidation risk when collateral loses value. Holders of the issued asset would bear collateral, oracle, liquidity and protocol risk.

A data commitment alone cannot lock ZEC or create a native stablecoin. General-purpose Zcash Shielded Assets would support unrelated tokens and issuers. ZSV instead proposes one monetary use case with a closed issuance path. The smaller asset domain reduces issuance cases, but the financial and consensus complexity remains substantial.

The intended benefit is native borrowing and a transferable dollar-targeting asset without an arbitrary token-launch interface. Whether that benefit justifies the oracle dependency, privacy cost and additional consensus rules is a question for community review.

Privacy Implications

This first version has public vaults and public stablecoin payments. It reveals collateral, principal, owner keys, payment amounts and liquidation events. Creating a vault from previously shielded ZEC requires exposing value to the permitted transparent input path. Key reuse and transaction graph analysis can link borrowing, payments and repayment. Liquidation can reveal financial distress.

Adding shielded payments later would not erase these disclosures. A fixed-asset shielded profile could reuse selected ZSA transfer, note and proof techniques, but would need its own precise proof statements, value-pool accounting and privacy review. Existing ZSA audits would not automatically apply to a changed circuit. Private vaults also need liquidation information to remain available without borrower cooperation; this version does not attempt that design.

Requirements

The design goals are:

  • Enforcement of collateral, debt, issuance and liquidation by Zcash consensus, with no bridge, external execution chain or custody committee.
  • Exactly one additional native asset, issued only through valid collateralized borrowing.
  • Atomic value accounting, with no alternative ordinary spend of locked collateral and no hidden erasure of stablecoin liabilities.
  • Repayment, collateral additions, debt-free exit and payment transfers during an oracle outage.
  • Deterministic branch-specific validation, bounded work and equivalent behavior across implementations.
  • Continued ordinary ZEC activity without requiring unrelated users to submit ZSV operations.
  • Defined vault operation without a reserve, Dev Fund allocation or privileged recovery key.

Non-requirements

This proposal does not add arbitrary issuers, asset descriptions, NFTs, a VM, a governance token, a bridge or external custody. It does not prevent off-protocol claims, inscriptions, derivatives or speculation. It does not allocate block rewards or NSM value, establish on-demand dollar redemption, or promise that zUSD trades at one dollar.

Dev Fund allocation and reserve stabilization are separate candidate work. Shielded payments and wallet address standardization may require companion ZIPs. The oracle rules are part of this core proposal; they are not an optional service that a node may configure differently.

Specification

Scope and activation parameters

This specification defines a new, dedicated transaction version with a fixed ZEC/zUSD domain. It does not modify the meaning of an allocated transaction version or enable general ZSA issuance. Deployment allocates its version/group identifiers, consensus branch context, activation heights and fixed oracle roster. Until those allocations and the required implementation/review exist, this is an unactivated proposal.

Validators MUST reject the new format before its network-specific activation. After activation, ordinary ZEC transaction versions MUST retain their applicable validation rules. The new format MUST NOT be interpreted as the existing v6 format or as withdrawn ZIP 230. An allocated existing format is not available for reassignment by this draft. The existing format and Zebra integration baseline is recorded in the pinned sources. 2

At activation, the ZSV vault set and public zUSD UTXO set are empty, stable supply and bad debt are zero, and no price report is active. Activation MUST NOT create ZEC, zUSD, a reserve balance or a privileged allocation. Borrowing remains unavailable until a valid report certificate is accepted.

Encoding conventions and bounds

All integers are unsigned little-endian unless stated otherwise. Hashes are raw 32-byte consensus digest bytes, not reversed display hex. A compressed secp256k1 key K is exactly 33 bytes: prefix 02 or 03 followed by the canonical field encoding of a valid non-infinity point's x-coordinate. Invalid/noncanonical keys fail. New asset outputs have no script, arbitrary metadata or asset identifier.

Counts are canonical CompactSize. Each count below is at most 64, so its only valid encoding is one byte. Resolved payment inputs and newly created payment outputs have strictly positive values. Zero is permitted only for operation fields that explicitly allow it. Each ZEC payment value, each transaction's ZEC input/output sums and value-balance sides, and the persistent vault-pool balance MUST fit MAX_MONEY = 2,100,000,000,000,000 zatoshi. Each zUSD payment value, each transaction's stable input/output sums and value-balance sides, and the persistent stable supply MUST fit S_MAX = 100,000,000,000 micro-zUSD. These are per-transaction and state bounds; they do not limit the sum of all transfers across a block, which can reuse value through valid same-block dependencies. Signed maximum-burn and minimum-receipt limits are unsigned u64 bounds, not additional input or output value. A serialized ZSV transaction, including authorization, is at most 65,536 bytes. Existing block size limits remain in force. Each block contains at most 256 transactions of this new version, at most 1,024 required secp256k1 signature slots across them, and at most 4,096 new native ZEC plus stable output entries across them. Slots count native input signatures, stable input signatures and any operation signature whether valid or not; outputs count both explicit output vectors. The oracle permits at most one certificate and at most 7 Ed25519 report signatures per block. Check these counts before signature verification and state insertion. These are exact proposed limits, not benchmark-calibrated production recommendations. There is no lifetime UTXO cap or permitted-wallet list.

The fee/residual calculation uses checked signed arithmetic of sufficient width. Core collateral-ratio and liquidation products use checked u128. A new format has no coinbase input, shielded bundle, JoinSplit, general script vector, asset selector or optional opaque extension field.

Effect body E

In the encoding tables, + denotes byte concatenation, and fields are listed in wire order.

A transaction consists of E followed immediately by its authorization tail A, with no trailing bytes. E is:

FieldBytes / rule
headeru32: high bit 1; remaining 31 bits equal the unallocated ZSV_VERSION
versionGroupIdu32: unallocated ZSV_VERSION_GROUP_ID
consensusBranchIdu32: exactly the active upgrade branch ID
lockTimeu32, required 0
expiryHeightu32, H<=expiryHeight<500,000,000 and expiryHeight>0, where H is inclusion height
nZecInputsCompactSize,0..64
zecInputsnZecInputs*(prevTxid32 + prevOutputIndexu32)
nZecOutputsCompactSize,0..64
zecOutputsnZecOutputs*(valueu64 + recipientKey33)
nStableInputsCompactSize,0..64
stableInputsnStableInputs*(prevTxid32 + prevOutputIndexu32)
nStableOutputsCompactSize,0..64
stableOutputsnStableOutputs*(valueu64 + recipientKey33)
operationTagu8, one of 0..5 below
operationPayloadexactly the tag's payload
hasOracleu8, exactly 0 or 1
oracleEffectsabsent if 0; if 1 the exact 45-byte certificate effects below

Native input sequence numbers are implicitly final (0xffffffff); they have no wire field. Native outputs create ordinary transparent ZEC UTXOs with the exact standard script 0x21||recipientKey33||0xac (P2PK). The input must resolve to a real unspent transparent output whose locking script matches that exact P2PK grammar. Its value comes from native branch state, not transaction data. Existing coinbase-maturity, duplicate-spend, money-range and other applicable native input rules continue to apply. Existing ZEC transaction versions and their accepted scripts remain unchanged; users may send ZEC to a P2PK output through an ordinary transaction before using it here.

Stable outputs create entries only in the new zUSD payment UTXO namespace. They are not native ZEC transparent outputs. Stable input indices resolve only in that namespace. Native and stable (txid,index) namespaces are distinct: an equal byte tuple does not make the two assets interchangeable. Every input must exist, be unspent on the exact candidate branch, have an in-range positive value and have the expected fixed-key ownership. Repeated outpoints within a namespace fail. Same-block dependencies use previously validated transaction outputs in block order.

Only one optional operation is allowed. Tag 0 means NONE and has no payload; it permits pure stable transfers. If hasOracle=0 and tag=NONE, at least one stable input is required. If hasOracle=1, tag must be NONE and both stable counts must be 0. Native fee inputs and outputs remain permitted in an oracle transaction. Other empty uses of the new version fail; ordinary ZEC transactions continue using their existing formats.

Vault state and operation payloads

A current vault is stored in a dedicated ZEC vault value pool, keyed by its VaultRef, the raw 32-byte effects txid of the OPEN or UPDATE that created it. Because each transaction has at most one operation, no operation index is needed. The state record is (ownerKey33, collateralu64, principalu64). OPEN/UPDATE create one successor at their own txid; UPDATE consumes the previous ref and preserves its owner. CLOSE/LIQUIDATE consume without a successor. Current refs are unique; inserting an existing current ref fails.

Vault collateral is not also an ordinary transparent output. There is no OP_TRUE collateral script and no spendable duplicate. Its amount enters the native ZEC vault value pool and can leave only through a valid operation and the fee equation below. All node paths need this accounting, including direct finalization/checkpoints; ordinary native spends can resolve only actual native UTXOs, never the vault pool.

TagPayload
0 NONEempty
1 OPENownerKey33 + newCollateralu64 + newPrincipalu64 + oracleID32
2 UPDATEpriorVaultRef32 + newCollateralu64 + newPrincipalu64 + oracleID32 + ownerPayoutIndexu32
3 CLOSEpriorVaultRef32 + ownerPayoutIndexu32
4 LIQUIDATEpriorVaultRef32 + oracleID32 + maxBurnu64 + minCollateralu64 + liquidatorKey33 + ownerPayoutIndexu32 + liquidatorPayoutIndexu32
5 RETIRE_BAD_DEBTburnAmountu64

OracleID is the exact identifier of the block's parent-selected pricing observation defined by the oracle specification. For OPEN with D=0, and UPDATE without a price requirement, oracleID is exactly 32 zero bytes. A price-requiring operation uses the active nonzero ID; no alternate or stale observation is accepted. This prevents an irrelevant observation from needlessly restricting oracle-free repayment/deposit, and makes its wire encoding unique.

OPEN requires C>0. UPDATE requires C'>0 and a changed (C,D) pair; (0,0) uses CLOSE. Nonzero D' must satisfy the minimum-debt rule. The owner auth key for OPEN is its encoded key; UPDATE/CLOSE use the actual prior state's owner. LIQUIDATE uses its encoded liquidator key. RETIRE has no operation-owner signature: actual stable input authorization controls the burn.

A mandatory payout index names an ordinary native ZEC output in this same transaction. The named output must have exactly the required amount and exact P2PK recipient. UPDATE with C'<C requires a payout of C-C' to the immutable owner. UPDATE without released collateral uses the sentinel 0xffffffff. CLOSE pays exactly oldC to the owner. Every valid current vault has positive collateral, so this payout always uses an output index. LIQUIDATE pays z to the liquidator and C-z to the owner. A zero required payout uses sentinel 0xffffffff and consumes no output. A positive payout requires an index in range; sentinel is invalid. Both positive liquidation payout indices must be distinct, even for self-liquidation when ownerKey=liquidatorKey. One output cannot satisfy two mandatory payouts. Extra native outputs remain allowed and are covered by all signatures and value conservation.

The exact collateral payouts cannot silently pay fees. Any positive residual fee must be supplied by other actual native value (normally free ZEC inputs). Minted stable outputs may use any valid recipient keys chosen in the fully signed transaction: borrowing and sending value to another recipient can be atomic. No amount is inferred from a memo or claimed caller balance.

Per-asset value equations

Define C_before as the actual prior vault collateral, or 0 for NONE/OPEN/RETIRE; C_after is OPEN/UPDATE's encoded collateral, or 0 for NONE/CLOSE/LIQUIDATE/RETIRE. Let Mint and Burn come only from the validated core operation:

  • OPEN: Mint=D', Burn=0.
  • UPDATE: Mint=max(D'-D,0), Burn=max(D-D',0).
  • CLOSE: Mint=0, Burn=D (close can atomically repay all principal).
  • LIQUIDATE: Mint=0, Burn=x from the specified exact liquidation formula.
  • RETIRE_BAD_DEBT: Mint=0, Burn=r, with 0 < r <= B at transaction entry.
  • NONE: Mint=Burn=0.

Then require:

sum(stableInputs) + Mint = sum(stableOutputs) + Burn
Fee = sum(nativeZecInputs) + C_before - C_after - sum(nativeZecOutputs)
0 <= Fee <= MAX_MONEY

The fee is counted in ordinary ZEC fee/coinbase accounting, including any separately active fee-disposition rules. Stable value cannot pay it. These rules do not introduce a positive consensus minimum fee; relay/mining fee policy remains separate. The vault value pool changes by C_after-C_before, and total stable supply changes by Mint-Burn. Live principal changes and bad-debt handling follow the core; RETIRE decreases B and S together without touching active collateral or principal. Borrowing requires B=0 at transaction entry, so it cannot be self-financed by another operation in the same transaction (and only one operation is allowed).

Hashes and authorization

Let H(label,bytes) be BLAKE2b-256 with the ASCII label right-padded with zero bytes to a 16-byte personalization. All labels below are exact and case-sensitive; no NUL is appended to message bytes. Let G be the raw 32-byte genesis block hash of the configured network. G comes from trusted network configuration, never transaction data. Hashing APIs therefore need explicit network context; branch IDs alone do not distinguish Mainnet from Testnet. The header already commits the branch ID.

txid = H("ZSV_TxId_v1", G || E)
contextHash = H("ZSV_Context_v1", R)
signatureDigest(role,index) = H("ZSV_Sign_v1", G || txid || contextHash || u8(role) || u16LE(index))
authDigest = H("ZSV_Auth_v1", txid || A)

R is authoritative resolved input context, encoded in this exact order:

CompactSize(nZecInputs)
  for each native input, in wire order:
    prevTxid32 || prevOutputIndexu32 || actualValueu64 || actualP2pkKey33
CompactSize(nStableInputs)
  for each stable input, in wire order:
    prevTxid32 || prevOutputIndexu32 || actualValueu64 || actualOwnerKey33
u8(hasPriorVault)  # 1 for UPDATE/CLOSE/LIQUIDATE, else 0
  if 1: priorVaultRef32 || actualOwnerKey33 || actualCollateralu64 || actualPrincipalu64

No caller-supplied prevout value/key or prior vault record can replace these state lookups. Signatures bind every effect, input reference, value, key, operation, oracle bound and payout. There are no selectable SIGHASH modes and no implicit whole-parent-state checkpoint. Same effects on the same live inputs can be valid on competing branches when the contextual oracle/expiry/policy rules also pass; this is normal fork behavior, not a replay across networks or upgrades.

A secp256k1 authorization consists of u8(signatureLength) ||strictDER(signature), with length 8..72. The DER is the shortest valid ASN.1 encoding of the ECDSA pair; round-trip canonical DER is required, 1<=r<n and 1<=s<=floor(n/2), where n is the secp256k1 subgroup order. No appended hash-type byte, extra push opcode or trailing data is allowed. Verify ECDSA against the indicated 32-byte digest and authoritative key. Low-S and minimal DER are consensus rules for this version.

A contains exactly these slots, without counts or padding:

  1. One signature for every native input, in wire order, role 0/index 0..nZecInputs-1, verified with its actual P2PK key.
  2. One signature for every stable input, in wire order, role 1/index 0..nStableInputs-1, verified with its actual owner key.
  3. For OPEN/UPDATE/CLOSE/LIQUIDATE, exactly one operation signature, role 2/index 0, using the operation auth key defined above. NONE/RETIRE have no such slot.
  4. If hasOracle=1, exactly popcount(signerMask) raw 64-byte Ed25519 report signatures in ascending reporter-set index order, under the separate oracle message/ZIP215 validation rules.

A signer may appear in several roles; every required role still has its own signature slot. This prevents treating a fee input signature as authority over an unrelated vault. The effects txid excludes all signatures, allowing dependent transactions to be constructed before authorization. authDigest commits the complete tail. For this version, the transaction Merkle tree leaf is txid, and the corresponding ZIP244 authorization-data tree leaf is authDigest. Existing Merkle and block-commitment algorithms are unchanged. It is not a pre-v5 placeholder leaf. ZIP239 witnessed relay treats this version as witness-bearing: wtxid is the exact 64 bytes txid||authDigest, in raw digest order. The version-dispatch rules are extended accordingly; older-version hashes and their relay behavior are unchanged. At the pinned Zebra source, these roles correspond to zebra-chain/src/block/merkle.rs::AuthDataRoot, block/commitment.rs and transaction/hash.rs::WtxId (whose byte conversion concatenates these two 32-byte hashes). 3 4

Oracle effects integration

The oracle effect structure is:

observedHeightu32 || observedBlockHash32 || priceu64 || signerMasku8

This is 45 bytes. The mask's high bit is 0 and popcount is 5..7 for the fixed seven-reporter deployment roster. The active roster ID is derived from the specified sorted keys and is not repeated on wire. Oracle signatures bind their separate network/branch/roster/report message. OracleID excludes the signer mask/signatures, so differing valid quorums over identical content identify the same observation. The Oracle rules below provide exact ancestry, age, ordering and rotation rules.

Units, ranges and arithmetic

SymbolProposed value or definition
q100,000,000 zatoshi per ZEC
m1,000,000 micro-zUSD per zUSD
CVault collateral in zatoshi; 0 <= C <= MAX_MONEY
MAX_MONEY21,000,000 * q = 2,100,000,000,000,000 zatoshi, subject to the base protocol's native ZEC amount rule
DVault principal in micro-zUSD
PAccepted micro-USD per ZEC; integer 1 <= P <= 2^64 - 1
D_MIN100 * m = 100,000,000 micro-zUSD
S_MAX100,000 * m = 100,000,000,000 micro-zUSD
R_OPEN250 percent
R_LIQ200 percent
K_LIQ105 percent, including the 5 percent liquidation bonus
SOutstanding native zUSD supply
BExplicit bad debt

These opening, liquidation, bonus, debt-floor and supply-cap values are fixed draft defaults carried from the prototype. In particular, the small initial supply ceiling bounds nominal outstanding liabilities; it does not prove their dollar value or economic safety. Changing a value requires the versioned consensus change defined by the activation specification; there is no runtime operator setter.

All encoded monetary amounts are unsigned integers. Additions and subtractions MUST reject overflow or underflow. Products and divisions in the rules below MUST produce the exact mathematical integer result. A checked unsigned 128-bit implementation suffices for these ranges: even MAX_MONEY * (2^64 - 1) * 100 is less than 2^128. No floating point, saturating arithmetic, silent truncation, approximate ratio, or overflow-dependent validity is permitted.

floor_div(a,b) is integer floor for a >= 0 and b > 0. ceil_div(a,b) is a / b + (a % b != 0) using integer quotient and remainder. An implementation using a + b - 1 must establish that the intermediate does not overflow.

A debt-bearing vault is safe for opening/withdrawal exactly when:

C * P * 100 >= D * q * 250

A vault is liquidatable exactly when D > 0 and:

C * P * 100 < D * q * 200

Equality at 250 percent passes the opening test. Equality at 200 percent is not liquidatable. Debt-free vault operations do not need a conversion price.

State and transaction shape

At activation, the new value pool, vault set, S and B start empty/zero. Existing ZEC is not reissued into a trusted fixture. Ordinary native input authorization and ZEC supply rules continue to apply.

Each live vault has an immutable owner authorization key, collateral C > 0, principal D, and one current native reference. D MUST be zero or at least D_MIN. The wire proposal defines a current reference as the creating transaction identifier alone, because each transaction has at most one financial operation. A transition consumes the current reference once and creates at most one successor. A spent or nonexistent reference cannot be revived by asserting its former balances.

A native transaction contains at most one optional financial operation: NONE, OPEN, UPDATE, CLOSE, LIQUIDATE or RETIRE_BAD_DEBT. It can also contain permitted native ZEC inputs/outputs and native zUSD inputs/outputs. NONE permits an ordinary zUSD transfer with no issuance or burn. OPEN incorporates initial borrowing; CLOSE incorporates full repayment. This gives atomic opening/borrowing and repayment/closure without an arbitrary program or action list.

A transaction with NONE and no oracle certificate requires at least one zUSD input, as specified by the wire grammar. Unrelated ordinary ZEC transactions remain valid under their existing format rules.

The owner signs the entire authorized transaction body and relevant resolved previous values under the new native signing domain. Recipient selection for newly borrowed zUSD is covered by that signature. This is equivalent in financial effect to the model's Borrow followed by Transfer, although the model credits the owner first. No conversion between Ed25519 and secp256k1 identities, pre-registered model/native key map, fixture chain domain, account nonce or whole-parent-state intent is adopted by these core rules.

For each transaction define native zUSD input value I_s, native output value O_s, permitted mint M and permitted burn R. The required native stable value balance is:

I_s + M = O_s + R

Only the explicit rules below authorize M or R. Output omission, a memo, a claimed burn field or an inaccessible key is not an accounting burn. S counts the total value of consensus-recognized unspent zUSD outputs, including outputs whose private keys have been lost.

For a vault-changing transaction, write C_old = 0 for OPEN and C_new = 0 for CLOSE/LIQUIDATE. Its ZEC balance is:

I_zec + C_old = O_zec + C_new + F_zec

For NONE and RETIRE_BAD_DEBT both collateral terms are zero. F_zec is a nonnegative native transaction fee accounted for by the ordinary block/coinbase fee rules. No zUSD fee, collateral subsidy or increase in ZEC supply is authorized. Normal relay/mining policy may require a positive fee; this equation does not invent a positive consensus minimum. Exact indexed collateral payouts described below cannot silently have fees subtracted from them.

After each ordered transaction:

S = sum(live vault principal) + B
S = sum(value in native unspent zUSD outputs)
vaultPoolBalance = sum(live vault collateral)
0 <= vaultPoolBalance <= MAX_MONEY
0 <= S <= S_MAX
0 <= B <= S

The second equality is replaced only by an explicitly specified public/shielded value-pool equality in a later shielded extension. It is not a requirement to determine whether an owner still knows a private key.

Oracle use

A price-dependent operation MUST bind the exact canonical report identifier accepted by the oracle specification, not merely a height or a caller-asserted price. It MUST reject if that report is absent, no longer canonical, or outside its allowed validity/anchoring window. The Oracle rules below use seven configured reporters, five matching signatures and a maximum twelve-block anchored observation age. Those acceptance and ordering rules are specified in the Oracle rules below, not redefined here.

Operations without a price requirement use the canonical zero/absent oracle identifier specified by the wire format. Repayment, collateral addition without borrowing, debt-free withdrawal/closure, ordinary transfer and bad-debt retirement remain possible during oracle outages. No validator fetches a price from the web or uses its local wall clock.

OPEN

OPEN supplies C_new > 0, immutable owner authorization and D_new. The owner authorizes the transaction. Native ZEC inputs fund C_new through the value balance above; existing collateral cannot simultaneously remain spendable elsewhere.

D_new MUST be zero or at least D_MIN. If D_new > 0, B at transaction entry MUST be zero; the exact fresh canonical report is required; the 250-percent inequality MUST hold for (C_new,D_new); and S + D_new MUST be <= S_MAX. Set M = D_new, R = 0, S' = S + D_new and B' = B. If D_new = 0, no oracle is required.

UPDATE

UPDATE consumes one current vault and retains its owner. Let delta_C = C_new - C_old and delta_D = D_new - D_old, understood as mathematical signed differences rather than a wrapping unsigned calculation.

C_new MUST be strictly positive. D_new MUST be zero or >= D_MIN. An unchanged pair (C_new,D_new) = (C_old,D_old) is rejected. A transition to (0,0) uses CLOSE; no empty live vault is retained. These restrictions remove two needless successor forms accepted by the account prototype and are explicit proposed encoding/state changes.

If delta_D > 0, B at transaction entry MUST be zero, a fresh exact canonical report MUST be bound, the final state MUST satisfy the 250-percent inequality, and S + delta_D MUST be <= S_MAX. Set M = delta_D and R = 0.

If delta_D < 0, burn R = -delta_D actual native zUSD and set M = 0. If delta_D = 0 both are zero. There is no owner-authorized debt forgiveness. Repayment that leaves debt strictly between zero and D_MIN is invalid.

If C_new < C_old and D_new > 0, a fresh exact canonical report and the final 250-percent inequality are also required, even when debt decreases. If C_new < C_old, the indexed owner payout MUST equal C_old - C_new; it MUST use the owner's prescribed native payment lock. If C_new >= C_old, no collateral-release payout exists. Deposits fund the increase through native inputs.

Set S' = S + M - R and B' = B. Owner authorization is required for all UPDATE forms, including pure deposit and pure repayment, matching the reference policy. A future third-party repayment/deposit facility requires an explicit change; it is not silently inferred from the absence of collateral release.

CLOSE

CLOSE consumes one current vault, requires its owner authorization, burns exactly R = D_old actual native zUSD, pays exactly C_old to the owner's indexed native payout and creates no successor. M = 0, S' = S - D_old and B' = B. It needs no oracle even when D_old > 0 because the entire debt is paid in the same transaction.

This combines the existing model's Repay(full debt) followed by Close. The model's standalone Close requires debt already zero; the native combined encoding does not forgive the old principal.

LIQUIDATE

LIQUIDATE consumes and closes one current vault. It requires the exact fresh canonical report, D_old > 0, and the strict 200-percent liquidation inequality. Borrower authorization is unnecessary. The liquidator authorizes the complete transaction and can be any holder, including the borrower.

Compute using the original C and D:

x = min(D, floor_div(C * P * 100, q * 105))
z = ceil_div(x * q * 105, P * 100)

The verifier MUST establish 0 <= z <= C. It MUST require x <= signed_max_burn and z >= signed_min_collateral. R = x and M = 0. The transaction consumes actual native zUSD sufficient for x and returns permitted change through the stable value equation.

The indexed liquidator payout MUST be exactly z zatoshi under the signed liquidator payment key. The indexed owner payout MUST be exactly C-z under the immutable owner's payment key. Payout indices MUST be unambiguous and cannot count the same output twice; the zero-value sentinel convention is specified by the wire format. No successor vault remains.

Set:

S' = S - x
B' = B + (D - x)

This retains the model's full-vault liquidation policy and rounding. If x = 0, z = 0 and the operation remains valid when the signed bounds permit it. No zUSD input is then required solely for the zero burn, and the owner receives C; the entire D becomes bad debt. The native ZEC fee/value rules still apply. There is no claim that anyone has an economic incentive to submit that dust liquidation.

Liquidation with insufficient actual liquidator zUSD rejects; it does not reduce x to the caller's balance or silently leave a partial vault. A price crash can create B > 0 even with honest reporters and immediate execution.

RETIRE_BAD_DEBT

This new proposed operation makes recovery possible without a reserve or privileged reset. A holder chooses an integer r satisfying 0 < r <= B at transaction entry. The holder must authorize spending actual zUSD inputs. Set R = r, M = 0, S' = S-r and B' = B-r. No vault is consumed or changed, no oracle is needed, and no ZEC is paid by the protocol.

The holder receives no redemption claim, future repayment right, reward entitlement, priority, governance privilege or ownership of another vault. Ordinary input change is returned under the signed transaction. The operation is an uncompensated token burn against recorded bad debt.

A NONE or RETIRE_BAD_DEBT transaction cannot create zUSD or borrow. The one-operation grammar prevents combining bad-debt retirement with newly minted debt in the same transaction. A later transaction can borrow only after preceding valid retirement has actually reduced B to zero.

This operation is absent from the current Rust model and collateral profile. Its new conformance vectors and future implementation must be tracked separately from passing legacy tests.

Loss and liveness policy

Borrowing is prohibited while B > 0. Existing transfers, valid debt repayment, deposits, permitted withdrawals/closure, liquidation and RETIRE_BAD_DEBT continue. The freeze is based on recorded B at the operation's ordered transaction entry; latent losses in unliquidated vaults are not magically detected.

Only a valid RETIRE_BAD_DEBT reduces B in this core version. No clock, oracle report, miner field, operator key or bookkeeping reset may erase liabilities. S decreases by the same amount, preserving the debt/supply identity. A future separately approved reserve mechanism could fund voluntary retirement using tokens it actually owns; no such funding or reserve authority is activated by this core.

Borrowing resumes automatically when the actual B value reaches zero and the normal fresh-price/collateral/cap conditions pass. If no holder is willing to retire tokens, the freeze can persist indefinitely. The protocol does not guarantee recapitalization, price recovery or a dollar redemption right. This is an explicit complete loss policy with a remaining economic availability risk, rather than an unspecified administrative recovery hook.

Complete block transition and persistence

Validation MUST begin from the candidate block's exact parent state, independently of the node's currently selected tip. The candidate includes ordinary native UTXOs and value pools, the ZEC vault pool/current vault set, stable UTXOs, S, B, canonical oracle state, fee accounting and activation context. No caller-asserted balance or post-state replaces validation against that parent.

Validators MUST apply every ordinary and ZSV transaction in block order to a private candidate state. Later transactions see the valid outputs, spent references and state changes of earlier transactions in that block. The oracle placement rules below determine when a certificate becomes usable. A missing, already spent or duplicate input/reference is invalid; creating an output or current vault reference MUST NOT overwrite an existing unspent entry in its namespace. The absence of a ZSV operation in an ordinary transaction cannot authorize access to the separate vault or stable value pools.

Each transaction MUST satisfy its native and ZSV rules, exact payouts, per-asset balances, monetary bounds and applicable state invariants before the next transaction is evaluated. The vault-pool delta is applied atomically with ordinary ZEC input/output and fee deltas, so collateral is never counted as both an ordinary output and a vault balance. A block that fails any transaction, oracle, resource, commitment or ordinary consensus check MUST be rejected with no persistent changes to any of these states.

Only a fully valid block may commit the complete candidate transition. Reorganization, finalization and restart MUST reconstruct the same parent-derived state as ordered replay of the accepted branch. Mempool admission is provisional and MUST NOT substitute for block-context validation. Checkpoint and direct-finalization paths MUST enforce equivalent rules and establish matching ZSV state; a path that merely skips the new checks or imports asserted ZSV balances is invalid. This specification requires equivalent results, not a particular database layout or caching strategy.

Oracle rules

Deployment roster and domains

For each activated ZSV consensus branch, the deployment specification MUST provide seven distinct Ed25519 public keys, ordered lexicographically by their 32-byte encodings. The keys MUST use canonical encodings of non-identity points in Ed25519's prime-order subgroup. In group notation, each decoded key A satisfies A != identity and [ell]A = identity, where ell is the Ed25519 subgroup order. Canonical encoding means re-encoding the decoded point using the RFC 8032 compressed encoding reproduces the 32-byte input exactly.

The reporter set is fixed for that consensus branch. There is no oracle transaction, administrator signature, voting token or runtime configuration that changes the roster, quorum or freshness period. A roster change requires an explicitly specified network upgrade. Local configuration MUST NOT override the deployment roster.

Define:

reporterSetID = SHA256(
    ASCII("ZSV/OracleSet/v1\0") ||
    u8(7) || u8(5) ||
    key[0] || ... || key[6]
)

Here \0 is one zero byte, concatenation is byte concatenation, and SHA256 returns its 32 raw digest bytes. The active network's genesis hash is the 32-byte consensus serialization used for block-hash fields, not the human-readable reversed hexadecimal presentation. The active consensus branch ID is encoded as an unsigned 32-bit little-endian integer.

Report encoding

A report's effect fields have the following exact encoding. There is no CompactSize or other length prefix within this structure:

FieldBytesEncoding
observedHeight4unsigned little-endian
observedBlockHash32raw consensus block-hash serialization
price8unsigned little-endian, micro-USD per ZEC
signerMask1bit i corresponds to roster key i

The first three fields form the 44-byte reportBody. The complete effect structure is 45 bytes. price MUST be in 1..=2^64-1; zero is invalid. Bit 7 of signerMask MUST be zero. Its population count MUST be 5, 6 or 7. The authorization section contains exactly that many 64-byte Ed25519 signatures, in increasing signer-index order. Signature count is derived from the mask; no independently asserted count is accepted.

Every included signature verifies the same 126-byte message:

oracleMessage =
    ASCII("ZSV/Oracle/v1\0") ||        # 14 bytes
    networkGenesisHash ||              # 32 bytes
    LE32(activeConsensusBranchID) ||    # 4 bytes
    reporterSetID ||                    # 32 bytes
    reportBody                         # 44 bytes

oracleID = SHA256(oracleMessage)

oracleID deliberately excludes the signer mask and signatures. Different valid quorum subsets for the same observation therefore identify the same price report. The transaction's ordinary effect/authentication digests still commit the mask and signatures as specified by the transaction format.

Ed25519 signature verification MUST follow ZIP 215's exact verification criteria 5: A and R decode to Ed25519 points, S_sig < ell, and the cofactored equation [8][S_sig]B_Ed = [8]R + [8][k]A holds with k = SHA512(R_bytes || A_bytes || oracleMessage) interpreted as a little-endian integer modulo ell, using the supplied R and A encodings as specified by ZIP 215 and RFC 8032. Implementations MUST NOT substitute a different library's unspecified "strict Ed25519" acceptance rules or the uncofactored equation. The deployment-key restrictions above are additional restrictions on the fixed roster; they do not change ZIP 215's acceptance rules for signature R encodings. An included invalid signature invalidates the certificate even when five other signatures verify. In this signature equation, S_sig is the signature scalar and B_Ed is the standard Ed25519 base point, independent of stable supply S and bad debt B. SHA512 here is the 64-byte SHA-512 hash; it is distinct from the SHA256 report identifier.

The effect structure and authorization together use 365 bytes for five signatures and 493 bytes for seven signatures, excluding enclosing transaction framing. There is no price median, averaging rule or node-local price selection: a certificate authorizes one identically signed price tuple.

Inclusion, observed ancestry and freshness

Let H be the candidate block height, A the first height of the active consensus branch and h the report's observedHeight. Validators MUST enforce all of the following before accepting a report:

  1. A - 1 <= h < H, with A positive for an activated ZSV branch.
  2. h + 12 is representable as a u32, and H <= h + 12.
  3. observedBlockHash equals the actual ancestor block hash at height h on the candidate block's parent chain.
  4. If an observation was already accepted under this active branch and reporter set, h is strictly greater than that observation's height.
  5. The price, signer mask, signature lengths and every signature satisfy the rules above.

A certificate included at height I can price operations only in descendants at heights I + 1 through h + 12, inclusive, while it remains the parent-selected observation. The earliest possible pricing height is h + 2. The twelve-block bound is a maximum observation age, not a guaranteed transaction-validity period. A certificate included at h + 12 has no remaining pricing lifetime. There is no signed arbitrary expiry field, no free-running sequence field and no future-dated report. The observed height supplies the monotonic sequence. An inclusion delay consumes the report's lifetime; re-inclusion cannot refresh it.

At the first ZSV activation or a change of the active consensus branch/domain, the block pricing observation is empty. A valid certificate under the new domain can be recorded in that block, but becomes usable only in its descendants under the rules below. An observation signed for an older branch cannot authorize new price-dependent operations. Reports may use the last block before activation as their observed ancestor, provided their signatures use the newly active domain. Existing vault/payment/liability state is not reset by this oracle-domain change.

Block order and transaction restrictions

A block contains at most one ZSV oracle certificate. If present, it MUST occur in the first non-coinbase transaction. That transaction MUST contain no vault operation or stablecoin input/output. Ordinary ZEC inputs and outputs may fund its network fee, subject to the new transaction format's ordinary ZEC rules. The certificate is not carried in coinbase and does not create value.

At block entry, validators MUST select the pricing observation from the exact parent's committed oracle state under the active domain. That selection is fixed for all operations in the candidate block. A valid certificate in the candidate block MUST update only the oracle state committed for descendants; it MUST NOT replace the block's pricing observation. With no certificate, retain the applicable parent observation in committed state, including its original observation height. Expiry makes it unusable for pricing but does not erase its height for the strictly advancing report rule. A second certificate, or one after any other non-coinbase transaction, invalidates the block. There is no requirement to include a certificate; a coinbase-only block remains valid under the ordinary block rules.

Every borrowing, debt-bearing withdrawal and liquidation operation MUST bind oracleID and MUST use the fixed parent-selected pricing observation. The candidate height MUST be no greater than that observation's observedHeight + 12. A newly included certificate cannot rescue an absent or expired pricing observation in the same block. An operation naming an observation other than the block's pricing observation, or naming an expired or absent observation, is invalid even if that observation carries an otherwise valid quorum certificate. The operation's ordinary transaction expiry applies independently and cannot extend the oracle window.

No node-local clock, URL fetch, API response, observed mempool certificate or external service result is consulted during consensus validation. A producer may omit a certificate or choose among admissible certificates for descendants; all operations in the current block still use its parent-selected observation.

Outages, conflicting reports and recovery

An absent or expired block pricing observation disables borrowing, debt-bearing withdrawal and liquidation. It does not disable collateral additions, repayment, stable transfers, debt-free withdrawal/closure or an explicitly specified bad-debt retirement that burns actual stable value. Expiry does not itself move collateral, forgive debt, trigger a privileged settlement or authorize liquidation using a fabricated fallback price.

A later valid certificate can resume price-dependent operations from the following block, if still fresh, subject to all other protocol rules, including any bad-debt issuance freeze. Report failure alone does not reset liabilities or replace reporter keys. If the committee cannot recover, the specified remedy is a reviewed network upgrade rather than an administrator bypass.

Two certificates with different prices for the same observation height cannot both be accepted on one branch because observed heights strictly advance and a block admits at most one certificate. Competing forks may accept different valid certificates. Fork choice and reorganization select the corresponding complete vault/oracle/payment state. Evidence of reporter equivocation does not automatically change the committee, seize funds or suspend the chain in v1; no slashable reporter bond is specified.

Rationale

A dedicated fixed-asset protocol

The proposal uses a dedicated transaction format and implicit zUSD payment fields. There is no caller-chosen asset identifier or issuer path. A dedicated ZEC vault value pool removes collateral from ordinary spendable outputs; the experimental OP_DROP OP_TRUE marker is not used as a native lock.

General ZSA issuance, a generic VM and external custody were considered. General issuance and a VM broaden the permitted activity, while external custody fails the requirement that Zcash enforce vault rules. Selected ZSA transfer and proof techniques may still be useful in a future shielded profile. The public first version trades payment privacy for a smaller initial specification; it does not claim to deliver a private dollar payment instrument.

One financial operation per transaction limits ordering ambiguity. Open-with-debt and repay-and-close remain atomic. A vault update replaces one current state; it does not accumulate reusable owner-local identifiers. The specified native signatures use one owner-key scheme instead of the prototype's permanent mapping between two key schemes.

Veylor is one example of dollar access alongside shielded ZEC: its swaps deliver USDC held in Railgun on Ethereum. 6

Economic policy

The 250% borrowing ratio, 200% liquidation ratio, 5% bonus, 100-zUSD minimum debt and 100,000-zUSD liability ceiling are concrete proposed initial values taken from the research model. They are not calibrated safety guarantees. A 20% collateral-price decline moves a vault from 250% to 200% before oracle delay, execution costs or market gaps. Full liquidation can create bad debt despite valid accounting.

The ceiling limits initial nominal exposure; it does not bound ZEC value lost to a false price, guarantee liquidation depth or ensure a market peg. Repayment supplies a source of demand, but holders receive no unconditional one-dollar redemption right. A reserve held in ZEC shares the collateral's drawdown risk.

Retiring bad debt requires burning actual tokens. It transfers no collateral and creates no privileged reset authority. A holder may nevertheless have no incentive to absorb the loss, so borrowing can remain stopped indefinitely. Small shortfalls can trigger the freeze. Adding an incentivized recapitalization or redemption system would change the economic proposal and needs separate specification and review.

Oracle and governance

Using the parent observation prevents a certificate included in the same block from invalidating already-signed operations, even when reports arrive every block. Wallets can sign for the next block using the current tip's committed oracle state. Transactions delayed beyond that block or affected by a reorganization may still need re-signing. The cost is one block of additional price delay: even a fresh crash report cannot change borrowing or liquidation prices in its inclusion block. This tradeoff requires economic calibration; no history of user-selectable prices or extra reporter authority is introduced.

The reporter quorum is an explicit external trust dependency. Signature thresholds do not prove independent organizations, independent market sources or truthful prices. Fixed upgrade-selected membership removes a runtime membership administrator but makes key replacement and recovery slower. The freshness bound is measured in blocks; its wall-clock duration depends on actual block production.

Changing consensus parameters or the reporter set requires a network upgrade under the specified deployment process. No token vote, issuer key or reserve operator can change them. The roster, parameter calibration and operational arrangements need public review before deployment.

Reserve and funding boundary

The core has no reserve state, block-reward allocation, discretionary incentive spending or NSM withdrawal. A companion proposal could add native reserve cash and already-issued token inventory, but would need explicit funding, trade, allowance, closure and final-disposition rules.

The research studies identify remaining risks: stale-price trading, limited market response, losses occurring after a one-time reserve burn, and cheap terminal auctions under weak competition. Accounting tests do not resolve these incentives. A proposed reduction in another funding recipient's allocation does not itself allocate the difference to ZSV.

Community discussion

The forum discussion opened on 5 November 2026. No community endorsement, editor approval or public review outcome is claimed. Objections, alternatives and responses will be recorded here when they exist. The mathematical studies and coding-agent adversarial reviews are preliminary evidence, not the qualified independent expert review required for release of a security-sensitive ZIP. 7

Backward compatibility

Activation introduces a consensus change and requires upgraded validators. Existing ZEC transaction formats and payment pools retain their rules. The new format supports a restricted public funding path; wallet support and safe address presentation remain necessary for users to construct it.

The prototype's V4/Sapling signatures, JSON envelopes, trusted funding coinbase, model account nonces and whole-parent-checkpoint intents are not native consensus formats. Existing model vectors remain research fixtures and are not migrated into network state. No experimental fixture balance or key becomes a live allocation.

Deployment

No target network upgrade or activation height is selected. Deployment needs explicit Mainnet and Testnet activation parameters, transaction identifiers, domain inputs and public reporter keys. These are deployment allocations, not node-local settings. Neither a ZIP number nor merging a draft activates the feature.

Before a Released status such as Proposed, ZIP 0 requires the applicable editor/community process and qualified independent security/privacy review for a proposal with these implications. A complete reference implementation generally need not precede Proposed. Implementation, testing and network activation are later, distinct milestones. 7

The deployment evidence should cover independent serialization and signature implementations, cross-implementation valid/invalid block vectors, value conservation, every acceptance/finalization path, reorgs and database restart, resource benchmarks, oracle compromise/outage scenarios and economic losses. These checks cannot establish a guaranteed market peg.

Reference implementation

The ZSV research repository provides a signed account model, economic studies and offline profiles linked to pinned Zebra code. Its original code is MIT-licensed; upstream dependencies retain their own licenses. It is not a complete implementation of this ZIP.

The model tests collateralized issuance, repayment, liquidation, explicit bad debt and optional reserve accounting. The collateral profile verifies serialized fixture ZEC UTXOs and native P2PK signatures with an experimental guard. The guard is outside Zebra's node verifier, and zUSD remains a model balance. At that revision, 110 reference Rust tests, 39 Zebra-linked tests and 96 Python tests passed; all 351 combined economic cases reproduced. 8

This candidate introduces rules beyond that code: a dedicated transaction and value-pool format, normalized single-operation transitions, a different oracle policy, native signature domains and permissionless retirement of bad debt. Passing the earlier tests is not evidence that those new rules are implemented. The accompanying conformance material contains 54 financial cases, seven unsigned wire/context/hash examples covering all operation tags and oracle framing, and a canonical-signature oracle fixture. These are not a complete signed native transaction or full-block consensus suite.

Outstanding allocations and review

The protocol choices in Specification are proposed rules, subject to public review. The remaining work before calling this a deployment-ready specification is:

  1. Allocate the new transaction/group identifiers and upgrade context; select Mainnet/Testnet activation and the public reporter roster through the deployment process.
  2. Independently review and implement the byte, digest, authorization and branch-state rules, and produce signed transaction and full-block conformance vectors.
  3. Obtain qualified consensus/cryptographic/privacy review and resource benchmarks, and justify the economic parameters, oracle operations and loss policy against adversarial conditions.
  4. Incorporate forum feedback and rerun upstream rendering after metadata changes. This draft has been rendered with the ZIP repository's render.sh --pandoc path.

The public-first scope and Owners contact are confirmed. No editor-assigned number, named reviewer, oracle operator or activation approval has been invented to fill these entries.

References

Footnotes

  1. BCP 14: Key words for requirement levels. ↩

  2. Pinned Zcash and Zebra research sources. The consensus research baseline is separate from the current ZIP-process reference above. ↩

  3. ZIP 244: Transaction Identifier Non-Malleability. ↩

  4. ZIP 239: Relay of Version 5 Transactions. ↩

  5. ZIP 215: Explicitly Defining and Modifying Ed25519 Validation Rules. ↩

  6. Veylor: How it works, read 5 November 2026. ↩

  7. ZIP 0: ZIP Process, reviewed 30 October 2026. The Markdown template provides the document structure. ↩ ↩2

  8. ZSV verification receipt at the collateral milestone. ↩

Changes

README.rst +1 −0
@@ -237,6 +237,7 @@ be deleted. <tr> <td class="left">draft-arya-jvff-p2p-quic-transport</td> <td class="left"><a href="zips/draft-arya-jvff-p2p-quic-transport.md">Version 2 Zcash P2P Network Protocol</a></td> <td class="left"><a href="https://github.com/zcash/zips/issues/352">zips#352</a></td> <tr> <td class="left">draft-ecc-authenticated-reply-addrs</td> <td class="left"><a href="zips/draft-ecc-authenticated-reply-addrs.md">Authenticated Reply Addresses</a></td> <td class="left"><a href="https://github.com/zcash/zips/issues/1230">zips#1230</a></td> <tr> <td class="left">draft-ecc-onchain-accountable-voting</td> <td class="left"><a href="zips/draft-ecc-onchain-accountable-voting.md">On-chain Accountable Voting</a></td> <td class="left"></td> + <tr> <td class="left">draft-ijaack-zsv</td> <td class="left"><a href="zips/draft-ijaack-zsv.md">Zcash Stablecoin Vaults</a></td> <td class="left"><a href="https://forum.zcashcommunity.com/t/zip-draft-zsv-one-zec-backed-stablecoin-on-zcash/58909">https://forum.zcashcommunity.com/t/zip-draft-zsv-one-zec-backed-stablecoin-on-zcash/58909</a></td> <tr> <td class="left">draft-mcgee-keyholders-organizations</td> <td class="left"><a href="zips/draft-mcgee-keyholders-organizations.md">Update to ZIP 1016 & ZIP 271: Key-Holder Organizations</a></td> <td class="left"><a href="URL TBD">URL TBD</a></td> <tr> <td class="left">draft-str4d-orchard-balance-proof</td> <td class="left"><a href="zips/draft-str4d-orchard-balance-proof.md">Air drops, Proof-of-Balance, and Stake-weighted Polling</a></td> <td class="left"><a href="https://github.com/zcash/zips/issues/1229">zips#1229</a></td> </table></embed>
zips/draft-ijaack-zsv.md +972 −0
@@ -0,0 +1,972 @@ + ZIP: Unassigned + Title: Zcash Stablecoin Vaults + Owners: Giacomo Barbieri <hey@jaack.me> + Status: Draft + Category: Consensus + Created: 2026-11-05 + License: MIT + Discussions-To: <https://forum.zcashcommunity.com/t/zip-draft-zsv-one-zec-backed-stablecoin-on-zcash/58909> + Pull-Request: <https://github.com/zcash/zips/pull/1438> + +No ZIP number or network upgrade has been assigned. Deployment allocations and outstanding +review are listed below. + +# Terminology + +The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD" and "MAY" in this document are to be +interpreted as described in BCP 14 [^BCP14] when, and only when, they appear in all capitals. +Conformance requirements appear in the Specification section. + +- **ZSV** means Zcash Stablecoin Vaults. +- **zUSD** is the provisional name of the single dollar-targeting stable asset defined here. +- **Principal** is debt attached to an active vault. +- **Bad debt** is stablecoin liability remaining after a vault closes without burning all its principal. +- **Stable supply** includes all unspent zUSD, including coins whose spending keys have been lost. +- A **report certificate** is one price report with the required matching reporter signatures. +- A **current vault reference** identifies the transaction that created its latest state. + +# Abstract + +This proposal adds native ZEC vaults and one protocol-defined stablecoin to Zcash. Borrowing +against sufficient collateral creates zUSD; repayment and liquidation destroy existing zUSD and +reduce the corresponding liability. Zcash consensus enforces ownership, collateral release, +issuance, burns and independent conservation of both assets. + +The protocol provides no arbitrary asset registration, issuer authority, governance token or +general execution environment. It uses a dedicated transaction format, a separate ZEC vault +value pool and public zUSD outputs. Existing ZEC transaction formats remain available under +their applicable consensus rules. Public vaults and payments expose balances, amounts and +financial relationships; shielded zUSD requires a subsequent specified and reviewed protocol. + +A fixed reporter quorum supplies ZEC/USD observations. Consensus verifies their authentication +and deterministic validity conditions, but cannot establish their economic truth. Full +liquidation records any uncovered principal as bad debt. Further borrowing stops while bad debt +remains; holders can retire that debt by burning existing zUSD without compensation. No +dollar-redemption claim or guaranteed market peg is created. + +The proposal is independent of a Dev Fund allocation or reserve. Those mechanisms may be +considered separately. Research code and economic tests support selected design choices; native +format, authorization and deployment rules require implementation and independent review. + +# Motivation + +ZEC holders may want dollar-denominated liquidity while retaining exposure to their remaining +collateral. A native vault would let them borrow under rules checked by Zcash nodes, accepting +liquidation risk when collateral loses value. Holders of the issued asset would bear collateral, +oracle, liquidity and protocol risk. + +A data commitment alone cannot lock ZEC or create a native stablecoin. General-purpose Zcash +Shielded Assets would support unrelated tokens and issuers. ZSV instead proposes one monetary +use case with a closed issuance path. The smaller asset domain reduces issuance cases, but the +financial and consensus complexity remains substantial. + + +The intended benefit is native borrowing and a transferable dollar-targeting asset without an +arbitrary token-launch interface. Whether that benefit justifies the oracle dependency, privacy +cost and additional consensus rules is a question for community review. + +# Privacy Implications + +This first version has public vaults and public stablecoin payments. It reveals collateral, +principal, owner keys, payment amounts and liquidation events. Creating a vault from previously +shielded ZEC requires exposing value to the permitted transparent input path. Key reuse and +transaction graph analysis can link borrowing, payments and repayment. Liquidation can reveal +financial distress. + +Adding shielded payments later would not erase these disclosures. A fixed-asset shielded profile +could reuse selected ZSA transfer, note and proof techniques, but would need its own precise +proof statements, value-pool accounting and privacy review. Existing ZSA audits would not +automatically apply to a changed circuit. Private vaults also need liquidation information to +remain available without borrower cooperation; this version does not attempt that design. + +# Requirements + +The design goals are: + +- Enforcement of collateral, debt, issuance and liquidation by Zcash consensus, with no bridge, external execution chain or custody committee. +- Exactly one additional native asset, issued only through valid collateralized borrowing. +- Atomic value accounting, with no alternative ordinary spend of locked collateral and no hidden erasure of stablecoin liabilities. +- Repayment, collateral additions, debt-free exit and payment transfers during an oracle outage. +- Deterministic branch-specific validation, bounded work and equivalent behavior across implementations. +- Continued ordinary ZEC activity without requiring unrelated users to submit ZSV operations. +- Defined vault operation without a reserve, Dev Fund allocation or privileged recovery key. + +# Non-requirements + +This proposal does not add arbitrary issuers, asset descriptions, NFTs, a VM, a governance +token, a bridge or external custody. It does not prevent off-protocol claims, inscriptions, +derivatives or speculation. It does not allocate block rewards or NSM value, establish on-demand +dollar redemption, or promise that zUSD trades at one dollar. + +Dev Fund allocation and reserve stabilization are separate candidate work. Shielded payments and +wallet address standardization may require companion ZIPs. The oracle rules are part of this +core proposal; they are not an optional service that a node may configure differently. + +# Specification + +## Scope and activation parameters + +This specification defines a new, dedicated transaction version with a fixed ZEC/zUSD domain. It +does not modify the meaning of an allocated transaction version or enable general ZSA issuance. +Deployment allocates its version/group identifiers, consensus branch context, activation heights +and fixed oracle roster. Until those allocations and the required implementation/review exist, +this is an unactivated proposal. + +Validators MUST reject the new format before its network-specific activation. After activation, +ordinary ZEC transaction versions MUST retain their applicable validation rules. The new format +MUST NOT be interpreted as the existing v6 format or as withdrawn ZIP 230. An allocated existing +format is not available for reassignment by this draft. The existing format and Zebra +integration baseline is recorded in the pinned sources. [^sources] + +At activation, the ZSV vault set and public zUSD UTXO set are empty, stable supply and bad debt +are zero, and no price report is active. Activation MUST NOT create ZEC, zUSD, a reserve balance +or a privileged allocation. Borrowing remains unavailable until a valid report certificate is +accepted. + +## Encoding conventions and bounds + +All integers are unsigned little-endian unless stated otherwise. Hashes are raw 32-byte +consensus digest bytes, not reversed display hex. A compressed secp256k1 key K is exactly 33 +bytes: prefix 02 or 03 followed by the canonical field encoding of a valid non-infinity point's +x-coordinate. Invalid/noncanonical keys fail. New asset outputs have no script, arbitrary +metadata or asset identifier. + +Counts are canonical CompactSize. Each count below is at most 64, so its only valid encoding is +one byte. Resolved payment inputs and newly created payment outputs have strictly positive +values. Zero is permitted only for operation fields that explicitly allow it. Each ZEC payment +value, each transaction's ZEC input/output sums and value-balance sides, and the persistent +vault-pool balance MUST fit MAX_MONEY = 2,100,000,000,000,000 zatoshi. Each zUSD payment value, +each transaction's stable input/output sums and value-balance sides, and the persistent stable +supply MUST fit S_MAX = 100,000,000,000 micro-zUSD. These are per-transaction and state bounds; +they do not limit the sum of all transfers across a block, which can reuse value through valid +same-block dependencies. Signed maximum-burn and minimum-receipt limits are unsigned u64 bounds, +not additional input or output value. A serialized ZSV transaction, including authorization, is +at most 65,536 bytes. Existing block size limits remain in force. Each block contains at most +256 transactions of this new version, at most 1,024 required secp256k1 signature slots across +them, and at most 4,096 new native ZEC plus stable output entries across them. Slots count +native input signatures, stable input signatures and any operation signature whether valid or +not; outputs count both explicit output vectors. The oracle permits at most one certificate and +at most 7 Ed25519 report signatures per block. Check these counts before signature verification +and state insertion. These are exact proposed limits, not benchmark-calibrated production +recommendations. There is no lifetime UTXO cap or permitted-wallet list. + +The fee/residual calculation uses checked signed arithmetic of sufficient width. Core +collateral-ratio and liquidation products use checked u128. A new format has no coinbase input, +shielded bundle, JoinSplit, general script vector, asset selector or optional opaque extension +field. + +## Effect body E + +In the encoding tables, `+` denotes byte concatenation, and fields are listed in wire order. + +A transaction consists of E followed immediately by its authorization tail A, with no trailing +bytes. E is: + +| Field | Bytes / rule | +|---|---| +| header | u32: high bit 1; remaining 31 bits equal the unallocated ZSV_VERSION | +| versionGroupId | u32: unallocated ZSV_VERSION_GROUP_ID | +| consensusBranchId | u32: exactly the active upgrade branch ID | +| lockTime | u32, required 0 | +| expiryHeight | u32, H<=expiryHeight<500,000,000 and expiryHeight>0, where H is inclusion height | +| nZecInputs | CompactSize,0..64 | +| zecInputs | nZecInputs*(prevTxid32 + prevOutputIndexu32) | +| nZecOutputs | CompactSize,0..64 | +| zecOutputs | nZecOutputs*(valueu64 + recipientKey33) | +| nStableInputs | CompactSize,0..64 | +| stableInputs | nStableInputs*(prevTxid32 + prevOutputIndexu32) | +| nStableOutputs | CompactSize,0..64 | +| stableOutputs | nStableOutputs*(valueu64 + recipientKey33) | +| operationTag | u8, one of 0..5 below | +| operationPayload | exactly the tag's payload | +| hasOracle | u8, exactly 0 or 1 | +| oracleEffects | absent if 0; if 1 the exact 45-byte certificate effects below | + +Native input sequence numbers are implicitly final (0xffffffff); they have no wire field. Native +outputs create ordinary transparent ZEC UTXOs with the exact standard script +`0x21||recipientKey33||0xac` (P2PK). The input must resolve to a real unspent transparent output +whose locking script matches that exact P2PK grammar. Its value comes from native branch state, +not transaction data. Existing coinbase-maturity, duplicate-spend, money-range and other +applicable native input rules continue to apply. Existing ZEC transaction versions and their +accepted scripts remain unchanged; users may send ZEC to a P2PK output through an ordinary +transaction before using it here. + +Stable outputs create entries only in the new zUSD payment UTXO namespace. They are not native +ZEC transparent outputs. Stable input indices resolve only in that namespace. Native and stable +`(txid,index)` namespaces are distinct: an equal byte tuple does not make the two assets +interchangeable. Every input must exist, be unspent on the exact candidate branch, have an +in-range positive value and have the expected fixed-key ownership. Repeated outpoints within a +namespace fail. Same-block dependencies use previously validated transaction outputs in block +order. + +Only one optional operation is allowed. Tag 0 means NONE and has no payload; it permits pure +stable transfers. If hasOracle=0 and tag=NONE, at least one stable input is required. If +hasOracle=1, tag must be NONE and both stable counts must be 0. Native fee inputs and outputs +remain permitted in an oracle transaction. Other empty uses of the new version fail; ordinary +ZEC transactions continue using their existing formats. + +## Vault state and operation payloads + +A current vault is stored in a dedicated ZEC vault value pool, keyed by its `VaultRef`, the raw +32-byte effects txid of the OPEN or UPDATE that created it. Because each transaction has at most +one operation, no operation index is needed. The state record is `(ownerKey33, collateralu64, +principalu64)`. OPEN/UPDATE create one successor at their own txid; UPDATE consumes the previous +ref and preserves its owner. CLOSE/LIQUIDATE consume without a successor. Current refs are +unique; inserting an existing current ref fails. + +Vault collateral is not also an ordinary transparent output. There is no OP_TRUE collateral +script and no spendable duplicate. Its amount enters the native ZEC vault value pool and can +leave only through a valid operation and the fee equation below. All node paths need this +accounting, including direct finalization/checkpoints; ordinary native spends can resolve only +actual native UTXOs, never the vault pool. + +| Tag | Payload | +|---|---| +|0 NONE|empty| +|1 OPEN|ownerKey33 + newCollateralu64 + newPrincipalu64 + oracleID32| +|2 UPDATE|priorVaultRef32 + newCollateralu64 + newPrincipalu64 + oracleID32 + ownerPayoutIndexu32| +|3 CLOSE|priorVaultRef32 + ownerPayoutIndexu32| +|4 LIQUIDATE|priorVaultRef32 + oracleID32 + maxBurnu64 + minCollateralu64 + liquidatorKey33 + ownerPayoutIndexu32 + liquidatorPayoutIndexu32| +|5 RETIRE_BAD_DEBT|burnAmountu64| + +OracleID is the exact identifier of the block's parent-selected pricing observation defined by +the oracle specification. For OPEN with D=0, and UPDATE without a price requirement, oracleID is +exactly 32 zero bytes. A price-requiring operation uses the active nonzero ID; no alternate or +stale observation is accepted. This prevents an irrelevant observation from needlessly +restricting oracle-free repayment/deposit, and makes its wire encoding unique. + +OPEN requires C>0. UPDATE requires C'>0 and a changed `(C,D)` pair; `(0,0)` uses CLOSE. Nonzero +D' must satisfy the minimum-debt rule. The owner auth key for OPEN is its encoded key; +UPDATE/CLOSE use the actual prior state's owner. LIQUIDATE uses its encoded liquidator key. +RETIRE has no operation-owner signature: actual stable input authorization controls the burn. + +A mandatory payout index names an ordinary native ZEC output in this same transaction. The named +output must have exactly the required amount and exact P2PK recipient. UPDATE with C'<C requires +a payout of C-C' to the immutable owner. UPDATE without released collateral uses the sentinel +0xffffffff. CLOSE pays exactly oldC to the owner. Every valid current vault has positive +collateral, so this payout always uses an output index. LIQUIDATE pays z to the liquidator and +C-z to the owner. A zero required payout uses sentinel 0xffffffff and consumes no output. A +positive payout requires an index in range; sentinel is invalid. Both positive liquidation +payout indices must be distinct, even for self-liquidation when ownerKey=liquidatorKey. One +output cannot satisfy two mandatory payouts. Extra native outputs remain allowed and are covered +by all signatures and value conservation. + +The exact collateral payouts cannot silently pay fees. Any positive residual fee must be +supplied by other actual native value (normally free ZEC inputs). Minted stable outputs may use +any valid recipient keys chosen in the fully signed transaction: borrowing and sending value to +another recipient can be atomic. No amount is inferred from a memo or claimed caller balance. + +## Per-asset value equations + +Define C_before as the actual prior vault collateral, or 0 for NONE/OPEN/RETIRE; C_after is +OPEN/UPDATE's encoded collateral, or 0 for NONE/CLOSE/LIQUIDATE/RETIRE. Let Mint and Burn come +only from the validated core operation: + +- OPEN: Mint=D', Burn=0. +- UPDATE: Mint=max(D'-D,0), Burn=max(D-D',0). +- CLOSE: Mint=0, Burn=D (close can atomically repay all principal). +- LIQUIDATE: Mint=0, Burn=x from the specified exact liquidation formula. +- RETIRE_BAD_DEBT: Mint=0, Burn=r, with 0 < r <= B at transaction entry. +- NONE: Mint=Burn=0. + +Then require: + +``` +sum(stableInputs) + Mint = sum(stableOutputs) + Burn +Fee = sum(nativeZecInputs) + C_before - C_after - sum(nativeZecOutputs) +0 <= Fee <= MAX_MONEY +``` + +The fee is counted in ordinary ZEC fee/coinbase accounting, including any separately active +fee-disposition rules. Stable value cannot pay it. These rules do not introduce a positive +consensus minimum fee; relay/mining fee policy remains separate. The vault value pool changes by +C_after-C_before, and total stable supply changes by Mint-Burn. Live principal changes and +bad-debt handling follow the core; RETIRE decreases B and S together without touching active +collateral or principal. Borrowing requires B=0 at transaction entry, so it cannot be +self-financed by another operation in the same transaction (and only one operation is allowed). + +## Hashes and authorization + +Let `H(label,bytes)` be BLAKE2b-256 with the ASCII label right-padded with zero bytes to a +16-byte personalization. All labels below are exact and case-sensitive; no NUL is appended to +message bytes. Let G be the raw 32-byte genesis block hash of the configured network. G comes +from trusted network configuration, never transaction data. Hashing APIs therefore need explicit +network context; branch IDs alone do not distinguish Mainnet from Testnet. The header already +commits the branch ID. + +``` +txid = H("ZSV_TxId_v1", G || E) +contextHash = H("ZSV_Context_v1", R) +signatureDigest(role,index) = H("ZSV_Sign_v1", G || txid || contextHash || u8(role) || u16LE(index)) +authDigest = H("ZSV_Auth_v1", txid || A) +``` + +R is authoritative resolved input context, encoded in this exact order: + +``` +CompactSize(nZecInputs) + for each native input, in wire order: + prevTxid32 || prevOutputIndexu32 || actualValueu64 || actualP2pkKey33 +CompactSize(nStableInputs) + for each stable input, in wire order: + prevTxid32 || prevOutputIndexu32 || actualValueu64 || actualOwnerKey33 +u8(hasPriorVault) # 1 for UPDATE/CLOSE/LIQUIDATE, else 0 + if 1: priorVaultRef32 || actualOwnerKey33 || actualCollateralu64 || actualPrincipalu64 +``` + +No caller-supplied prevout value/key or prior vault record can replace these state lookups. +Signatures bind every effect, input reference, value, key, operation, oracle bound and payout. +There are no selectable SIGHASH modes and no implicit whole-parent-state checkpoint. Same +effects on the same live inputs can be valid on competing branches when the contextual +oracle/expiry/policy rules also pass; this is normal fork behavior, not a replay across networks +or upgrades. + +A secp256k1 authorization consists of `u8(signatureLength) ||strictDER(signature)`, with length +8..72. The DER is the shortest valid ASN.1 encoding of the ECDSA pair; round-trip canonical DER +is required, 1<=r<n and 1<=s<=floor(n/2), where n is the secp256k1 subgroup order. No appended +hash-type byte, extra push opcode or trailing data is allowed. Verify ECDSA against the +indicated 32-byte digest and authoritative key. Low-S and minimal DER are consensus rules for +this version. + +A contains exactly these slots, without counts or padding: + +1. One signature for every native input, in wire order, role 0/index 0..nZecInputs-1, verified with its actual P2PK key. +2. One signature for every stable input, in wire order, role 1/index 0..nStableInputs-1, verified with its actual owner key. +3. For OPEN/UPDATE/CLOSE/LIQUIDATE, exactly one operation signature, role 2/index 0, using the operation auth key defined above. NONE/RETIRE have no such slot. +4. If hasOracle=1, exactly popcount(signerMask) raw 64-byte Ed25519 report signatures in ascending reporter-set index order, under the separate oracle message/ZIP215 validation rules. + +A signer may appear in several roles; every required role still has its own signature slot. This +prevents treating a fee input signature as authority over an unrelated vault. The effects txid +excludes all signatures, allowing dependent transactions to be constructed before authorization. +authDigest commits the complete tail. For this version, the transaction Merkle tree leaf is +txid, and the corresponding ZIP244 authorization-data tree leaf is authDigest. Existing Merkle +and block-commitment algorithms are unchanged. It is not a pre-v5 placeholder leaf. ZIP239 +witnessed relay treats this version as witness-bearing: wtxid is the exact 64 bytes +txid||authDigest, in raw digest order. The version-dispatch rules are extended accordingly; +older-version hashes and their relay behavior are unchanged. At the pinned Zebra source, these +roles correspond to `zebra-chain/src/block/merkle.rs::AuthDataRoot`, `block/commitment.rs` and +`transaction/hash.rs::WtxId` (whose byte conversion concatenates these two 32-byte hashes). +[^zip-0244] [^zip-0239] + +## Oracle effects integration + +The oracle effect structure is: + +``` +observedHeightu32 || observedBlockHash32 || priceu64 || signerMasku8 +``` + +This is 45 bytes. The mask's high bit is 0 and popcount is 5..7 for the fixed seven-reporter +deployment roster. The active roster ID is derived from the specified sorted keys and is not +repeated on wire. Oracle signatures bind their separate network/branch/roster/report message. +OracleID excludes the signer mask/signatures, so differing valid quorums over identical content +identify the same observation. The Oracle rules below provide exact ancestry, age, ordering and +rotation rules. + + +## Units, ranges and arithmetic + +| Symbol | Proposed value or definition | +|---|---| +| q | 100,000,000 zatoshi per ZEC | +| m | 1,000,000 micro-zUSD per zUSD | +| C | Vault collateral in zatoshi; 0 <= C <= MAX_MONEY | +| MAX_MONEY | 21,000,000 * q = 2,100,000,000,000,000 zatoshi, subject to the base protocol's native ZEC amount rule | +| D | Vault principal in micro-zUSD | +| P | Accepted micro-USD per ZEC; integer 1 <= P <= 2^64 - 1 | +| D_MIN | 100 * m = 100,000,000 micro-zUSD | +| S_MAX | 100,000 * m = 100,000,000,000 micro-zUSD | +| R_OPEN | 250 percent | +| R_LIQ | 200 percent | +| K_LIQ | 105 percent, including the 5 percent liquidation bonus | +| S | Outstanding native zUSD supply | +| B | Explicit bad debt | + +These opening, liquidation, bonus, debt-floor and supply-cap values are fixed draft defaults +carried from the prototype. In particular, the small initial supply ceiling bounds nominal +outstanding liabilities; it does not prove their dollar value or economic safety. Changing a +value requires the versioned consensus change defined by the activation specification; there is +no runtime operator setter. + +All encoded monetary amounts are unsigned integers. Additions and subtractions MUST reject +overflow or underflow. Products and divisions in the rules below MUST produce the exact +mathematical integer result. A checked unsigned 128-bit implementation suffices for these +ranges: even MAX_MONEY * (2^64 - 1) * 100 is less than 2^128. No floating point, saturating +arithmetic, silent truncation, approximate ratio, or overflow-dependent validity is permitted. + +`floor_div(a,b)` is integer floor for a >= 0 and b > 0. `ceil_div(a,b)` is `a / b + (a % b != +0)` using integer quotient and remainder. An implementation using `a + b - 1` must establish +that the intermediate does not overflow. + +A debt-bearing vault is safe for opening/withdrawal exactly when: + + C * P * 100 >= D * q * 250 + +A vault is liquidatable exactly when D > 0 and: + + C * P * 100 < D * q * 200 + +Equality at 250 percent passes the opening test. Equality at 200 percent is not liquidatable. +Debt-free vault operations do not need a conversion price. + +## State and transaction shape + +At activation, the new value pool, vault set, S and B start empty/zero. Existing ZEC is not +reissued into a trusted fixture. Ordinary native input authorization and ZEC supply rules +continue to apply. + +Each live vault has an immutable owner authorization key, collateral C > 0, principal D, and one +current native reference. D MUST be zero or at least D_MIN. The wire proposal defines a current +reference as the creating transaction identifier alone, because each transaction has at most one +financial operation. A transition consumes the current reference once and creates at most one +successor. A spent or nonexistent reference cannot be revived by asserting its former balances. + +A native transaction contains at most one optional financial operation: NONE, OPEN, UPDATE, +CLOSE, LIQUIDATE or RETIRE_BAD_DEBT. It can also contain permitted native ZEC inputs/outputs and +native zUSD inputs/outputs. NONE permits an ordinary zUSD transfer with no issuance or burn. +OPEN incorporates initial borrowing; CLOSE incorporates full repayment. This gives atomic +opening/borrowing and repayment/closure without an arbitrary program or action list. + +A transaction with NONE and no oracle certificate requires at least one zUSD input, as specified +by the wire grammar. Unrelated ordinary ZEC transactions remain valid under their existing +format rules. + +The owner signs the entire authorized transaction body and relevant resolved previous values +under the new native signing domain. Recipient selection for newly borrowed zUSD is covered by +that signature. This is equivalent in financial effect to the model's Borrow followed by +Transfer, although the model credits the owner first. No conversion between Ed25519 and +secp256k1 identities, pre-registered model/native key map, fixture chain domain, account nonce +or whole-parent-state intent is adopted by these core rules. + +For each transaction define native zUSD input value I_s, native output value O_s, permitted mint +M and permitted burn R. The required native stable value balance is: + + I_s + M = O_s + R + +Only the explicit rules below authorize M or R. Output omission, a memo, a claimed burn field or +an inaccessible key is not an accounting burn. S counts the total value of consensus-recognized +unspent zUSD outputs, including outputs whose private keys have been lost. + +For a vault-changing transaction, write C_old = 0 for OPEN and C_new = 0 for CLOSE/LIQUIDATE. +Its ZEC balance is: + + I_zec + C_old = O_zec + C_new + F_zec + +For NONE and RETIRE_BAD_DEBT both collateral terms are zero. F_zec is a nonnegative native +transaction fee accounted for by the ordinary block/coinbase fee rules. No zUSD fee, collateral +subsidy or increase in ZEC supply is authorized. Normal relay/mining policy may require a +positive fee; this equation does not invent a positive consensus minimum. Exact indexed +collateral payouts described below cannot silently have fees subtracted from them. + +After each ordered transaction: + + S = sum(live vault principal) + B + S = sum(value in native unspent zUSD outputs) + vaultPoolBalance = sum(live vault collateral) + 0 <= vaultPoolBalance <= MAX_MONEY + 0 <= S <= S_MAX + 0 <= B <= S + +The second equality is replaced only by an explicitly specified public/shielded value-pool +equality in a later shielded extension. It is not a requirement to determine whether an owner +still knows a private key. + +## Oracle use + +A price-dependent operation MUST bind the exact canonical report identifier accepted by the +oracle specification, not merely a height or a caller-asserted price. It MUST reject if that +report is absent, no longer canonical, or outside its allowed validity/anchoring window. The +Oracle rules below use seven configured reporters, five matching signatures and a maximum +twelve-block anchored observation age. Those acceptance and ordering rules are specified in the +Oracle rules below, not redefined here. + +Operations without a price requirement use the canonical zero/absent oracle identifier specified +by the wire format. Repayment, collateral addition without borrowing, debt-free +withdrawal/closure, ordinary transfer and bad-debt retirement remain possible during oracle +outages. No validator fetches a price from the web or uses its local wall clock. + +## OPEN + +OPEN supplies C_new > 0, immutable owner authorization and D_new. The owner authorizes the +transaction. Native ZEC inputs fund C_new through the value balance above; existing collateral +cannot simultaneously remain spendable elsewhere. + +D_new MUST be zero or at least D_MIN. If D_new > 0, B at transaction entry MUST be zero; the +exact fresh canonical report is required; the 250-percent inequality MUST hold for +(C_new,D_new); and S + D_new MUST be <= S_MAX. Set M = D_new, R = 0, S' = S + D_new and B' = B. +If D_new = 0, no oracle is required. + +## UPDATE + +UPDATE consumes one current vault and retains its owner. Let delta_C = C_new - C_old and delta_D += D_new - D_old, understood as mathematical signed differences rather than a wrapping unsigned +calculation. + +C_new MUST be strictly positive. D_new MUST be zero or >= D_MIN. An unchanged pair (C_new,D_new) += (C_old,D_old) is rejected. A transition to (0,0) uses CLOSE; no empty live vault is retained. +These restrictions remove two needless successor forms accepted by the account prototype and are +explicit proposed encoding/state changes. + +If delta_D > 0, B at transaction entry MUST be zero, a fresh exact canonical report MUST be +bound, the final state MUST satisfy the 250-percent inequality, and S + delta_D MUST be <= +S_MAX. Set M = delta_D and R = 0. + +If delta_D < 0, burn R = -delta_D actual native zUSD and set M = 0. If delta_D = 0 both are +zero. There is no owner-authorized debt forgiveness. Repayment that leaves debt strictly between +zero and D_MIN is invalid. + +If C_new < C_old and D_new > 0, a fresh exact canonical report and the final 250-percent +inequality are also required, even when debt decreases. If C_new < C_old, the indexed owner +payout MUST equal C_old - C_new; it MUST use the owner's prescribed native payment lock. If +C_new >= C_old, no collateral-release payout exists. Deposits fund the increase through native +inputs. + +Set S' = S + M - R and B' = B. Owner authorization is required for all UPDATE forms, including +pure deposit and pure repayment, matching the reference policy. A future third-party +repayment/deposit facility requires an explicit change; it is not silently inferred from the +absence of collateral release. + +## CLOSE + +CLOSE consumes one current vault, requires its owner authorization, burns exactly R = D_old +actual native zUSD, pays exactly C_old to the owner's indexed native payout and creates no +successor. M = 0, S' = S - D_old and B' = B. It needs no oracle even when D_old > 0 because the +entire debt is paid in the same transaction. + +This combines the existing model's Repay(full debt) followed by Close. The model's standalone +Close requires debt already zero; the native combined encoding does not forgive the old +principal. + +## LIQUIDATE + +LIQUIDATE consumes and closes one current vault. It requires the exact fresh canonical report, +D_old > 0, and the strict 200-percent liquidation inequality. Borrower authorization is +unnecessary. The liquidator authorizes the complete transaction and can be any holder, including +the borrower. + +Compute using the original C and D: + + x = min(D, floor_div(C * P * 100, q * 105)) + z = ceil_div(x * q * 105, P * 100) + +The verifier MUST establish 0 <= z <= C. It MUST require x <= signed_max_burn and z >= +signed_min_collateral. R = x and M = 0. The transaction consumes actual native zUSD sufficient +for x and returns permitted change through the stable value equation. + +The indexed liquidator payout MUST be exactly z zatoshi under the signed liquidator payment key. +The indexed owner payout MUST be exactly C-z under the immutable owner's payment key. Payout +indices MUST be unambiguous and cannot count the same output twice; the zero-value sentinel +convention is specified by the wire format. No successor vault remains. + +Set: + + S' = S - x + B' = B + (D - x) + +This retains the model's full-vault liquidation policy and rounding. If x = 0, z = 0 and the +operation remains valid when the signed bounds permit it. No zUSD input is then required solely +for the zero burn, and the owner receives C; the entire D becomes bad debt. The native ZEC +fee/value rules still apply. There is no claim that anyone has an economic incentive to submit +that dust liquidation. + +Liquidation with insufficient actual liquidator zUSD rejects; it does not reduce x to the +caller's balance or silently leave a partial vault. A price crash can create B > 0 even with +honest reporters and immediate execution. + +## RETIRE_BAD_DEBT + +This new proposed operation makes recovery possible without a reserve or privileged reset. A +holder chooses an integer r satisfying 0 < r <= B at transaction entry. The holder must +authorize spending actual zUSD inputs. Set R = r, M = 0, S' = S-r and B' = B-r. No vault is +consumed or changed, no oracle is needed, and no ZEC is paid by the protocol. + +The holder receives no redemption claim, future repayment right, reward entitlement, priority, +governance privilege or ownership of another vault. Ordinary input change is returned under the +signed transaction. The operation is an uncompensated token burn against recorded bad debt. + +A NONE or RETIRE_BAD_DEBT transaction cannot create zUSD or borrow. The one-operation grammar +prevents combining bad-debt retirement with newly minted debt in the same transaction. A later +transaction can borrow only after preceding valid retirement has actually reduced B to zero. + +This operation is absent from the current Rust model and collateral profile. Its new conformance +vectors and future implementation must be tracked separately from passing legacy tests. + +## Loss and liveness policy + +Borrowing is prohibited while B > 0. Existing transfers, valid debt repayment, deposits, +permitted withdrawals/closure, liquidation and RETIRE_BAD_DEBT continue. The freeze is based on +recorded B at the operation's ordered transaction entry; latent losses in unliquidated vaults +are not magically detected. + +Only a valid RETIRE_BAD_DEBT reduces B in this core version. No clock, oracle report, miner +field, operator key or bookkeeping reset may erase liabilities. S decreases by the same amount, +preserving the debt/supply identity. A future separately approved reserve mechanism could fund +voluntary retirement using tokens it actually owns; no such funding or reserve authority is +activated by this core. + +Borrowing resumes automatically when the actual B value reaches zero and the normal +fresh-price/collateral/cap conditions pass. If no holder is willing to retire tokens, the freeze +can persist indefinitely. The protocol does not guarantee recapitalization, price recovery or a +dollar redemption right. This is an explicit complete loss policy with a remaining economic +availability risk, rather than an unspecified administrative recovery hook. + + +## Complete block transition and persistence + +Validation MUST begin from the candidate block's exact parent state, independently of the node's +currently selected tip. The candidate includes ordinary native UTXOs and value pools, the ZEC +vault pool/current vault set, stable UTXOs, S, B, canonical oracle state, fee accounting and +activation context. No caller-asserted balance or post-state replaces validation against that +parent. + +Validators MUST apply every ordinary and ZSV transaction in block order to a private candidate +state. Later transactions see the valid outputs, spent references and state changes of earlier +transactions in that block. The oracle placement rules below determine when a certificate +becomes usable. A missing, already spent or duplicate input/reference is invalid; creating an +output or current vault reference MUST NOT overwrite an existing unspent entry in its namespace. +The absence of a ZSV operation in an ordinary transaction cannot authorize access to the +separate vault or stable value pools. + +Each transaction MUST satisfy its native and ZSV rules, exact payouts, per-asset balances, +monetary bounds and applicable state invariants before the next transaction is evaluated. The +vault-pool delta is applied atomically with ordinary ZEC input/output and fee deltas, so +collateral is never counted as both an ordinary output and a vault balance. A block that fails +any transaction, oracle, resource, commitment or ordinary consensus check MUST be rejected with +no persistent changes to any of these states. + +Only a fully valid block may commit the complete candidate transition. Reorganization, +finalization and restart MUST reconstruct the same parent-derived state as ordered replay of the +accepted branch. Mempool admission is provisional and MUST NOT substitute for block-context +validation. Checkpoint and direct-finalization paths MUST enforce equivalent rules and establish +matching ZSV state; a path that merely skips the new checks or imports asserted ZSV balances is +invalid. This specification requires equivalent results, not a particular database layout or +caching strategy. + +## Oracle rules + +### Deployment roster and domains + +For each activated ZSV consensus branch, the deployment specification MUST provide seven +distinct Ed25519 public keys, ordered lexicographically by their 32-byte encodings. The keys +MUST use canonical encodings of non-identity points in Ed25519's prime-order subgroup. In group +notation, each decoded key A satisfies A != identity and [ell]A = identity, where ell is the +Ed25519 subgroup order. Canonical encoding means re-encoding the decoded point using the RFC +8032 compressed encoding reproduces the 32-byte input exactly. + +The reporter set is fixed for that consensus branch. There is no oracle transaction, +administrator signature, voting token or runtime configuration that changes the roster, quorum +or freshness period. A roster change requires an explicitly specified network upgrade. Local +configuration MUST NOT override the deployment roster. + +Define: + +```text +reporterSetID = SHA256( + ASCII("ZSV/OracleSet/v1\0") || + u8(7) || u8(5) || + key[0] || ... || key[6] +) +``` + +Here `\0` is one zero byte, concatenation is byte concatenation, and SHA256 returns its 32 raw +digest bytes. The active network's genesis hash is the 32-byte consensus serialization used for +block-hash fields, not the human-readable reversed hexadecimal presentation. The active +consensus branch ID is encoded as an unsigned 32-bit little-endian integer. + +### Report encoding + +A report's effect fields have the following exact encoding. There is no CompactSize or other +length prefix within this structure: + +| Field | Bytes | Encoding | +|---|---:|---| +| observedHeight | 4 | unsigned little-endian | +| observedBlockHash | 32 | raw consensus block-hash serialization | +| price | 8 | unsigned little-endian, micro-USD per ZEC | +| signerMask | 1 | bit i corresponds to roster key i | + +The first three fields form the 44-byte `reportBody`. The complete effect structure is 45 bytes. +`price` MUST be in `1..=2^64-1`; zero is invalid. Bit 7 of `signerMask` MUST be zero. Its +population count MUST be 5, 6 or 7. The authorization section contains exactly that many 64-byte +Ed25519 signatures, in increasing signer-index order. Signature count is derived from the mask; +no independently asserted count is accepted. + +Every included signature verifies the same 126-byte message: + +```text +oracleMessage = + ASCII("ZSV/Oracle/v1\0") || # 14 bytes + networkGenesisHash || # 32 bytes + LE32(activeConsensusBranchID) || # 4 bytes + reporterSetID || # 32 bytes + reportBody # 44 bytes + +oracleID = SHA256(oracleMessage) +``` + +`oracleID` deliberately excludes the signer mask and signatures. Different valid quorum subsets +for the same observation therefore identify the same price report. The transaction's ordinary +effect/authentication digests still commit the mask and signatures as specified by the +transaction format. + +Ed25519 signature verification MUST follow ZIP 215's exact verification criteria [^zip-0215]: A +and R decode to Ed25519 points, S_sig < ell, and the cofactored equation [8][S_sig]B_Ed = [8]R + +[8][k]A holds with k = SHA512(R_bytes || A_bytes || oracleMessage) interpreted as a +little-endian integer modulo ell, using the supplied R and A encodings as specified by ZIP 215 +and RFC 8032. Implementations MUST NOT substitute a different library's unspecified "strict +Ed25519" acceptance rules or the uncofactored equation. The deployment-key restrictions above +are additional restrictions on the fixed roster; they do not change ZIP 215's acceptance rules +for signature R encodings. An included invalid signature invalidates the certificate even when +five other signatures verify. In this signature equation, S_sig is the signature scalar and B_Ed +is the standard Ed25519 base point, independent of stable supply S and bad debt B. SHA512 here +is the 64-byte SHA-512 hash; it is distinct from the SHA256 report identifier. + +The effect structure and authorization together use 365 bytes for five signatures and 493 bytes +for seven signatures, excluding enclosing transaction framing. There is no price median, +averaging rule or node-local price selection: a certificate authorizes one identically signed +price tuple. + +### Inclusion, observed ancestry and freshness + +Let H be the candidate block height, A the first height of the active consensus branch and h the +report's `observedHeight`. Validators MUST enforce all of the following before accepting a +report: + +1. `A - 1 <= h < H`, with A positive for an activated ZSV branch. +2. `h + 12` is representable as a u32, and `H <= h + 12`. +3. `observedBlockHash` equals the actual ancestor block hash at height h on the candidate block's parent chain. +4. If an observation was already accepted under this active branch and reporter set, h is strictly greater than that observation's height. +5. The price, signer mask, signature lengths and every signature satisfy the rules above. + +A certificate included at height I can price operations only in descendants at heights `I + 1` +through `h + 12`, inclusive, while it remains the parent-selected observation. The earliest +possible pricing height is `h + 2`. The twelve-block bound is a maximum observation age, not a +guaranteed transaction-validity period. A certificate included at `h + 12` has no remaining +pricing lifetime. There is no signed arbitrary expiry field, no free-running sequence field and +no future-dated report. The observed height supplies the monotonic sequence. An inclusion delay +consumes the report's lifetime; re-inclusion cannot refresh it. + +At the first ZSV activation or a change of the active consensus branch/domain, the block pricing +observation is empty. A valid certificate under the new domain can be recorded in that block, +but becomes usable only in its descendants under the rules below. An observation signed for an +older branch cannot authorize new price-dependent operations. Reports may use the last block +before activation as their observed ancestor, provided their signatures use the newly active +domain. Existing vault/payment/liability state is not reset by this oracle-domain change. + +### Block order and transaction restrictions + +A block contains at most one ZSV oracle certificate. If present, it MUST occur in the first +non-coinbase transaction. That transaction MUST contain no vault operation or stablecoin +input/output. Ordinary ZEC inputs and outputs may fund its network fee, subject to the new +transaction format's ordinary ZEC rules. The certificate is not carried in coinbase and does not +create value. + +At block entry, validators MUST select the pricing observation from the exact parent's committed +oracle state under the active domain. That selection is fixed for all operations in the +candidate block. A valid certificate in the candidate block MUST update only the oracle state +committed for descendants; it MUST NOT replace the block's pricing observation. With no +certificate, retain the applicable parent observation in committed state, including its original +observation height. Expiry makes it unusable for pricing but does not erase its height for the +strictly advancing report rule. A second certificate, or one after any other non-coinbase +transaction, invalidates the block. There is no requirement to include a certificate; a +coinbase-only block remains valid under the ordinary block rules. + +Every borrowing, debt-bearing withdrawal and liquidation operation MUST bind `oracleID` and MUST +use the fixed parent-selected pricing observation. The candidate height MUST be no greater than +that observation's `observedHeight + 12`. A newly included certificate cannot rescue an absent +or expired pricing observation in the same block. An operation naming an observation other than +the block's pricing observation, or naming an expired or absent observation, is invalid even if +that observation carries an otherwise valid quorum certificate. The operation's ordinary +transaction expiry applies independently and cannot extend the oracle window. + +No node-local clock, URL fetch, API response, observed mempool certificate or external service +result is consulted during consensus validation. A producer may omit a certificate or choose +among admissible certificates for descendants; all operations in the current block still use its +parent-selected observation. + +### Outages, conflicting reports and recovery + +An absent or expired block pricing observation disables borrowing, debt-bearing withdrawal and +liquidation. It does not disable collateral additions, repayment, stable transfers, debt-free +withdrawal/closure or an explicitly specified bad-debt retirement that burns actual stable +value. Expiry does not itself move collateral, forgive debt, trigger a privileged settlement or +authorize liquidation using a fabricated fallback price. + +A later valid certificate can resume price-dependent operations from the following block, if +still fresh, subject to all other protocol rules, including any bad-debt issuance freeze. Report +failure alone does not reset liabilities or replace reporter keys. If the committee cannot +recover, the specified remedy is a reviewed network upgrade rather than an administrator bypass. + +Two certificates with different prices for the same observation height cannot both be accepted +on one branch because observed heights strictly advance and a block admits at most one +certificate. Competing forks may accept different valid certificates. Fork choice and +reorganization select the corresponding complete vault/oracle/payment state. Evidence of +reporter equivocation does not automatically change the committee, seize funds or suspend the +chain in v1; no slashable reporter bond is specified. + + +# Rationale + +## A dedicated fixed-asset protocol + +The proposal uses a dedicated transaction format and implicit zUSD payment fields. There is no +caller-chosen asset identifier or issuer path. A dedicated ZEC vault value pool removes +collateral from ordinary spendable outputs; the experimental `OP_DROP OP_TRUE` marker is not +used as a native lock. + +General ZSA issuance, a generic VM and external custody were considered. General issuance and a +VM broaden the permitted activity, while external custody fails the requirement that Zcash +enforce vault rules. Selected ZSA transfer and proof techniques may still be useful in a future +shielded profile. The public first version trades payment privacy for a smaller initial +specification; it does not claim to deliver a private dollar payment instrument. + +One financial operation per transaction limits ordering ambiguity. Open-with-debt and +repay-and-close remain atomic. A vault update replaces one current state; it does not accumulate +reusable owner-local identifiers. The specified native signatures use one owner-key scheme +instead of the prototype's permanent mapping between two key schemes. + +Veylor is one example of dollar access alongside shielded ZEC: its swaps deliver USDC held in +Railgun on Ethereum. [^veylor] + +## Economic policy + +The 250% borrowing ratio, 200% liquidation ratio, 5% bonus, 100-zUSD minimum debt and +100,000-zUSD liability ceiling are concrete proposed initial values taken from the research +model. They are not calibrated safety guarantees. A 20% collateral-price decline moves a vault +from 250% to 200% before oracle delay, execution costs or market gaps. Full liquidation can +create bad debt despite valid accounting. + +The ceiling limits initial nominal exposure; it does not bound ZEC value lost to a false price, +guarantee liquidation depth or ensure a market peg. Repayment supplies a source of demand, but +holders receive no unconditional one-dollar redemption right. A reserve held in ZEC shares the +collateral's drawdown risk. + +Retiring bad debt requires burning actual tokens. It transfers no collateral and creates no +privileged reset authority. A holder may nevertheless have no incentive to absorb the loss, so +borrowing can remain stopped indefinitely. Small shortfalls can trigger the freeze. Adding an +incentivized recapitalization or redemption system would change the economic proposal and needs +separate specification and review. + +## Oracle and governance + +Using the parent observation prevents a certificate included in the same block from invalidating +already-signed operations, even when reports arrive every block. Wallets can sign for the next +block using the current tip's committed oracle state. Transactions delayed beyond that block or +affected by a reorganization may still need re-signing. The cost is one block of additional +price delay: even a fresh crash report cannot change borrowing or liquidation prices in its +inclusion block. This tradeoff requires economic calibration; no history of user-selectable +prices or extra reporter authority is introduced. + +The reporter quorum is an explicit external trust dependency. Signature thresholds do not prove +independent organizations, independent market sources or truthful prices. Fixed upgrade-selected +membership removes a runtime membership administrator but makes key replacement and recovery +slower. The freshness bound is measured in blocks; its wall-clock duration depends on actual +block production. + +Changing consensus parameters or the reporter set requires a network upgrade under the specified +deployment process. No token vote, issuer key or reserve operator can change them. The roster, +parameter calibration and operational arrangements need public review before deployment. + +## Reserve and funding boundary + +The core has no reserve state, block-reward allocation, discretionary incentive spending or NSM +withdrawal. A companion proposal could add native reserve cash and already-issued token +inventory, but would need explicit funding, trade, allowance, closure and final-disposition +rules. + +The research studies identify remaining risks: stale-price trading, limited market response, +losses occurring after a one-time reserve burn, and cheap terminal auctions under weak +competition. Accounting tests do not resolve these incentives. A proposed reduction in another +funding recipient's allocation does not itself allocate the difference to ZSV. + +## Community discussion + +The [forum discussion](https://forum.zcashcommunity.com/t/zip-draft-zsv-one-zec-backed-stablecoin-on-zcash/58909) opened on 5 November 2026. +No community endorsement, editor approval or public review outcome is claimed. Objections, alternatives and responses will be +recorded here when they exist. The mathematical studies and coding-agent adversarial reviews are +preliminary evidence, not the qualified independent expert review required for release of a +security-sensitive ZIP. [^zip-0000] + +# Backward compatibility + +Activation introduces a consensus change and requires upgraded validators. Existing ZEC +transaction formats and payment pools retain their rules. The new format supports a restricted +public funding path; wallet support and safe address presentation remain necessary for users to +construct it. + +The prototype's V4/Sapling signatures, JSON envelopes, trusted funding coinbase, model account +nonces and whole-parent-checkpoint intents are not native consensus formats. Existing model +vectors remain research fixtures and are not migrated into network state. No experimental +fixture balance or key becomes a live allocation. + +# Deployment + +No target network upgrade or activation height is selected. Deployment needs explicit Mainnet +and Testnet activation parameters, transaction identifiers, domain inputs and public reporter +keys. These are deployment allocations, not node-local settings. Neither a ZIP number nor +merging a draft activates the feature. + +Before a Released status such as Proposed, ZIP 0 requires the applicable editor/community +process and qualified independent security/privacy review for a proposal with these +implications. A complete reference implementation generally need not precede Proposed. +Implementation, testing and network activation are later, distinct milestones. [^zip-0000] + +The deployment evidence should cover independent serialization and signature implementations, +cross-implementation valid/invalid block vectors, value conservation, every +acceptance/finalization path, reorgs and database restart, resource benchmarks, oracle +compromise/outage scenarios and economic losses. These checks cannot establish a guaranteed +market peg. + +# Reference implementation + +The [ZSV research +repository](https://github.com/iJaack/zsv/tree/d0f8529f08653744b7702670bc628b20b7d17b00) +provides a signed account model, economic studies and offline profiles linked to pinned Zebra +code. Its original code is MIT-licensed; upstream dependencies retain their own licenses. It is +not a complete implementation of this ZIP. + +The model tests collateralized issuance, repayment, liquidation, explicit bad debt and optional +reserve accounting. The collateral profile verifies serialized fixture ZEC UTXOs and native P2PK +signatures with an experimental guard. The guard is outside Zebra's node verifier, and zUSD +remains a model balance. At that revision, 110 reference Rust tests, 39 Zebra-linked tests and +96 Python tests passed; all 351 combined economic cases reproduced. [^verification] + +This candidate introduces rules beyond that code: a dedicated transaction and value-pool format, +normalized single-operation transitions, a different oracle policy, native signature domains and +permissionless retirement of bad debt. Passing the earlier tests is not evidence that those new +rules are implemented. The accompanying [conformance +material](https://github.com/iJaack/zsv/tree/main/zips/draft-ijaack-zsv) contains 54 financial +cases, seven unsigned wire/context/hash examples covering all operation tags and oracle framing, +and a canonical-signature oracle fixture. These are not a complete signed native transaction or +full-block consensus suite. + +# Outstanding allocations and review + +The protocol choices in Specification are proposed rules, subject to public review. The +remaining work before calling this a deployment-ready specification is: + +1. Allocate the new transaction/group identifiers and upgrade context; select Mainnet/Testnet activation and the public reporter roster through the deployment process. +2. Independently review and implement the byte, digest, authorization and branch-state rules, and produce signed transaction and full-block conformance vectors. +3. Obtain qualified consensus/cryptographic/privacy review and resource benchmarks, and justify the economic parameters, oracle operations and loss policy against adversarial conditions. +4. Incorporate forum feedback and rerun upstream rendering after metadata changes. This draft has been rendered with the ZIP repository's `render.sh --pandoc` path. + +The public-first scope and Owners contact are confirmed. No editor-assigned number, named +reviewer, oracle operator or activation approval has been invented to fill these entries. + +# References + +[^BCP14]: [BCP 14: Key words for requirement levels](https://www.rfc-editor.org/info/bcp14). + +[^zip-0000]: [ZIP 0: ZIP Process](https://github.com/zcash/zips/blob/ff594de6e95bb9e9e7d2ace09510281531c30447/zips/zip-0000.rst), reviewed 30 October 2026. The [Markdown template](https://github.com/zcash/zips/blob/ff594de6e95bb9e9e7d2ace09510281531c30447/zips/zip-template.md) provides the document structure. + +[^verification]: [ZSV verification receipt at the collateral milestone](https://github.com/iJaack/zsv/blob/d0f8529f08653744b7702670bc628b20b7d17b00/docs/verification.md). + +[^sources]: [Pinned Zcash and Zebra research sources](https://github.com/iJaack/zsv/blob/d0f8529f08653744b7702670bc628b20b7d17b00/docs/sources.md). The consensus research baseline is separate from the current ZIP-process reference above. + +[^zip-0215]: [ZIP 215: Explicitly Defining and Modifying Ed25519 Validation Rules](https://github.com/zcash/zips/blob/2bdf39494320e5e04057ac93b1794031ebfb00b5/zips/zip-0215.rst). + +[^zip-0244]: [ZIP 244: Transaction Identifier Non-Malleability](https://github.com/zcash/zips/blob/2bdf39494320e5e04057ac93b1794031ebfb00b5/zips/zip-0244.rst). + +[^zip-0239]: [ZIP 239: Relay of Version 5 Transactions](https://github.com/zcash/zips/blob/2bdf39494320e5e04057ac93b1794031ebfb00b5/zips/zip-0239.rst). + +[^veylor]: [Veylor: How it works](https://veylor.cash/how-it-works/), read 5 November 2026.
zips/draft-ijaack-zsv/README.md +23 −0
@@ -0,0 +1,23 @@ +# ZSV draft conformance material + +These files support [draft-ijaack-zsv.md](../draft-ijaack-zsv.md). They test selected proposed rules beyond the existing Rust research model. They are not a full native consensus implementation or a cryptographic audit. + +```sh +python3 zips/draft-ijaack-zsv/check_financial_vectors.py +python3 zips/draft-ijaack-zsv/check_wire_vectors.py +python3 zips/draft-ijaack-zsv/check_oracle_vector.py +``` + +Run from the repository root with Python 3.10 or later. Financial and wire checks use the standard library. The oracle fixture checker also needs `cryptography` (verified with version 50.0.0); it tests ordinary canonical Ed25519 signatures, not all ZIP 215 acceptance cases. The scripts check adjacent JSON by default. The wire and oracle scripts accept `--write` to regenerate their fixtures. + +| Files | What they check | What they do not check | +|---|---|---| +| `financial-vectors.json`, `check_financial_vectors.py` | 54 operation/range/rounding cases, exact Fraction recomputation and u128 domain bounds | Native parsing, keys, signatures, actual UTXO membership or full branch state | +| `wire-vectors.json`, `check_wire_vectors.py` | Seven unsigned effect bodies covering all operation tags and oracle framing, authoritative-context encoding, transaction/context/signature digests and value equations | ECDSA authorization tails, full node parsing, real input history or allocated network identifiers | +| `oracle-vector.json`, `check_oracle_vector.py` | Report bytes/domains, seven canonical signatures, quorum subsets, rejection/context probes and 13 block-selection cases | Full ZIP 215 noncanonical edge cases, operator independence, data truth or complete native block validation | + +All keys, inputs and network identifiers are fictional test data. The oracle seeds are intentionally public. No fixture supplies a deployment roster or an allocation. + +Remaining conformance work includes independently implemented serialization/digests, complete signed transaction and block vectors, strict-DER/low-S and ZIP 215 edge cases, every operation with real native state, reorg/restart equivalence, resource-limit boundaries and full-node acceptance paths. The [readiness record](https://github.com/iJaack/zsv/blob/d0fe22d/docs/zip-readiness.md) records review and execution results. + +Pricing uses the exact parent's committed observation for the whole block. A newly included certificate takes effect in descendants, so unchanged-price renewal cannot invalidate operations in its inclusion block. The fixture's report included at height 101 is first usable at 102 and expires after 112. Block-selection checks cover renewal, price changes, expiry, activation/domain changes and sibling parents; they are helper-level checks, not native block validation.
zips/draft-ijaack-zsv/check_financial_vectors.py +188 −0
@@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Check ZIP candidate arithmetic with Fraction, not native consensus validation.""" + +import json +import sys +from fractions import Fraction +from pathlib import Path + +Q = 100_000_000 +UNIT = 1_000_000 +MAX_C = 21_000_000 * Q +CAP = 100_000 * UNIT +MIN_DEBT = 100 * UNIT +U64_MAX = (1 << 64) - 1 +U128_MAX = (1 << 128) - 1 +PARAMETERS = { + "zatoshi_per_zec": Q, + "micro_units_per_zusd": UNIT, + "max_collateral": MAX_C, + "max_price": U64_MAX, + "min_nonzero_principal": MIN_DEBT, + "supply_cap": CAP, + "borrow_ratio": "5/2", + "liquidation_ratio": "2/1", + "liquidation_payout_multiplier": "21/20", + "intermediate_bits": 128, +} +FIELDS = { + "open": {"collateral", "principal", "supply", "bad_debt", "price", "oracle_fresh"}, + "update": {"collateral", "principal", "new_collateral", "new_principal", "supply", "bad_debt", "price", "oracle_fresh", "stable_input"}, + "close": {"collateral", "principal", "supply", "bad_debt", "stable_input"}, + "liquidate": {"collateral", "principal", "supply", "bad_debt", "price", "oracle_fresh", "stable_input", "max_burn", "min_collateral"}, + "retire_bad_debt": {"supply", "bad_debt", "amount", "stable_input"}, +} + + +class Invalid(ValueError): + """A financial precondition failed; no state is committed.""" + + +def require(condition, reason): + if not condition: + raise Invalid(reason) + + +def bounded(value, limit): + require(type(value) is int and 0 <= value <= limit, "range") + + +def debt_valid(value): + return value == 0 or value >= MIN_DEBT + + +def price_for(data): + require(data["oracle_fresh"] and data["price"] is not None, "oracle") + return Fraction(data["price"], Q) + + +def enough_collateral(collateral, principal, price): + return collateral * price >= Fraction(5, 2) * principal + + +def post(supply, bad_debt, collateral, principal, minted=0, burned=0, + deposit=0, owner=0, liquidator=0): + return { + "accepted": True, + "supply": supply, + "bad_debt": bad_debt, + "vault": None if collateral is None else {"collateral": collateral, "principal": principal}, + "minted": minted, + "burned": burned, + "zec_deposited": deposit, + "owner_zec": owner, + "liquidator_zec": liquidator, + } + + +def evaluate(operation, data): + if operation not in FIELDS or set(data) != FIELDS[operation]: + raise ValueError("unknown operation or input fields") + try: + for name, value in data.items(): + if name == "oracle_fresh": + require(type(value) is bool, "range") + elif name == "price": + if value is not None: + bounded(value, U64_MAX) + require(value > 0, "range") + else: + limit = MAX_C if "collateral" in name else U64_MAX if name == "max_burn" else CAP + bounded(value, limit) + supply, bad = data["supply"], data["bad_debt"] + require(bad <= supply, "prestate") + collateral, debt = data.get("collateral", 0), data.get("principal", 0) + if operation not in {"open", "retire_bad_debt"}: + require(collateral > 0 and debt_valid(debt) and supply >= debt + bad, "prestate") + other_principal = supply - bad - (debt if operation not in {"open", "retire_bad_debt"} else 0) + require(debt_valid(other_principal), "prestate") + if "stable_input" in data: + require(data["stable_input"] <= supply, "prestate") + + if operation == "open": + require(collateral > 0, "collateral") + require(debt_valid(debt), "minimum_debt") + if debt: + require(bad == 0, "bad_debt_freeze") + require(enough_collateral(collateral, debt, price_for(data)), "collateral_ratio") + require(supply + debt <= CAP, "supply_cap") + return post(supply + debt, bad, collateral, debt, minted=debt, deposit=collateral) + + if operation == "update": + new_c, new_d = data["new_collateral"], data["new_principal"] + require(new_c > 0, "collateral") + require((new_c, new_d) != (collateral, debt), "no_change") + require(debt_valid(new_d), "minimum_debt") + if new_d > debt: + require(bad == 0, "bad_debt_freeze") + if new_d > debt or (new_c < collateral and new_d > 0): + require(enough_collateral(new_c, new_d, price_for(data)), "collateral_ratio") + burn, mint = max(0, debt - new_d), max(0, new_d - debt) + require(burn <= data["stable_input"], "stable_input") + require(supply + mint <= CAP, "supply_cap") + return post(supply + mint - burn, bad, new_c, new_d, minted=mint, burned=burn, + deposit=max(0, new_c - collateral), owner=max(0, collateral - new_c)) + + if operation == "close": + require(debt <= data["stable_input"], "stable_input") + return post(supply - debt, bad, None, None, burned=debt, owner=collateral) + + if operation == "liquidate": + require(debt > 0, "healthy") + price = price_for(data) + require(collateral * price < 2 * debt, "healthy") + # Fraction derives affordability and payout independently of the ZIP's + # cross-multiplied integer formulas. + affordable = collateral * price / Fraction(21, 20) + burn = min(debt, affordable.numerator // affordable.denominator) + payment = Fraction(burn) * Fraction(21, 20) / price + paid = -(-payment.numerator // payment.denominator) + require(paid <= collateral, "collateral") + require(burn <= data["stable_input"], "stable_input") + require(burn <= data["max_burn"] and paid >= data["min_collateral"], "intent_bounds") + require(supply - burn == (supply - debt - bad) + (bad + debt - burn), "liability_invariant") + if burn < debt: + require((collateral - paid) * price < Fraction(21, 20), "rounding_invariant") + return post(supply - burn, bad + debt - burn, None, None, burned=burn, + owner=collateral - paid, liquidator=paid) + + amount = data["amount"] + require(amount > 0 and amount <= bad, "retirement_amount") + require(amount <= data["stable_input"], "stable_input") + require((supply - amount) - (bad - amount) == supply - bad, "liability_invariant") + return {"accepted": True, "supply": supply - amount, "bad_debt": bad - amount, + "burned": amount, "zec_payout": 0, "live_principal": supply - bad} + except Invalid as error: + return {"accepted": False, "reason": str(error)} + + +def check(path): + document = json.loads(path.read_text()) + if document["format"] != "zsv-zip-financial-vectors-v1": + raise ValueError("unsupported vector format") + if document["parameters"] != PARAMETERS: + raise ValueError("parameter mismatch") + # This domain cannot overflow the specified unsigned 128-bit intermediates. + assert MAX_C * U64_MAX * 100 <= U128_MAX + assert CAP * Q * 250 <= U128_MAX + assert CAP * Q * 105 <= U128_MAX + names = set() + for case in document["cases"]: + if case["id"] in names: + raise ValueError(f"duplicate case {case['id']}") + names.add(case["id"]) + if type(case["expected"]["accepted"]) is not bool: + raise ValueError(f"{case['id']}: accepted must be boolean") + actual = evaluate(case["operation"], case["input"]) + if actual != case["expected"]: + raise ValueError(f"{case['id']}: expected {case['expected']}, computed {actual}") + print(f"Financial ZIP vectors checked: {len(names)} cases; exact Fraction arithmetic and u128 bounds.") + + +if __name__ == "__main__": + if len(sys.argv) > 2: + sys.exit("usage: check_financial_vectors.py [vectors.json]") + try: + check(Path(sys.argv[1]) if len(sys.argv) == 2 else Path(__file__).with_name("financial-vectors.json")) + except (KeyError, ValueError, AssertionError) as error: + sys.exit(str(error))
zips/draft-ijaack-zsv/check_oracle_vector.py +266 −0
@@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Check the ZIP's canonical oracle fixture; --write regenerates its exact JSON. + +Requires the optional `cryptography` package (python3 -m pip install cryptography). +This exercises ordinary canonical Ed25519 signatures, not ZIP 215's complete +acceptance set, noncanonical point encodings or a native transaction verifier. +All seeds and network fields below are explicitly public, synthetic fixtures. +""" + +import argparse +import hashlib +import json +import struct +from pathlib import Path + +try: + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +except ImportError as error: + raise SystemExit("Optional dependency missing: python3 -m pip install cryptography") from error + + +PATH = Path(__file__).with_name("oracle-vector.json") +SET_PREFIX = b"ZSV/OracleSet/v1\0" +REPORT_PREFIX = b"ZSV/Oracle/v1\0" +U32_MAX = (1 << 32) - 1 +U64_MAX = (1 << 64) - 1 +FIELD_MODULUS = (1 << 255) - 19 +SUBGROUP_ORDER = (1 << 252) + 27742317777372353535851937790883648493 + + +def sha256(data): + return hashlib.sha256(data).digest() + + +def set_id(keys): + return sha256(SET_PREFIX + bytes([7, 5]) + b"".join(keys)) + + +def body(height, ancestor, price): + return struct.pack("<I", height) + ancestor + struct.pack("<Q", price) + + +def message(genesis, branch, roster_id, report_body): + return REPORT_PREFIX + genesis + struct.pack("<I", branch) + roster_id + report_body + + +def mask_indices(mask): + if type(mask) is not int or not 0 <= mask < 128 or not 5 <= mask.bit_count() <= 7: + raise ValueError("invalid signer mask") + return [index for index in range(7) if mask & (1 << index)] + + +def verify_subset(keys, signed_message, mask, signatures): + indices = mask_indices(mask) + if len(signatures) != len(indices): + raise ValueError("signature count does not match mask") + for index, signature in zip(indices, signatures): + keys[index].public_key().verify(signature, signed_message) + + +def context_valid(height, price, candidate, branch_start, previous, ancestor_matches=True): + """Check only the listed numeric/ancestry preconditions, not full consensus.""" + return ( + type(price) is int and 1 <= price <= U64_MAX + and type(height) is int and 0 <= height <= U32_MAX + and 0 < branch_start <= candidate <= U32_MAX + and branch_start - 1 <= height < candidate <= height + 12 <= U32_MAX + and (previous is None or height > previous) + and ancestor_matches + ) + + +def fixture(): + rows = [] + for seed_byte in range(1, 8): + seed = bytes([seed_byte]) * 32 + key = Ed25519PrivateKey.from_private_bytes(seed) + public = key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + rows.append((public, seed, key)) + rows.sort(key=lambda row: row[0]) + public_keys = [row[0] for row in rows] + keys = [row[2] for row in rows] + roster_bytes = SET_PREFIX + bytes([7, 5]) + b"".join(public_keys) + roster_id = set_id(public_keys) + genesis = bytes([0x11]) * 32 + branch = 0xA1B2C3D4 + ancestor = bytes([0x22]) * 32 + height, price = 100, 100_000_000 + report_body = body(height, ancestor, price) + signed_message = message(genesis, branch, roster_id, report_body) + signatures = [key.sign(signed_message) for key in keys] + mask = 0x1F + document = { + "format": "zsv-zip-canonical-oracle-vector-v1", + "scope": "Public deterministic fixture keys and synthetic context only; no deployment key or branch-ID allocation. Canonical signatures exercise common Ed25519 behavior, not all ZIP 215 edge cases.", + "genesis_hash_hex": genesis.hex(), + "active_branch_id_u32": branch, + "active_branch_start_height": 90, + "candidate_height": 101, + "prior_observed_height": 99, + "keys_sorted": [ + {"index": index, "public_key_hex": row[0].hex(), "public_fixture_seed_hex": row[1].hex()} + for index, row in enumerate(rows) + ], + "roster_bytes_hex": roster_bytes.hex(), + "reporter_set_id_hex": roster_id.hex(), + "observed_height": height, + "observed_block_hash_hex": ancestor.hex(), + "price_micro_usd_per_zec": price, + "valid_from_height": 102, + "valid_through_height": 112, + "body_hex": report_body.hex(), + "signing_message_hex": signed_message.hex(), + "oracle_id_hex": sha256(signed_message).hex(), + "signer_mask_u8": mask, + "effect_hex": (report_body + bytes([mask])).hex(), + "authorization_hex": b"".join(signatures[:5]).hex(), + "signatures_by_index": [ + {"index": index, "signature_hex": signature.hex()} + for index, signature in enumerate(signatures) + ], + "expected_accept": True, + } + return document, keys + + +def expect_failure(call): + try: + call() + except (ValueError, InvalidSignature): + return + raise AssertionError("invalid mutation was accepted") + + +def check(document, keys): + report_body = bytes.fromhex(document["body_hex"]) + signed_message = bytes.fromhex(document["signing_message_hex"]) + signatures = [bytes.fromhex(row["signature_hex"]) for row in document["signatures_by_index"]] + assert len(report_body) == 44 and len(signed_message) == 126 + assert len(bytes.fromhex(document["effect_hex"])) == 45 + assert len(bytes.fromhex(document["authorization_hex"])) == 320 + assert len(set(row["public_key_hex"] for row in document["keys_sorted"])) == 7 + for key, signature in zip(keys, signatures): + assert len(signature) == 64 + assert (int.from_bytes(signature[:32], "little") & ((1 << 255) - 1)) < FIELD_MODULUS + assert int.from_bytes(signature[32:], "little") < SUBGROUP_ORDER + key.public_key().verify(signature, signed_message) + + for mask in (0x1F, 0x3F, 0x7F, 0x7C): + selected = [signatures[index] for index in mask_indices(mask)] + verify_subset(keys, signed_message, mask, selected) + effect = report_body + bytes([mask]) + reconstructed_message = signed_message[:-44] + effect[:44] + assert sha256(reconstructed_message) == bytes.fromhex(document["oracle_id_hex"]) + assert len(effect) + sum(map(len, selected)) == 45 + 64 * mask.bit_count() + + # Mutate each signed field: genesis, branch, roster, height, ancestor, price. + for offset in (14, 46, 50, 82, 86, 118): + mutated = bytearray(signed_message) + mutated[offset] ^= 1 + expect_failure(lambda: keys[0].public_key().verify(signatures[0], bytes(mutated))) + mutated_signature = bytearray(signatures[0]) + mutated_signature[0] ^= 1 + expect_failure(lambda: keys[0].public_key().verify(bytes(mutated_signature), signed_message)) + expect_failure(lambda: keys[1].public_key().verify(signatures[0], signed_message)) + + for invalid_mask in (0, 0x0F, 0x9F, 0xFF): + expect_failure(lambda: mask_indices(invalid_mask)) + expect_failure(lambda: verify_subset(keys, signed_message, 0x1F, signatures[:4])) + expect_failure(lambda: verify_subset(keys, signed_message, 0x1F, signatures[:6])) + expect_failure(lambda: verify_subset(keys, signed_message, 0x1F, list(reversed(signatures[:5])))) + expect_failure(lambda: verify_subset(keys, signed_message, 0x3F, signatures[:5] + [bytes(mutated_signature)])) + + cases = [ + ((100, 100_000_000, 100, 90, 99), False), + ((100, 100_000_000, 101, 90, 99), True), + ((100, 100_000_000, 112, 90, 99), True), + ((100, 100_000_000, 113, 90, 99), False), + ((100, 100_000_000, 101, 90, 99, False), False), + ((88, 100_000_000, 90, 90, None), False), + ((89, 100_000_000, 90, 90, None), True), + ((100, 100_000_000, 101, 90, 100), False), + ((100, 0, 101, 90, 99), False), + ((100, U64_MAX, 101, 90, 99), True), + ((100, U64_MAX + 1, 101, 90, 99), False), + ((U32_MAX - 1, 100_000_000, U32_MAX, 90, None), False), + ] + for arguments, expected in cases: + assert context_valid(*arguments) is expected, arguments + return len(cases) + + +def block_oracle(parent, accepted, height, domain_changed=False): + """Oracle selection only; inputs are already-validated (ID, observed height). + + This fixture helper does not validate a block, ancestry or certificate. + """ + parent = None if domain_changed else parent + pricing = parent if parent is not None and height <= parent[1] + 12 else None + return pricing, accepted if accepted is not None else parent + + +def check_rollover(document, keys): + old_message = bytes.fromhex(document["signing_message_hex"]) + old = (sha256(old_message), 100) + # Same price, later observation and synthetic ancestor: both quorums verify. + new_message = old_message[:-44] + body(101, bytes([0x33]) * 32, 100_000_000) + verify_subset(keys, new_message, 0x1F, [key.sign(new_message) for key in keys[:5]]) + new = (sha256(new_message), 101) + assert old[0] != new[0] + assert context_valid(101, 100_000_000, 102, 90, 100) + crash_message = old_message[:-44] + body(101, bytes([0x33]) * 32, 20_000_000) + verify_subset(keys, crash_message, 0x1F, [key.sign(crash_message) for key in keys[:5]]) + crash = (sha256(crash_message), 101) + next_domain_message = message(bytes.fromhex(document["genesis_hash_hex"]), + document["active_branch_id_u32"] + 1, + bytes.fromhex(document["reporter_set_id_hex"]), + body(101, bytes([0x33]) * 32, 100_000_000)) + verify_subset(keys, next_domain_message, 0x1F, + [key.sign(next_domain_message) for key in keys[:5]]) + next_domain = (sha256(next_domain_message), 101) + cases = [ + # (parent, newly accepted, block height, domain change), (pricing, committed) + ((None, old, 101, False), (None, old)), + ((old, new, 102, False), (old, new)), + ((new, None, 103, False), (new, new)), + ((old, None, 102, False), (old, old)), + ((old, None, 112, False), (old, old)), + ((old, None, 113, False), (None, old)), + ((old, new, 113, False), (None, new)), + ((new, None, 114, False), (None, new)), + ((old, None, 102, True), (None, None)), + # A new-domain certificate must be verified separately before this helper. + ((old, next_domain, 102, True), (None, next_domain)), + ((old, crash, 102, False), (old, crash)), + ((crash, None, 103, False), (crash, crash)), + # Replaying a sibling uses its own parent, not the selected tip's report. + ((old, None, 103, False), (old, old)), + ] + for arguments, expected in cases: + assert block_oracle(*arguments) == expected, arguments + return len(cases) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--write", action="store_true", help="regenerate the checked-in fixture") + args = parser.parse_args() + document, keys = fixture() + boundary_count = check(document, keys) + rollover_count = check_rollover(document, keys) + encoded = json.dumps(document, indent=2) + "\n" + if args.write: + PATH.write_text(encoded) + if PATH.read_text() != encoded: + raise ValueError("oracle-vector.json differs from its deterministic fixture") + print(f"Oracle fixture checked: 7 canonical signers, 4 quorum subsets, 16 rejection probes, {boundary_count} context cases; {rollover_count} block-selection cases; exact JSON match.") + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, AssertionError) as error: + raise SystemExit(str(error)) from error
zips/draft-ijaack-zsv/check_wire_vectors.py +213 −0
@@ -0,0 +1,213 @@ +"""Unsigned ZSV draft vectors; fictional identifiers, no consensus allocations. + +Standard-library encoding/hash examples, not a node verifier or signature test. +The default checks the adjacent wire-vectors.json without changing it. +Use --check PATH for another file, or --write to regenerate the adjacent file. +""" +import argparse +import hashlib +import json +import struct +from pathlib import Path + +# Deliberately fictional test-network allocations, never deployment values. +TEST_VERSION = 0x7FFFFFFE +TEST_VERSION_GROUP_ID = 0x5A535600 +TEST_BRANCH_ID = 0x01020304 +TEST_GENESIS = bytes.fromhex('44' * 32) +OWNER = bytes.fromhex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') +LIQUIDATOR = bytes.fromhex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5') +PATH = Path(__file__).with_name("wire-vectors.json") + + +def u16(n): + return struct.pack('<H', n) + + +def u32(n): + return struct.pack('<I', n) + + +def u64(n): + return struct.pack('<Q', n) + + +def count(n): + assert 0 <= n <= 64 + return bytes([n]) + + +def h(label, value): + label = label.encode('ascii') + assert len(label) <= 16 + return hashlib.blake2b(value, digest_size=32, person=label.ljust(16, b'\0')).digest() + + +def coin(previous_byte, index, amount, key): + return (bytes([previous_byte]) * 32 + u32(index), amount, key) + + +def body(zec_inputs, zec_outputs, stable_inputs, stable_outputs, tag, payload, oracle_effects=None): + assert len(payload) == {0: 0, 1: 81, 2: 84, 3: 36, 4: 121, 5: 8}[tag] + header = b''.join(u32(v) for v in ( + TEST_VERSION | 0x80000000, TEST_VERSION_GROUP_ID, + TEST_BRANCH_ID, 0, 100)) + fields = [header] + for inputs, outputs in ((zec_inputs, zec_outputs), (stable_inputs, stable_outputs)): + fields.append(count(len(inputs))) + fields.extend(outpoint for outpoint, _, _ in inputs) + fields.append(count(len(outputs))) + fields.extend(u64(amount) + key for amount, key in outputs) + fields.extend((bytes([tag]), payload, bytes([oracle_effects is not None]))) + if oracle_effects is not None: + assert len(oracle_effects) == 45 + assert tag == 0 and not stable_inputs and not stable_outputs + assert oracle_effects[-1] < 128 and 5 <= oracle_effects[-1].bit_count() <= 7 + fields.append(oracle_effects) + return b''.join(fields) + + +def context(zec_inputs, stable_inputs, previous_vault): + fields = [] + for inputs in (zec_inputs, stable_inputs): + fields.append(count(len(inputs))) + fields.extend(outpoint + u64(amount) + key for outpoint, amount, key in inputs) + fields.append(bytes([previous_vault is not None])) + if previous_vault: + reference, key, collateral, principal = previous_vault + fields.append(reference + key + u64(collateral) + u64(principal)) + return b''.join(fields) + + +def vector(name, zec_inputs, zec_outputs, stable_inputs, stable_outputs, + tag, payload, previous_vault=None, action_key=None, + collateral_after=0, minted=0, burned=0, oracle_effects=None, payouts=()): + effects = body(zec_inputs, zec_outputs, stable_inputs, stable_outputs, tag, payload, oracle_effects) + resolved = context(zec_inputs, stable_inputs, previous_vault) + txid = h('ZSV_TxId_v1', TEST_GENESIS + effects) + context_hash = h('ZSV_Context_v1', resolved) + roles = [(0, i, key) for i, (_, _, key) in enumerate(zec_inputs)] + roles += [(1, i, key) for i, (_, _, key) in enumerate(stable_inputs)] + if action_key: + roles.append((2, 0, action_key)) + digests = [{ + 'role': role, 'index': index, 'verification_key_hex': key.hex(), + 'digest_hex': h('ZSV_Sign_v1', TEST_GENESIS + txid + context_hash + + bytes([role]) + u16(index)).hex(), + } for role, index, key in roles] + collateral_before = previous_vault[2] if previous_vault else 0 + fee = sum(c[1] for c in zec_inputs) + collateral_before - collateral_after - sum(c[0] for c in zec_outputs) + stable_in = sum(c[1] for c in stable_inputs) + stable_out = sum(c[0] for c in stable_outputs) + assert fee >= 0 + assert stable_in + minted == stable_out + burned + assert len({index for index, _, _ in payouts}) == len(payouts) + for index, key, amount in payouts: + assert amount > 0 and zec_outputs[index] == (amount, key) + result = { + 'name': name, 'operation_tag': tag, 'effects_length': len(effects), + 'effects_hex': effects.hex(), 'txid_hex': txid.hex(), + 'resolved_context_hex': resolved.hex(), 'context_hash_hex': context_hash.hex(), + 'signature_digests': digests, + 'value_check': { + 'zec_input': sum(c[1] for c in zec_inputs), + 'zec_output': sum(c[0] for c in zec_outputs), + 'collateral_before': collateral_before, 'collateral_after': collateral_after, + 'zec_fee': fee, 'stable_input': stable_in, 'stable_output': stable_out, + 'minted': minted, 'burned': burned, + }, + } + if payouts: + result['mandatory_payouts'] = [ + {'index': index, 'recipient_key_hex': key.hex(), 'value': amount} + for index, key, amount in payouts + ] + if oracle_effects is not None: + slots = oracle_effects[-1].bit_count() + result['oracle_effect_check'] = { + 'effect_hex': oracle_effects.hex(), 'effect_length': len(oracle_effects), + 'expected_secp256k1_signature_slots': len(digests), + 'expected_ed25519_signature_slots': slots, + 'expected_total_signature_slots': len(digests) + slots, + 'expected_ed25519_authorization_bytes': 64 * slots, + 'scope': 'Unsigned certificate framing only. No reporter signatures are supplied or verified; this fictional context is separate from oracle-vector.json.', + } + return result + + +def vectors(): + zero_oracle = bytes(32) + previous_vault = (bytes.fromhex('55' * 32), OWNER, 1_000_000_000, 400_000_000) + liquidation = (previous_vault[0] + bytes.fromhex('66' * 32) + + u64(400_000_000) + u64(840_000_000) + + LIQUIDATOR + u32(2) + u32(1)) + update = (previous_vault[0] + u64(800_000_000) + u64(300_000_000) + + bytes.fromhex('88' * 32) + u32(1)) + # Unlike oracle-vector.json's signed context, this framing example has no + # supplied reporter roster or signatures. A height91 inclusion fits expiry100. + oracle_effects = u32(90) + bytes.fromhex('99' * 32) + u64(100_000_000) + b'\x1f' + return { + 'scope': 'Unsigned effect/context/hash vectors only; no signatures, node execution or assigned network identifiers. Previous outputs, prior vaults and operation oracle IDs are fictional context inputs, not certified chain state.', + 'hash_hex_order': 'Raw digest bytes; not reversed display hashes.', + 'test_allocations': { + 'version': TEST_VERSION, 'version_group_id': TEST_VERSION_GROUP_ID, + 'branch_id': TEST_BRANCH_ID, 'genesis_hex': TEST_GENESIS.hex(), + 'expiry_height': 100, + }, + 'cases': [ + vector('open_debt_free', + [coin(0x22, 3, 1_100_000_000, OWNER)], [(99_990_000, OWNER)], [], [], + 1, OWNER + u64(1_000_000_000) + u64(0) + zero_oracle, + action_key=OWNER, collateral_after=1_000_000_000), + vector('stable_transfer', + [coin(0x33, 1, 100_000_000, OWNER)], [(99_990_000, OWNER)], + [coin(0x77, 2, 400_000_000, OWNER)], + [(125_000_000, LIQUIDATOR), (275_000_000, OWNER)], 0, b''), + vector('solvent_liquidation_at_50', + [coin(0x33, 1, 100_000_000, LIQUIDATOR)], + [(99_990_000, LIQUIDATOR), (840_000_000, LIQUIDATOR), (160_000_000, OWNER)], + [coin(0x77, 2, 400_000_000, LIQUIDATOR)], [], 4, liquidation, + previous_vault=previous_vault, action_key=LIQUIDATOR, burned=400_000_000, + payouts=[(1, LIQUIDATOR, 840_000_000), (2, OWNER, 160_000_000)]), + vector('retire_bad_debt', + [coin(0x33, 1, 100_000_000, OWNER)], [(99_990_000, OWNER)], + [coin(0x77, 2, 400_000_000, OWNER)], [(300_000_000, OWNER)], + 5, u64(100_000_000), burned=100_000_000), + vector('update_repay_and_withdraw', + [coin(0x33, 1, 100_000_000, OWNER)], + [(99_990_000, OWNER), (200_000_000, OWNER)], + [coin(0x77, 2, 100_000_000, OWNER)], [], 2, update, + previous_vault=previous_vault, action_key=OWNER, + collateral_after=800_000_000, burned=100_000_000, + payouts=[(1, OWNER, 200_000_000)]), + vector('close_with_full_repayment', + [coin(0x33, 1, 100_000_000, OWNER)], + [(99_990_000, OWNER), (1_000_000_000, OWNER)], + [coin(0x77, 2, 400_000_000, OWNER)], [], + 3, previous_vault[0] + u32(1), + previous_vault=previous_vault, action_key=OWNER, + burned=400_000_000, payouts=[(1, OWNER, 1_000_000_000)]), + vector('oracle_certificate_effects', + [coin(0x33, 1, 100_000_000, OWNER)], [(99_990_000, OWNER)], + [], [], 0, b'', oracle_effects=oracle_effects), + ], + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', type=Path, nargs='?', const=PATH, default=PATH, + help='compare a vector file; defaults to the adjacent file') + parser.add_argument('--write', action='store_true', + help='regenerate the adjacent checked-in vector file') + args = parser.parse_args() + actual = json.dumps(vectors(), indent=2, sort_keys=True) + '\n' + if args.write: + PATH.write_text(actual) + if args.check.read_text() != actual: + raise SystemExit(f'Vector mismatch: {args.check}') + print('Seven unsigned ZSV wire/context/hash vectors match: all six operation tags and one oracle effect body.') + + +if __name__ == '__main__': + main()
zips/draft-ijaack-zsv/financial-vectors.json +1134 −0
@@ -0,0 +1,1134 @@ +{ + "format": "zsv-zip-financial-vectors-v1", + "scope": "Candidate ZIP arithmetic only. JSON is not a native encoding. oracle_fresh is an assumed validated precondition; signatures, ownership, prevouts, fee funding, oracle certificates, transaction bytes and full consensus are not checked. Rejection means the financial transition is not committed. RetireBadDebt is proposed behavior absent from the existing Rust model.", + "parameters": { + "zatoshi_per_zec": 100000000, + "micro_units_per_zusd": 1000000, + "max_collateral": 2100000000000000, + "max_price": 18446744073709551615, + "min_nonzero_principal": 100000000, + "supply_cap": 100000000000, + "borrow_ratio": "5/2", + "liquidation_ratio": "2/1", + "liquidation_payout_multiplier": "21/20", + "intermediate_bits": 128 + }, + "cases": [ + { + "id": "open_borrow_exact_250_percent", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 100000000, + "supply": 0, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": true, + "supply": 100000000, + "bad_debt": 0, + "vault": { + "collateral": 250000000, + "principal": 100000000 + }, + "minted": 100000000, + "burned": 0, + "zec_deposited": 250000000, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "open_borrow_one_zatoshi_below_ratio", + "operation": "open", + "input": { + "collateral": 249999999, + "principal": 100000000, + "supply": 0, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "collateral_ratio" + } + }, + { + "id": "open_nonzero_debt_below_minimum", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 99999999, + "supply": 0, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "minimum_debt" + } + }, + { + "id": "open_collateral_only_without_oracle", + "operation": "open", + "input": { + "collateral": 1, + "principal": 0, + "supply": 0, + "bad_debt": 0, + "price": null, + "oracle_fresh": false + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "vault": { + "collateral": 1, + "principal": 0 + }, + "minted": 0, + "burned": 0, + "zec_deposited": 1, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "open_zero_collateral", + "operation": "open", + "input": { + "collateral": 0, + "principal": 0, + "supply": 0, + "bad_debt": 0, + "price": null, + "oracle_fresh": false + }, + "expected": { + "accepted": false, + "reason": "collateral" + } + }, + { + "id": "open_collateral_only_during_bad_debt", + "operation": "open", + "input": { + "collateral": 1, + "principal": 0, + "supply": 1000000, + "bad_debt": 1000000, + "price": null, + "oracle_fresh": false + }, + "expected": { + "accepted": true, + "supply": 1000000, + "bad_debt": 1000000, + "vault": { + "collateral": 1, + "principal": 0 + }, + "minted": 0, + "burned": 0, + "zec_deposited": 1, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "open_borrow_during_bad_debt", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 100000000, + "supply": 1, + "bad_debt": 1, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "bad_debt_freeze" + } + }, + { + "id": "open_exact_supply_cap", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 100000000, + "supply": 99900000000, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": true, + "supply": 100000000000, + "bad_debt": 0, + "vault": { + "collateral": 250000000, + "principal": 100000000 + }, + "minted": 100000000, + "burned": 0, + "zec_deposited": 250000000, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "open_one_unit_above_supply_cap", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 100000000, + "supply": 99900000001, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "supply_cap" + } + }, + { + "id": "open_borrow_without_fresh_observation", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 100000000, + "supply": 0, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": false + }, + "expected": { + "accepted": false, + "reason": "oracle" + } + }, + { + "id": "open_zero_price", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 100000000, + "supply": 0, + "bad_debt": 0, + "price": 0, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "range" + } + }, + { + "id": "open_negative_collateral", + "operation": "open", + "input": { + "collateral": -1, + "principal": 100000000, + "supply": 0, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "range" + } + }, + { + "id": "open_collateral_above_native_maximum", + "operation": "open", + "input": { + "collateral": 2100000000000001, + "principal": 100000000, + "supply": 0, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "range" + } + }, + { + "id": "open_price_above_u64", + "operation": "open", + "input": { + "collateral": 250000000, + "principal": 100000000, + "supply": 0, + "bad_debt": 0, + "price": 18446744073709551616, + "oracle_fresh": true + }, + "expected": { + "accepted": false, + "reason": "range" + } + }, + { + "id": "open_valid_domain_maximum_products", + "operation": "open", + "input": { + "collateral": 2100000000000000, + "principal": 100000000000, + "supply": 0, + "bad_debt": 0, + "price": 18446744073709551615, + "oracle_fresh": true + }, + "expected": { + "accepted": true, + "supply": 100000000000, + "bad_debt": 0, + "vault": { + "collateral": 2100000000000000, + "principal": 100000000000 + }, + "minted": 100000000000, + "burned": 0, + "zec_deposited": 2100000000000000, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "update_borrow_exact_ratio", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 1000000000, + "new_principal": 400000000, + "supply": 100000000, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true, + "stable_input": 0 + }, + "expected": { + "accepted": true, + "supply": 400000000, + "bad_debt": 0, + "vault": { + "collateral": 1000000000, + "principal": 400000000 + }, + "minted": 300000000, + "burned": 0, + "zec_deposited": 0, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "update_borrow_one_unit_above_ratio", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 1000000000, + "new_principal": 400000001, + "supply": 100000000, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true, + "stable_input": 0 + }, + "expected": { + "accepted": false, + "reason": "collateral_ratio" + } + }, + { + "id": "update_borrow_frozen_at_entry", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 1000000000, + "new_principal": 400000000, + "supply": 100000001, + "bad_debt": 1, + "price": 100000000, + "oracle_fresh": true, + "stable_input": 0 + }, + "expected": { + "accepted": false, + "reason": "bad_debt_freeze" + } + }, + { + "id": "update_withdraw_exact_ratio", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 250000000, + "new_principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true, + "stable_input": 0 + }, + "expected": { + "accepted": true, + "supply": 100000000, + "bad_debt": 0, + "vault": { + "collateral": 250000000, + "principal": 100000000 + }, + "minted": 0, + "burned": 0, + "zec_deposited": 0, + "owner_zec": 750000000, + "liquidator_zec": 0 + } + }, + { + "id": "update_withdraw_one_zatoshi_too_far", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 249999999, + "new_principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true, + "stable_input": 0 + }, + "expected": { + "accepted": false, + "reason": "collateral_ratio" + } + }, + { + "id": "update_deposit_without_oracle", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 1100000000, + "new_principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "price": null, + "oracle_fresh": false, + "stable_input": 0 + }, + "expected": { + "accepted": true, + "supply": 100000000, + "bad_debt": 0, + "vault": { + "collateral": 1100000000, + "principal": 100000000 + }, + "minted": 0, + "burned": 0, + "zec_deposited": 100000000, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "update_repay_without_oracle", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 200000000, + "new_collateral": 1000000000, + "new_principal": 100000000, + "supply": 200000000, + "bad_debt": 0, + "price": null, + "oracle_fresh": false, + "stable_input": 100000000 + }, + "expected": { + "accepted": true, + "supply": 100000000, + "bad_debt": 0, + "vault": { + "collateral": 1000000000, + "principal": 100000000 + }, + "minted": 0, + "burned": 100000000, + "zec_deposited": 0, + "owner_zec": 0, + "liquidator_zec": 0 + } + }, + { + "id": "update_repay_and_withdraw", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 200000000, + "new_collateral": 250000000, + "new_principal": 100000000, + "supply": 200000000, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true, + "stable_input": 100000000 + }, + "expected": { + "accepted": true, + "supply": 100000000, + "bad_debt": 0, + "vault": { + "collateral": 250000000, + "principal": 100000000 + }, + "minted": 0, + "burned": 100000000, + "zec_deposited": 0, + "owner_zec": 750000000, + "liquidator_zec": 0 + } + }, + { + "id": "update_full_repay_and_debt_free_withdraw", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 1, + "new_principal": 0, + "supply": 100000000, + "bad_debt": 0, + "price": null, + "oracle_fresh": false, + "stable_input": 100000000 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "vault": { + "collateral": 1, + "principal": 0 + }, + "minted": 0, + "burned": 100000000, + "zec_deposited": 0, + "owner_zec": 999999999, + "liquidator_zec": 0 + } + }, + { + "id": "update_repay_leaves_below_minimum", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 200000000, + "new_collateral": 1000000000, + "new_principal": 99999999, + "supply": 200000000, + "bad_debt": 0, + "price": null, + "oracle_fresh": false, + "stable_input": 101000000 + }, + "expected": { + "accepted": false, + "reason": "minimum_debt" + } + }, + { + "id": "update_repay_without_enough_tokens", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 1000000000, + "new_principal": 0, + "supply": 100000000, + "bad_debt": 0, + "price": null, + "oracle_fresh": false, + "stable_input": 99999999 + }, + "expected": { + "accepted": false, + "reason": "stable_input" + } + }, + { + "id": "update_no_change", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 1000000000, + "new_principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "price": 100000000, + "oracle_fresh": true, + "stable_input": 0 + }, + "expected": { + "accepted": false, + "reason": "no_change" + } + }, + { + "id": "update_zero_collateral_requires_close", + "operation": "update", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "new_collateral": 0, + "new_principal": 0, + "supply": 100000000, + "bad_debt": 0, + "price": null, + "oracle_fresh": false, + "stable_input": 100000000 + }, + "expected": { + "accepted": false, + "reason": "collateral" + } + }, + { + "id": "close_repay_and_return_all_collateral", + "operation": "close", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "stable_input": 100000000 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "vault": null, + "minted": 0, + "burned": 100000000, + "zec_deposited": 0, + "owner_zec": 1000000000, + "liquidator_zec": 0 + } + }, + { + "id": "close_debt_free", + "operation": "close", + "input": { + "collateral": 1000000000, + "principal": 0, + "supply": 0, + "bad_debt": 0, + "stable_input": 0 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "vault": null, + "minted": 0, + "burned": 0, + "zec_deposited": 0, + "owner_zec": 1000000000, + "liquidator_zec": 0 + } + }, + { + "id": "close_insufficient_tokens", + "operation": "close", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "stable_input": 99999999 + }, + "expected": { + "accepted": false, + "reason": "stable_input" + } + }, + { + "id": "close_preserves_unrelated_bad_debt", + "operation": "close", + "input": { + "collateral": 1000000000, + "principal": 100000000, + "supply": 100000001, + "bad_debt": 1, + "stable_input": 100000000 + }, + "expected": { + "accepted": true, + "supply": 1, + "bad_debt": 1, + "vault": null, + "minted": 0, + "burned": 100000000, + "zec_deposited": 0, + "owner_zec": 1000000000, + "liquidator_zec": 0 + } + }, + { + "id": "liquidate_exact_200_percent_is_healthy", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 80000000, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": false, + "reason": "healthy" + } + }, + { + "id": "liquidate_just_below_200_percent_rounds_up", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 79999999, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "vault": null, + "minted": 0, + "burned": 400000000, + "zec_deposited": 0, + "owner_zec": 474999993, + "liquidator_zec": 525000007 + } + }, + { + "id": "liquidate_solvent", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 50000000, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "vault": null, + "minted": 0, + "burned": 400000000, + "zec_deposited": 0, + "owner_zec": 160000000, + "liquidator_zec": 840000000 + } + }, + { + "id": "liquidate_crash_floor_and_ceiling", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 20000000, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": true, + "supply": 209523810, + "bad_debt": 209523810, + "vault": null, + "minted": 0, + "burned": 190476190, + "zec_deposited": 0, + "owner_zec": 2, + "liquidator_zec": 999999998 + } + }, + { + "id": "liquidate_crash_preserves_other_liabilities", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 550000000, + "bad_debt": 50000000, + "price": 20000000, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": true, + "supply": 359523810, + "bad_debt": 259523810, + "vault": null, + "minted": 0, + "burned": 190476190, + "zec_deposited": 0, + "owner_zec": 2, + "liquidator_zec": 999999998 + } + }, + { + "id": "liquidate_no_fresh_observation", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 50000000, + "oracle_fresh": false, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": false, + "reason": "oracle" + } + }, + { + "id": "liquidate_insufficient_tokens", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 50000000, + "oracle_fresh": true, + "stable_input": 399999999, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": false, + "reason": "stable_input" + } + }, + { + "id": "liquidate_maximum_burn_bound", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 50000000, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 399999999, + "min_collateral": 0 + }, + "expected": { + "accepted": false, + "reason": "intent_bounds" + } + }, + { + "id": "liquidate_minimum_collateral_bound", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 50000000, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 840000001 + }, + "expected": { + "accepted": false, + "reason": "intent_bounds" + } + }, + { + "id": "liquidate_exact_intent_bounds", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 400000000, + "supply": 400000000, + "bad_debt": 0, + "price": 50000000, + "oracle_fresh": true, + "stable_input": 400000000, + "max_burn": 400000000, + "min_collateral": 840000000 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "vault": null, + "minted": 0, + "burned": 400000000, + "zec_deposited": 0, + "owner_zec": 160000000, + "liquidator_zec": 840000000 + } + }, + { + "id": "liquidate_zero_affordable_burn_dust", + "operation": "liquidate", + "input": { + "collateral": 1, + "principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "price": 1, + "oracle_fresh": true, + "stable_input": 0, + "max_burn": 0, + "min_collateral": 0 + }, + "expected": { + "accepted": true, + "supply": 100000000, + "bad_debt": 100000000, + "vault": null, + "minted": 0, + "burned": 0, + "zec_deposited": 0, + "owner_zec": 1, + "liquidator_zec": 0 + } + }, + { + "id": "liquidate_one_micro_unit_exact_collateral", + "operation": "liquidate", + "input": { + "collateral": 105, + "principal": 100000000, + "supply": 100000000, + "bad_debt": 0, + "price": 1000000, + "oracle_fresh": true, + "stable_input": 1, + "max_burn": 1, + "min_collateral": 105 + }, + "expected": { + "accepted": true, + "supply": 99999999, + "bad_debt": 99999999, + "vault": null, + "minted": 0, + "burned": 1, + "zec_deposited": 0, + "owner_zec": 0, + "liquidator_zec": 105 + } + }, + { + "id": "liquidate_debt_free_is_healthy", + "operation": "liquidate", + "input": { + "collateral": 1000000000, + "principal": 0, + "supply": 0, + "bad_debt": 0, + "price": 50000000, + "oracle_fresh": true, + "stable_input": 0, + "max_burn": 400000000, + "min_collateral": 0 + }, + "expected": { + "accepted": false, + "reason": "healthy" + } + }, + { + "id": "retire_partial_bad_debt", + "operation": "retire_bad_debt", + "input": { + "supply": 500000000, + "bad_debt": 100000000, + "amount": 40000000, + "stable_input": 40000000 + }, + "expected": { + "accepted": true, + "supply": 460000000, + "bad_debt": 60000000, + "burned": 40000000, + "zec_payout": 0, + "live_principal": 400000000 + } + }, + { + "id": "retire_all_bad_debt", + "operation": "retire_bad_debt", + "input": { + "supply": 500000000, + "bad_debt": 100000000, + "amount": 100000000, + "stable_input": 100000000 + }, + "expected": { + "accepted": true, + "supply": 400000000, + "bad_debt": 0, + "burned": 100000000, + "zec_payout": 0, + "live_principal": 400000000 + } + }, + { + "id": "retire_no_active_principal", + "operation": "retire_bad_debt", + "input": { + "supply": 100000000, + "bad_debt": 100000000, + "amount": 100000000, + "stable_input": 100000000 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "burned": 100000000, + "zec_payout": 0, + "live_principal": 0 + } + }, + { + "id": "retire_zero_rejected", + "operation": "retire_bad_debt", + "input": { + "supply": 500000000, + "bad_debt": 100000000, + "amount": 0, + "stable_input": 40000000 + }, + "expected": { + "accepted": false, + "reason": "retirement_amount" + } + }, + { + "id": "retire_above_bad_debt_rejected", + "operation": "retire_bad_debt", + "input": { + "supply": 500000000, + "bad_debt": 100000000, + "amount": 100000001, + "stable_input": 100000001 + }, + "expected": { + "accepted": false, + "reason": "retirement_amount" + } + }, + { + "id": "retire_actual_tokens_required", + "operation": "retire_bad_debt", + "input": { + "supply": 500000000, + "bad_debt": 100000000, + "amount": 40000000, + "stable_input": 39999999 + }, + "expected": { + "accepted": false, + "reason": "stable_input" + } + }, + { + "id": "retire_no_bad_debt_rejected", + "operation": "retire_bad_debt", + "input": { + "supply": 500000000, + "bad_debt": 0, + "amount": 40000000, + "stable_input": 40000000 + }, + "expected": { + "accepted": false, + "reason": "retirement_amount" + } + }, + { + "id": "retire_bad_debt_cannot_exceed_supply", + "operation": "retire_bad_debt", + "input": { + "supply": 99000000, + "bad_debt": 100000000, + "amount": 40000000, + "stable_input": 40000000 + }, + "expected": { + "accepted": false, + "reason": "prestate" + } + }, + { + "id": "retire_one_micro_unit", + "operation": "retire_bad_debt", + "input": { + "supply": 1, + "bad_debt": 1, + "amount": 1, + "stable_input": 1 + }, + "expected": { + "accepted": true, + "supply": 0, + "bad_debt": 0, + "burned": 1, + "zec_payout": 0, + "live_principal": 0 + } + } + ] +}
zips/draft-ijaack-zsv/oracle-vector.json +90 −0
@@ -0,0 +1,90 @@ +{ + "format": "zsv-zip-canonical-oracle-vector-v1", + "scope": "Public deterministic fixture keys and synthetic context only; no deployment key or branch-ID allocation. Canonical signatures exercise common Ed25519 behavior, not all ZIP 215 edge cases.", + "genesis_hash_hex": "1111111111111111111111111111111111111111111111111111111111111111", + "active_branch_id_u32": 2712847316, + "active_branch_start_height": 90, + "candidate_height": 101, + "prior_observed_height": 99, + "keys_sorted": [ + { + "index": 0, + "public_key_hex": "6e7a1cdd29b0b78fd13af4c5598feff4ef2a97166e3ca6f2e4fbfccd80505bf1", + "public_fixture_seed_hex": "0505050505050505050505050505050505050505050505050505050505050505" + }, + { + "index": 1, + "public_key_hex": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "public_fixture_seed_hex": "0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "index": 2, + "public_key_hex": "8a875fff1eb38451577acd5afee405456568dd7c89e090863a0557bc7af49f17", + "public_fixture_seed_hex": "0606060606060606060606060606060606060606060606060606060606060606" + }, + { + "index": 3, + "public_key_hex": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c", + "public_fixture_seed_hex": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "index": 4, + "public_key_hex": "ca93ac1705187071d67b83c7ff0efe8108e8ec4530575d7726879333dbdabe7c", + "public_fixture_seed_hex": "0404040404040404040404040404040404040404040404040404040404040404" + }, + { + "index": 5, + "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", + "public_fixture_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707" + }, + { + "index": 6, + "public_key_hex": "ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1", + "public_fixture_seed_hex": "0303030303030303030303030303030303030303030303030303030303030303" + } + ], + "roster_bytes_hex": "5a53562f4f7261636c655365742f76310007056e7a1cdd29b0b78fd13af4c5598feff4ef2a97166e3ca6f2e4fbfccd80505bf18139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3948a875fff1eb38451577acd5afee405456568dd7c89e090863a0557bc7af49f178a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5cca93ac1705187071d67b83c7ff0efe8108e8ec4530575d7726879333dbdabe7cea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22ced4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1", + "reporter_set_id_hex": "1efdc9d0b1b0065494a5697252e22484db0a94e008355e68dc696c0f796f52e4", + "observed_height": 100, + "observed_block_hash_hex": "2222222222222222222222222222222222222222222222222222222222222222", + "price_micro_usd_per_zec": 100000000, + "valid_from_height": 102, + "valid_through_height": 112, + "body_hex": "64000000222222222222222222222222222222222222222222222222222222222222222200e1f50500000000", + "signing_message_hex": "5a53562f4f7261636c652f7631001111111111111111111111111111111111111111111111111111111111111111d4c3b2a11efdc9d0b1b0065494a5697252e22484db0a94e008355e68dc696c0f796f52e464000000222222222222222222222222222222222222222222222222222222222222222200e1f50500000000", + "oracle_id_hex": "77a6593c112512e54bc893c3679d643625d325748d9f59c98dc8c3e5df7ef24e", + "signer_mask_u8": 31, + "effect_hex": "64000000222222222222222222222222222222222222222222222222222222222222222200e1f505000000001f", + "authorization_hex": "203d63deedd196916c72bf4e495268fbcd8c92eb6e5270efd94482141eda07ccea721d4f83857a3b7850626644327a627cc6cf83c7f01d8fd5482550cac6ed005c69a353543835d000ef685333173154bf0a77adf8409f0b1cad0dca55d442670282bb1e3b61f804258384a06bb935a9110d2c575749d052b874be48715fb00b0bfe3c89822e95c713de9a2f7ba029d0675469b27a69c292d9665a8c7052200ebbbcc2d62ab96dd6f52b10a5e04f776d604ff837165b149720de47964795490a25624e1d55c8a56af41b207932ba2d82800d3b159024f257fcb27efb36ce54185762d753100949d0116d59c9c5e469e0c40efab25c14aab74cec8873ac963607be366d6d13e6d206370a381c5924cb4f15eaf18793a0aae8e84572254bffb824bc4f7e84b0b67ba0b20a9ae728e259c7b2fec0f321706a604f08c5d8742ce00f", + "signatures_by_index": [ + { + "index": 0, + "signature_hex": "203d63deedd196916c72bf4e495268fbcd8c92eb6e5270efd94482141eda07ccea721d4f83857a3b7850626644327a627cc6cf83c7f01d8fd5482550cac6ed00" + }, + { + "index": 1, + "signature_hex": "5c69a353543835d000ef685333173154bf0a77adf8409f0b1cad0dca55d442670282bb1e3b61f804258384a06bb935a9110d2c575749d052b874be48715fb00b" + }, + { + "index": 2, + "signature_hex": "0bfe3c89822e95c713de9a2f7ba029d0675469b27a69c292d9665a8c7052200ebbbcc2d62ab96dd6f52b10a5e04f776d604ff837165b149720de47964795490a" + }, + { + "index": 3, + "signature_hex": "25624e1d55c8a56af41b207932ba2d82800d3b159024f257fcb27efb36ce54185762d753100949d0116d59c9c5e469e0c40efab25c14aab74cec8873ac963607" + }, + { + "index": 4, + "signature_hex": "be366d6d13e6d206370a381c5924cb4f15eaf18793a0aae8e84572254bffb824bc4f7e84b0b67ba0b20a9ae728e259c7b2fec0f321706a604f08c5d8742ce00f" + }, + { + "index": 5, + "signature_hex": "528057e54f180d6f814db0b5f75f0720cf2c31a6b5eb3309f9247b2b69a81693c19fc946470c846fbcead8c55f30d73c54769098295702b4b818f0b60b733e0d" + }, + { + "index": 6, + "signature_hex": "12b445bf670ad1d33e38a735d402500d85b59ad51dfb222df5aef2a0c942c08410e1954d2edf88945e15a54c33eb98711006101656fc85da62f5fa9411475d0d" + } + ], + "expected_accept": true +}
zips/draft-ijaack-zsv/wire-vectors.json +298 −0
@@ -0,0 +1,298 @@ +{ + "cases": [ + { + "context_hash_hex": "91741f9bc6c08eb6392557e4f31dfca69badba1741e57786f00cd78a245f2547", + "effects_hex": "feffffff0056535a0403020100000000640000000122222222222222222222222222222222222222222222222222222222222222220300000001f0b9f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980000010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800ca9a3b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "effects_length": 184, + "name": "open_debt_free", + "operation_tag": 1, + "resolved_context_hex": "0122222222222222222222222222222222222222222222222222222222222222220300000000ab9041000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980000", + "signature_digests": [ + { + "digest_hex": "08e47466fa5699e0dfde6db8483b4ddb2dc3d77354c039039d0e9effb3696b0f", + "index": 0, + "role": 0, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "digest_hex": "5dc1a8ca9d5983eac7d5cc6c92248a9ec5a1bf0a505fadf150d7017468f99a3f", + "index": 0, + "role": 2, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ], + "txid_hex": "882cc2387534994f748c2723d6446cbeac5ababb8e55da424ba6224f9cba50d8", + "value_check": { + "burned": 0, + "collateral_after": 1000000000, + "collateral_before": 0, + "minted": 0, + "stable_input": 0, + "stable_output": 0, + "zec_fee": 10000, + "zec_input": 1100000000, + "zec_output": 99990000 + } + }, + { + "context_hash_hex": "8fab1a794d5411f454b7ad2ad099c7d157a61c91736037659d553f47b9879919", + "effects_hex": "feffffff0056535a0403020100000000640000000133333333333333333333333333333333333333333333333333333333333333330100000001f0b9f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980177777777777777777777777777777777777777777777777777777777777777770200000002405973070000000002c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5c02a6410000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980000", + "effects_length": 221, + "name": "stable_transfer", + "operation_tag": 0, + "resolved_context_hex": "0133333333333333333333333333333333333333333333333333333333333333330100000000e1f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798017777777777777777777777777777777777777777777777777777777777777777020000000084d717000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800", + "signature_digests": [ + { + "digest_hex": "c73b0c7c9fbcf1d273884be696f4d408a364d867672b4f0a6c78b375e916a8cb", + "index": 0, + "role": 0, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "digest_hex": "29b7f8094ec5d024954d328401c3242cc724b5e86bf65c76f84f13f6a3431357", + "index": 0, + "role": 1, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ], + "txid_hex": "3c888b3658376f15050f95a77a0eac64cb10de1018df0e72ecd5c868497053a7", + "value_check": { + "burned": 0, + "collateral_after": 0, + "collateral_before": 0, + "minted": 0, + "stable_input": 400000000, + "stable_output": 400000000, + "zec_fee": 10000, + "zec_input": 100000000, + "zec_output": 99990000 + } + }, + { + "context_hash_hex": "68dd5fe8ae9384097568ab8674a76f29bfd99573d90f764729561c717774901d", + "effects_hex": "feffffff0056535a0403020100000000640000000133333333333333333333333333333333333333333333333333333333333333330100000003f0b9f5050000000002c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5006211320000000002c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee500688909000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798017777777777777777777777777777777777777777777777777777777777777777020000000004555555555555555555555555555555555555555555555555555555555555555566666666666666666666666666666666666666666666666666666666666666660084d71700000000006211320000000002c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5020000000100000000", + "effects_length": 342, + "mandatory_payouts": [ + { + "index": 1, + "recipient_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "value": 840000000 + }, + { + "index": 2, + "recipient_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "value": 160000000 + } + ], + "name": "solvent_liquidation_at_50", + "operation_tag": 4, + "resolved_context_hex": "0133333333333333333333333333333333333333333333333333333333333333330100000000e1f5050000000002c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5017777777777777777777777777777777777777777777777777777777777777777020000000084d7170000000002c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee50155555555555555555555555555555555555555555555555555555555555555550279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800ca9a3b000000000084d71700000000", + "signature_digests": [ + { + "digest_hex": "542b940e5a16bec9eab9984a03c7863f304b77c148ca8ac81b63dc5be622de3a", + "index": 0, + "role": 0, + "verification_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }, + { + "digest_hex": "cfaf14158a3568c54d8637272cb685e03558f418623e8df9000a97b79b1f02f3", + "index": 0, + "role": 1, + "verification_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }, + { + "digest_hex": "91850b01bb223a9828703e3a2efb85cb533e5372e79a60a2cec7eeda7730296a", + "index": 0, + "role": 2, + "verification_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + ], + "txid_hex": "8262970985d048b708307cc801be46c659e72ef517103d12819d212207bebf1f", + "value_check": { + "burned": 400000000, + "collateral_after": 0, + "collateral_before": 1000000000, + "minted": 0, + "stable_input": 400000000, + "stable_output": 0, + "zec_fee": 10000, + "zec_input": 100000000, + "zec_output": 1099990000 + } + }, + { + "context_hash_hex": "8fab1a794d5411f454b7ad2ad099c7d157a61c91736037659d553f47b9879919", + "effects_hex": "feffffff0056535a0403020100000000640000000133333333333333333333333333333333333333333333333333333333333333330100000001f0b9f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798017777777777777777777777777777777777777777777777777777777777777777020000000100a3e111000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980500e1f5050000000000", + "effects_length": 188, + "name": "retire_bad_debt", + "operation_tag": 5, + "resolved_context_hex": "0133333333333333333333333333333333333333333333333333333333333333330100000000e1f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798017777777777777777777777777777777777777777777777777777777777777777020000000084d717000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800", + "signature_digests": [ + { + "digest_hex": "a009f7fe0a398cf42492dd9553824603f8c8fc4e741f9d4653f28724a236a8fd", + "index": 0, + "role": 0, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "digest_hex": "9c73fc800b94cd0007ddf186e84a73a58bec23c2fdc36d75fb77efb8794fc055", + "index": 0, + "role": 1, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ], + "txid_hex": "a45e7893591eb07093fec90a68a7e83017a4ee53b731dce3d8966f568bbd75e3", + "value_check": { + "burned": 100000000, + "collateral_after": 0, + "collateral_before": 0, + "minted": 0, + "stable_input": 400000000, + "stable_output": 300000000, + "zec_fee": 10000, + "zec_input": 100000000, + "zec_output": 99990000 + } + }, + { + "context_hash_hex": "fdee3a9f7337f5e12f0a21f69a82eec86b2620a4ef7fb385abeb3e1b2689efa4", + "effects_hex": "feffffff0056535a0403020100000000640000000133333333333333333333333333333333333333333333333333333333333333330100000002f0b9f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800c2eb0b000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179801777777777777777777777777777777777777777777777777777777777777777702000000000255555555555555555555555555555555555555555555555555555555555555550008af2f0000000000a3e1110000000088888888888888888888888888888888888888888888888888888888888888880100000000", + "effects_length": 264, + "mandatory_payouts": [ + { + "index": 1, + "recipient_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "value": 200000000 + } + ], + "name": "update_repay_and_withdraw", + "operation_tag": 2, + "resolved_context_hex": "0133333333333333333333333333333333333333333333333333333333333333330100000000e1f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980177777777777777777777777777777777777777777777777777777777777777770200000000e1f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980155555555555555555555555555555555555555555555555555555555555555550279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800ca9a3b000000000084d71700000000", + "signature_digests": [ + { + "digest_hex": "f14a98eec8db1b1c1013960877858244baba875a65dd2214b0be299715146f76", + "index": 0, + "role": 0, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "digest_hex": "bd29a395faf56879ca37142562d34f96bf65ed06d502ecb06f4f99b9134f4344", + "index": 0, + "role": 1, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "digest_hex": "7a1be0bfb48ae575bb348a30e06b86f8614103efe11ad467cafa0396288bbab8", + "index": 0, + "role": 2, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ], + "txid_hex": "31eeba82dfa395d35eb81e09e47135c1b2a9f7ec56ebdda1599614853112eae4", + "value_check": { + "burned": 100000000, + "collateral_after": 800000000, + "collateral_before": 1000000000, + "minted": 0, + "stable_input": 100000000, + "stable_output": 0, + "zec_fee": 10000, + "zec_input": 100000000, + "zec_output": 299990000 + } + }, + { + "context_hash_hex": "6a780de6403e2c5d35836e61011d0dbef46fde1d4c0bb3ce9cefe41b0b410a7d", + "effects_hex": "feffffff0056535a0403020100000000640000000133333333333333333333333333333333333333333333333333333333333333330100000002f0b9f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800ca9a3b000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179801777777777777777777777777777777777777777777777777777777777777777702000000000355555555555555555555555555555555555555555555555555555555555555550100000000", + "effects_length": 216, + "mandatory_payouts": [ + { + "index": 1, + "recipient_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "value": 1000000000 + } + ], + "name": "close_with_full_repayment", + "operation_tag": 3, + "resolved_context_hex": "0133333333333333333333333333333333333333333333333333333333333333330100000000e1f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798017777777777777777777777777777777777777777777777777777777777777777020000000084d717000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980155555555555555555555555555555555555555555555555555555555555555550279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800ca9a3b000000000084d71700000000", + "signature_digests": [ + { + "digest_hex": "bda6fbe96c252fff2977ef1a74320f71c564470aab89c8f9f57db1fa52f88f8e", + "index": 0, + "role": 0, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "digest_hex": "8ffd8ba45127bb00f9dfd0adca9635a3d4944942042b5653f14f5bf41a69dc29", + "index": 0, + "role": 1, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "digest_hex": "b60337da7b7cc330a8dd8870597ca713d09c0143610fe730900d74249e34a84f", + "index": 0, + "role": 2, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ], + "txid_hex": "57d98f2ffd080959c368fc94f3c08ed64b137a78f67ede25bf5c710ee83669f8", + "value_check": { + "burned": 400000000, + "collateral_after": 0, + "collateral_before": 1000000000, + "minted": 0, + "stable_input": 400000000, + "stable_output": 0, + "zec_fee": 10000, + "zec_input": 100000000, + "zec_output": 1099990000 + } + }, + { + "context_hash_hex": "b97c6231ca2286f72b5d22d30c79d5078c944d6e56e9706e32bad30a7b937972", + "effects_hex": "feffffff0056535a0403020100000000640000000133333333333333333333333333333333333333333333333333333333333333330100000001f0b9f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798000000015a000000999999999999999999999999999999999999999999999999999999999999999900e1f505000000001f", + "effects_length": 148, + "name": "oracle_certificate_effects", + "operation_tag": 0, + "oracle_effect_check": { + "effect_hex": "5a000000999999999999999999999999999999999999999999999999999999999999999900e1f505000000001f", + "effect_length": 45, + "expected_ed25519_authorization_bytes": 320, + "expected_ed25519_signature_slots": 5, + "expected_secp256k1_signature_slots": 1, + "expected_total_signature_slots": 6, + "scope": "Unsigned certificate framing only. No reporter signatures are supplied or verified; this fictional context is separate from oracle-vector.json." + }, + "resolved_context_hex": "0133333333333333333333333333333333333333333333333333333333333333330100000000e1f505000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980000", + "signature_digests": [ + { + "digest_hex": "88d9cabb3321e4909d204af66a39ce48c979b171449a58f9599f4747b4c3121e", + "index": 0, + "role": 0, + "verification_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ], + "txid_hex": "195e70de28f766a55058f596f40e86f4ac5c85c07d4bcce9978ee45466cbbe7a", + "value_check": { + "burned": 0, + "collateral_after": 0, + "collateral_before": 0, + "minted": 0, + "stable_input": 0, + "stable_output": 0, + "zec_fee": 10000, + "zec_input": 100000000, + "zec_output": 99990000 + } + } + ], + "hash_hex_order": "Raw digest bytes; not reversed display hashes.", + "scope": "Unsigned effect/context/hash vectors only; no signatures, node execution or assigned network identifiers. Previous outputs, prior vaults and operation oracle IDs are fictional context inputs, not certified chain state.", + "test_allocations": { + "branch_id": 16909060, + "expiry_height": 100, + "genesis_hex": "4444444444444444444444444444444444444444444444444444444444444444", + "version": 2147483646, + "version_group_id": 1515410944 + } +}