Mt_index, writeCoin's privacy trade-off, and MIP-0012 — a build log from Incoterms Escrow

A while back I posted here about some SDK gaps I hit building UBLP ( GitHub - ekacin/UBLP: Open, multi-chain protocol for cryptographically secured logistics. ZK customs clearance (zero-knowledge proofs, BLS threshold signing, L2 settlement) and Incoterms-based trade escrow on Midnight Network (ZK-shielded fund custody, encrypted agent-to-agent delivery). · GitHub )'s Incoterms Escrow module — an open-source escrow contract that holds a buyer’s payment as a shielded coin until an independent party attests delivery. Wanted to follow up on how the flagship one played out, since it ended up touching a few different places.

The problem: (originally servicedesk#187 ( [Bug]: queryZSwapAndContractState's firstFree is always 0 for contract-owned shielded coins, making the real mtIndex unobtainable through any documented API · Issue #187 · midnightntwrk/servicedesk · GitHub )): to later spend the held coin, I needed its allocated Merkle index (mt_index). But no API returns just the index — receiveShielded doesn’t give it back, and the one path that does expose it, writeCoin-ing the coin into a ledger-tracked cell, unavoidably writes the coin’s full QualifiedShieldedCoinInfo — including value — into public ledger state.

What I did instead: never call writeCoin for the held coin at all. I recover mt_index by scraping it out of the transaction’s own toString(true) serialization at the point receiveShielded allocates it, then cache it locally on the witness side. Undocumented, a little ugly, but it keeps the escrowed amount exactly as private as the contract is supposed to make it.

The back-and-forth that got me there: nstanford5 confirmed the original diagnosis and caught a sharper edge case (a contract-owned coin at index 0 silently passing as valid). Later Kanasjnr actually ran the writeCoin idea through the compiler and confirmed it leaks the value — his words: “your current design isn’t the hack to be replaced… switching to writeCoin would trade a scraping workaround for a real plaintext leak of the exact thing the module exists to protect.” So the scraping stays — not technical debt, the only thing currently holding.

Filed as a feature request: servicedesk#213 ( Feature request: a way to capture an allocated shielded-coin Merkle index without persisting the full coin (and its value) on-chain · Issue #213 · midnightntwrk/servicedesk · GitHub ) — either writeCoin targeting a bare Uint cell, or receiveShielded returning the qualified coin directly, so this doesn’t need a workaround going forward.

Turned out to connect to something bigger: Kanasjnr pointed me at MIP-0012, Contract Custody of Midnight Native Assets a formal proposal for stateless shielded custody where a held coin’s description never touches public ledger state at all. Exactly the gap this whole thread is about, solved properly at the protocol level. Posted the escrow’s case as real-world input on the discussion ( MIP-0012: Contract Custody of Midnight Native Assets · midnightntwrk/midnight-improvement-proposals · Discussion #240 · GitHub ).

And separately: the escrow module itself got reviewed and merged into the Awesome Midnight dApps list yesterday — a maintainer compiled the contract and verified the privacy design before approving, a nice outside confirmation the approach holds up.

Thanks to nstanford5, Kanasjnr, and oduameh for the genuinely substantive engagement on all of this — exactly the kind of back-and-forth that makes building on a still-young SDK worth it.