Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6c3c077
chore: add tests for event sequence validation and improve error hand…
itsyaasir Aug 28, 2026
d7957a4
chore: enhance README verification checks and add detailed proof veri…
itsyaasir Aug 28, 2026
48d874a
chore: enhance proof verification to return VerifiedProof with authen…
itsyaasir Aug 31, 2026
65e908d
chore: enhance committee cache to use complete key for network and ep…
itsyaasir Aug 31, 2026
7705701
chore: enhance committee resolution to validate genesis checkpoint an…
itsyaasir Aug 31, 2026
fff7c0c
chore: stop tracking local audit
itsyaasir Aug 31, 2026
d8157e7
chore: enhance examples and documentation for trusted-node resolution
itsyaasir Aug 31, 2026
4c3ecc9
chore: update documentation and comments for object proof handling an…
itsyaasir Aug 31, 2026
272a7a4
chore: add stable error handling for Proof of Inclusion with detailed…
itsyaasir Aug 31, 2026
f733afe
chore: enhance committee serialization with toJSON method and update …
itsyaasir Aug 31, 2026
fbff3dd
chore: add tests for proof verification including target requirements…
itsyaasir Aug 31, 2026
632d16c
Update proof serialization fixtures to version 1 format
itsyaasir Aug 31, 2026
25a8c6f
chore: update checkpoint handling to return undefined for non-existen…
itsyaasir Aug 31, 2026
029d233
chore: update terminology from "claims" to "targets" in documentation…
itsyaasir Sep 1, 2026
ef5d093
chore: add JSON compatibility section to README and proof.rs for vers…
itsyaasir Sep 1, 2026
3215d50
chore: update test-wasm-notarization job dependencies and add Wasm ex…
itsyaasir Sep 1, 2026
173969a
chore: enhance genesis blob handling and validation for mainnet and t…
itsyaasir Sep 1, 2026
24a2f02
chore: enhance transaction verification to include user signatures an…
itsyaasir Sep 1, 2026
b8c24be
chore: reorganize imports for clarity in poi.rs
itsyaasir Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ jobs:
additional-artifact-paths: bindings/wasm/poi_wasm/package.json

test-wasm-notarization:
needs: [build-wasm-notarization, build-wasm-poi, check-for-run-condition]
needs: [build-wasm-notarization, check-for-run-condition]
if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }}
runs-on: ubuntu-24.04
strategy:
Expand All @@ -273,22 +273,49 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 24.x
node-version: 20.x

- name: Install JS dependencies
run: npm ci
working-directory: bindings/wasm/notarization_wasm

- name: Install Proof of Inclusion JS dependencies
run: npm ci
working-directory: bindings/wasm/poi_wasm

- name: Download bindings/wasm/notarization_wasm artifacts
uses: actions/download-artifact@v4
with:
name: notarization-wasm-bindings-build
path: bindings/wasm/notarization_wasm

- name: Start iota sandbox
uses: "./.github/actions/iota/setup"
with:
iota-version: ${{ env.IOTA_VERSION }}
with-grpc: true

- name: publish Notarization Move package
run: echo "IOTA_NOTARIZATION_PKG_ID=$(./publish_package.sh)" >> "$GITHUB_ENV"
working-directory: notarization-move/scripts/

- name: Run Wasm examples
run: npm run test:node
working-directory: bindings/wasm/notarization_wasm

test-wasm-poi:
needs: [build-wasm-poi, check-for-run-condition]
if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }}
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v3

- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 24.x

- name: Install JS dependencies
run: npm ci
working-directory: bindings/wasm/poi_wasm

- name: Download bindings/wasm/poi_wasm artifacts
uses: actions/download-artifact@v4
with:
Expand All @@ -305,10 +332,6 @@ jobs:
run: echo "IOTA_NOTARIZATION_PKG_ID=$(./publish_package.sh)" >> "$GITHUB_ENV"
working-directory: notarization-move/scripts/

- name: Run Wasm examples
run: npm run test:node
working-directory: bindings/wasm/notarization_wasm

- name: Run Proof of Inclusion Wasm examples
run: |
export IOTA_GENESIS_PATH="$HOME/.iota/iota_config/genesis.blob"
Expand Down
47 changes: 38 additions & 9 deletions bindings/wasm/poi_wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
The Proof of Inclusion Wasm Package provides the Node.js and TypeScript interface for Proof of Inclusion in the IOTA
Notarization Toolkit. It connects a generated IOTA `LedgerService` client to `poi-rs` compiled as WebAssembly.

Use the Package to construct portable proofs for IOTA transactions, events, and object states and to verify those proofs
locally. `PoiClient` hides the generated protobuf client, ConnectRPC transport, and JavaScript-to-WASM source adapter,
while Rust owns proof construction, committee resolution, and verification.
Use the Package to construct portable proofs for IOTA transactions, events, and specific object versions and to verify
those proofs locally. `PoiClient` hides the generated protobuf client, ConnectRPC transport, and
JavaScript-to-WASM source adapter, while Rust owns proof construction, committee resolution, and verification.

Proof of Inclusion operates on existing ledger activity and does not define a separate Move Package.

Expand Down Expand Up @@ -61,8 +61,8 @@ targets. Event sequence numbers and all other 64-bit values use JavaScript `bigi

The serialized proof records the targets explicitly selected by the caller. Its checkpoint summary and checkpoint
contents are sibling fields, while the required transaction proof contains the transaction, effects, and optional event
evidence. Object targets contain the selected object values, and event targets select events from the authenticated
transaction event list.
evidence. Object targets contain the selected historical object values, and event targets select events from the
authenticated transaction event list.

The JavaScript source adapter passes only opaque BCS bytes and checkpoint sequence numbers into WASM. Rust decodes those
values into existing IOTA domain types and delegates target resolution and proof construction to `poi-rs`.
Expand All @@ -76,7 +76,8 @@ inside the caller's trust boundary.
import { CommitteeResolution } from "@iota/poi-wasm";

const verifier = client.verifier(CommitteeResolution.trustedNode());
await verifier.verify(proof);
const verified = await verifier.verify(proof);
console.log(verified.transaction);
```

Use genesis-anchored resolution to authenticate committee lineage independently from the node:
Expand All @@ -88,20 +89,48 @@ const trustedGenesisBlob = await readFile("genesis.blob");
const resolution = CommitteeResolution.fromGenesis(trustedGenesisBlob);
const verifier = client.verifier(resolution);

