Chio/Docs
LOGIN · JOIN

Join & Terminal Receipts

Check that a delegated task graph closed as declared: every fan-in joined its parents and the terminal receipt accounts for the whole graph.

Concept chio@5f63e6e

On this page

Delegated work can close with evidence another operator can check. Each fan-in in a task graph carries a signed join receipt naming the parent receipts it waited for, and a signed terminal graph receipt accounts for every task, join, route plan, and budget unit the graph declared.

In an application, run.joinTree() waits for a run’s whole execution family, delegated helpers included, to close. In a signed task graph, these two receipt types record the closure for the authority verifier. The running example is the valid-recursive-delegation fixture: task-root delegates to three children whose results meet at join-child-results.

Two signed receipts close a delegated graph

A join receipt reconciles one fan-in that the graph declares. A terminal graph receipt reconciles the complete graph: its task, join, and route sets, plus budget totals computed from its allocations.

Signed task graphChoose a view.
depth 0depth 13 of maxFanout 3maxDepth 2joinpool · 10,000 usd_minornext task: task-rootρterminal graph receipt4 tasks · 1 join · 3 routes · 7,500 usd_minorκtask-roota1task-child-a2,500a2task-child-b2,500a3task-child-c2,500ρjoin-child-resultsmaxDepth 2 · maxFanout 3pool 10,000ρterminal graph receipt4 tasks · 1 join · 3 routes · 7,500κtask-roota12,500a22,500a32,500ρjoin-child-resultsnext task: task-root

a1task-child-aa2task-child-ba3task-child-c

The terminal receipt accounts for the whole graph

The terminal graph receipt lists all four tasks, the one join, and the three route plans, and its rollup equals the allocations summed by dimension: 7,500 active usd_minor units under the 10,000-unit pool. A subset of any of those sets fails reconciliation. Join & Terminal Receipts.

The signed task graph and budget pool from the valid-recursive-delegation fixture, with widths drawn to the pool’s units; the overdrawn pool is the budget-allocations-exceed-pool fixture and the two-hop chain is the verifier’s source test. Route plans, continuations, and the revocation epoch appear by name only.Task Graphs

The fixture’s terminal receipt lists all four tasks, the root included, the one join, and the three route plans. Each child holds 2,500 usd_minor units of the 10,000-unit pool, so 7,500 is allocated and 2,500 stays unallocated. The verifier checks that each allocation’s counters sum to its maxUnits and that the ceilings fit inside the pool.

Both receipts carry an issuer signature over their canonical body with the signature removed, checked against the same pinned trust set as every other swarm artifact family; closing a graph needs no separate authority role. Every declared join needs exactly one matching join receipt, the bundle needs at least one terminal receipt, and every supplied terminal receipt must pass. These requirements hold wherever the authority verifier runs, its runtime admission path included.

A join pairs each parent task with its receipt by position

In the fixture, join-child-results expects receipts from three child tasks under the predicate all_success, finds all three in its actual list, and names task-root as its next task.

The verifier zips the graph join’s parentTaskIds with the receipt’s expectedParentReceiptIds, keeps the pairs whose receipt id appears in actualParentReceiptIds, and requires the explicit parentTaskReceipts array to hold exactly those pairs, each task id and receipt id unique. Reordering the explicit pairs is harmless. Reordering only the expected list reassigns receipts to different tasks, so build the pairing in graph order.

  • all_success requires the actual and expected id sets to be equal.
  • any_success requires at least one expected receipt id.
  • quorum:N requires at least N expected receipt ids, with 1 ≤ N ≤ the expected parent count.

Every actual id must belong to the expected set, neither list may repeat an id, and the graph requires at least two parent tasks per join. An invalid quorum or an unknown predicate is unsupported. The predicate names describe a selection rule over receipt ids and signed fields; the verifier reads no success verdict inside a parent receipt.

A join authorizes nothing on its own. A join-mode continuation binds the join, the same target task, and the same actual parent ids; the fixture’s join targets the root, which has no continuation token.

Read more in Continuation Tokens.

The terminal receipt must match the graph exactly

Three id sets must equal the validated bundle’s, and the budget rollup must equal the allocations dimension by dimension.

  1. completedTaskIds. Every task id in the graph, the root included.
  2. joinReceiptIds. Every validated join receipt id.
  3. routePlanReceiptIds. Every validated route-plan receipt id.
  4. budgetRollups. The dimension-to-counter map computed from the allocations, with a combined total inside the pool.

