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.
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.
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 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_successrequires the actual and expected id sets to be equal.any_successrequires at least one expected receipt id.quorum:Nrequires 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.
completedTaskIds. Every task id in the graph, the root included.joinReceiptIds. Every validated join receipt id.routePlanReceiptIds. Every validated route-plan receipt id.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.
$ chio proof verify ./swarm/negatives/join-parent-set-mismatch --trust ./swarm/trust.tomlREFUSED join-parent-set-mismatch swarm join receipt parent set mismatch: join-child-results trust ./swarm/trust.toml (fixture keys)
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
cargo run --locked --bin terminalImplementation
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(())
}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)
}Captured result and refusal
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-analysisRemoving 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
Try parentSetHash, budgetRollups, or a diagnostic fragment.
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.
| Position | Graph parent task | Expected 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:
{
"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.
| Field | Contract |
|---|---|
schema | chio. |
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. |
parent | The exact task/receipt pairs selected by position and actual membership. |
expected | Unique ids whose count equals the graph join’s parent count. Preserve positional correspondence. |
actual | 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.
| Field | Contract |
|---|---|
schema | chio. |
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. |
completed | The complete graph task set, with no duplicates. |
joinReceiptIds | The complete validated join receipt set, with no duplicates. |
route | The complete validated route receipt set, with no duplicates. |
budgetPoolId | Equals the bundle’s budget pool id. |
budgetRollups | Unique dimensions and exact allocation- |
revocation | Equals the bundle’s epoch id. |
resultDigest | A signed, lowercase SHA-256 digest. No result payload is fetched or recomputed. |
completed | 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 fragment | Inspect |
|---|---|
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:
| Check | Swarm authority bundle | Execution-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
- Join issuance, predicate and pairing checks, and terminal reconciliation.
- Join receipt schema and terminal receipt schema.
- The positive join,
./swarm/proof-room-bundle/join-receipt.json, its terminal receipt beside it, and the shortfall at./swarm/negatives/join-parent-set-mismatch/join-receipt.json, all written bychio proof fixture generate recursive-runtime-swarm --with-negatives --out ./swarm. The parent hash and the set and budget relationships on this page were recomputed from these files. - The execution-lease join checks.