Production, resources, and transport: who owns what
Use this page when your game or model has stock that can run out, such as grain, cloth, ammunition, or building materials. It shows which crate owns each step, the order the steps happen in, and the rules for demand sources, access grants, loss, exchange, local acceptance, production output, and custom consumers.
The optional canwu-resource and canwu-production extensions handle this. The simulation core stays generic: grain, factories, prices, historical deposits, and armies live in extensions, content, and your application.
Who owns what
Section titled “Who owns what”| Owner | Owns |
|---|---|
| Simulation core | Command admission, deterministic time, phase order, atomic commit, rollback, visibility, persistence, and replay |
canwu-resource |
Resource and unit revisions, accounts (ResourceAccount), protected floors, demands (ResourceDemand), reservations (ResourceReservation), allocation legs, transfers, consumption, loss, and fulfillment |
canwu-production |
Processes, production sites, facilities, capacity, work orders, work in progress, execution, projects, and output settlement. It uses resource outcomes and technology evidence and keeps no inventory of its own. |
canwu-transport and canwu-movement |
Itineraries, legs, custody handoffs, and delivery completion; canwu-movement runs movement executions and capacity pools. Cargo balance, loss, acceptance, return, and destination credit stay in canwu-resource. |
| Your population, military, and other domains | Forming demand and reacting to fulfillment results |
| Your application | Money, markets, wages, building UI, combat formulas, and historical content |
canwu-resource is the only writer of stock balances. Other domains submit resource operations and read the outcomes; they must not write balances or another domain’s records. All of these crates sit on top of the public canwu-api.
Key terms
Section titled “Key terms”| Term | Meaning |
|---|---|
| Account | One stock balance (balance) of one exact resource and unit revision, held by a custodian: the holder responsible for that stock. |
| Protected floor | An amount an account holds back from ordinary allocation, such as seed grain (ProtectedFloorPolicyRevision). A demand whose protection_override_class the policy lists may draw below it. The scenario authors floors, and an account’s custodian can also set one at runtime; see protected floors. |
| Demand | A requester’s request for a quantity, with a minimum useful amount, a partial-fulfillment policy, due and expiry times, a priority, and a source policy. |
| Reservation and allocation leg | The result of allocation: a quantity set aside for one demand in one account. A released or expired reservation frees the quantity again. |
| Transfer escrow | Stock that has left its source account and is in transit. It stays counted until it is accepted, lost, or returned. |
| Completion lease | A holder’s right to perform one irreversible operation, named by its operation key. Activating the lease yields a CompletionLeaseActivationCertificateV1, which pins the operation key, the time, and the exact records the operation depends on. Irreversible requests carry this certificate. A lease takes four operations, or one when the resource plugin is its only participant; see completion leases. |
| Adapter ingress | The path provider plugins use (enqueue_resource_adapter_operation). Each operation cites the exact provider record version that justifies it. |
A tracked command is a command sent with a CommandRequest (request ID and expected revision). Build the resource command with resource_command(&ResourceCommandV1 { subject, request }); the issuer must control subject.
Lifecycle
Section titled “Lifecycle”View diagram source
flowchart TB
Demand["Demand: requester sends SubmitDemand command"] --> Alloc["Allocation: host queues enqueue_resource_allocation, canwu-resource reserves stock"]
Alloc --> Consume["Consume: consumer plugin via adapter ingress"]
Alloc --> Begin["BeginTransfer: source custodian or grantee command, stock moves to escrow"]
Begin --> Move["Transport: canwu-transport and canwu-movement"]
Move --> Accept["Accept via adapter ingress"]
Begin -. "same place_scope" .-> Local["AcceptLocal: destination custodian"]
Move --> LoseReturn["Lose or Return: transfer controller"]
Accept --> Credit["Destination account credited"]
Local --> Credit
Credit --> Later["Later boundary: consumers use the stock"]
- Demand. The requester submits
SubmitDemand(and laterAmendDemandorCancelDemand) as a tracked resource command. Adapter ingress cannot submit or amend demands. - Allocation. The host queues an allocation pass for one requester with
enqueue_resource_allocation.canwu-resourceallocates deterministically, using protected floors, minimum useful amounts, partial-fulfillment policies, due times, priorities, and stable tie-break keys, and records reservations and allocation legs. - Debit. A consumer plugin consumes an allocation through adapter ingress (
Consume), or the source custodian (the grantee, for a granted allocation) starts a transfer (BeginTransfer) that moves the quantity into escrow. - Transport.
canwu-transportandcanwu-movementcarry the cargo. Transfer progress (AdvanceTransfer) arrives through adapter ingress and cites transport evidence. For a multi-leg execution the provider sends anotherAdvanceTransferfor each later leg or handoff, an intermediate leg; the transfer records each in order in its link history (link_history, at most 16 entries, 14 in transit). - Arrival.
CompleteTransfersettles the transfer:Acceptthrough adapter ingress credits the destination;AcceptLocalhandles a same-place handover;LoseandReturnend it otherwise. - Use. Production, force supply, and other consumers use the credited stock in a later boundary. They cite exact allocation and fulfillment evidence and write only their own records.
Every transfer step names the transfer’s expected_transfer_revision, so a stale or repeated step is rejected and one arrival cannot be credited twice. If a conservation, revision, or authority check fails, the operation’s changes roll back together.
Each operation has an operation key. Sending the same request again under its key returns the recorded outcome and settles nothing. A tracked command that reuses a key for a different request is refused at admission with IdempotencyConflict; if it was admitted before the first request settled, its packet is skipped with an operation_key_taken event and the outcome under the key names the request that owns it. Neither fails a boundary, and no packet that does not fit the resource root does either: it is skipped with an event and sent again when room returns, or, for work another plugin scheduled, deferred (see skipped packets and deferrals).
canwu-resource checks conservation with ResourceState::validate_conservation over ConservationTotalsV1:
closing balances + closing transfer escrow = opening balances + opening escrow + admitted production + external inflow - admitted consumption - admitted loss - external outflowOnly balance is stored. Available, reserved, and protected amounts are derived (ResourceState::account_quantities). A route estimate, an accepted arrival, and a consumption are three separate facts, and each needs its own step.
For a runnable fourteen-month grain loop, run cargo run -p canwu-economy-reference --example grain_loop and read the grain, production, and military supply case.
Completion leases
Section titled “Completion leases”Every irreversible request (a consumption, a transfer start, a terminal transfer disposition, a credit, an external outflow, an account-level loss, a partial transfer loss) carries the activation certificate of a completion lease. Queue lease operations with enqueue_resource_completion_operation. A lease’s recipe may reserve a completion report for a holder: an Acquire, SingleStepLease, or GrantExternalParticipant that reserves one for a holder the engine refuses knowledge is a recorded invalid_authority rejection, and a reserved report is given up when its grant has no observation head, or the engine refuses the holder when the report falls due, so the participant archives without it and nothing wakes the report system again. To receive the mandatory report, the provider records the holder’s observation head at or after the completion’s revision, citing only current versions as its sources.
A lease can have several participants, each of which grants and prepares on its own, so the general path takes four operations: Acquire, Grant, Prepare, and Activate. Each leaves an operation outcome and a lease receipt. Use it when the lease needs a grant owned by another plugin, as an adapter operation does, or when another plugin coordinates the lease, as canwu-production and the force-supply reference do.
When the resource plugin is the only participant, take the lease in one operation, a single-step completion lease:
ResourceCompletionOperationV1::SingleStepLease(SingleStepCompletionLeaseV1 { lease, // RequestCompletionLeaseV1, as for Acquire grant_id, // the one grant, owned by canwu-resource target_versions, // every exact target the lease locks current_boundary,})- When it applies. “Only participant” means exactly that
lease.expected_participantsis{"canwu-resource"}. That is the lease behind a trackedBeginTransfer, an exchange leg,CompleteTransferwithAcceptLocal,Lose, orReturn,ExternalOutflow, external-inflowCredit,RecordLoss, andRecordTransferLoss. A single-step request that names any other participant set is refused withinvalid_lifecycle. - Same checks, same certificate. The operation runs the four steps in order with the checks each has on its own. Its certificate equals the one the four steps produce for the same acquisition, grant, and targets at the same boundary, so the request that carries the certificate is unchanged.
- Fewer records. It leaves one
CompletionLeaseoutcome, keyedresource:completion:lease:{acquisition}, and one receipt, where the four steps leave four of each. In the committed fixture, a same-place transfer leaves 15 terminal records and makes 9 archive lookups instead of 27 and 15. - How it enters. A tracked holder command cannot carry it, because an active lease does not expire and holds its target locks. Your host queues it, or a plugin that declares the resource completion ingress schedules it.
- Refusals are recorded. A domain refusal is the operation’s rejected outcome, and it leaves no acquisition, grant, lock, or spent request token behind. A refused request has used its operation key, so retry under a new acquisition ID. A resend of the same request returns its recorded outcome and is not a second lease. Neither path resumes an acquisition the resource record already holds: a single-step request or an
Acquirethat names one is a recordedidempotency_conflictrejection. What the plugin finds against the simulation view (an eligibility time that is not the boundary time, an external record version that is not current) is a recorded rejection too, where the four-step path skips the packet.
Room is measured on the result, as for every resource operation. A refused single step needs room for its outcome only. An applied one takes 2 + 16 archive candidate slots and 1 + 16 hot outcome slots, beside the 16 of each that every active lease already reserves: with the canonical budget of 256 and no backlog, the fifteenth concurrently active lease is the last that fits. One that does not fit is not failed but skipped, with archive_backlog when an archive commit would make room and reserved_capacity when the room is held by the reservations of active leases, which only their completion, release, or expiry frees; send it again when room returns. A failed archive lookup (a missing or damaged archive object, never a miss) still fails the boundary.
Two things to watch:
- An active lease never expires, and a single-step lease is active at once. If its operation is never issued, or is rejected, the lease keeps its target locks and its 16 reserved slots until your host sends
Release. - A holder may still send a tracked
Acquire. It must be eligible at the instant it is admitted and cite current evidence, and while that acquisition waits for its first grant, every single-step request that sorts after it in the fairness order is refused withinvalid_lifecycle. A host that uses only single-step leases watches for such acquisitions and aborts them. If the holder’s own rejectedAborthas already used the abort key, grant the stray acquisition and release that grant instead.
An ordinary request cannot use an operation key under the reserved resource:completion: prefix; one that does is skipped with reserved_operation_key. A holder can still take a completion key with the two completion operations it may send as tracked commands: an Acquire, and an Abort of a lease it holds. A packet whose key a different request already holds settles nothing on any ingress, and its boundary commits; the skip event names it with operation_key_taken, and the outcome under the key is the other request’s.
Lease bindings
Section titled “Lease bindings”A completion lease’s targets say what its operation may touch, not which operation it is: an outflow, an inflow, and an account loss with a record cause all lock an account revision and one record version. The lease’s issuer (your host, or the plugin that coordinates the lease) can bind it to one operation: EligibilityEnvelopeV1::bound_to sets the envelope’s operation_binding (CompletionOperationBindingV1::new(kind, quantity), and for a CompleteTransfer lease optionally with_disposition). The acquisition stores the envelope’s digest and the activation certificate carries it, so a binding cannot change after the lease is requested. An unbound envelope encodes and digests as in 0.18.0.
-
Only the issuer binds. A holder’s tracked
Acquirewhose envelope carries a binding is a recordedinvalid_authorityrejection. -
Every redemption is checked. Redeeming a bound lease for another kind, another disposition (when the binding names one), or another quantity is a recorded
invalid_authorityrejection. The bound quantity is the credit, outflow, or loss quantity; the allocation leg’s quantity for a consumption or a transfer start (each leg’s own for an exchange); the items’ legs together for a consumption batch; and the escrow a terminal transfer disposition settles. -
Tracked commands are refused early. A tracked command is refused at admission, settling nothing and taking no operation key, when:
- its lease includes another plugin’s participant grant or names another plugin as an expected participant (such a lease authorizes that plugin’s operation, which arrives through adapter ingress);
- the request itself shows that the lease’s binding does not admit it; or
- it is an external-inflow
Creditwith an unbound lease.
The operation the lease was issued for can still settle under its own key. Settlement checks again: another plugin’s lease that appeared since admission is skipped with
failed_settlement_checksand takes no key; a lease whose binding changed, a disposition whose escrow differs from its binding, and an adapter redemption the binding does not admit are recordedinvalid_authorityrejections, which take their key.
Protected floors
Section titled “Protected floors”An account’s protected floor is the ProtectedFloorPolicyRevision that its protected_floor_policy names. A revision is immutable and is one of two kinds:
| Kind | Identity | Who installs it | How long it stays |
|---|---|---|---|
| Authored | Chosen by the author | The scenario, with ResourceState::install_protected_floor_policy |
Always |
| Content-addressed | Derived from the content, as resource:protected-floor:{digest} |
The account’s custodian at runtime, or the scenario | Only while an account or a demand refers to it |
To set a floor of any quantity during a run, the custodian builds a content-addressed revision and sends its body in install and its ID in policy. SetProtectedFloor installs the revision, if the resource record does not hold it yet, and points the account at it, together or not at all:
let floor = ProtectedFloorPolicyRevision::content_addressed( ResourceRevision::INITIAL, 180, BTreeSet::new(),)?;ResourceOperationRequestV1::SetProtectedFloor(ResourceProtectedFloorRequestV1 { operation_key, account, expected_account_revision, policy: Some(floor.id.clone()), install: Some(floor),})- Always send the body. A content-addressed revision is named with its body every time. Its ID alone is refused with
invalid_definition, whether or not the record holds the revision, so the answer tells a custodian nothing about what other accounts protect.policyalone names an authored revision, orNoneto clear the floor. - Authority. The operation needs no completion lease, leaves one
SetProtectedFlooroutcome, and advances the account revision. A tracked command must come from the account’s custodian. An access grant is consent to draw on stock, not custody, so a grantee cannot set the grantor’s floor. - One shared quota. The record holds at most 4,096 revisions of both kinds (
MAX_PROTECTED_FLOOR_POLICY_REVISIONS). A runtime install past it settles as a recordedlimit_exceededrejection. The quota is shared by every custodian and is below the canonical 8,192 accounts, so if your game lets holders set floors, limit how many distinct floors each may hold. - Unused revisions are retired. Every applied
SetProtectedFloorremoves the content-addressed revisions that no account and no demand refers to. A floor that changes every day keeps one revision. Accounts that set equal content share one. A demand keeps the revision it cites until the demand is archived. - Keep the body if you need it later. Retirement removes the only copy of the body the engine holds. An archived demand keeps the ID of the floor it was admitted under and nothing else.
- Open demands go stale. A floor change advances the account revision. An open demand cites the floor it was admitted under, so its allocation pass settles as a
version_conflictrejection until the demand is amended to the floor in force.
Demand source policy
Section titled “Demand source policy”Every ResourceDemand stores a source_policy (ResourceDemandSourcePolicyV1) that picks which accounts may supply it:
| Policy | Eligible accounts |
|---|---|
Pooled (default) |
Every open account with the matching resource and unit revisions, visited in account-ID order |
ExactAccounts(accounts) |
The listed accounts only. Each must exist, be open, match both revisions, and have the requester as custodian. |
Granted { grant_id, accounts } |
The listed accounts only. Each must be custodied by the grantor of the named access grant, and the requester must be its grantee (see delegated access grants). |
Rules:
ExactAccountsandGrantedlists hold 1 to 256 accounts (MAX_DEMAND_SOURCE_ACCOUNTS), in strictly increasing ID order with no duplicates. An invalid list rejects the command before any stock changes, and allocation checks the list again.- Allocation computes available supply, minimum useful quantity, and partial fulfillment from the selected accounts alone. If the listed accounts cannot cover the demand, the demand gets only what they hold. Protected floors and later transfer and consumption checks still apply.
- You can change the policy with the expected demand revision until the demand’s first allocation. After it has any reservation (including a consumed one that backs an in-flight transfer) or any fulfillment, the policy is fixed; cancel the demand and submit a new one. Cancelling a demand leaves transfers it already started in place.
AmendDemandcannot change a demand’s status, rejection reason, or requester; such an amendment is recorded as a rejection. It also cannot ask for less than what the demand already holds: fulfilled, actively reserved, and in flight together (a returning transfer does not count). Such an amendment is a recordedinvalid_lifecyclerejection. Terminal demands cannot be amended.
Authority: Pooled and ExactAccounts choose where supply comes from; they give no right to spend another custodian’s stock. Your application controls who may submit pooled demands and under which requester. To let one custodian draw on another’s stock, with a cap and a time window, use an access grant and a Granted policy. Appointments, purposes, and who may grant to whom stay with your application.
Persistence: the policy is part of request digests, runtime snapshots, exact replay, and terminal demand archives. Holder-relative reports do not reveal the source list. A JSON demand without source_policy deserializes as Pooled.
Delegated access grants
Section titled “Delegated access grants”An access grant (ResourceAccessGrantV1) lets a grantee draw on a grantor custodian’s stock without a prior transfer. It records:
grantor_custodianandgrantee;- the exact
resource_revisionandunit_revision; cap_quantity, the most the grant can ever supply;- the window
valid_from..valid_until(end exclusive); authority_evidence, the exact record version that justifies the grant, such as your application’s accepted requisition record.
The grantor issues it as a tracked command:
use canwu_api::{CommandEnvelope, CommandRequest, CommandRequestId, Issuer};use canwu_resource::{ ResourceCommandV1, ResourceIssueAccessGrantRequestV1, ResourceOperationRequestV1, resource_command,};
let command = resource_command(&ResourceCommandV1 { subject: grant.grantor_custodian.clone(), request: ResourceOperationRequestV1::IssueAccessGrant(ResourceIssueAccessGrantRequestV1 { operation_key, grant, // a ResourceAccessGrantV1 }),})?;canwu.enqueue_command( now, 0, CommandRequest::new( CommandRequestId::new(41), canwu.revision(), CommandEnvelope::new(Issuer::Actor(grantor_person), command).at_time(now), ),)?;How a grant is used:
- Issue. The grantor custodian submits
IssueAccessGrantas a tracked command. The authority evidence must be the exact current version of its record; an earlier version is rejected. Adapter ingress cannot issue or revoke grants. - Demand. The grantee submits a demand with
ResourceDemandSourcePolicyV1::Granted { grant_id, accounts }. The grant must be active and name the requester as grantee; the resource and unit must match; the demand’s due and expiry times must fall inside the grant window; every listed account must be open and custodied by the grantor. - Allocate. Allocation checks the grant before it divides scarce supply among demands. A revoked or out-of-window grant supplies nothing, and no allocation exceeds the remaining cap. Supply comes only from the listed accounts.
- Debit. The grantee debits the allocation, by consumption or by starting a transfer or exchange leg, under its own completion lease. A lease held by anyone else, including the grantor, cannot debit a granted allocation. The debit settles only in a boundary whose time equals the lease’s certified time, and the grant window must contain that time.
Accounting: ResourceAccessGrantRecordV1 keeps cap_quantity = remaining + reserved + debited. Allocation moves quantity from remaining to reserved, and the debit moves it to debited, so each unit is charged once. A released or expired reservation returns its quantity to remaining. Expired reservations of a granted demand are released by the next allocation pass for the grantee, which the host queues with enqueue_resource_allocation.
Transfers: a transfer started from a granted allocation records its access_grant, and the grantee then controls its cancellation, return, and loss. Local acceptance still belongs to the destination custodian.
Revocation: the grantor submits RevokeAccessGrant with the expected grant revision. Once anything has been reserved or debited under the grant, revocation is refused, so choose the cap and window to fit the delegation.
Limits and reads: grants, including revoked ones, stay in the resource root for its life, in live buckets, up to 16,384 per resource state (MAX_RESOURCE_ACCESS_GRANTS); they are not archived. Control who may issue them as you control account creation. resource_access_grant_status(canwu, holder, grant_id) returns a grant and its accounting to its grantor or grantee.
For example, a requisition flow in your application could first record the population owner’s acceptance. The owner then issues a grant citing that record, the army submits a Granted demand, and the army’s own lease debits the allocation. The access-grant test issues grants, allocates under them, debits by transfer, and revokes them through the public API.
Account-level loss
Section titled “Account-level loss”ResourceOperationRequestV1::RecordLoss(ResourceAccountLossRequestV1) records spoilage, theft, or any other loss directly on one account. The request carries a loss_id, the account’s expected revision, a quantity, a cause, allow_protected, a time, and a completion certificate.
- It settles as a
ResourceLosswithaccount: Some(..)andtransfer: None, counted as admitted loss in the conservation totals. - The debit leaves reserved stock untouched and respects the protected floor unless
allow_protectedis set. A stale account revision is rejected. - As a tracked command it must come from the account’s custodian. Through adapter ingress, the cause must be the provider’s exact source record.
- Holder reports include
ResourceLossObservationV1entries, visible under the same rules as transfer details.
Partial transfer loss
Section titled “Partial transfer loss”ResourceOperationRequestV1::RecordTransferLoss(ResourceTransferLossRequestV1) records the loss of part of an open transfer’s escrow, such as grain that spoils on the way. The request carries a loss_id, the transfer, its expected revision, a quantity, a cause, a time, and a completion certificate.
- The transfer keeps its state and the rest of its escrow: the quantity moves from
escrowtolost, the transfer revision advances, and aResourceLosswithtransfer: Some(..)counts as admitted loss. It is allowed in every state that is not terminal, any number of times. - The quantity must be above zero (
invalid_definitionotherwise) and below the escrow (invalid_lifecycle). To lose everything that is left, end the transfer withCompleteTransferandLose, which loses whatever the partial losses left. - The certificate locks the transfer at the expected revision and, for a record cause, that record version. The loss completes only its own lease; the lease the transfer was started under stays until its terminal disposition. A lease that already locks the transfer’s current revision for a terminal disposition keeps the loss’s lease from locking it: release that lease, and take a new one for the revision the loss leaves.
- Because the transfer revision advances, a queued advance or cancellation that expects the earlier revision is rejected with
version_conflict. - As a tracked command it must come from the transfer’s controller, as for
Lose: the source custodian, or the grantee of a transfer started under an access grant. Through adapter ingress, the cause must be the provider’s exact source record. - Acceptance credits the demand with what is accepted, not with the allocation: a transfer of 100 that lost 30 and is accepted records a fulfillment of 70, so the demand keeps 30 of remainder, which the next allocation pass may supply. An allocation pass counts what open transfers carry toward a demand as drawn, so escrow in flight is never allocated twice; a returning transfer is never accepted, so its escrow is needed again as soon as it turns back. A demand that ends while a transfer for it is in flight keeps its terminal status when the transfer is accepted, and its records stay in the live root until the transfer ends.
- The transfer does not list its losses; each loss names the transfer, and
losttotals them. A loss identity is used once for the run, even after its loss is archived.
Atomic exchange
Section titled “Atomic exchange”ResourceOperationRequestV1::BeginExchange(ResourceExchangeStartRequestV1) starts two transfers together: both are created, or neither is. The single outcome lists both transfer IDs in cited_transfers, whether it applied or was rejected.
- The parties agree on
ResourceExchangeTermsV1: the exchange operation key and, for each leg, its transfer ID, exact allocation, and destination. ResourceExchangeTermsV1::leg_operation_keysderives each leg’s operation key from the digest of those terms.- Each leg’s source custodian (or grantee, for a granted leg) acquires a completion lease for that leg’s derived key. The lease therefore consents to the whole exchange and fits no other terms.
- The debit holder of
leg_asubmits the exchange as a tracked command.
The exchange has no certificate of its own. After the start, the two transfers settle independently, so one may be accepted while the other is lost or returned. Each side spends only stock it holds or may draw under a grant.
Local acceptance
Section titled “Local acceptance”ResourceTransferDispositionV1::AcceptLocal { destination, expected_destination_revision, handover_evidence } settles a transfer without a transport execution, for example a handover inside one warehouse or town. It applies only while the transfer is PendingDispatch with no transport link, and only when both accounts declare the same ResourceAccount::place_scope.
- The scenario declares
place_scopewhen it installs an account; afterwards only the host application changes it, withenqueue_resource_account_rescope, citing the current version of the record of the move. Canwu does not infer it. The default isNone, which leaves local acceptance unavailable for that account. A trackedCreateAccountcommand cannot set it. - A rescope (
ResourceRescopeAccountRequestV1 { operation_key, account, expected_account_revision, place_scope, evidence }) needs no completion lease and advances the account revision. Its evidence must be the current version of a record of a kind passed toResourcePlugin::new; the engine checks only that the evidence is current and of an allowed kind, not that it shows the stock at the new place. It is refused while a transfer that is not terminal names the account or a lease locks the account’s current revision. Reservations, demands, and access grants that name the account carry over. - A missing or mismatched scope is rejected as
invalid_definition; a transfer with a transport link, asinvalid_lifecycle. - The terminal certificate locks the exact handover record. As a tracked command,
AcceptLocalmust come from the destination custodian; through adapter ingress, the handover record must be the provider’s source. - Both legs of an exchange may be accepted locally against one handover record, but not in one boundary: the first leg’s terminal certificate locks the record’s exact version from its activation until its completion. Settle the second leg in a further boundary at the same simulated instant (a host may settle several boundaries at one instant; take the second leg’s lease once the first acceptance has settled, as each holder’s lease cooldown allows), or give each leg its own handover record.
External inflow
Section titled “External inflow”Credit with ResourceCreditSourceV1::ExternalInflow(DomainRecordVersionRef) books value from outside the modelled accounts, such as tribute or a purchase from abroad, the counterpart of ExternalOutflow. It counts in ConservationTotalsV1::external_inflow, and its applied outcome keeps the cited record version as evidence.
- As a tracked command from the account’s custodian, it needs a single-step lease that locks the account revision and the inflow’s record version and is bound to exactly that credit: build the lease’s eligibility envelope with
bound_to(CompletionOperationBindingV1::new(ResourceOperationKind::Credit, quantity))and queue the lease withenqueue_resource_completion_operation. A tracked inflow whose lease is unbound, or bound to another kind or quantity, is refused at admission, so a custodian books only the quantity your host bound, once. - Through adapter ingress, a provider plugin may carry it with the record as its provider source, under its own participant grant. No binding is needed there, and a bound lease is checked as any other.
- The engine sets no cap on inflow: what bounds a holder’s inflow is the leases your host issues. A production credit stays refused as a tracked command and on adapter ingress; it settles only through the production output batch.
Production output
Section titled “Production output”Realized output
Section titled “Realized output”ProductionOperation::CompleteExecution may carry realized_output_per_mille and realization_evidence. The ratio is in thousandths of the process’s nominal output; None means 1,000. At completion, every output settlement quantity is scaled by the ratio and rounded down, and both values are stored on the execution. The resource credit and the output acknowledgement then settle exactly the scaled quantities.
- A ratio other than 1,000 needs an evidence record of a kind listed in the process revision’s
realization_evidence_kinds. The list is empty by default, so a default process accepts only nominal output. Evidence sent with the nominal ratio is rejected. - The evidence must be the exact current version of its record. An earlier version is rejected even while the run still keeps it, so a run sealed with
seal_evidenceaccepts the same completions as its exact replay. - The ratio must be positive and must not scale any output leg to zero. To record a total loss, cancel the work order.
- The ratio may not exceed the process revision’s
max_realized_per_mille, which defaults to 1,000; set it higher to accept evidenced yields above nominal. - Holder, lifecycle, ratio, and kind rules run before the evidence record is looked up, so a rejection reveals nothing about whether another record exists.
How output settles
Section titled “How output settles”After an execution completes, a phase-12 system in canwu-production pins the current production runtime record version on the execution as its output_source and schedules one output batch to canwu-resource. The resource plugin credits each output account through its production output batch ingress, which requires the credit to cite exactly that pinned source, every credit or none. The generic adapter ingress rejects production credits. A batch that finds no room is deferred until an archive commit or a lease release makes room, and its acknowledgement carries settled_at, the instant the credits settled, which the execution records as output_settled_at and the archive rebuilds the request digests with. StartExecution refuses an output key that could never be credited (one under the reserved prefix, one another unsettled execution names, or one a resource outcome already holds), more than 64 outputs, and an output set the resource root can never hold, so an execution never starts into a batch that must be skipped.
Authorize a custom consumer
Section titled “Authorize a custom consumer”A consumer plugin of your own can consume allocations through the ResourceConsumptionIntentV1 contract:
- In your plugin’s own active domain record, keep a top-level
resource_consumption_intentsmap. Each key is the sealed intent’sid. No callback or reference-provider name is needed. - Pass that record kind to
ResourcePlugin::newso the resource adapter may read the exact source. - Before submitting, complete the allocation and completion-lease steps.
- Submit
ResourceOperationRequestV1::Consumethrough adapter ingress (enqueue_resource_adapter_operation). Itsconsumer_evidencemust equal the provider’s exact current source record, and the completion certificate must lock that source and use the same operation key. The usual holder, participant, time, revision, and lease checks apply.
The resource plugin accepts the consumption only when exactly one Authorized intent in the map matches the whole allocation leg, the demand and account revisions, the consumption ID, and the operation key. Consuming part of a leg is outside this contract. The map may hold at most max_operation_outcomes entries (from the resource state’s ResourceLimitsV1); a larger map is rejected before any entry is decoded. Missing or malformed maps, identity or digest mismatches, and retired, ambiguous, or mismatched intents are rejected, and nothing is debited. A sealed digest proves the intent’s content is consistent; the permission comes from your plugin authorizing it.
Your plugin owns authorizing and retiring intents and any local consequences after the resource receipt. canwu-resource owns the conserved balance and duplicate-operation handling. The force-supply reference consumer (canwu-force-supply-reference) keeps its own adapter, which also accepts an earlier version of its source record; custom consumers use the current-source contract above.
Versions and saves
Section titled “Versions and saves”Pin exact versions of canwu-api, canwu-resource, canwu-production, and the plugin semantic hashes from one release line. Loading restores the saved Canwu snapshot and revalidates extension records against the plugins you pass. In 0.20.0 the resource state format is 6 and both resource bucket formats are 2; a continuation checkpoint or seal taken while a completion lease is in progress loads. See save compatibility.
Continue with the grain, production, and military supply case and the mechanism decision.