A subset fails, and every supplied terminal receipt must reconcile the same complete sets. terminalTaskIds takes a narrower check: its entries are unique and name known tasks, and the verifier derives no graph leaves from it.

For each dimension the verifier sums reserved, active, consumed, released, and reversed units with checked arithmetic. A verified terminal receipt can therefore carry active allocations, as the fixture’s 7,500 units are. These checks relate the supplied artifacts: the swarm verifier loads no parent receipt body, observes no execution, and leaves settlement to the settlement records.

Read more in Sub-Agent Budgets.

A missing parent stops the join before its hash is compared

The parent-set-mismatch fixture keeps all_success with two expected parents and only receipt-child-a actual. Its parent-set hash is recomputed over the shorter list and the receipt is signed, so the predicate check is the one that fails.

An all_success join one parent shorttranscript
$ chio proof verify ./swarm/negatives/join-parent-set-mismatch --trust ./swarm/trust.toml
REFUSED   join-parent-set-mismatch  swarm join receipt parent set mismatch: join-child-results
trust     ./swarm/trust.toml (fixture keys)
exit 1

The retained invocation exits 1 and names join-child-results. The same message covers an unmet quorum, an empty any_success selection, and an unexpected actual id, so read both lists and the predicate to find the cause.

Source tests accept any_success and quorum subsets and reordered explicit pairs. They refuse an unmet quorum, a changed pair, and a terminal rollup re-signed with one unit of drift.

Read more in Swarm Denial Codes.

Issuers mint the receipts and the authority verifier admits the bundle

The public Rust API exports mint_swarm_join_receipt and signing helpers for both receipt types. The mint helper checks for non-empty actual and paired-parent lists and a positive ordinal, computes the parent-set hash, fills the issuer, and signs. Run the full authority verifier over the assembled bundle before relying on it.

The workflow that runs the graph decides when to issue each receipt, gathers the parent evidence, and publishes one consistent bundle to its admission store. The admission hook reuses the authority verifier, so the stored bundle carries its terminal evidence: the request’s seven artifact references name no terminal receipt directly, and the hook validates every terminal receipt in the stored bundle.

Worked Rust example: Join parent receipts and reconcile completion

Join parent receipts and reconcile completion

Mint a join from two task-to-receipt bindings and an unordered list of actual parent receipt IDs. The signed terminal record declares the completed set and the allocation rollup.

Use the Rust example setup, or download the complete source project. The files below belong to that project; shared constructors and configuration are included in the download.

Run it

bash
cargo run --locked --bin terminal

Implementation

terminal.rsrust
use chio_swarm_authority::*;
use swarm_authority_examples::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut bundle = build_bundle()?;
    let join = &bundle.join_receipts[0];
    let request = SwarmJoinReceiptMintRequest {
        join_id: join.join_id.clone(),
        graph_id: join.graph_id.clone(),
        chain_id: join.chain_id.clone(),
        dag_ordinal: 2,
        hlc_unix_ms: now_ms(),
        parent_task_receipts: join.parent_task_receipts.clone(),
        expected_parent_receipt_ids: vec!["receipt-child-a".into(), "receipt-child-b".into()],
        actual_parent_receipt_ids: vec!["receipt-child-b".into(), "receipt-child-a".into()],
        join_predicate: "all_success".into(),
        result_digest: join.result_digest.clone(),
        next_task_id: "task-root".into(),
    };
    bundle.join_receipts[0] = mint_swarm_join_receipt(request, &witness_keypair())?;
    verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())?;
    println!("join: both parent IDs bound, independent of input order");
    println!(
        "terminal: {} declared completed tasks; {} active units",
        bundle.terminal_receipts[0].completed_task_ids.len(),
        bundle.terminal_receipts[0].budget_rollups[0].active_units
    );
    // These are signed declarations; this example does not execute the tasks.
    bundle.terminal_receipts[0].completed_task_ids.pop();
    sign_terminal_graph_receipt(&mut bundle.terminal_receipts[0])?;
    println!(
        "missing completion: {}",
        verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())
            .expect_err("terminal must cover every graph task")
    );
    Ok(())
}

Open src/bin/terminal.rs

