Moolam prove-it run: one image gets a passport, six mangled copies still find it, the C2PA manifest is stripped and it still finds it, a forgery is answered, and every attack in the threat model is executed. All of it against Monad mainnet. started 2026-09-09T09:17:19.321Z network Monad mainnet, chain 143, block 103287213 registry 0xa19188801E5DC93CD925884d73e4DaFc2bcb80C0 policy 0x54e8Ed8c2c3Cf2A36F8B3AC4c7f02acFD2455821 receiver 0x0d69055c43EAcb3B1ca687ca2263A049Bc7Eff04 ERC-8004 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432, demo agent 10248 creator 0x559F357aDa3A96d11AEa679eC0D622E4AF15F67c, 35.019 MON outsider 0xC5ead1E4E5C6d56E87e02317f3fB3731cB0b6Ef7, 11.396 MON the outsider is the wallet the attacks are sent from. It is not the creator, not a listed receiver, not the dispute resolver and not the owner of anything here. 1. Register: a new image gets a passport ======================================== passkey already bound to 0x559F357aDa3A96d11AEa679eC0D622E4AF15F67c image 1024 by 768, 354,015 bytes once the C2PA manifest is signed in fingerprint phash 0xd5d92295680a24fe, blockhash 0x03ff01ff003f061f0f070f871f870f870fff02fd00fc00fc03ff00ff00ff003f manifest present, soft binding com.moolam.phash-blockhash.v1 files image ipfs://bafybeiazxcvlezt7bnoovpc6k7kyabi6qfimaquleor25mlokxl2ity6nu thumb ipfs://bafkreibemerbcdrb43zttvveu5yqzo4v3p6jwnksajpcbwcfrcseyipvrm meta ipfs://bafkreigbpa7qkrk3rdzpo6be72v2c5drpsftccphpyzo62psml7pmrcube, public gateway answered 200 passport 0x1d9dfdba01c1ef72921337f08fafab55375f6cca15b704e2b04280a7fa6dab6e token id 13396218754645842369151432616860628869051295541075675045123073309833364876142 registered block 103287293, 2026-09-09T09:17:44.000Z transaction 0x599b33957f4efced343ee93c88acd48e6339b1afed08078d3bedd61a078e5834 https://monadvision.com/tx/0x599b33957f4efced343ee93c88acd48e6339b1afed08078d3bedd61a078e5834 gas 376,128 used of a 376,128 limit, and Monad bills the limit PASS: the passport is on Monad and its four files are on IPFS. Not proved here: who the creator is offline. A passport proves which key signed and when, not the identity of the person holding it. service started on http://127.0.0.1:4013, holding 1 passport 2. Survive: six copies of the same picture ========================================== copy bytes matched as phash blockhash confidence resolves to the passport ------------ ------- ---------- ------- --------- ---------- ------------------------ reencode-q60 51,879 identity 1 of 64 2 of 256 0.92 yes resize-25 8,506 identity 0 of 64 2 of 256 0.97 yes rotate-90 218,297 rot90 0 of 64 0 of 256 0.99 yes mirror 224,535 mirror 0 of 64 0 of 256 0.99 yes screenshot 64,808 identity 1 of 64 2 of 256 0.92 yes social-chain 34,562 identity 2 of 64 2 of 256 0.86 yes PASS: 6 of 6 copies resolve to 0x1d9dfdba01c1ef72921337f08fafab55375f6cca15b704e2b04280a7fa6dab6e. Not proved here: crops. Cutting 10 percent off each edge breaks both fingerprints. That is measured in proofs/robustness.txt and stated in the threat model. 3. Strip: the C2PA manifest removed entirely ============================================ signed copy C2PA manifest present: yes stripped copy C2PA manifest present: no, and the service read it as absent too stripped copy 224,393 bytes verify answer phash 0 of 64, blockhash 0 of 256, confidence 0.99 PASS: the manifest is gone and the passport still resolves. Not proved here: that the manifest was trustworthy to begin with. It is signed with a throwaway certificate, so every reader marks it untrusted. The chain record carries the signatures that matter. 4. Forge: someone else tries to claim the picture ================================================= 4a. the same bytes, a different creator attack register the same bytes again under a different creator call register on 0xa19188801E5DC93CD925884d73e4DaFc2bcb80C0 from 0x559F357aDa3A96d11AEa679eC0D622E4AF15F67c expected PassportAlreadyExists eth_call refused with PassportAlreadyExists, by the live contract at the same gas limit sent 0xdfcbc05a0060030d794d6401b3ad280c8a2f03f171036608fb663a558d1728d6 https://monadvision.com/tx/0xdfcbc05a0060030d794d6401b3ad280c8a2f03f171036608fb663a558d1728d6 receipt reverted in block 103287311, 400,000 gas used of the 400,000 limit set result REFUSED 4b. a re-encoded copy, registered as a fresh original by that second creator passport 0xcf7bc9c06bede5b6c3029a1d4f02add73e10b657b0875317096045eb00074a5a creator 0x85a88Ca81ff5f681D96AB86fa60ccB8452A139a6 transaction 0xe4e503215600ef169ce94c3c0a9480f57a2ba1a80a9091cf8f2faee3fa7e4d4e https://monadvision.com/tx/0xe4e503215600ef169ce94c3c0a9480f57a2ba1a80a9091cf8f2faee3fa7e4d4e registered 2026-09-09T09:17:55.000Z, 11 seconds after the original The chain accepted it, and it was always going to: different bytes are a different image as far as a hash is concerned. What answers the theft is the next step. service started on http://127.0.0.1:4013, holding 2 passports 4c. a third copy, re-encoded at quality 30, handed to the verify service passport creator registered phash ------------------------------------------------------------------ ------------------------------------------ ------------------------ ------- 0x1d9dfdba01c1ef72921337f08fafab55375f6cca15b704e2b04280a7fa6dab6e 0x559F357aDa3A96d11AEa679eC0D622E4AF15F67c 2026-09-09T09:17:44.000Z 0 of 64 0xcf7bc9c06bede5b6c3029a1d4f02add73e10b657b0875317096045eb00074a5a 0x85a88Ca81ff5f681D96AB86fa60ccB8452A139a6 2026-09-09T09:17:55.000Z 1 of 64 The impostor's own file is still an exact byte match for the impostor's record (0xcf7bc9c06bede5b6c3029a1d4f02add73e10b657b0875317096045eb00074a5a), and that record is the newer of the two. PASS: the chain refused the duplicate with PassportAlreadyExists, and the oldest passport for this picture is the first one, 0x1d9dfdba01c1ef72921337f08fafab55375f6cca15b704e2b04280a7fa6dab6e. Not proved here: who really made the picture. Moolam records who registered first and says so plainly in the threat model. For an AI image the two are the same moment, because the generator registers before publication. 5. Attacks: every claim in the threat model, executed ===================================================== PASS passkey-replay InvalidPasskeySignature: the creator's own assertion, replayed on a different image hash, on chain in 0xf99e40ccc6e51621bec3442644c2fb8c43f55c376a773b46672f3d4e5be3b54c PASS generator-signature-forged InvalidGeneratorSignature: a stranger's wallet signing for someone else's agent, on chain in 0xc498ec253d8bf7ac7459519bac8c4143caeb81dfcf4557f1f9c8f50f5c7a6e9c PASS unknown-agent UnknownAgent: agent 10000000000 does not exist on the ERC-8004 registry, on chain in 0xca37cf297ebf5494f3ff82aef7efc6307a06a499273860b17fc29f1760ebd9fb PASS expired-deadline SignatureExpired: both signatures are valid, and both are a minute too late, on chain in 0x5a54b73b30ca7933e2d2575c040938f36cefdd8ed2790c9c57c55a650c53bb85 PASS edit-by-non-owner NotParentOwner: an outsider tries to hang an edit off someone else's passport, on chain in 0x03122e4486d41a8d40d130a428e30a0044d7c5497dee6389daa8bea2005de6a0 PASS attest-from-stranger NotReceiver: a stranger claims the image checked out, on chain in 0x35b3b83d1b5f112859b9e1cdcf01b9e38da631825baafb784516a9f9fd99aff3 PASS report-from-fake-forwarder InvalidSender: a hand written report from the wrong sender, on chain in 0x0c7a4f5ce4064f73c929fc2da938b1b9fbe9c73a4c19d9c60753f457bb57cee4 PASS receiver-replay-and-wrong-chain covered by three Foundry tests, not by a mainnet transaction: only Chainlink's forwarder can reach the check PASS privy-policy-refusal policy_violation twice: the agent's wallet cannot move MON and cannot call appendEdit PASS revoked-session-signer the app's key is no longer a signer on the creator's wallet, and Privy refuses it PASS dispute-money-rules three refusals: InvalidBond, NotResolver, NothingToWithdraw PASS verifier-api-limits 413 on 25 MB, 400 on a text file, 403 on a loopback URL, 429 once the minute's 60 requests are spent PASS: 12 of 12 attacks refused, each for the reason the design gives. Not proved here: an attack nobody thought of. This is the list in docs/security/threat-model.md, self-audited, with no third party audit behind it. The run started the verify service itself on port 4013 with PASSPORT_SOURCE=json, and stopped it again just now. In production the same service reads passports from Envio. 6. Summary ========== section result evidence ----------- ------ --------------------------------------------------------------------------------------------- 1. Register PASS https://monadvision.com/tx/0x599b33957f4efced343ee93c88acd48e6339b1afed08078d3bedd61a078e5834 2. Survive PASS 6 of 6 copies matched 3. Strip PASS manifest gone, confidence 0.99 4. Forge PASS PassportAlreadyExists, oldest passport is the first one 5. Attacks PASS 12 of 12 refused attack result saved output ------------------------------- ------- -------------------------------------------------------------- passkey-replay REFUSED docs/security/attacks/passkey-replay.txt generator-signature-forged REFUSED docs/security/attacks/generator-signature-forged.txt unknown-agent REFUSED docs/security/attacks/unknown-agent.txt expired-deadline REFUSED docs/security/attacks/expired-deadline.txt edit-by-non-owner REFUSED docs/security/attacks/edit-by-non-owner.txt attest-from-stranger REFUSED docs/security/attacks/attest-from-stranger.txt report-from-fake-forwarder REFUSED docs/security/attacks/report-from-fake-forwarder.txt receiver-replay-and-wrong-chain REFUSED docs/security/attacks/receiver-replay-and-wrong-chain.txt privy-policy-refusal REFUSED docs/security/attacks/privy-policy-refusal.txt revoked-session-signer REFUSED docs/security/attacks/revoked-session-signer.txt dispute-money-rules REFUSED docs/security/attacks/dispute-money-rules.txt verifier-api-limits REFUSED docs/security/attacks/verifier-api-limits.txt PASS: the Moolam prove-it run, end to end, against Monad mainnet. finished 2026-09-09T09:19:22.530Z, 123 seconds