Introduction
The worktop is the resource staging area that exists for the life of a single transaction intent. When a manifest withdraws tokens from an account, the bucket the account returns lands on the worktop, and it sits there until a later instruction takes it off β to deposit it somewhere, or to pass it into a method call. Nothing in a manifest can hold a balance in mid-air: on Radix a resource is always somewhere, and between two instructions the worktop is that somewhere.
It is often described as a countertop, and the metaphor is load-bearing in one respect: whatever you leave on it at the end has to be accounted for. The engine enforces that by dropping the worktop when the intent's instructions run out and calling drop_empty on each bucket still attached to it. An empty bucket vanishes; a bucket with a balance in it makes drop_empty fail, and the whole transaction fails with it. That is why a manifest cannot lose funds by forgetting about them β not a commit-time balance sweep, but a deliberate refusal to destroy a non-empty bucket.
Where it lives in the engine
The worktop is not a special case bolted onto the manifest runner. It is an ordinary native blueprint named Worktop, defined in radix-engine/src/blueprints/resource/worktop.rs and published in the same RESOURCE_PACKAGE as buckets, proofs, vaults and the fungible and non-fungible resource managers. It is only ever reached through the Transaction Processor, which is why it is usually described alongside it, but the code belongs to the resource subsystem.
Its blueprint definition declares is_transient: true, so the object can never be persisted: the intent processor allocates a node id for it, creates it with a fresh empty substate, pins it in the kernel for the duration of the intent, and drops it at the end. Its authorisation template is AllowAll on both functions and methods β unusual for a native blueprint, and safe here because the worktop node is owned by the processor's own call frame and its Own reference is never handed to a component. Nobody else can address it, so there is nothing to gate.
State: one bucket per resource
The entire state is a single field: WorktopSubstate { resources: IndexMap<ResourceAddress, Own> } β a map from resource address to exactly one owned bucket. Ten withdrawals of XRD do not put ten buckets on the worktop; the first creates the entry and the rest are merged into it. Directly above the implementation the source states the rule the methods exist to preserve: no empty buckets in the worktop.
That invariant drives the zero-amount behaviour, which is worth knowing because it is where the methods stop being symmetrical:
- Putting an empty bucket does not create an entry. The bucket is passed straight to
drop_emptyand the worktop is left untouched. - Taking a zero amount never reads the worktop at all; the resource manager mints a new empty bucket and returns it. Asking for nothing always succeeds, even for a resource that was never on the worktop.
- Taking all of an absent resource likewise returns a new empty bucket rather than failing β
TAKE_ALL_FROM_WORKTOPis total, and a manifest that takes all of a resource it never received gets an empty bucket, not an error. - Taking a positive amount is the only one that can fail on absence. A missing entry, or an entry holding less than requested, raises
WorktopError::InsufficientBalance. When the request happens to equal the whole balance the engine moves the existing bucket out wholesale rather than splitting it, which also removes the entry and keeps the invariant.
The instruction surface
Nine methods are declared in the blueprint definition. Seven of them are what manifest instructions compile down to; the other two are reached differently. The mapping, from the engine's instruction dispatch:
| Manifest instruction | Worktop method | Effect |
|---|---|---|
TAKE_FROM_WORKTOP | take | Take a stated amount of one resource into a named bucket |
TAKE_NON_FUNGIBLES_FROM_WORKTOP | take_non_fungibles | Take a stated set of local ids |
TAKE_ALL_FROM_WORKTOP | take_all | Take the whole entry for one resource |
RETURN_TO_WORKTOP | put | Put a named bucket back |
ASSERT_WORKTOP_CONTAINS_ANY | assert_contains | Fail unless the balance is non-zero |
ASSERT_WORKTOP_CONTAINS | assert_contains_amount | Fail unless the balance is at least an amount |
ASSERT_WORKTOP_CONTAINS_NON_FUNGIBLES | assert_contains_non_fungibles | Fail unless every listed id is present |
β (the ENTIRE_WORKTOP expression) | drain | Hand every bucket over at once |
| β (end of intent) | drop | Destroy the worktop, dropping each remaining bucket empty |
drain is the mechanism behind the Expression("ENTIRE_WORKTOP") argument that makes deposit_batch idiomatic: when the processor resolves that expression it clears the map and passes the whole set of buckets into the call. drop has no instruction at all β only the processor calls it, exactly once, when the intent finishes.
Assertions, and what they actually check
The three original assertions read the worktop under a read-only lock and raise WorktopError::AssertionFailed with a typed reason: ExpectedNonZeroAmount for ASSERT_WORKTOP_CONTAINS_ANY, ExpectedAtLeastAmount β carrying both the expected and the actual amount β for the amount form, and NonFungibleMissing naming the first absent id for the non-fungible form. A resource with no entry is read as a zero balance rather than as an error, so all three fail with the same shape whether the resource is short or simply never arrived.
The Cuttlefish protocol update added two more through a WorktopBlueprintCuttlefishExtension, which are stronger in kind rather than in degree. Both aggregate every balance currently on the worktop and check it against a ManifestResourceConstraints set: ASSERT_WORKTOP_RESOURCES_INCLUDE requires the listed constraints to hold, while ASSERT_WORKTOP_RESOURCES_ONLY additionally requires that nothing else is present. The second is the one that closes the gap the older assertions leave open β it lets a manifest state that the worktop holds the expected assets and no others, which is what makes an unexpected token arriving mid-transaction a failure rather than a surprise. These are the worktop half of the pre-authorization work, whose other half bounds what the next call is allowed to return.
One historical detail is visible in the error type: WorktopError still carries a BasicAssertionFailed variant that nothing raises. The source notes it is kept so that legacy errors can still be serialized by the node β an error model, once public, outlives the code that produced it.
Lifecycle within an intent
The worktop is created before the first instruction runs and dropped after the last one. In between, two things put resources on it without an explicit instruction saying so.
The first is the return path. Whenever a call returns, the processor walks the owned nodes in the returned value and auto-moves them: buckets are put on the worktop, proofs are pushed onto the auth zone, and anything else is left alone. This is why a manifest can call withdraw and then TAKE_FROM_WORKTOP without an instruction in between to move the result β the move already happened.
The second is the drop. Because the processor drops the worktop when its instruction queue empties β including on the path where an intent yields to its parent with nothing left to run β the check is per-intent rather than per-transaction. Each intent in a Cuttlefish transaction gets its own processor, and therefore its own worktop and its own auth zone: resources do not flow from one intent to another by being left lying on a shared countertop, but only through the value a yield carries. An intent's worktop does survive a yield to a child and is still there when control returns.
