Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ rust-version = "1.85"

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

[workspace.dependencies]
Expand All @@ -17,8 +17,11 @@ async-trait = "0.1"
bcs = "0.1"
chrono = { version = "0.4", default-features = false }
hyper = "1"
iota-grpc-client = { git = "https://github.com/iotaledger/iota-rust-sdk.git", package = "iota-sdk-grpc-client", rev = "36bffd625e1b1d38307eb9e49bd2cb7dd988bb11" }
iota-grpc-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", package = "iota-sdk-grpc-types", rev = "36bffd625e1b1d38307eb9e49bd2cb7dd988bb11" }
iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v1.26.1" }
iota-sdk-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", rev = "36bffd625e1b1d38307eb9e49bd2cb7dd988bb11", default-features = false }
iota-types = { git = "https://github.com/iotaledger/iota.git", tag = "v1.26.1" }
iota_interaction = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.22", default-features = false, package = "iota_interaction" }
iota_interaction_rust = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.22", default-features = false, package = "iota_interaction_rust" }
iota_interaction_ts = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.22", default-features = false, package = "iota_interaction_ts" }
Expand Down
22 changes: 22 additions & 0 deletions poi-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "poi-rs"
version = "0.1.0-alpha"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
keywords = ["iota", "proof", "inclusion", "notarization"]
license.workspace = true
repository.workspace = true
rust-version.workspace = true
description = "Proof of Inclusion support for the IOTA Notarization Toolkit."

[dependencies]
async-trait.workspace = true
iota-grpc-client.workspace = true
iota-grpc-types.workspace = true
iota-sdk-types.workspace = true
iota-types.workspace = true
serde.workspace = true
serde_json = { workspace = true, features = ["alloc"] }
thiserror.workspace = true
tokio.workspace = true
54 changes: 54 additions & 0 deletions poi-rs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# IOTA Proof of Inclusion Rust Package

The Proof of Inclusion Rust package provides proof data types and offline verification for inclusion claims in the IOTA
Notarization Toolkit.

Use Proof of Inclusion when a verifier needs cryptographic evidence that a transaction, event, or object state is tied to
a certified IOTA checkpoint. The package verifies supplied proof material locally. It does not fetch checkpoints, resolve
committees, or trust the node that supplied the proof.

## Proof Model

A `Proof` contains three layers of evidence:

- A `CertifiedCheckpointSummary` signed by the committee for the checkpoint epoch.
- A `TransactionProof` containing the checkpoint contents, transaction, effects, and optional events.
- `ProofTargets` describing the object, event, or committee claims the caller wants to authenticate.

The transaction proof is required. A Proof of Inclusion proves inclusion in a certified checkpoint, so the proof envelope
must carry the transaction evidence that links the target claim to the checkpoint contents.

## Verification

`ProofVerifier` is the public verification entry point. It receives the authoritative committee for the proof checkpoint
and verifies only the proof material passed by the caller.

Verification checks:

- the proof format version is supported
- the checkpoint summary is certified by the supplied committee
- the checkpoint contents match the certified checkpoint summary
- the transaction digest matches the transaction effects
- the transaction effects are included in the checkpoint contents
- packaged events match the event digest recorded in the effects
- requested event targets belong to the transaction and match the packaged event contents
- requested object targets match their object references and appear in the transaction effects
- requested committee targets match the next committee recorded in an end-of-epoch checkpoint

## Trust Boundaries

`ProofVerifier` is intentionally offline. It does not make RPC calls and does not decide which committee is authoritative.
Callers must provide the committee that should certify the checkpoint. A higher-level client or cache can resolve committee
history before calling the verifier.

The verifier treats all proof payloads as untrusted until verification succeeds. After verification succeeds, callers can
trust the authenticated target claims relative to the supplied committee.

## Main Types

- `Proof`: Versioned Proof of Inclusion envelope.
- `ProofVersion`: Proof format version used for compatibility checks.
- `TransactionProof`: Transaction, effects, events, and checkpoint contents used to prove inclusion.
- `ProofTargets`: Object, event, and committee claims to authenticate.
- `ProofVerifier`: Offline verifier for `Proof` values.
- `VerifyError`, `SourceError`, `SerializationError`, and `VersionError`: Operation-specific errors.
45 changes: 45 additions & 0 deletions poi-rs/src/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2020-2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use iota_types::committee::{Committee, EpochId};

use crate::BoxError;

mod in_memory;

pub use in_memory::MemoryCommitteeCache;

/// Error returned by a committee cache.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CommitteeCacheError {
/// A cached committee conflicts with authenticated committee data.
#[error("cached committee conflicts at epoch {epoch}")]
Conflict {
/// Epoch whose cached material conflicts.
epoch: EpochId,
},
/// A cache backend failed to read or write committee material.
#[error("committee cache backend failed at epoch {epoch}")]
Backend {
/// Epoch being accessed when the backend failed.
epoch: EpochId,
/// Underlying backend error.
#[source]
source: BoxError,
},
}

/// Stores authenticated committees for anchored resolution.
///
/// A cache is part of the caller's trust boundary. Implementations must return
/// only committees previously authenticated for the same network and must
/// preserve their integrity after storage.
#[async_trait::async_trait]
pub trait CommitteeCache: Send + Sync {
/// Returns the authenticated committee for `epoch`, when available.
async fn committee(&self, epoch: EpochId) -> Result<Option<Committee>, CommitteeCacheError>;

/// Stores a committee after the resolver has authenticated it.
async fn store(&self, committee: &Committee) -> Result<(), CommitteeCacheError>;
}
122 changes: 122 additions & 0 deletions poi-rs/src/cache/in_memory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2020-2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use std::{collections::BTreeMap, sync::Arc};

use iota_types::committee::{Committee, EpochId};
use tokio::sync::RwLock;

use super::{CommitteeCache, CommitteeCacheError};

/// In-memory committee cache for library usage and tests.
#[derive(Clone, Debug, Default)]
pub struct MemoryCommitteeCache {
committees: Arc<RwLock<BTreeMap<EpochId, Committee>>>,
}

impl MemoryCommitteeCache {
/// Creates an empty in-memory committee cache.
pub fn new() -> Self {
Self::default()
}

/// Returns the number of cached committees.
pub async fn len(&self) -> usize {
self.committees.read().await.len()
}

/// Returns whether the cache contains no committees.
pub async fn is_empty(&self) -> bool {
self.committees.read().await.is_empty()
}
}

#[async_trait::async_trait]
impl CommitteeCache for MemoryCommitteeCache {
async fn committee(&self, epoch: EpochId) -> Result<Option<Committee>, CommitteeCacheError> {
Ok(self.committees.read().await.get(&epoch).cloned())
}

async fn store(&self, committee: &Committee) -> Result<(), CommitteeCacheError> {
let epoch = committee.epoch;
let mut committees = self.committees.write().await;

if committees.get(&epoch).is_some_and(|cached| cached != committee) {
return Err(CommitteeCacheError::Conflict { epoch });
}

committees.entry(epoch).or_insert_with(|| committee.clone());

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

fn committee_at(epoch: EpochId) -> Committee {
let (committee, _) = Committee::new_simple_test_committee();

Committee::new(epoch, committee.voting_rights.iter().cloned().collect())
}

#[tokio::test]
async fn new_cache_is_empty() {
let cache = MemoryCommitteeCache::new();

assert!(cache.is_empty().await);
assert_eq!(cache.len().await, 0);
assert!(cache.committee(7).await.unwrap().is_none());
}

#[tokio::test]
async fn store_makes_a_committee_available_by_epoch() {
let cache = MemoryCommitteeCache::new();
let committee = committee_at(7);

cache.store(&committee).await.unwrap();

assert_eq!(cache.committee(7).await.unwrap(), Some(committee));
assert_eq!(cache.len().await, 1);
assert!(!cache.is_empty().await);
}

#[tokio::test]
async fn storing_the_same_committee_is_idempotent() {
let cache = MemoryCommitteeCache::new();
let committee = committee_at(7);

cache.store(&committee).await.unwrap();
cache.store(&committee).await.unwrap();

assert_eq!(cache.committee(7).await.unwrap(), Some(committee));
assert_eq!(cache.len().await, 1);
}

#[tokio::test]
async fn conflicting_committee_is_rejected_without_replacing_the_original() {
let cache = MemoryCommitteeCache::new();
let original = committee_at(7);
let (conflicting, _) = Committee::new_simple_test_committee_of_size(5);
let conflicting = Committee::new(7, conflicting.voting_rights.iter().cloned().collect());
cache.store(&original).await.unwrap();

let error = cache.store(&conflicting).await.unwrap_err();

assert!(matches!(error, CommitteeCacheError::Conflict { epoch: 7 }));
assert_eq!(cache.committee(7).await.unwrap(), Some(original));
assert_eq!(cache.len().await, 1);
}

#[tokio::test]
async fn clones_share_cached_committees() {
let cache = MemoryCommitteeCache::new();
let clone = cache.clone();
let committee = committee_at(7);

cache.store(&committee).await.unwrap();

assert_eq!(clone.committee(7).await.unwrap(), Some(committee));
}
}
Loading
Loading