Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
19ef8ff
feat: Implement Proof of Inclusion support in poi-rs module
itsyaasir Jul 2, 2026
8f3722e
feat: Add Proof of Inclusion support with verification and error hand…
itsyaasir Jul 3, 2026
40fd79a
Merge pull request #306 from iotaledger/feat/290-poi-proof-envelope-api
itsyaasir Jul 7, 2026
8e1d9c9
feat: Implement ProofVersion conversion and add verifier tests for tr…
itsyaasir Jul 7, 2026
6b7b0b7
test: Cover PoI verifier failure cases
itsyaasir Jul 7, 2026
66ce368
Merge pull request #307 from iotaledger/feat/291-poi-verifier-dispatch
itsyaasir Jul 7, 2026
f410224
feat: Add gRPC client support for Proof of Inclusion and enhance erro…
itsyaasir Jul 7, 2026
414b5ca
Merge pull request #308 from iotaledger/feat/292-poi-grpc-proof-const…
itsyaasir Jul 8, 2026
a9ba01b
feat: Add support for object proofs in source and enhance error handling
itsyaasir Jul 9, 2026
fd02c62
Merge pull request #309 from iotaledger/feat/293-poi-object-proof-sup…
itsyaasir Jul 9, 2026
477f15b
feat: Add support for event proofs in source and enhance related erro…
itsyaasir Jul 9, 2026
5f4a41c
Merge pull request #310 from iotaledger:feat/294-poi-event-proof-support
itsyaasir Jul 9, 2026
6dd9352
Merge branch 'main' into feat/poi-implementation
itsyaasir Jul 10, 2026
9827f43
feat: Implement committee resolution for checkpoint verification and …
itsyaasir Jul 10, 2026
043abeb
Merge branch 'feat/poi-implementation' into feat/295-poi-committee-re…
itsyaasir Jul 10, 2026
b9d3da5
Merge pull request #316 from iotaledger/feat/295-poi-committee-resolu…
itsyaasir Jul 10, 2026
c2a4621
refactor: Simplify error handling in GrpcSource implementation
itsyaasir Jul 10, 2026
3fa6e6e
feat: Add verified committee cache
itsyaasir Jul 13, 2026
7bcaae4
test: Cover committee cache behavior
itsyaasir Jul 13, 2026
7f7b5ba
refactor: Remove unused tests from committee cache module
itsyaasir Jul 13, 2026
c85eea5
refactor: Rename committee resolution mode
itsyaasir Jul 13, 2026
5176c43
Merge pull request #317 from iotaledger/feat/296-poi-committee-cache
itsyaasir Jul 13, 2026
7cc4d24
feat: Add golden tests for proof verification and include fixture data
itsyaasir Jul 13, 2026
62b543b
feat: Add dev-dependencies for iota-config and test-cluster; remove o…
itsyaasir Jul 13, 2026
27fd80b
feat: Implement ProofBuilder and related components for proof constru…
itsyaasir Jul 14, 2026
3a13e5c
feat: Enhance ProofBuilder to support stacking multiple object and ev…
itsyaasir Jul 14, 2026
2e1d26e
feat: Add chain identifier support in ProofBuilder and related tests
itsyaasir Jul 15, 2026
17f5840
Merge pull request #318 from iotaledger/feat/297-poi-golden-fixtures
itsyaasir Jul 16, 2026
7b60b9f
feat: Enhance Proof of Inclusion CLI and underlying logic
itsyaasir Jul 16, 2026
11fb1e0
feat: Refactor committee authentication logic and improve error handling
itsyaasir Jul 16, 2026
b98d8a4
feat: Update CLI command name and enhance dependencies for Proof of I…
itsyaasir Jul 20, 2026
a6dd16c
Merge pull request #320 from iotaledger/feat/poi/cli
itsyaasir Jul 20, 2026
e1fd7e3
feat: add tests for NodePoiSource and WASM integration
itsyaasir Jul 27, 2026
300b9fb
feat: implement LedgerSource and PoiClient for IOTA Proof of Inclusion
itsyaasir Jul 27, 2026
2743727
feat: remove custom endpoint creation method from PoiClient
itsyaasir Jul 27, 2026
a0b207e
feat: implement committee management and resolver in PoiClient and Le…
itsyaasir Jul 27, 2026
45dc296
feat: implement anchored committee resolver and proof deserialization…
itsyaasir Jul 27, 2026
c4be42e
feat: introduce PoiError for error handling and refactor committee an…
itsyaasir Jul 27, 2026
7d9823e
Refactor tests to use updated fixture paths and remove deprecated JSO…
itsyaasir Jul 27, 2026
4083483
Merge pull request #322 from iotaledger/chore/poi/update-anchored-com…
itsyaasir Jul 28, 2026
af3ffc2
Merge remote-tracking branch 'origin/feat/poi-implementation' into fe…
itsyaasir Jul 28, 2026
42d213e
feat: refactor source and committee handling, implement gRPC source f…
itsyaasir Jul 28, 2026
7d5e78d
Refactor ProofBuilder to use ProofTarget enum and improve error handling
itsyaasir Jul 28, 2026
4307eca
feat: update currentEpoch method to return undefined and improve erro…
itsyaasir Jul 29, 2026
0c44a3e
feat: enhance PoiClient to support explicit endpoints and improve com…
itsyaasir Jul 29, 2026
ca7bdab
feat: introduce PoiClient for proof construction and verification, re…
itsyaasir Jul 29, 2026
65506e7
feat: update PoiClient methods for committee verification and add sup…
itsyaasir Jul 30, 2026
886a359
feat: refactor tests to utilize PoiClient and improve proof verificat…
itsyaasir Aug 3, 2026
d9463cc
feat: add comprehensive tests for committee resolution and proof cons…
itsyaasir Aug 3, 2026
560b757
feat: refactor committee resolution handling in PoiClient and related…
itsyaasir Aug 3, 2026
9cc98d3
feat: remove target module and integrate ProofTargets into proof module
itsyaasir Aug 4, 2026
7b188fc
feat: update ProofTargets to remove committee claims and adjust relat…
itsyaasir Aug 5, 2026
f887eb9
Refactor proof structure to use 'targets' instead of 'target'
itsyaasir Aug 5, 2026
b5c4eaa
Merge pull request #323 from iotaledger/feat/poi/nodejs-implementation
itsyaasir Aug 5, 2026
6f81c49
feat: update proof request terminology and refactor related code
itsyaasir Aug 5, 2026
8527f54
Merge remote-tracking branch 'origin/main' into feat/poi-implementation
itsyaasir Aug 7, 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
19 changes: 16 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,42 @@ rust-version = "1.85"

