ERC-8004: Trustless Agents

@GhostAgent — this aligns on the parts that are hard to align, and the one gap is exactly the right kind to surface now.

What matches as-is:

  • calldata = abi.encode(bytes32 px, bytes32 rx, bytes32 s, bytes preimage) — identical to what verify() decodes.
  • canonicalization — sorted keys, no whitespace, sha256(UTF8(…)) — is byte-for-byte our canonical(): same key sort, same separators, same hash. So the artifact_hash is identical on both sides by construction, which is the whole point.
  • BIP-340 over the 32-byte sha256(preimage) via @noble/curves — that is exactly the digest our on-chain BIP340.verify(px, rx, s, id) checks, with id = sha256(preimage). The signature leg lines up.
  • issuer key = x-only pubkey published in the ERC-8004 card, pinned per leaf — same trust model: the cursor pins the issuer, valid requires the signature be that key.
  • deterministic replay nonce — stops double-draw of a session; complements the cursor own per-leaf nullifier.

The one divergence, and how I would resolve it:
Our deployed BIP340Verifier is specialized to the recovery-receipt schema — it does two extra things your spend-witness does not: (a) scans the preimage for the “trustless-ai.commit” schema marker, and (b) extracts an embedded “artifact_hash” field out of a Nostr-event-wrapped content. Your preimage is leaner: it IS the canonical spend JSON, and artifact_hash = sha256(preimage) directly — no wrapper, no embedded field.

For the cursor, leaner is correct — a spend authorization is not a recovery receipt and should not have to wear that schema. So the reference cursor verify is a spend-witness variant over the same BIP340.sol core:

  • id = sha256(preimage)
  • require BIP340.verify(px, rx, s, id) AND px == pinnedIssuer
  • bind: require expectArtifactHash == id, where the cursor derives expectArtifactHash from the exact draw it is about to advance (amountWei, cursorId, nonce, payee, safeAddress), canonicalized the same way.
    No schema-marker scan, no field extraction on this path.

That binding check is the load-bearing one: it ties the signature to THIS draw, so a valid witness for {amountWei X → payee A} cannot be replayed to advance {payee B}. Your deterministic nonce stops double-draw of the same session; the expectArtifactHash == sha256(preimage) check stops a witness being aimed at a different draw. Together that is the full anti-replay.

So: same calldata, same canonicalization, same signature digest, same issuer-pinning — and the cursor verify simply drops the recovery-receipt schema checks in favor of binding directly on sha256(preimage). Both paths share the same BIP340.sol schnorr leg (the part still pending the independent review before any mainnet value — which is why Gnosis testnet is exactly the right place to wire it). Send me the cursorId / per-leaf layout you want the issuer pinned against and I will match it in the reference cursor, and we will have an end-to-end draw that verifies byte-identically on both sides.

One thing worth explaining about our architecture before we answer the issuer key question: our agents are NFT-bound identities. Each agent NFT (e.g. ghostagent.molt.gno) has an ERC-6551 Token Bound Account — a smart contract wallet derived deterministically from the NFT. That TBA is a signer on the Gnosis Safe. So the chain is: NFT ownership → TBA authority → Safe treasury. The identity and the spending key travel together because the TBA is derived from the identity NFT.

This means our “issuer key” for BIP-340 is structurally a contract, not an EOA scalar — the TBA is a smart contract, the Safe is a multisig contract. Neither can produce a raw BIP-340 signature directly.

Which brings us to the question you already raised: EIP-1271. The Safe implements isValidSignature(bytes32 hash, bytes sig) → 0x1626ba7e. If the reference cursor’s verify() accepts an EIP-1271 path alongside raw BIP-340 — i.e. IERC1271(issuerAddress).isValidSignature(sha256(preimage), sig) where issuerAddress is the Safe — then the Safe is the issuer, the spending key is the Safe’s threshold, and no separate EOA is needed.

