Skip to content

Latest commit

 

History

49 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Unicity State Transition SDK -- Rust

A Rust SDK for the Unicity token state-transition protocol.

The crate is no_std-first: the verification core has no C dependencies (RustCrypto sha2 + k256), is allocation-light, and runs inside a zkVM guest (SP1 / RISC0) or on wasm32. The default build adds the necessary client for minting and transferring tokens.

[dependencies]
unicity-token = "3.0"

The version line is shared with the TypeScript and Java SDKs: a 3.0.1 client interoperates with state-transition-sdk-js 3.0.1 and state-transition-sdk-java 3.0.1, and with an aggregator at ghcr.io/unicitynetwork/aggregator-go:sha-ae08165 or later.

Security model

Decoding a token proves its structural integrity only. Trust is established only by Token::verify, which walks an unbroken chain of cryptographic checks from a caller-supplied root of trust down to every state in the token's history.

Token::verify treats application data and any mint justification as opaque and rejects a token that carries either — unless you explicitly register a verifier for it. There is no implicit trust in the API.

The root of trust is the Unicity Trust Base json. An authentic trust base must be bundled with the application or left user-configurable.

Verify a token (no_std core, no features needed)

use unicity_token::Token;
use unicity_token::api::bft::RootTrustBase;

// Root of trust (validator set + quorum), supplied out-of-band.
let trust_base = RootTrustBase::from_json(trust_base_json)?; // ::new(..) in no_std

let token = Token::from_cbor(&token_bytes)?; // decoding confers NO trust
token.verify(&trust_base)?;                  // verifies the cryptographic history

Mint & transfer against a live aggregator (http feature)

use std::time::Duration;
use unicity_token::api::bft::RootTrustBase;
use unicity_token::client::{self, HttpAggregatorClient};

let trust_base = RootTrustBase::from_json(&std::fs::read_to_string("trust-base.json")?)?;
let aggregator = HttpAggregatorClient::new("https://gateway.testnet2.unicity.network/")
    .with_api_key("sk_…")
    .with_polling(Duration::from_secs(2), 90);
let token = client::mint(&aggregator, &trust_base, trust_base.network_id,
    &recipient, token_type, salt, /* data */ None, /* justification */ None,
    /* expires_at */ None)?;

expires_at is the exclusive request deadline in Unix seconds. Use None to let the Unicity Service assign a default one from consensus time, or explicitly Some(deadline).

The SDK is generic over the AggregatorClient trait, so you can plug in any transport (or an in-memory one for tests); HttpAggregatorClient is the batteries-included blocking JSON-RPC implementation. Inclusion polling accepts either way a server reports a leaf that is not certified yet: an explicit -32021 pending status, or a successful response whose leaf fields are absent. get_inclusion_proof.v2 never answers with a non-inclusion proof, so the empty response is unambiguous. Only the explicit status lets the client tell "not yet" apart from "no such state": against a server that reports it, an unknown StateID fails immediately as HttpError::StateNotFound rather than consuming the polling budget; against one that does not, it polls to the attempt limit.

Prove that a state is absent

use unicity_token::client::NonInclusionAggregatorClient;

let proof = aggregator.get_non_inclusion_proof(&state_id)?;
proof.verify(&trust_base)?; // verifies the StateId bound by the client

Verification authenticates the terminal leaf and every branch choice against the quorum-signed SMT root, then checks that the terminal key differs from the requested state id. A proof is a snapshot statement at its embedded certified root; a caller that needs “still absent now” must separately enforce an acceptable certified round or timestamp. The HTTP client distinguishes an already-included state (HttpError::StateIncluded) from the absence of any certified root (HttpError::CertifiedStateUnavailable).

If the caller does not know which relation holds, the HTTP client hides the endpoint selection:

use unicity_token::client::MembershipStatus;

match aggregator.membership_status(&state_id)? {
    MembershipStatus::Included(proof) => proof.verify_for(&state_id, &trust_base)?,
    MembershipStatus::Absent(proof) => proof.verify(&trust_base)?,
}

Payment tokens & splits

A token can carry a fungible payment payload (a canonical set of asset id → amount entries) in its mint data, and can be split into new tokens whose per-asset allocations sum to the original. Splitting burns the source and proves each output's share with a radix sparse Merkle sum tree (RSMST) inclusion proof.

Payment verification is fail-closed and policy-gated — cryptographic validity never authorizes an asset issuer on its own. You register the verifiers and issuance policy you trust, then call payment::verify_payment_token instead of bare Token::verify:

use unicity_token::payment::{
    verify_payment_token, PaymentAssetCollection, PaymentDataVerifier,
    SplitMintJustificationVerifier,
};
use unicity_token::verify::MintJustificationRegistry;

// `authorize` is your closure: given the genesis + decoded assets, decide
// whether this issuance is allowed (issuer key, supply caps, …).
let mut registry = MintJustificationRegistry::new();
registry
    .register(Box::new(SplitMintJustificationVerifier::new()))?         // accept split outputs
    .register_token_data(Box::new(PaymentDataVerifier::new(            // validate the payload…
        my_token_type, authorize)))?;                                  // …then run *your* policy

let assets = verify_payment_token(
    &token, &trust_base, &registry, PaymentAssetCollection::from_cbor_bytes,
)?; // returns the validated assets; fails closed if anything is off

Constructing a split is a client-feature operation (payment::TokenSplit::split) that verifies the source token fully before building the irreversible burn. See src/payment/tests.rs for the end-to-end split → verify flow.

Feature flags

Flag Default Purpose
alloc (transitive) Required by the core (Vec/BTreeMap for CBOR + structures).
std y std error integration, host RNG, JSON trust-base parsing.
client y Transaction construction, signing, mint/transfer flow, split construction.
http Blocking JSON-RPC HttpAggregatorClient (host only; pulls in a TLS stack).

Payment/asset verification is provided by no_std core and needs no feature. The zkVM/WASM guest build is --no-default-features --features alloc.

Building & testing

cargo test                                         # default features
cargo test --all-features                          # adds the http transport tests
cargo test --no-default-features --features alloc  # verification core only
cargo build --no-default-features --features alloc --target wasm32-unknown-unknown

The cross-SDK fixture under tests/vectors/ is generated by the TypeScript SDK; see the README there before changing anything on the wire.

Live end-to-end test against an aggregator. It reads e2e/.env (copy e2e/.env.example and set UNICITY_API_KEY), the same configuration the examples and the e2e/ demo crate use:

cargo test --features http --test e2e -- --ignored --nocapture

Examples

Runnable flows in examples/, talking to a live aggregator via HttpAggregatorClient (config from e2e/.env):

cargo run --example mint     --features http   # mint a token, print its CBOR
cargo run --example transfer --features http   # mint then transfer
cargo run --example split    --features http   # mint a coin, split it, verify outputs

A self-contained demo application is provided under e2e/.

License

MIT OR Apache-2.0.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages