Read Zcash from a contract
A Sova contract can read Zcash. Every node answers from its own Zcash
node, and every node gives the same answer. The reads live at the SIP-4
precompile, 0x0000000000000000000000000000000000005a00, live on the
public testnet.
What a contract can read
Section titled “What a contract can read”| Method | Answer |
|---|---|
anchor() |
The Zcash block this Sova block commits to: height and hash. |
blockAt(h) |
A Zcash block’s hash and header time. |
txInfo(txid) |
Where a transaction was mined and how deep. Fully shielded transactions too. |
txOutput(txid, vout) |
What a transparent output pays, and to which script. |
burnInfo(txid) |
The SIP-1 burn a transaction carries: who it credits and how much ZEC it destroyed. |
spentBy(txid, vout), for “has this output been spent”, follows in v1.1.
Checking a payment
Section titled “Checking a payment”Most contracts use ZcashLib, which turns a check into one call:
// paid at least `price` zatoshis to the seller, 3 or more blocks deep?ZcashLib.Payment memory p = ZcashLib.requireOutputPays( txid, vout, ZcashLib.p2pkh(sellerHash), price, 3);txids and hashes are display order, as explorers print them. Values are integer zatoshis.
The rules
Section titled “The rules”- Deterministic. Every answer is a pure function of the Zcash chain the block commits to. Two nodes that accept the same block give the same answers, whatever their own Zcash tip.
- Not found is a result. A status, identical on every honest node. A node that can’t answer yet holds the block and retries.
- Confirmations are counted from the committed Zcash block.
ZcashLibasks for a minimum depth on every check: 3 on testnet, 10 on mainnet (about 12.5 minutes), more for large amounts. - Reorgs. A Zcash reorg deeper than a payment reorgs Sova with it, identically on every node. Your minimum depth protects whatever happens off Sova.
- txids. Pre-v5 txids can change before they are mined. Key on what an output pays, or reserve the order before payment.
Shielded stays shielded
Section titled “Shielded stays shielded”The precompile reads transparent Zcash state. Shielded amounts, recipients, senders and memos stay private, from contracts as from everyone. A shielded wallet pays a transparent address with a z→t send; that one output is public, and it is what a contract verifies.
Pool state and events (SIP-7)
Section titled “Pool state and events (SIP-7)”The same precompile also answers pool-level questions: the value in each Zcash pool after a block, the change a block made, a block’s public activity counts, and a transaction’s shielded flow. These are aggregates and public counts.
Each Sova block also records a summary of its Zcash block in the
ZcashBlocks system contract at 0x0000000000000000000000000000000000005A01.
It keeps the last 8,191 summaries. Anyone can read them, and anyone can
call publish(fromH, toH) to turn recorded heights into ordinary
ZcashBlock logs. Nodes also stream the summaries over RPC:
sova_getZcashBlocks.
Patterns
Section titled “Patterns”- Sell for ZEC. The buyer reserves an order, pays the seller’s own
Zcash address from any wallet, and anyone calls claim. The contract
verifies the payment and delivers.
ZecCheckout.sol, live in the Ashwings mint. - Swap ZEC for SOVA. A maker locks SOVA and names a price. A taker
reserves with a small bond, pays the ZEC straight to the maker on Zcash,
and claims the SOVA.
ZecEscrow.sol. - Proof-of-burn. Apps recognize their own burns, marked with their own payload: names, Sybil-resistant badges, spam fees that burn. SIP-4 §9.
Source: contracts/src/zcash/. The full
text: SIP-4 and SIP-7.
Try it locally
Section titled “Try it locally”The contracts, a stand-in for the precompile, the checkout page and its
relayer, all on your machine. Needs Foundry, Node 22 and a Chromium
(CHROME=/path).
git clone https://github.com/sova-chain/sova && cd sova(cd contracts && forge build && forge test)(cd site && npm ci && npm run build)cd tools/checkout-relayer && npm cinpm run e2ee2e starts anvil, puts a mock at 0x…5a00, deploys the checkout, and
drives a headless browser through a ZEC purchase: reserve, pay, confirm,
claim, owl.
The interfaces
Section titled “The interfaces”// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
/// @dev Provisional SIP-4 precompile address (SIP-4 §3).address constant ZCASH_PRECOMPILE = 0x0000000000000000000000000000000000005a00;
/// @title SIP-4 status codes/// @notice Every IZcash lookup returns one of these as its first value./// A non-OK status is a *result*, identical on every honest node; it is/// never an RPC failure (a node that cannot answer holds the block)./// On a non-OK status every other returned field is zero / empty.library ZcashStatus { /// @dev The answer is in the anchored segment Z[B .. E_N]. uint8 internal constant OK = 0; /// @dev Not in Z[B .. E_N] on the anchored chain. For txid-keyed /// methods this includes "mined above E_N" and "mined before B": /// a node never reveals index entries past the anchor. uint8 internal constant NOT_FOUND = 1; /// @dev Height-keyed: h > E_N (the anchor has not reached it yet). uint8 internal constant NOT_YET = 2; /// @dev Height-keyed: h < B (v1 indexes only from the epoch base). uint8 internal constant OUT_OF_RANGE = 3; /// @dev txOutput: the tx is found but has no transparent output `vout` /// (vout >= nOut; a fully shielded tx has nOut = 0). uint8 internal constant NO_SUCH_OUTPUT = 4; /// @dev burnInfo: the tx is found but is not a SIP-1 burn. uint8 internal constant NOT_A_BURN = 5; /// @dev SIP-7 poolValue: the pool id is not defined at this height (a /// status, not a revert, so code written for a future pool behaves the /// same before and after that pool's fork). See IZcashPools. uint8 internal constant NO_SUCH_POOL = 6;}
/// @title IZcash: SIP-4 Zcash state precompile (v1)/// @notice Read-only view of *transparent* Zcash chain state, as a pure/// function of the Zcash chain prefix the executing Sova block commits to./// Sova block N anchors Zcash block E_N = N + B - 1, where B is the epoch/// base; answers cover Z[B .. E_N] and nothing else (no tip, no mempool,/// no "unspent now").////// Conventions (the node side must match these byte for byte):/// - ABI: ordinary Solidity ABI, 4-byte selectors, standard return tuple/// encoding, every word clean (zero-padded). Negative answers return the/// full tuple with zeroed fields (for txOutput: an empty `bytes`), never/// empty returndata. Malformed calldata reverts. `value > 0` reverts./// Callers use STATICCALL (all methods are `view`)./// - txid and block hashes are DISPLAY-ORDER bytes: the hex string an/// explorer or zebrad RPC prints, read left to right, is the bytes32 from/// its most significant byte down. So txid "ab12..ef" is/// `bytes32(0xab12...ef)`. This is the reverse of Zcash's internal/// (wire) byte order./// - Values are integer zatoshis (zebrad `valueZat`; 1 ZEC = 1e8 zat)./// - Scripts are raw scriptPubKey bytes (not hex text, not ASM)./// - Confirmations are E_N - height + 1, computed from the anchor, never/// zebrad's tip-relative `confirmations` field.////// Library caveats (see ZcashLib): pre-v5 txids are malleable before they/// are mined, and coinbase transactions are included. Recommended/// confirmation depths: testnet 3, mainnet 10 (about 12.5 min at 75 s),/// more for large values.interface IZcash { /// @notice The Zcash block this Sova block commits to. /// @return height E_N. /// @return hash Display-order hash of Zcash block E_N (equals /// `header.parent_beacon_block_root`). function anchor() external view returns (uint64 height, bytes32 hash);
/// @notice Header data at a Zcash height. /// @return status OK for B <= h <= E_N; NOT_YET for h > E_N; /// OUT_OF_RANGE for h < B. /// @return hash Display-order block hash. /// @return time Header `time` (miner-set; not monotonic). function blockAt(uint64 h) external view returns (uint8 status, bytes32 hash, uint32 time);
/// @notice Where a transaction was mined. Works for any tx, fully /// shielded ones included (txid, height and position are public). /// @return status OK or NOT_FOUND. /// @return height Zcash height of the containing block. /// @return index Position of the tx in its block (coinbase = 0). /// @return confirmations E_N - height + 1 (>= 1 when OK). /// @return nOut Number of transparent outputs (vout count). /// @return version Transaction version (4, 5, 6, ...). function txInfo(bytes32 txid) external view returns (uint8 status, uint64 height, uint32 index, uint64 confirmations, uint32 nOut, uint32 version);
/// @notice A transparent output of a mined transaction. /// @return status OK, NOT_FOUND (tx unknown) or NO_SUCH_OUTPUT /// (vout >= nOut). /// @return valueZat Output value in zatoshis. /// @return script Raw scriptPubKey bytes (<= 10,000 bytes). function txOutput(bytes32 txid, uint32 vout) external view returns (uint8 status, uint64 valueZat, bytes memory script);
/// @notice The SIP-1 burn carried by a tx, via the exact consensus /// parser (`sip1::extract_burn`). /// @return status OK, NOT_FOUND (tx unknown or outside Z[B .. E_N]), /// or NOT_A_BURN (tx found, not a SIP-1 burn). /// @return credited Sova address the burn credits. /// @return signal SIP-1 signal field. /// @return weightZat Burned weight in zatoshis. function burnInfo(bytes32 txid) external view returns (uint8 status, address credited, uint32 signal, uint64 weightZat);
// Reserved for v1.1 (not in v1; do not call): // function spentBy(bytes32 txid, uint32 vout) // external view returns (uint8 status, bytes32 spender, uint64 height); // Outputs created at >= B; spentness as of E_N only.}IZcashPools.sol (SIP-7 pool reads, same address)
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
/// @title Zcash value-pool ids (SIP-7 §2)/// @notice Zebra's `valuePools` order. A future Zcash pool is appended as/// id 6 at a Sova fork height, so ids never change meaning and the/// `poolTotals` arrays grow instead of changing shape.library ZcashPool { uint8 internal constant TRANSPARENT = 0; uint8 internal constant SPROUT = 1; uint8 internal constant SAPLING = 2; uint8 internal constant ORCHARD = 3; /// @dev The NU6 dev-fund lockbox. Not a shielded pool. uint8 internal constant LOCKBOX = 4; /// @dev NU6.3. Shielded coinbase lands here from NU6.3 on. uint8 internal constant IRONWOOD = 5; /// @dev Pools defined in SIP-7 v1.1 (ids 0..5). uint8 internal constant COUNT = 6;
/// @notice Sprout, Sapling, Orchard and Ironwood. Transparent and the /// lockbox are not shielded. function isShielded(uint8 pool) internal pure returns (bool) { return pool == SPROUT || pool == SAPLING || pool == ORCHARD || pool == IRONWOOD; }}
/// @title IZcashPools: SIP-7 v1.1 pool-state queries on the SIP-4 precompile/// @notice Same address as IZcash (`0x…5A00`), same Solidity-ABI dispatch,/// same read-only rules and the same coverage-then-answer rule (SIP-4 §3,/// §5). Every answer is a pure function of the Zcash chain up to the/// queried block, identical on every honest node. See SIP-7 §2.////// Sign convention: POOL DELTA, positive = value INTO the pool, everywhere/// (blocks and transactions alike). It equals Zebra's per-block/// `valuePools[i].valueDeltaZat`, and for a transaction it is/// `-valueBalance` of that pool's bundle (Zcash's `valueBalance > 0` means/// value LEAVES the pool). For Sprout it is `sum(vpub_old - vpub_new)`.////// Statuses (ZcashStatus in IZcash.sol): OK = 0; NOT_FOUND = 1 (txShielded:/// tx not in Z[B .. E_N]); NOT_YET = 2 (h > E_N); OUT_OF_RANGE = 3 (h < B);/// NO_SUCH_POOL = 6 (poolValue: pool id not defined at height h). A non-OK/// status zeroes every other field (poolTotals: empty arrays). Statuses are/// results, never reverts; malformed calldata (including a non/// sign-extended int64 word or a uint8 word above 255) reverts.////// Gas (SIP-7 §2): poolValue, poolTotals, blockStats 2,600; txShielded/// 4,000. Negative answers cost the same as positive ones.////// Privacy (SIP-7 §4.4): these are aggregates and public counts. Amounts,/// senders, recipients and memos of shielded transfers stay private./// Caveats: action/spend/output counts are an upper bound on payments/// (Orchard and Ironwood pad to >= 2 actions, wallets add dummy Sapling/// outputs); a tx's net shielded flow includes its fee (the fee alone is not/// available).interface IZcashPools { /// @notice Total value in `pool` after Zcash block `h`, and the change /// block `h` made to it. /// @param pool A {ZcashPool} id. /// @return status OK, NOT_YET, OUT_OF_RANGE or NO_SUCH_POOL. /// @return chainValueZat The pool's total (Zebra `chainValueZat`). A /// level, not a counter: it can go up or down. /// @return deltaZat chainValue(h) - chainValue(h-1) (Zebra /// `valueDeltaZat`); + means value entered the pool. function poolValue(uint64 h, uint8 pool) external view returns (uint8 status, uint64 chainValueZat, int64 deltaZat);
/// @notice Every pool at once, indexed by {ZcashPool} id. Length 6 in /// v1.1; later pools append. /// @return status OK, NOT_YET or OUT_OF_RANGE (arrays empty unless OK). function poolTotals(uint64 h) external view returns (uint8 status, uint64[] memory chainValueZat, int64[] memory deltaZat);
/// @notice Public activity counts for Zcash block `h`. /// @return status OK, NOT_YET or OUT_OF_RANGE. /// @return txCount Transactions in the block, coinbase included. /// @return shieldedTxCount Transactions with any Sprout, Sapling, Orchard /// or Ironwood component, coinbase included. /// @return tIn Transparent inputs (vin entries) in the block. /// @return tOut Transparent outputs (vout entries) in the block. /// @return saplingSpends Sapling spends (one nullifier each). /// @return saplingOutputs Sapling outputs (one note commitment each). /// @return orchardActions Orchard actions (one nullifier and one note /// commitment each). /// @return ironwoodActions Ironwood actions (likewise). /// @return joinSplits Sprout JoinSplit descriptions. /// @return saplingNotes Cumulative Sapling note-commitment tree size /// after block `h` (every Sapling note ever created). Per-block counts /// are differences of consecutive heights. /// @return orchardNotes Cumulative Orchard tree size after `h`. /// @return ironwoodNotes Cumulative Ironwood tree size after `h` (0 /// before its first note). function blockStats(uint64 h) external view returns ( uint8 status, uint32 txCount, uint32 shieldedTxCount, uint32 tIn, uint32 tOut, uint32 saplingSpends, uint32 saplingOutputs, uint32 orchardActions, uint32 ironwoodActions, uint32 joinSplits, uint64 saplingNotes, uint64 orchardNotes, uint64 ironwoodNotes );
/// @notice The shielded side of a mined transaction. Works for any tx /// in Z[B .. E_N]; a tx with no shielded component returns OK with zero /// deltas and counts. /// @return status OK or NOT_FOUND. /// @return height Zcash height of the containing block. /// @return sproutDelta Pool delta for Sprout (+ = into the pool). /// @return saplingDelta Pool delta for Sapling (= -valueBalanceZat). /// @return orchardDelta Pool delta for Orchard (= -orchard.valueBalanceZat; /// <= 0 from NU6.3 on, since Orchard can only shrink). /// @return ironwoodDelta Pool delta for Ironwood (= -ironwood.valueBalanceZat). /// @return nIn Transparent input count ("fully shielded" is /// `nIn == 0 && nOut == 0`, with nOut from IZcash.txInfo). /// @return saplingSpends Sapling spends in this tx. /// @return saplingOutputs Sapling outputs in this tx. /// @return orchardActions Orchard actions in this tx. /// @return ironwoodActions Ironwood actions in this tx. /// @return joinSplits Sprout JoinSplits in this tx. /// @dev Net value that LEFT the shielded pools in this tx is /// -(sproutDelta + saplingDelta + orchardDelta + ironwoodDelta). It /// includes the fee, and nets to about zero for an Orchard->Ironwood /// migration. function txShielded(bytes32 txid) external view returns ( uint8 status, uint64 height, int64 sproutDelta, int64 saplingDelta, int64 orchardDelta, int64 ironwoodDelta, uint32 nIn, uint32 saplingSpends, uint32 saplingOutputs, uint32 orchardActions, uint32 ironwoodActions, uint32 joinSplits );}