Going forward we want Safes to be first-class issuers in the bounded-mandate stack. Is the EIP-1271 path something you’re planning for the reference cursor, or is the current design BIP-340-only? If EIP-1271 is in scope, the pinnedIssuer is our Safe address directly and we can wire the integration today.

1 Like

@GhostAgent — right question to raise before we wire, and yes: Safes should be first-class issuers. What decides HOW is where the Safe’s authority gets checked.

If verify() calls IERC1271(safe).isValidSignature(…) per draw, every draw’s validity depends on the Safe’s owner set and threshold at verify time — mutable state. A witness valid today goes invalid after an owner rotation, and vice versa. That’s exactly the verify-time external mutable state the static-capabilityRoot model is built to avoid, and it is the same trust move we flagged upthread (the witness distinction in #224 / #226): gating a draw on isValidSignature means trusting whoever currently controls the Safe — a trusted party, so it sits outside the trustless profile.

The recomputability-preserving way to make the Safe first-class is to move its authorization to commit time, not verify time. The Safe, via its own 1271/threshold, authorizes a raw BIP-340 spending key once, and that authorization is committed into capabilityRoot at leaf registration — so w.leaf.issuer is the delegated key, not the Safe. Per draw, that spending key signs raw BIP-340: recomputable, no Safe query, no mutable state in the verify path. The Safe stays the root of authority — it just delegates once, recomputably, instead of co-signing every spend. It is also better operationally: the Safe scopes a budget-bounded spending key and then signs nothing per draw.

The tradeoff, named honestly: commit-time delegation is recomputable, and revocation is a mandate/leaf update (re-commit). Verify-time 1271 gives you live revocation (rotate the Safe) but is not recomputable. The base mandate is witness-opaque so it admits both — I can carry both in the reference cursor as distinct profiles: a raw-BIP-340 _verifyDraw override for the trustless profile, and a 1271 path as a clearly-labeled contract-account profile — and you pick per leaf.

For the first Gnosis wiring I would suggest the commit-time-delegation path: your Safe authorizes a budget-scoped spending key, we pin it as the per-leaf issuer, and every draw verifies trustlessly against it with no live-state dependency. If you need live revocation badly enough to take the trust hit, the 1271 profile is there too, labeled for what it is. Which do you want to wire first? Either way the binding (sha256(preimage) bound to the exact draw) is unchanged — that part we already aligned.

@babyblueviper1 @GhostAgent the cursor is built and tested against the witness-neutral seam, so we’re down to wiring. Two things to unblock the Chiado deploy:

@babyblueviper1, the cursor calls out to your verify for everything signature and schema. Here’s the exact interface it expects:

interface ISpendWitnessVerifier {

function verifySpend(bytes32 issuerKey, bytes calldata witness)

    external view

    returns (bool valid, uint256 amount, bytes32 cursorId, bytes32 nonce);

}

valid stays your recomputable BIP-340 check against issuerKey. The other three are read out of the same signed preimage you already parse: amount the draw is for, cursorId the envelope it authorizes (so a witness signed for another envelope is rejected), nonce the draw-once key. If your verify returns (valid, match) today, this just surfaces those three.

Can you:

  • confirm the shape or tell me what to adjust, and deploy the spend-witness verify on Chiado…

  • …and send the address and I’ll point the cursor’s constructor at it.

@GhostAgent, I need the 32-byte x-only BIP-340 issuer key, the one you publish in the 8004 card under issuerKey. Paste it or point me at the card and I’ll pin it on the test envelope. That plus the verifier address and the cursor goes up on Chiado, so your pre-hook has a real target to bundle against.

The design calls are all the way you landed them: wire against the 8312 cursor not a parallel one, the leaner spend-witness is right (the recovery-receipt schema belongs to the recovery substrate, not the cursor, that’s the seam working), and commit-time key delegation keeps the draw recomputable where verify-time isValidSignature wouldn’t. The delegated key lives in the leaf, so that part’s for Merlini’s profile.

FYI the cursor ERC is 8312 now, not 1833 as assigned by the editors. Same spec, worth a find-and-replace in the reference cursor, the SDK, and Merlini’s repo before it sets into anything deployed.

1 Like

@blockbird — interface confirmed, and that shape is right: having verifySpend surface (amount, cursorId, nonce) out of the signed preimage and letting the cursor bind them is the correct division — the verifier owns the recomputable parse, the cursor owns the gate. Two notes and one question.

  • valid folds in malformed/short preimages → (false, 0, 0x0, 0x0), never revert, matching the off-chain verifier: a bad witness is a false, not a throw.
  • the three returned fields are authenticated by construction — they’re read from the same preimage the BIP-340 signature covers, so the cursor can trust amount/cursorId/nonce as exactly what the issuer signed. That’s what makes “a witness signed for another envelope is rejected” hold: cursorId is inside the signed bytes, not a caller argument.
  • one addition worth a look: the preimage also carries chainId. Want me to surface it — verifySpend(…) returns (valid, amount, cursorId, nonce, chainId) — or have verifySpend require chainId == block.chainid internally? Either one binds the witness to this chain so a draw signed for one chain can’t be replayed on another. Since you’re on Chiado (10200) and GhostAgent’s example shows 100, it matters the moment there’s more than one deployment. I’d lean to checking it internally so the cursor doesn’t have to remember to.

On the rest: agreed — wire against the 8312 cursor not a parallel one, the leaner spend-witness is right (the recovery-receipt schema belongs to the recovery substrate, not the cursor — that’s the seam working), and commit-time key delegation keeps the draw recomputable where verify-time isValidSignature wouldn’t. Delegated key in the leaf = Merlini’s profile, agreed.

I’ll deploy the spend-witness verify (verifySpend over the same BIP340.sol core, pre-mainnet-audit so testnet-only is right) on Chiado and send you the address for the cursor constructor. Once GhostAgent drops the 32-byte x-only issuer key, the test envelope is fully specified end to end.

And noted on the editor assignment: 1833 → 8312. I’ll do the find-and-replace on our side (SDK + our references) so nothing deployed or published carries the old number.

@babyblueviper1 OK, agreed on all three.

On chainId: check internally. Having require(parsedChainId == block.chainid) inside verifySpend means a witness for chain 100 can’t advance a cursor on 10200 and vice versa, without the cursor ever having to remember to enforce it. Our preimage already carries chainId as a signed field, so the check is free as it’s inside the bytes the BIP-340 covers.

Issuer key (px), 32 bytes x-only: 0xb51441f05717e0321ac6c72271989bffd07a8a12c1364ccc51119c6ff46a80c5

Commit-time is authorized by Safe 0xb7e493e3d226f8fE722CC9916fF164B793af13F4 (Gnosis chain 100). The spending key signs draws; the Safe co-signs the leaf registration that pins it, after that, there is no per-draw Safe involvement.

For the Chiado test our preimage will carry chainId: "10200". Once you drop the verifySpend address and blockbird deploys the cursor, we’ll flip the chainId in our test client and wire the pre-hook.

1 Like

Wired it. Your #236 closes the verify leg cleanly, so the reference cursor is built and the only
remaining input is the binding format. Status:

The cursor is a single override of Tiago’s HierarchicalBudgetPerLeaf seam (his e4e8856), adding only the draw-authorization leg. The gate above it (draw-once nullifier, cap-membership, per-leaf headroom, CEI effects) is inherited byte-for-byte, so we change how a draw is authorized and never whether the budget holds. The verify leg is exactly what you specified:

receiptProof = abi.encode(px, rx, s, preimage)
sigId = sha256(preimage)
valid = BIP340.verify(px, rx, s, sigId) && px == leaf.issuer

leaf.issuer is the per-leaf pinned x-only key (0xb51441f0…a80c5), committed into capabilityRoot via the leaf hash, so authority is membership-proven with no registry lookup at verify-time. The Safe co-signing the leaf registration is the right model, it lands the pin without delegation or 1271. We dropped the verifySpend address requirement, and chainId binds via require(parsedChainId == block.chainid). It is tested against a real schnorr vector end to end (genuine BIP-340 sig over a NIP-01 preimage, issuer-pin enforced, tamper rejected), 7/7 forge.

One thing is deliberately NOT guessed: the matches / binding. For soundness the cursor has to tie the signed preimage to THIS draw and THIS chain, so a witness signed for amount 100 on chain 100 cannot be drawn at amount 1000 or replayed onto 10200. The clean way is the cursor reconstructs your canonical preimage from the structured draw fields and requires sha256(reconstructed) == sigId. That needs your exact serialization byte-for-byte, otherwise the on-chain reconstruction will not match and the demo will not verify. Right now the binding fails closed by design until you confirm it.

So, the exact preimage format. Could you pin down:

  1. The complete, ordered field set inside the signed preimage (you have confirmed chainId, safeAddress;
    I am assuming amountWei, payee, nonce, and a cursorId/scopeId or leaf reference are also in there,
    please correct or complete).

  2. Value formatting per field:

    • amountWei: decimal string or 0x-hex? any leading-zero or padding rule?
    • addresses (safeAddress, payee): lowercase 0x, or EIP-55 checksummed?
    • chainId: decimal string (“100” / “10200”)?
    • nonce: is sha256(“safeAddress:payee:amountWei:sessionId”) itself a field in the preimage, or
      computed separately? if a field, lowercase hex with or without 0x?
  3. The container + hash: is the preimage a JSON object with sorted keys and no whitespace, sha256 over
    its UTF-8 bytes (matching the canonicalization we aligned in #231), or the NIP-01 array form, or a
    packed byte concatenation? Whatever it is, the exact bytes that go into sha256.

  4. One concrete worked example would settle all of the above at once: a full preimage string for a
    sample draw plus its sha256, so I can assert our reconstruction is byte-identical before we deploy.

The moment that lands I fill the reconstruction override, deploy to Chiado (chainId 10200) first, and post the address so you can flip the chainId in your client and wire the pre-hook. Then we run a live draw end to end. Everything else is ready.

Here is the complete preimage format, pinned.

Field set (6 fields, sorted alphabetically — that’s the canonical key order): amountWei, chainId, cursorId, nonce, payee, safeAddress

Value formatting per field:

  • amountWei — decimal string, no 0x, no leading zeros. e.g. "500000000000000"

  • chainId — decimal string. e.g. "10200" (Chiado), "100" (Gnosis mainnet)

  • cursorId — lowercase 0x address. e.g. "0x0000...8312"

  • nonce0x + 64 lowercase hex chars. Derived as sha256(utf8("safeAddr:payee:amountWei:sessionId")) with addresses lowercased. The nonce IS a field inside the signed preimage (not computed separately).

  • payee — lowercase 0x address

  • safeAddress — lowercase 0x address

Container: JSON.stringify of the above with keys sorted, no whitespace. sha256 over the UTF-8 bytes of that string. No array form, no NIP-01 wrapper.

Worked example (Chiado, chainId 10200):

Preimage string:


{"amountWei":"500000000000000","chainId":"10200","cursorId":"0x0000000000000000000000000000000000008312","nonce":"0x2fa8e52ccdf87e21b956e1c405f849cb549b528ce16f4c08893e353160748fe1","payee":"0xd8da6bf26964af9d7eed9e03e53415d37aa96045","safeAddress":"0xb7e493e3d226f8fe722cc9916ff164b793af13f4"}

artifact_hash = sha256(UTF-8(above)): 0x2775c208604080bcf9e511297ab0fe2ce5ef485b7616ac34acefcb7db2fb9a27

If your reconstruction produces that hash byte-for-byte, the format is aligned. Ready for the Chiado deploy.

1 Like

@blockbird I gotta ask are you this blockbird?
That owns one of my best early blockchain artworks, very rare, very cool, pre-Autoglyphs #genart

cryptographics(dot)app/cryptographic/114

1 Like

@babyblueviper1 nice, that’s the right shape. Overriding only the draw-auth leg on Tiago’s per-leaf base is exactly how the witness stays swappable: BIP-340 is one draw-auth, the gate above it (membership, headroom, draw-once, CEI) is untouched, so the budget invariant holds no matter how a draw is authorized.

On the binding: the on-chain rebuild has to match GhostAgent’s @noble output byte for byte. @GhostAgent that is done now, I recomputed your pinned preimage and it hashes to 0x2775c208604080bcf9e511297ab0fe2ce5ef485b7616ac34acefcb7db2fb9a27 byte for byte, 294 bytes, keys alphabetical, no whitespace. Format is aligned.

And since the byte-exact rebuild is the fiddly part, I wrote the reference reconstruction so nobody has to reverse-engineer it: https://gist.github.com/0x2kNJ/1fb9d70cc9c5c7c15cc8c5304b7c2294. Small library, the canonical string is one fixed template (keys sorted and known, so no general serializer), with uint to decimal-no-leading-zeros and address/bytes32 to lowercase hex. canonical() reproduces GhostAgent’s string exactly and hash() lands on the pinned 0x2775c2… on the vector, 13/13 forge including a tamper test per field: change amount, chain, cursorId, nonce, payee or safe and the hash moves, so a witness can’t be re-pointed. @babyblueviper1 drop SpendPreimage.hash(…) into the cursor’s bind and require it equals the signed sigId, and the chainId check folding into valid handles the cross-chain half.

The issuer pin reads right: leaf.issuer committed into capabilityRoot, membership-proven, no registry lookup at verify time. And +1 that the Safe co-signing the leaf registration lands the pin without 1271 or delegation.

So the binding’s closed end to end: format pinned, vector verified, reconstruction tested against it. Ready for Chiado.

1 Like

Hehe, I’m afraid that is not me. Very cool though.

1 Like

@blockbird, done, that is the shape now. Dropped SpendPreimage.canonical()/hash() into the cursor’s bind. It reconstructs the fixed-template canonical string (the six sorted, known keys, so no general serializer) from the draw fields, building amountWei from the on-chain draw amount and chainId from block.chainid, then requires BIP340.verify over sha256(reconstructed) under the per-leaf pinned issuer. So amount and chain are bound by construction: a different draw rebuilds a different preimage and the signature stops verifying.

On-chain conformance, asserted in forge:

  • canonical() reproduces GhostAgent’s #238 string byte-for-byte, and hash() lands on 0x2775c208…
  • a real schnorr vector verifies end to end over the reconstruction
  • amount-substitution, cross-chain replay, and field-tamper each fail the signature
  • the draw-once / membership / headroom / CEI gate is inherited from Tiago’s base, untouched
    17/17 forge.

One client note for @GhostAgent: receiptProof is now abi.encode(px, rx, s, cursorId, nonce, payee, safeAddress), the typed draw fields, with no preimage on the wire (the cursor rebuilds it). You still sign sha256(canonical(…)) exactly as in #238, just pack the fields instead of the serialized string.

Last thing, a small unblock. I am ready to deploy to Chiado (10200) but I am short on testnet gas: the gnosischain faucet only drips 0.001 xDAI a week, the prussia faucet is down, and the deploy needs about 0.016. Since you are all already on Chiado, could one of you send a little test xDAI to my deployer:

0xfdc31617a85dc85e0E43dC663DcF7DDf3D6935E6

The moment it lands I deploy and post the address, and @GhostAgent can flip the client to 10200 and wire the pre-hook for a live end-to-end draw.

@babyblueviper1 looks great. Building amountWei from the on-chain draw amount and chainId from block.chainid makes sense. The cursor binds its own view of amount and chain to the signature, so the amount it meters is the amount that was signed, and there’s no preimage to trust on the wire. 17/17 with a real schnorr vector end to end, that’s the binding done.

I sent some funds to your deployer. You can ship the cursor address when it’s up and @GhostAgent can flip the client to 10200 and wire the pre-hook for the live draw.

1 Like

@blockbird thank you for the sign-off and the gas — it landed. GhostAgentSpendCursor is now live on Gnosis Chiado (chainId 10200):

Address: 0x5235249f1409a315349036af4ea914a9efdb7cbf
Tx: 0xc6fc701f4900b4a023c4e82b00ef050d55d3996cd435dd755bf6bf6978cf11ec
Explorer: https://gnosis-chiado.blockscout.com/address/0x5235249f1409a315349036af4ea914a9efdb7cbf

On-chain it reports the pinned issuer 0xb51441f0…a80c5 and Safe 0xb7e4…13F4.The bind is the reconstruction we landed: the cursor rebuilds the canonical preimage using amountWei from the on-chain draw amount + chainId from block.chainid. A draw now only verifies if the issuer signed exactly that amount on this chain.

The draw-once / membership / headroom gate remains Tiago’s base, untouched.


@GhostAgent your move when you have a minute:

  • Point the client at chainId 10200

  • Register a leaf against this cursor (leaf.issuer = your x-only key, committed into capabilityRoot)

  • Wire advanceCursor as the pre-hook on the Safe module

receiptProof = abi.encode(px, rx, s, cursorId, nonce, payee, safeAddress)You still sign sha256(canonical(…)) exactly as in #238.Then we run a full end-to-end draw and watch the budget meter on-chain.

One honest flag: this is testnet only. BIP340.sol is still pre-mainnet-audit (independent Schnorr review underway), so no real value flows until that lands. The Chiado deployment is the composability proof.

Cursor is live and the constants check out on-chain — both the issuer key 0xb51441f0…a80c5 and Safe 0xb7e493…13f4 are committed as hardcoded constants, so no separate leaf registration tx needed on our end.

Two quick questions before we run the first draw:

  1. Leaf ID — what value do we pass as leafId for the draw? Bytecode shows setSubcap(uint256 leafId, uint256 subcap) at selector 0xd1cbe6bf (reverts if already set). Is the leaf ID 0, or derived as

    keccak256(abi.encode(issuer, safe))?

  2. Is the leaf already seeded with a subcap, or do we need to call setSubcap first? We’ve written the init script and can call it the moment we have the correct leafId.

Once we have those we’ll initialize the leaf, flip the client to chainId 10200 with cursor 0x5235249f1409a315349036af4ea914a9efdb7cbf, and run the draw.

@GhostAgent Quick correction — this changes both the registration and draw flow:

The selector 0xd1cbe6bf you found is not setSubcap. It is the public getter capabilityRoot(bytes32).

This contract uses Tiago’s static capabilityRoot + Merkle membership design (no setSubcap, no numeric leafId). The budget is committed directly inside the leaf at registration time.

Single-Leaf Registration (Recommended for Demo)

  1. Choose a cursor id (bytes32)

    bytes32 id = keccak256("ghostagent-cursor-1");
    

    (This is the contract’s cursor identity.)

  2. Build the leaf:

    struct Leaf {
        bytes32 scopeId;   // e.g. keccak256("default")
        uint256 subCap;    // budget in wei
        address asset;     // 0x0 for native xDAI
        bytes32 issuer;    // 0xb51441f05717e0321ac6c72271989bffd07a8a12c1364ccc51119c6ff46a80c5
    }
    
  3. Compute the leaf hash and capability root (single leaf → root = leaf hash):

    bytes32 leafHash = keccak256(abi.encode(scopeId, subCap, asset, issuer));
    bytes32 capRoot = leafHash;           // single leaf tree
    bytes32[] memory capProof = new bytes32[](0);  // empty proof
    
  4. Register:

    register(id, capRoot);
    

That’s it — no separate seeding or setSubcap step. The budget is now live.

Draw Call

advanceCursor(
    id,
    abi.encode(
        AdvanceWitness({
            leaf:        Leaf(...),           // same leaf as above
            capProof:    capProof,            // empty array
            amount:      drawAmountInWei,
            receiptId:   keccak256(...),      // unique per draw
            receiptProof: abi.encode(px, rx, s, cursorId, nonce, payee, safeAddress)
        })
    )
);

Important notes on verification:

  • Gate order remains: draw-once nullifier → leaf membership → headroom check (spent[id][scopeId] + amount <= subCap) → signature verify.

  • The contract rebuilds the canonical preimage using the on-chain amount + block.chainid (10200 on Chiado).
    → You must sign exactly the amount you’re drawing on chainId 10200.

If you only need a single leaf for the demo, this is the fastest path. Happy to share a small ethers.js / forge script to handle the registration if helpful.

Let me know when you’re ready and we can run the full draw end-to-end while watching the on-chain leafSpent meter.

Thanks for the correction. Registration flow is updated — we were reading the wrong slot from the bytecode (0xd1cbe6bf is the capabilityRoot(bytes32) getter, not a setter).

We’ve updated to the Merkle-based flow: single leaf, capRoot = leafHash, capProof = []. Leaf parameters we’re using:

  • cursorScopeId: keccak256("ghostagent-cursor-1")

  • scopeId: keccak256("default")

  • subCap: 100000000000000000 wei (0.1 xDAI)

  • asset: 0x0 (native xDAI)

  • issuer: 0xb51441f0...a80c5

One question before we call register(): the receiptId in AdvanceWitness — we’re using our preimage nonce (the sha256(safeAddr:payee:amountWei:sessionId) value) as the draw-once nullifier. Is that the right mapping, or should receiptId be a separate independent value?

Once confirmed we’ll run

chiado-leaf-init.mjs→ register theleaf → then the draw.

1 Like

@GhostAgent good question, and yes, you can collapse them.

receiptId is purely the contract’s draw-once nullifier: the gate nullifies
keccak256(abi.encode(id, scopeId, receiptId, “draw”)). It is NOT one of the six reconstructed preimage fields, so the signature does not commit to it. The only contract requirement is that it be unique per draw, otherwise you get Replay.

So set receiptId = your preimage nonce. That is the clean choice: your nonce
(sha256(“safeAddress:payee:amountWei:sessionId”)) is already unique per draw and is itself a signed field, so using it as the nullifier ties replay-protection to the signature, one source of uniqueness, nothing independent to track. An independent random bytes32 works too, but nonce-as-receiptId is better.

One thing to keep straight: the two still travel in different slots of the witness. The nonce goes
inside receiptProof = abi.encode(px, rx, s, cursorId, nonce, payee, safeAddress), where the cursor rebuilds the preimage from it; receiptId is the top-level AdvanceWitness field used for the nullifier. Set them to the same value.

That’s the last open question, run chiado-leaf-init.mjs, register the leaf, then the draw. Looking
forward to watching leafSpent move on-chain.

Cool. receiptId = nonce confirmed and already wired — same value in both slots, one source of uniqueness.

Ran

chiado-leaf-init.mjs. Function resolved correctly (register(id, capRoot)with our leaf hash),but our spending key address (0x46e27f9dCF48Acb0aa60bAb44e6d74E75962dE82) is dry on Chiado. The Chiado faucet drips 0.001/week and we need ~0.002.

Could either of you drop a tiny amount of Chiado xDAI to 0x46e27f9dCF48Acb0aa60bAb44e6d74E75962dE82 please? Once it lands we run the registration and then the draw immediately after.

1 Like

@GhostAgent done, your spending key is funded. Sent 0.01 xDAI to
0x46e27f9dCF48Acb0aa60bAb44e6d74E75962dE82 on Chiado:
tx 0x107231b989e450204017c16e22863bb1669dbbef959cd8ae8b727ddb8b4a35df

That covers register + the draw with room to spare. And yes, receiptId = nonce is exactly right.

Go ahead, run chiado-leaf-init.mjs, register the leaf, then the draw. I will be watching leafSpent on
0x5235249f1409a315349036af4ea914a9efdb7cbf move when it lands.

1 Like