lib.rs · terminal_graph_receiptrust
pub fn terminal_graph_receipt() -> Result<SwarmTerminalGraphReceipt, Box<dyn Error>> {
    let mut receipt = SwarmTerminalGraphReceipt {
        schema: CHIO_SWARM_TERMINAL_GRAPH_RECEIPT_SCHEMA.to_string(),
        receipt_id: "terminal-analysis".to_string(),
        graph_id: "analysis-graph".to_string(),
        chain_id: "analysis-chain".to_string(),
        terminal_task_ids: vec!["task-root".to_string()],
        completed_task_ids: vec![
            "task-root".to_string(),
            "task-child-a".to_string(),
            "task-child-b".to_string(),
        ],
        join_receipt_ids: vec!["join-child-results".to_string()],
        route_plan_receipt_ids: vec!["route-child-a".to_string(), "route-child-b".to_string()],
        budget_pool_id: "budget-pool-analysis".to_string(),
        budget_rollups: vec![SwarmTerminalBudgetRollup {
            dimension_id: "usd_minor".to_string(),
            reserved_units: 0,
            active_units: 5_000,
            consumed_units: 0,
            released_units: 0,
            reversed_units: 0,
            total_units: 5_000,
        }],
        revocation_epoch_ref: "revocation-epoch-analysis".to_string(),
        result_digest: sha256_hex(b"joined-child-results"),
        completed_at_unix_ms: now_ms(),
        issuer: witness_issuer(),
        signature: String::new(),
    };
    sign_terminal_graph_receipt(&mut receipt)?;
    Ok(receipt)
}

Open src/lib.rs

Captured result and refusal

Captured executable outputtext
join: both parent IDs bound, independent of input order
terminal: 3 declared completed tasks; 5000 active units
missing completion: swarm terminal completed task set mismatch: terminal-analysis

Removing a completion and re-signing still fails coverage checks. The verifier checks these declarations; it does not read the parent receipt bodies or observe task execution.

Exit 0 indicates a successful example run. Expected refusals shown in the output are checked by the program; invalid command inputs exit nonzero. Keys, digests, timestamps and run-directory names vary.

The downloadable project mints a join from two task-to-receipt bindings and an unordered list of actual parents. Its captured output binds both parent ids regardless of input order, declares three completed tasks with 5,000 active units, and refuses a terminal receipt re-signed with one completion removed.

Join and terminal receipt reference

Parent positions

The fixture’s graph join lists its parent tasks in this order, and the receipt’s expected list follows it position for position.

PositionGraph parent taskExpected receipt
0
task-child-a
receipt-child-a
1
task-child-b
receipt-child-b
2
task-child-c
receipt-child-c

Parent-set hash

Sort actualParentReceiptIds, place them beside chainId in this object, serialize it as canonical JSON, and take SHA-256:

Parent-set hash material · positive fixturejson
{
  "chainId": "swarm-chain-swarm-graph-proof-valid",
  "parentReceiptIds": [
    "receipt-child-a",
    "receipt-child-b",
    "receipt-child-c"
  ]
}

The resulting parentSetHash is e23cfc5d097cbce5f0ec16529869e57ddeaadb67f8fb3e561ae1c3125993657e, and the verifier recomputes it. The hash binds the actual id list and the chain string; the signature covers the receipt’s other fields too.

The kernel’s receipt-lineage helper hashes a sorted, deduplicated bare id array, a different recipe from this swarm object. Use the recipe that belongs to the artifact you issue.

Budget rollup reconciliation

The fixture’s three allocations each hold 2,500 active usd_minor units. The terminal rollup holds 7,500 active units, zero in the other four counters, and a total of 7,500 under the 10,000-unit pool.

Every declared rollup total must equal its own five counters, the complete rollup map must equal the allocation-derived map, and the combined total must fit the pool. A verified terminal receipt can retain active allocations. Its completion timestamp is a producer declaration held to a future-time check, so finished execution and realized payment come from their own records.

Join receipt fields

The schema requires fifteen fields, Rust uses the matching snake_case names internally, and the wire type rejects unknown fields.