[workspace]
resolver = "2"
members = ["audit-trail-rs", "examples", "notarization-rs"]
exclude = ["bindings/wasm/notarization_wasm", "bindings/wasm/audit_trail_wasm"]
members = ["audit-trail-rs", "examples", "notarization-rs", "poi-rs"]
exclude = [
"bindings/wasm/notarization_wasm",
"bindings/wasm/audit_trail_wasm",
"bindings/wasm/poi_wasm",
]

[workspace.dependencies]
anyhow = "1.0"
async-trait = "0.1"
bcs = "0.1"
chrono = { version = "0.4", default-features = false }
clap = { version = "4.6.1", features = ["derive"] }
hyper = "1"
iota-grpc-client = { git = "https://github.com/iotaledger/iota-rust-sdk.git", package = "iota-sdk-grpc-client", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339" }
iota-grpc-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", package = "iota-sdk-grpc-types", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339" }
iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v1.27.0" }
iota-sdk-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", rev = "ce81341ac3fdb7204df112182c68319f26d5896b", default-features = false }
iota-sdk-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339", default-features = false }
iota-types = { git = "https://github.com/iotaledger/iota.git", rev = "420df58ea50ce916a927ebba2dcaee192832e436" }
iota_interaction = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "iota_interaction" }
iota_interaction_rust = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "iota_interaction_rust" }
iota_interaction_ts = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "iota_interaction_ts" }
product_common = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "product_common" }
reqwest = { version = "0.12.28", default-features = false, features = ["rustls-tls"] }
secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.3.0", default-features = false }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
serde-aux = { version = "4.7.0", default-features = false }
serde_json = { version = "1.0", default-features = false }
sha2 = { version = "0.10", default-features = false }
strum = { version = "0.27", default-features = false, features = ["std", "derive"] }
tempfile = "3.27.0"
thiserror = { version = "2.0", default-features = false }
tokio = { version = "1.52.2", default-features = false, features = ["macros", "sync", "rt", "process"] }

[patch."https://github.com/MystenLabs/fastcrypto"]
fastcrypto = "=0.1.11"

[profile.release.package.iota_interaction_ts]
opt-level = 's'
27 changes: 15 additions & 12 deletions bindings/wasm/build/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { lintAll } = require("./lints");
const generatePackage = require("./utils/generatePackage");

const artifact = process.argv[2];
const skipFetchPolyfill = process.argv.includes("--skip-fetch-polyfill");