await verifier.verify(proof);
const verified = await verifier.verify(proof);
```

Successful verification returns a read-only `VerifiedProof`. It exposes the authenticated transaction digest,
checkpoint metadata, and `ProofTargets`. Use `objectBcs(index)` and `eventContents(index)` to read the authenticated
target payloads. Continue using `Proof` only as the untrusted transport and serialization envelope.

`CommitteeResolution.fromGenesis()` decodes the BCS-encoded IOTA genesis blob and extracts its committee in Rust.
Callers that already possess an extracted trusted committee can use `CommitteeResolution.anchored(committee)` instead.
`Committee.fromJSON()` accepts the Rust `Committee` fields `epoch` and `voting_rights`, validates public keys, rejects
duplicate authorities, requires total voting power to equal 10,000, and reconstructs the committee's derived lookup
state.
state. Use `Committee.toJSON()` to persist a resolved committee and restore it later with `Committee.fromJSON()`.

The verifier fetches the certified checkpoint in each epoch-close proof, verifies it with the current committee, and
only then accepts and caches the next committee. Each anchored verifier owns a fresh in-memory cache; the WASM Package
does not accept a caller-provided committee cache. Retain the verifier when checking multiple proofs so it can reuse the
committees authenticated during its lifetime. `CommitteeResolver.resolve(epoch)` and `Proof.verify(committee)` remain
available for lower-level committee resolution and offline verification.
available for lower-level committee resolution and offline verification; both verification methods return a
`VerifiedProof` on success.

Verification failures are normal JavaScript errors with a stable `code`. Use `isPoiError(error)` before reading the
code. Only `PROOF_INVALID` means the proof was rejected.

## What a Verified Proof Proves

Successful verification authenticates the following targets relative to the supplied committee:

- A transaction target proves that the selected transaction, its user signatures, and its effects are included in the
certified checkpoint.
- An object target proves the exact object version returned by `objectBcs(index)`. For an object ID without a transaction
or event target, `makeProof()` resolves its latest version at proof construction time; the proof does not claim that it
remains latest. Deleted and wrapped objects are unsupported.
- An event target proves that `eventContents(index)` returns the selected event's authenticated contents.

## JSON Compatibility

Proof JSON is a versioned persistence and exchange format. Releases that support `ProofV1` continue to deserialize its
existing JSON shape and serialize the same field structure. Frozen V1 fixtures enforce this contract for transaction,
object, and event proofs.

Dependency upgrades must not silently change the `ProofV1` representation. Preserve the existing shape with custom
serialization when necessary, or introduce a new `Proof` variant for an incompatible format change.

## Trust Boundaries

Expand Down
15 changes: 11 additions & 4 deletions bindings/wasm/poi_wasm/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ IOTA_GENESIS_PATH="$HOME/.iota/iota_config/genesis.blob" \
npm run example:node -- 02_multi_target_proof
```

Run the trusted-node example against any active network without a genesis blob:

```bash
npm run example:node -- 06_trusted_node
```

Run against the active public faucet network. The utility funds the active CLI
wallet and publishes Single Notarization when necessary:

Expand All @@ -124,19 +130,20 @@ npm run example:node -- 05_event_proof
| [03_reuse_verifier](./src/03_reuse_verifier.ts) | Two creation transactions while reusing one verifier and its committee cache. |
| [04_object_proof](./src/04_object_proof.ts) | A freshly created `Notarization` object, starting from only its object ID. |
| [05_event_proof](./src/05_event_proof.ts) | A fresh `LockedNotarizationCreated` event, starting from only its event ID. |
| [06_trusted_node](./src/06_trusted_node.ts) | A transaction proof verified through a trusted endpoint on any network. |

## Committee Trust

The gRPC endpoint supplies transaction, checkpoint, and committee-transition
evidence. That evidence remains untrusted until verification succeeds.

Examples 01, 02, and 03 use genesis-anchored committee resolution. The utility
Examples 01 through 05 use genesis-anchored committee resolution. The utility
downloads and caches the official genesis blob for mainnet, testnet, and
devnet. Set `IOTA_GENESIS_PATH` for localnet and custom networks.

Examples 04 and 05 use trusted-node resolution so they can focus on
target-driven object and event discovery. In this mode, the selected gRPC node
is inside the verifier's trust boundary.
Example 06 demonstrates trusted-node resolution on any active network without a
genesis blob. In this mode, the selected gRPC node is inside the verifier's
trust boundary and must be operated by a party the verifier trusts.

Obtain custom genesis blobs independently, verify that each blob belongs to the
selected network, and do not accept a trust anchor from the same untrusted party
Expand Down
5 changes: 3 additions & 2 deletions bindings/wasm/poi_wasm/examples/src/01_transaction_proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ export async function createAndVerifyTransactionProof(): Promise<void> {

console.log("Stage 5 - Verify the received proof");
const verifier = context.poiClient.verifier(trust.resolution);
await verifier.verify(receivedProof);
const verified = await verifier.verify(receivedProof);

console.log(" transaction proof verified successfully.");
console.log(`The transaction is included in a checkpoint authenticated through ${trust.description}.`);
console.log(` transaction: ${verified.transaction}`);
console.log(` checkpoint: ${verified.checkpointSequenceNumber}`);
}
7 changes: 4 additions & 3 deletions bindings/wasm/poi_wasm/examples/src/02_multi_target_proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/**
* # Create and Verify a Multi-Target Proof
*
* A single Proof of Inclusion can authenticate several claims about the same
* A single Proof of Inclusion can authenticate several targets from the same
* transaction. This example proves the transaction itself, one object changed
* by it, and one event emitted by it.
*
Expand Down Expand Up @@ -54,8 +54,9 @@ export async function createAndVerifyMultiTargetProof(): Promise<void> {

console.log("Stage 4 - Verify every target in the proof");
const verifier = context.poiClient.verifier(trust.resolution);
await verifier.verify(proof);
const verified = await verifier.verify(proof);

console.log(" multi-target proof verified successfully.");
console.log("The transaction, changed object, and emitted event are authenticated by one proof.");
console.log(` object targets: ${verified.targets.objects.length}`);
console.log(` event targets: ${verified.targets.events.length}`);
}
7 changes: 5 additions & 2 deletions bindings/wasm/poi_wasm/examples/src/03_reuse_verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ export async function reuseVerifierForMultipleProofs(): Promise<void> {

console.log("Stage 4 - Verify both proofs with one verifier");
console.log(" verifying the first proof; this resolves its checkpoint committee...");
await verifier.verify(firstProof);
const firstVerified = await verifier.verify(firstProof);

console.log(" verifying the second proof with the same verifier...");
await verifier.verify(secondProof);
const secondVerified = await verifier.verify(secondProof);

console.log("\n both transaction proofs verified successfully.");
console.log(
` authenticated checkpoints: ${firstVerified.checkpointSequenceNumber}, ${secondVerified.checkpointSequenceNumber}`,
);
console.log(`Both proofs used committee resolution through ${trust.description}.`);
}
37 changes: 23 additions & 14 deletions bindings/wasm/poi_wasm/examples/src/04_object_proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,35 @@
/**
* # Create and Verify an Object Proof
*
* Starting from only an object ID, the builder fetches the object's latest
* version, discovers the transaction that produced it, and packages the object
* and transaction evidence into one proof.
* Starting from only an object ID, the builder fetches its latest version at
* proof construction time, discovers the transaction that produced it, and
* packages both as one proof.
*
* The discovered transaction supports the object claim but does not become an
* explicit transaction target. Trusted-node resolution keeps the example
* focused on object-driven discovery.
* The discovered transaction supports the object target but does not become an
* explicit transaction target. Verification authenticates committee history
* from a trusted genesis blob.
*/

import { strict as assert } from "node:assert";

import { fromHex, normalizeIotaObjectId } from "@iota/iota-sdk/utils";
import { CommitteeResolution } from "@iota/poi-wasm";
import { createNotarization, preparePoiExample } from "./util.js";
import { createNotarization, loadGenesisCommitteeResolution, preparePoiExample } from "./util.js";