FieldContract
schema
chio.swarm.join-receipt.v1.
joinId
Names one graph-declared join, with no second receipt for that id.
graphId
Equals the supplied graph id.
chainId
A nonempty string included in the parent-set hash.
parentSetHash
A lowercase SHA-256 digest recomputed from the chain and sorted actual ids.
dagOrdinal
Positive. This stage does not compare it with parent ordinals.
hlcUnixMs
At or before evaluation time. It may be zero and is not ordered against parent timestamps.
parentTaskReceipts
The exact task/receipt pairs selected by position and actual membership.
expectedParentReceiptIds
Unique ids whose count equals the graph join’s parent count. Preserve positional correspondence.
actualParentReceiptIds
A unique subset satisfying the selected predicate.
joinPredicate
all_success, any_success, or a valid quorum:N.
resultDigest
A signed, lowercase SHA-256 digest. The result payload is not recomputed here.
nextTaskId
Matches the graph join’s next task and names a known node.
issuer
A key accepted by the shared swarm issuer parser and present in the pinned trust set.
signature
Verifies the canonical receipt with only signature removed.

Terminal graph receipt fields

The schema requires fifteen fields and the wire type rejects unknown fields.

FieldContract
schema
chio.swarm.terminal-graph-receipt.v1.
receiptId
Nonempty and unique among the bundle’s terminal receipts.
graphId
Equals the supplied graph id.
chainId
Nonempty. It is not cross-checked with the joins’ chain ids.
terminalTaskIds
Unique known task ids. Leafness and completeness are not checked.
completedTaskIds
The complete graph task set, with no duplicates.
joinReceiptIds
The complete validated join receipt set, with no duplicates.
routePlanReceiptIds
The complete validated route receipt set, with no duplicates.
budgetPoolId
Equals the bundle’s budget pool id.
budgetRollups
Unique dimensions and exact allocation-counter sums; each total equals its five counters.
revocationEpochRef
Equals the bundle’s epoch id.
resultDigest
A signed, lowercase SHA-256 digest. No result payload is fetched or recomputed.
completedAtUnixMs
At or before evaluation time, with no maximum age or cross-artifact causal-order check.
issuer
A key in the shared pinned trust set.
signature
Verifies the canonical receipt with only signature removed.

Schema validation and authority verification

The JSON schemas require bare 128-character lowercase hexadecimal signatures; Rust also accepts the signature encodings its key parser supports. A schema check covers shape only. Trusted signatures, graph membership, parent pairing, current time, and allocation sums belong to the authority verifier.

The terminal schema requires nonempty id arrays and budgetRollups. The terminal Rust stage takes its set and map comparisons from the supplied graph instead of those local minimum lengths, and the full verifier’s continuation and witness requirements still apply to every bundle.

Result digests and timestamps

A resultDigest is an issuer-signed commitment; checking that the payload matches it is a separate step. The shape and time rules order no timestamps across artifacts. Per-call lineage and parent ordering follow the Receipts & Audit contract.

Refusal diagnostics

Diagnostic fragmentInspect
swarm join receipt parent set mismatch
The selected predicate and actual membership in the expected set.
swarm join receipt parent set hash mismatch
The exact chain string and canonical sorted actual-id object.
swarm join receipt parent task receipts mismatch
The positional graph/expected pairing against every explicit task/receipt pair.
missing swarm terminal graph receipt
The bundle needs at least one terminal receipt.
swarm terminal completed task set mismatch
All graph task ids, including the root.
swarm terminal budget rollup mismatch
Every dimension and each of its five allocation counters.

An unmet supported predicate and an unexpected actual id share the parent set mismatch diagnostic.

Execution-lease join checks

The transaction-passport execution-lease path reads the same join wire shape with the same canonical body recipe, under different acceptance rules:

CheckSwarm authority bundleExecution-lease path
Parent-set hash
Recomputed with chainId.
Digest shape checked.
Parent selection
The selected predicate and subset rule.
Expected and actual lists equal element for element; the lease’s parent must appear.
Task pairing
Every actual pair matches the positional graph mapping.
The lease’s parent receipt is paired with the child’s declared parent task.
Ordering fields
Positive ordinal; timestamp not in the future.
Both ordinal and timestamp positive, without a clock comparison here.
Issuer trust
The supplied swarm key pins.
The runtime trust roots and accepted runtime identity contract.
Terminal closure
Required for the full bundle.
This join/lease check does not validate a terminal receipt.

The lease path also binds joinId, graphId, and nextTaskId to its lease and task. Each checker’s verdict stands for its own contract, even when the signature body agrees.

Sources

Next: authorize the step after a join