@zga/shielded-sdk
v0.1.1
Published
The client library for the shielded pool. It holds what a wallet, a relayer or a test needs and nothing that touches a chain.
Readme
@zga/shielded-sdk
The client library for the shielded pool. It holds what a wallet, a relayer or a test needs and nothing that touches a chain.
What is in it
| Module | Contents |
|---|---|
| field | The BN254 scalar field, 32-byte big-endian encoding, 64-bit little-endian encoding, field reduction of a hash |
| hasher | Poseidon over BN254, the hash the circuit and the pool use |
| tree | The pool's incremental Merkle tree (depth 20), paths, and root recomputation from a path |
| notes | A note (asset, value, owner key, blinding), its commitment and its nullifier |
| keys | Spending key, owner key and encryption key pair derived from one seed; the shielded address format |
| encryption | Encryption of a note's secrets to its owner, 88 bytes against the pool's limit of 96 |
| extdata | The external data hashes a proof binds: shield, transfer and unshield |
| circuit | A transaction's circuit input, padding, and the choice of notes to spend |
| prover | The byte layouts of a proof and a key, proving, and checking a proof in JavaScript |
| builders | A shield, a private transfer and an unshield, each returning the program's arguments |
| events, scanner, chain | Decoding the pool's events, a wallet's view of the pool, and a reader that gets the events from a chain |
Artifacts
The circuit's WebAssembly (transact_main.wasm), the proving key (transact_main_final.zkey) and the verification key (vk.bin, vkey.json) come
from the ceremony's publication, which lists each file's SHA-256, its size and the addresses it is served from (ArtifactManifest). loadArtifact
tries each address in order and returns only bytes that match the manifest, so no host is trusted; loadVerifyingKey also checks that vk.bin hashes
to the key hash the pool records.
Relaying an unshield
A wallet that does not pay for an exit itself sends a relayer a RelayerUnshieldRequest. GET /v1/info gives the relayer's address, its lookup tables and
its fee for each asset (a minimum, and a share of the amount in basis points); POST /v1/unshield takes the request and answers with the
transaction's signature, or with a refusal and a code. checkUnshieldRequest is what a relayer runs before it spends anything: the shape of every
field, that it takes the asset, that the nullifiers differ, and that the fee is enough and not more than the amount. The relayer sees the recipient and
the nullifiers, and nothing that links the exit to a note.
Sending the transactions
A shield, an unshield and a transfer carry a proof, two encrypted notes and about a dozen accounts, which is close to the 1,232-byte limit of a legacy
transaction. Measured with the encrypted notes a wallet sends: a shield is 1,191 bytes, an unshield 1,203, and an unshield that names a target pool
(1,235), that a NOP relays by passing its NOP and fee accounts (1,267), or both (1,299, and 1,331 when the NOP also holds a bond) is over. They are
sent as versioned transactions with the lookup table the deployment records, which brings the largest of them to 1,119 bytes. Every transaction type
is measured both ways by scripts/test-shielded-real.ts in the contracts repository.
Finding a wallet's notes
const scanner = new WalletScanner(hasher, keys);
await scanner.sync(new ChainEventSource(connection, async () => poolAddresses())); // every pool's address
scanner.tree(poolIndex); // the tree of one pool: a spend is proved against the tree of the pool its note is in
scanner.balance(assetId); // what the wallet can spend
scanner.unspent(assetId); // the notes, with their places in the tree
save(scanner.serialize()); // and WalletScanner.restore(...) to carry on laterThe scanner takes its events from anything that implements PoolEventSource. The chain reader goes back as far as the node keeps history; a
gap shows up as a ScanGapError, and then the events have to come from a source that has all of them.
Facts a client depends on
- A shield, an unshield and a transfer consume between 220,000 and 242,000 compute units on chain. The default limit is 200,000, so a
transaction must request more.
COMPUTE_UNIT_LIMITis 400,000. - A proof is bound to what the transaction does outside the circuit. A shield binds the depositor and the amount, a transfer binds the submitter, and an unshield binds the recipient's token account, the relayer's token account, the fee and the amount. A proof submitted for other accounts or amounts is refused by the program.
- The pool's verification key cannot change once it is installed.
- A note is announced by an event carrying its commitment and its encrypted secrets. A wallet finds its notes by trying to decrypt each event with its encryption secret and checking that the commitment matches.
Building and testing
npm install
npm run build
npm testThe tests in reference.test.mjs compare the library with values made by the circuit library, which the circuit's tests and the program were
checked against. Make those values once, from a checkout of the contracts repository whose circuit library has its dependencies installed:
npm run reference -- <path to the contracts repository>