Block transactions
Current contract for bulk block edits on the server: the one native
submission API every block writer lowers to. Design:
Enki B §2 (docs/superpowers/specs/2026-09-27-enki-b-mod-platform-contract-design.md)
(§2.11 for non-chunk sections, and its “As built” issues 16–25). Source:
crates/manifold-server-state/src/transaction/,
crates/manifold-server-state/src/block_world_apply.rs (single edits),
crates/manifold-world/src/{chunk_cell,chunk_map,world_space}.rs,
crates/manifold-net-protocol/src/chunk_edit.rs.
The rule
A block write goes through transaction::submit as a TxnRequest: an actor
(Player, ModSelf or the server-only Rule), an optional owner, options
and one or more sections. Console /fill and /setblock, the block-world
handler’s decided edits and Talos’s use-block
(navigation) already do. A scheduled system, which
holds the world shared, declares add_resource_write::<TxnEngine>() and
submits with TxnEngine::submit on the engine it reaches through that write:
the same checks and queue as transaction::submit, one code path, except that
a player actor needs the world’s connections and is refused there
(Other("player-actor-needs-world")). Such worlds install the engine up front
with transaction::install, which is idempotent (an installed engine, its
queue and configuration stay as they are); submit installs it on first use.
A section is either the engine’s BlockOps (fill, set, revert,
reapply, expect) or a program’s own TxnSection. ECS state that must
change atomically with blocks (an item consumed by a placement, loot deposited
by a break) changes in a non-chunk section of the same transaction, never in a
follow-up.
Single-block edits
The handler keeps the block-world decision ABI (take-edit-requests,
queue-edit, reject-edit, plus take-results). The orchestrator lends the
handler (the first mod importing block-world) the world, shared, for its
server-tick systems: ModOrchestrator::tick_server opens the loan before
the schedule walk and closes it before the write-back, and the calls reach
the server through the BlockWorldPort resource ServerSession::new
installs (block_world_apply::install_port). queue-edit calls
block_world_apply::submit_decided_edit during the guest call, which
submits the edit as a one-cell transaction and returns its batch id: actor
and owner the requesting player, requester
Requester::Handler(<handler load-order index>), no undo record,
Replication::BlockUpdates, and a section that applies within the
validating slot. Handler edits validate first in the slot, so an edit
decided this tick commits this tick and is broadcast as BlockUpdate with it.
Locks, reservations, nodes (build.edit, which every player holds by
default), water intake, the commit log, lighting and hierarchy apply as to
any transaction.
- An edit that writes the value already there commits with
changed_cells: 0and sends nothing; the chunk is not dirtied. - A refusal is sent to the requesting client as
BlockEditRejectedin the same tick, under theengine.edit.rejected.*keys (block_world_apply::rejection_text). The handler’s ownreject-edittakes the same path asTxnReject::Protected(reason). queue-editfails at once only with the sharedsubmit-error:malformed(a value wider than a block id) andactor-unknown(the peer is not a connected player), both also sent to the client, andother("not-handler")(a later importer) orother("outside-server-tick")(no loan: outside a server-tick system, or no port installed).- Each edit’s
TxnResultwaits in the handler’s results ring. The guest drains it withtake-results(thetxn-resultspayload, postcardVec<TxnResult>; SDK:manifold_mod_substrate::block_world::take_results); native code withblock_world_apply::take_block_edit_results(world, handler). Results of edits queued in tick N arrive from tick N + 1’s call. Feedback such as a break or place sound belongs toCommittedresults withchanged_cells > 0(block_world::committed_changes), matched toqueue-edit’s batch id. - The loan is shared, so other per-call world views (Daedalus G’s physics
window) can be held alongside it: the port writes
only the transaction engine and
TickEvents, throughVoxelWorld::structural(transaction::submit_in,take_results_in). Executor parity harnesses lend a scratch world instead (BlockWorldRecord::record,RECORDING_PORT).
Lifecycle
- Queue.
submitchecks framing and the actor and returns a batch id. - Validate at B’s slot in
ServerSession::step_tick, after the F3 flush, before the console drains; the handler’s single edits first, then other native requests, then guest transactions: nodes (requires,build.editfor players; every node goes throughanim_contract::player_has_nodeand the permission catalogue), the cell cap (TxnConfig::max_cells, 262,144 in the slice), residency, locks, values, compare-and-set, reservations and water intake. All-or-nothing: a refusal writes nothing. - Lock. Every touched chunk is locked (other transactions get
RegionBusy, which a handler edit’s client sees asengine.edit.rejected.region_busy), pinned by a loader view centre fromPIN_VIEW_BASEup, and held out of save batches until commit. - Apply chunk by chunk inside the “world edits” budget (4 ms, up to 8 ms
with tick headroom); a larger transaction continues on later ticks. Each
chunk is one
WorldSpace::apply_chunk_edit: one snapshot and one diff, one change-log entry, one dirty mark, emitter updates for emissive cells, one hierarchy invalidation and oneChunkEditsdelta (aBlockUpdateper changed cell withReplication::BlockUpdates). - Commit. Non-chunk sections run
apply_oncein submission order, the undo record (MUR1) is written if requested, locks and pins are released, the commit is logged inTxnCommitLog, and the requester’s results ring getsCommitted. Each section’sfinishhook runs in that tick for every outcome; a transaction it submits queues for the next slot.
Before a shutdown save (native shutdown, the browser worker’s shutdown and
flush), hosts call transaction::settle, which applies every queued and
in-flight transaction to completion: held chunks would otherwise miss the
final save.
Non-chunk sections
A section that returns Some(AccessSet) from TxnSection::access is a
non-chunk section: it reads the ECS while validating (ValidateCx::ecs(), a
read-only EcsView) and writes it once at commit (apply_once(&mut ApplyOnceCx)), both only through that set (manifold_ecs::AccessSet, as a
system declares; set_structural for spawn, despawn, insert, remove).
- No ambient world: the contexts expose typed accessors that check the set. Undeclared access panics in debug builds with the scheduler’s §15 message; release skips the check. An exclusive set is refused at submit.
- A transaction holding one is single-tick (validate, chunks,
apply_once, commit in one slot;TooLargeabovesingle_tick_max_cells), so nothing changes the ECS between its validation and its commit.apply_oncecannot fail. Sections validate against the world before the transaction, so a check that depends on another section’s effect belongs in one section; queued transactions validate in turn against what earlier ones committed. - No chunks listed (an inventory move): no residency, locks, pins, previews,
snapshots, change log or
ChunkEdits, and nobuild.edit; it commits in the slot that validates it. Measured bytxn_non_chunk_section(target ≤ 5 µs for three sections). - Structural changes apply at commit through the funnel with
Cause::Transaction { batch, mod_index }; their lifecycle events reach observers at F4. A later transaction in the same slot sees them. TxnCommitLogrecords every commit that changed a cell or ran a non-chunk section, with all section kinds (CommittedTxn::sections).- Undo records hold a section’s data only if its
record()writes bytes. Container contents are never recorded (the items program’s sections enforce it).
Guarantees
- Nothing is written per cell outside the chunk writer; capture, logging and replication cost is per chunk.
- Unknown blocks keep their names through fill, revert and reapply.
- Revert and reapply are compare-and-set over the record; any mismatch is a
Conflictand nothing changes. - Clients receive one
S2C::ChunkEditsentry per changed chunk they hold (uniform, box or lz4 runs) and remesh and relight the chunk once. - The browser worker runs the same engine through the same
step_tick.
Not yet
Loading unloaded chunks (unloaded = load), reveal at commit, parallel
apply, the per-connection edit backlog, the in-flight ChunkLoad race
(edits for chunks a client does not hold yet are dropped; the chunk streams
fresh), protections and reviews, per-player build.txn-cells.max, the
single-edit rate limit and reach check (Enki B §3.4), and the guest
block-transaction WIT interface (its commit should use the same world
loan as queue-edit).
Tests
manifold-server-state unit tests transaction::tests, transaction::record,
block_world_apply and console::world_edit_commands; manifold-host-core
suite step_tick_block_edits; manifold-server suite txn_two_client,
block_edit and block_world_roundtrip; the benches txn_bulk_section and
txn_non_chunk_section.