const RELEASE_FOLDER = path.join(__dirname, "..", artifact, "node");
const entryFilePathNode = path.join(RELEASE_FOLDER, `${artifact}.js`);
Expand All @@ -12,26 +13,28 @@ console.log(`[build/node.js] Processing entryFile '${entryFilePathNode}' for art

lintAll(entryFileNode);

// Add node-fetch polyfill (https://github.com/seanmonstar/reqwest/issues/910).
let changedFileNode = entryFileNode.replace(
"let imports = {};",
`if (!globalThis.fetch) {
if (!skipFetchPolyfill) {
// Add node-fetch polyfill (https://github.com/seanmonstar/reqwest/issues/910).
const changedFileNode = entryFileNode.replace(
"let imports = {};",
`if (!globalThis.fetch) {
const fetch = require('node-fetch')
globalThis.Headers = fetch.Headers
globalThis.Request = fetch.Request
globalThis.Response = fetch.Response
globalThis.fetch = fetch
}
let imports = {};`,
);
);

fs.writeFileSync(
entryFilePathNode,
changedFileNode,
);
console.log(
`[build/node.js] Added node-fetch polyfill to entryFile '${entryFilePathNode}'. Starting generatePackage().`,
);
fs.writeFileSync(
entryFilePathNode,
changedFileNode,
);
console.log(`[build/node.js] Added node-fetch polyfill to entryFile '${entryFilePathNode}'.`);
} else {
console.log(`[build/node.js] Skipped node-fetch polyfill for artifact '${artifact}'.`);
}

// Generate `package.json`.
const newPackage = generatePackage({
Expand Down
5 changes: 5 additions & 0 deletions bindings/wasm/poi_wasm/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build]
target = "wasm32-unknown-unknown"

[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
4 changes: 4 additions & 0 deletions bindings/wasm/poi_wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.cache/
target/
node/
42 changes: 42 additions & 0 deletions bindings/wasm/poi_wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "poi_wasm"
version = "0.1.0-alpha"
authors = ["IOTA Stiftung"]
edition = "2024"
homepage = "https://www.iota.org"
keywords = ["iota", "proof", "inclusion", "wasm"]
license = "Apache-2.0"
publish = false
readme = "README.md"
repository = "https://github.com/iotaledger/notarization"
rust-version = "1.85"
description = "Node.js WASM bindings for the IOTA Proof of Inclusion Package."

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
async-trait = { version = "0.1", default-features = false }
bcs = "0.1.6"
console_error_panic_hook = "0.1"
fastcrypto = "=0.1.11"
iota-sdk-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339", default-features = false, features = ["serde"] }
iota-types = { git = "https://github.com/iotaledger/iota.git", rev = "420df58ea50ce916a927ebba2dcaee192832e436" }
js-sys = "=0.3.85"
poi-rs = { path = "../../../poi-rs", default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6.5"
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
thiserror = { version = "2.0", default-features = false }
wasm-bindgen = "=0.2.108"
wasm-bindgen-futures = "=0.4.58"

[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies]
getrandom = { version = "0.3", default-features = false, features = ["wasm_js"] }

[profile.release]
lto = true
opt-level = "s"

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] }
139 changes: 139 additions & 0 deletions bindings/wasm/poi_wasm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Proof of Inclusion Node.js Package

This package generates a typed Node.js client for IOTA's `LedgerService` and
connects it to `poi-rs` compiled as WebAssembly.

The generated client uses:

- protobuf definitions pinned to the same `iota-rust-sdk` revision as the Rust
workspace;
- Protobuf-ES generated messages and service descriptors;
- ConnectRPC's native Node.js gRPC transport over HTTP/2.

## Schema workflow

[`grpc/iota-schema.lock.json`](grpc/iota-schema.lock.json) records the approved
repository, exact Git revision and SHA-256 digest of the committed Buf image.
Normal generation does not access the network.

To intentionally download a different upstream schema:

```sh
npm run grpc:schema:update -- <full-iota-rust-sdk-commit>
```

To regenerate the TypeScript client from the committed schema image:

```sh
npm run grpc:generate
```

Review the lock file, Buf image and generated TypeScript changes together.

## Client creation

```ts
import { PoiClient } from "@iota/poi-wasm";

const mainnet = PoiClient.mainnet();
const testnet = PoiClient.testnet();
const devnet = PoiClient.devnet();
const custom = new PoiClient("http://localhost:9000");
```

No network is selected implicitly. The named constructors use the public IOTA
gRPC endpoints. Construct `PoiClient` with an explicit endpoint for private
nodes, archives, local networks, or alternative endpoints.

## Proof construction

```ts
import { PoiClient } from "@iota/poi-wasm";

const client = PoiClient.testnet();
const proof = await client
.proof()
.transaction(transactionDigest)
.build();

console.log(proof.toJSON());
```

The same builder also exposes `object(objectId)` and
`event(transactionDigest, eventSequence)`. All 64-bit values use JavaScript
`bigint`.

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; event
targets contain event IDs whose contents are selected from the authenticated
transaction event list.

`PoiClient` hides the generated protobuf client, gRPC transport, and
JavaScript/WASM source adapter. The 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`.

## Verification

```ts
import { CommitteeResolution } from "@iota/poi-wasm";

const verifier = client.verifier(CommitteeResolution.trustedNode());
await verifier.verify(proof);
```

The verifier asks the client's node for the committee governing the proof
checkpoint epoch. Rust validates the returned committee representation and
performs proof verification locally with `poi-rs`.

This mode places the node inside the caller's trust boundary. It does not
authenticate committee lineage from genesis. To authenticate committee
lineage from an already trusted committee:

```ts
import { readFile } from "node:fs/promises";

const trustedGenesisBlob = await readFile("genesis.blob");
const resolution = CommitteeResolution.fromGenesis(trustedGenesisBlob);
const verifier = client.verifier(resolution);

await verifier.verify(proof);
```

`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.

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. The node supplies evidence but is not trusted to choose the
committee.

Retain the verifier when checking multiple proofs so its authenticated
committee cache is reused. `CommitteeResolver.resolve(epoch)` and
`Proof.verify(committee)` remain available for callers that need the
lower-level committee or offline-verification APIs.

## Package verification

```sh
npm install
npm run verify
```

Verification regenerates the Node.js protobuf client from the committed schema
image, builds `poi-rs` for `wasm32-unknown-unknown`, type-checks the TypeScript
boundary, and runs the tests. The tests use an in-memory generated service
implementation and do not require a running IOTA node. To query a live
endpoint with the development diagnostic:

```sh
npm run example:service-info -- https://grpc.testnet.iota.cafe:443
```
20 changes: 20 additions & 0 deletions bindings/wasm/poi_wasm/examples/service-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { createIotaGrpcClient } from "../lib/client.js";

const endpoint = process.argv[2] ?? "https://grpc.testnet.iota.cafe:443";
const client = createIotaGrpcClient(endpoint);
const serviceInfo = await client.getServiceInfo({
readMask: { paths: ["chain_id"] },
});
const chainIdentifier = serviceInfo.chainId?.digest;

if (!chainIdentifier) {
throw new Error("getServiceInfo returned no chain identifier");
}

console.log({
endpoint,
chainIdentifier: Buffer.from(chainIdentifier).toString("hex"),
});
13 changes: 13 additions & 0 deletions bindings/wasm/poi_wasm/grpc/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: v2
clean: true

plugins:
- local: protoc-gen-es
out: lib/grpc/generated
opt:
- target=ts
- import_extension=js
include_imports: true

inputs:
- binary_image: grpc/iota-ledger.binpb
Binary file added bindings/wasm/poi_wasm/grpc/iota-ledger.binpb
Binary file not shown.
9 changes: 9 additions & 0 deletions bindings/wasm/poi_wasm/grpc/iota-schema.lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"repository": "https://github.com/iotaledger/iota-rust-sdk",
"revision": "b77fcd5ac5fedb3dfbc77ba7d183140e43512339",
"protoRoot": "crates/iota-sdk-grpc-types/proto",
"entrypoints": [
"iota/grpc/v1/ledger_service.proto"
],
"imageSha256": "sha256:8ed56353f07bbf508442cab56c592b2cc34fe39f696f3bc815ee01cff9dd6186"
}
43 changes: 43 additions & 0 deletions bindings/wasm/poi_wasm/lib/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { createClient, type Client, type Transport } from "@connectrpc/connect";
import { createGrpcTransport } from "@connectrpc/connect-node";

import { LedgerService } from "./grpc/generated/iota/grpc/v1/ledger_service_pb.js";

const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_READ_MAX_BYTES = 128 * 1024 * 1024;

export type IotaGrpcClient = Client<typeof LedgerService>;

export interface IotaGrpcClientOptions {
defaultTimeoutMs?: number;
readMaxBytes?: number;
transport?: Transport;
}

/**
* Creates a Node.js gRPC client from the generated IOTA LedgerService
* descriptor.
*/
export function createIotaGrpcClient(
endpoint: string,
options: IotaGrpcClientOptions = {},
): IotaGrpcClient {
const baseUrl = endpoint.trim().replace(/\/+$/, "");

if (!baseUrl) {
throw new Error("IOTA gRPC endpoint must not be empty");
}

const transport =
options.transport ??
createGrpcTransport({
baseUrl,
defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
readMaxBytes: options.readMaxBytes ?? DEFAULT_READ_MAX_BYTES,
});

return createClient(LedgerService, transport);
}
Loading
Loading