/** Demonstrates target-driven object discovery and verification. */
export async function createAndVerifyObjectProof(): Promise<void> {
console.log("=== Proof of Inclusion: Create and Verify an Object Proof ===\n");

console.log("Stage 1 - Create a Notarization object using Locked Notarization");
console.log("Stage 1 - Configure the proof source and establish committee trust");
const context = await preparePoiExample();
const trust = await loadGenesisCommitteeResolution(context);
console.log(` trust anchor: ${trust.description}`);

console.log("\nStage 2 - Create a Notarization object using Locked Notarization");
const targets = await createNotarization(context);

// No transaction digest is supplied. The builder discovers the transaction
// that produced the latest object version and constructs its evidence.
console.log("\nStage 2 - Construct a proof from only the Notarization object ID");
// that produced the version returned at build time and constructs its evidence.
console.log("\nStage 3 - Construct a proof from only the Notarization object ID");
const object = fromHex(normalizeIotaObjectId(targets.objectId, false, true));
const proof = await context.poiClient.makeProof({ objects: [object] });
const proofTargets = proof.targets;
Expand All @@ -44,9 +47,15 @@ export async function createAndVerifyObjectProof(): Promise<void> {
console.log(` object version: ${proofTargets.objects[0]?.version}`);
console.log(` object targets: ${proofTargets.objects.length}\n`);

console.log("Stage 3 - Verify the object proof");
await context.poiClient.verifier(CommitteeResolution.trustedNode()).verify(proof);
console.log("Stage 4 - Verify the object proof");
const verified = await context.poiClient.verifier(trust.resolution).verify(proof);

console.log(" object proof verified successfully.");
console.log("The resolved object version was changed by a transaction trusted through the selected node.");
console.log(
` authenticated object: ${verified.targets.objects[0]?.objectId} at version ${
verified.targets.objects[0]?.version
}`,
);
console.log(` object BCS: ${verified.objectBcs(0).length} bytes`);
console.log("The resolved object version was authenticated from the trusted network genesis.");
}
28 changes: 18 additions & 10 deletions bindings/wasm/poi_wasm/examples/src/05_event_proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,31 @@
* An event ID contains its emitting transaction digest and sequence number, so
* it can select an execution without a separate transaction target. The proof
* carries the transaction's complete event list because the effects commit to
* that complete list.
*
* Trusted-node resolution keeps the example focused on event-driven discovery.
* that complete list. Verification authenticates committee history from a
* trusted genesis blob.
*/

import { strict as assert } from "node:assert";

import { fromBase58 } from "@iota/iota-sdk/utils";
import { CommitteeResolution } from "@iota/poi-wasm";
import { createNotarization, preparePoiExample } from "./util.js";
import { createNotarization, loadGenesisCommitteeResolution, preparePoiExample } from "./util.js";

/** Demonstrates event-driven transaction discovery and verification. */
export async function createAndVerifyEventProof(): Promise<void> {
console.log("=== Proof of Inclusion: Create and Verify an Event Proof ===\n");

console.log("Stage 1 - Emit a LockedNotarizationCreated event as fresh proof evidence");
console.log("Stage 1 - Configure the proof source and establish committee trust");
const context = await preparePoiExample();
const trust = await loadGenesisCommitteeResolution(context);
console.log(` trust anchor: ${trust.description}`);

console.log("\nStage 2 - Emit a LockedNotarizationCreated event as fresh proof evidence");
const targets = await createNotarization(context);
const transaction = fromBase58(targets.transactionDigest);

// The event ID already identifies the emitting transaction, allowing the
// builder to fetch all required transaction, effects, and event evidence.
console.log("\nStage 2 - Construct a proof from only the event ID");
console.log("\nStage 3 - Construct a proof from only the event ID");
const proof = await context.poiClient.makeProof({
events: [{ transaction, sequence: targets.eventSequence }],
});
Expand All @@ -45,9 +47,15 @@ export async function createAndVerifyEventProof(): Promise<void> {
console.log(` checkpoint epoch: ${proof.checkpointEpoch}`);
console.log(` event targets: ${proofTargets.events.length}\n`);

console.log("Stage 3 - Verify the event proof");
await context.poiClient.verifier(CommitteeResolution.trustedNode()).verify(proof);
console.log("Stage 4 - Verify the event proof");
const verified = await context.poiClient.verifier(trust.resolution).verify(proof);

console.log(" event proof verified successfully.");
console.log("The selected event was emitted by a transaction trusted through the selected node.");
console.log(
` authenticated event: ${verified.targets.events[0]?.transactionDigest}:${
verified.targets.events[0]?.eventSequence
}`,
);
console.log(` event contents: ${verified.eventContents(0).length} BCS bytes`);
console.log("The selected event was authenticated from the trusted network genesis.");
}
Loading
Loading