This tutorial goes from an empty directory to a long session running in your
own program, with the cost of each step shown. It is alpha plan item C5. The
shorter session-quickstart.md explains the same
pieces in more depth; this page is the path to follow.
In consensus mode every step of your state machine runs on chain, in your own program, with DCG's runtime embedded in it. Nobody needs to watch it and there is no challenge window: once a step lands, it is final.
The example is a tally: each input byte adds to a running sum and a count, and the input 0 is rejected (consumed, with the state unchanged).
Copy the template application to the top of your DCG checkout:
cp -r examples/session-app my-app
cd my-appThen edit these files:
Cargo.toml:-
The
dcg-programpath and the[patch.crates-io] curve25519-dalekpath are relative toexamples/session-app. Formy-appat the checkout's top, change../../crates/to../crates/in both. -
Rename the package if you like (
name = "my-app").build.shnames the image after it (out/my_app.so). -
In your own repository, depend on DCG by git revision instead, and point the patch at that revision's
crates/dcg-program/vendor:dcg-program = { git = "<DCG repository URL>", rev = "<commit>", default-features = false, features = ["no-entrypoint"] }
-
src/bin/tally-conform.rs: if you renamed the package, changeuse dcg_session_app::TALLY;to your crate name with underscores (use my_app::TALLY;). Rename the[[bin]]inCargo.tomltoo if you like.
build.sh finds DCG's Python package through the checkout's git top level,
so it needs no edit.
Cost: none (local files).
src/lib.rs holds the kernel. Change three things for your own machine:
- The manifest: a name, versions, the input width, the state size and a
compute ceiling per step, declared once with
KernelDecl. initial_state_spans: the state a new session starts in.transition_spans_with_outcome: one step. Read the input and the state, write the new state, and returnContinue,HaltBefore,HaltAfterorReject. Return an error to refuse the whole advance.
The rules: deterministic, integer-only, and no reads beyond the input and
the state. session-quickstart.md section 1 walks
through the tally's version.
Cost: none.
mirror.py is the same transition in Python. Clients use it to predict
state; the kernel kit checks it against the Rust kernel at every edge.
cargo build --features conform,no-entrypoint --bin tally-conform
PYTHONPATH=../python python -m dcg.kernel_kit check --bin target/debug/tally-conform mirror.py
# dcg-tally-v1 v1/1 (stateful): 221 cases, 0 disagreements (continue 128, init state 3, refused 67, reject 23)Fix any disagreement before going on. A disagreement means a client would predict a state the chain does not reach.
Cost: one host build (80 to 185 s from cold on an M-series Mac, measured).
./build.sh out # with DCG's Python environment activeThis runs cargo build-sbf and writes out/<package>.so (for the
template, out/dcg_session_app.so) and a receipt with its sha256 and the DCG runtime version it embeds. The receipt
step runs python3; set PYTHON= to another interpreter if python3 does
not have DCG's packages.
The host build prints many warnings from dcg-program (unused imports in
code your program does not link); they are harmless.
Cost: one SBF build (128 s from cold, measured 2026-10-05). The image is about 376 KB.
python quickstart.py out/dcg_session_app.soIt starts a local validator with your program at a fresh address, opens a session, writes four inputs (one of them a rejected 0), advances, reads the state, checks it against the mirror, and closes the session.
Cost (measured, local): 11 to 13 s in all. The open creates the session,
its input stream, and one account per state span (three accounts for the
tally). Their rent comes back at close. The ledger directory is removed at
the end; pass --keep to keep it.
python long_session.py out/dcg_session_app.so --steps 2000 # about 250 transactionslong_session.py is the real shape of a consensus application:
- A ring input stream, so the session can run any number of steps. The writer stays at most 64 inputs ahead of the cursor.
- Batches:
Session.write_and_advance(batch)writes 8 inputs and advances over them in one transaction. All of it applies, or none does. - The sequencer sends each transaction, journals it, and resends it if
it does not land. To show this, the example drops one send on purpose
(
--drop-at). Either a rebroadcast of the same bytes lands, or, once the dropped signature's blockhash has expired, the sequencer checks the session's state and signs the step again. Both happened in the measured runs; in both, every step applied exactly once. - At the end it reads the state, checks it against the mirror, reads the session's info (cursor and rejections), and closes everything.
Output (measured 2026-10-05, local validator with 50 ms slots, 400 steps):
{"step": "open", "session": "…", "accounts": 3, "rent_lamports": 27728640}
{"step": "result", "count": 399, "sum": …, "cursor": 400, "rejected": 1, "mirror_agrees": true, "expected_rejections": 1}
{"step": "dropped send", "send_number": 10, "resent_same_bytes": 2, "landed": true, …}
{"step": "compute", "median_units_per_8_step_transaction": 63726, …}
{"step": "close", "rent_returned_lamports": 27728640, "accounts_closed": 3, "net_cost_lamports": 565000}
{"step": "done", "ok": true, "steps": 400, "transactions": 58, "session_transactions": 54, "steps_per_second": 15.7, "fee_lamports_per_transaction": 9741, …}
Costs:
| Item | Measured (local validator, 2026-10-05) |
|---|---|
| Rent while the session is open | 27,728,640 lamports for the session, a 128-slot ring stream and the state; all of it is returned at close, to the session authority |
| Fees | about 10,000 lamports per transaction (two signatures: payer and authority) |
| Transactions | one per 8 steps, plus 8 to open and close (400 steps took 58) |
| Compute | 63,726 units per 8-step transaction (about 8,000 per tally step) |
| Speed | about 16 steps per second, one transaction confirmed at a time |
| A long run | 16,000 steps in 2,008 transactions, 1,113 s (14.5 steps/s); mirror agrees; rent all returned; fees 20,065,000 lamports (about 10,000 per transaction) |
-
Change the transition (step 2), re-run the kernel kit (step 3), and rebuild (step 4).
-
Describe your kernel to the session in
quickstart.py'sTALLY_SESSION. Its keys:Key Value idthe kernel id: UTF-8 text of 1 to 16 bytes, as in KernelDecl::newsemantic_version,abi_versionas in KernelDecl::newmode{"id": 0x434F4E53, "version": 3}(consensus v3)schema{"id": <your STATE_SCHEMA id>, "version": 1}input_widthbytes per input, 1 to 8 state_spansthe state's span lengths, 1 to 8 spans, summing to your declared state size input_codec"u8"(inputs are integers 0 to 255) or"bytes"(inputs areinput_widthbytes)state_codec"bytes", or"counter-u64-pair"for two u64srejects_inputtrueif the manifest declares.rejects_input()stream_rootoptional: 32 bytes as hex; the default is fine -
Check the guarantee for a live session:
print(await session.explain(decl=MANIFEST))(guarantees.md). -
Go faster: the sequencer guide (
sequencer.md) explainsOrderedLane, which sends a chain of transactions without waiting for each one. That is how Doom reaches 3.81 frames per second on testnet. -
Deploy to testnet: deploy
out/dcg_session_app.soat your own program address withsolana program deploy. Rundcg verifyto check the deployed image against your receipt, then pointSolanaRpcEndpointat the testnet RPC. The session code is unchanged. Deploying needs rent for the program's data account;solana rent <bytes>gives the amount.
- Views, lanes, resources and phased initialization through the Python
Session. They are built and tested in Rust (stateful-workloads-v3.md), and Doom drives them directly. - Network throughput. The numbers above are from a local validator. On a remote testnet node, each confirmation round trip costs more; measure from the host you will run on.