
Ton Bug Triage
- 109 installs
- 15 repo stars
- Updated May 28, 2026
- ton-blockchain/ton-triage-skill
Use ton-bug-triage for development tasks
About
ton-bug-triage: A skill for development. This provides functionality for development workflows.
- ton-bug-triage
Ton Bug Triage by the numbers
- 109 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,939 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ton-blockchain/ton-triage-skill --skill ton-bug-triageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 109 |
|---|---|
| repo stars | ★ 15 |
| Last updated | May 28, 2026 |
| Repository | ton-blockchain/ton-triage-skill ↗ |
What it does
Use ton-bug-triage for development tasks
Files
TON Bug Triage
Use this skill when the job is to prove something on a local tontester network, not just to launch validators.
Typical triggers:
- deploy a contract and trigger a bug with an internal message
- compare baseline and probing validator builds
- verify a crash, liveness failure, malformed-packet path, or compatibility claim
- collect maintainer-ready evidence for a local TON repro
The standard is: choose the smallest topology that answers the question, define the success condition before running, and collect enough evidence that the result is interpretable.
Working Model
Keep these paths distinct:
- Skill scripts: files under this skill directory, such as
scripts/run_basic_network.py - Source tree: the real TON checkout passed as
--repo-root - Build directory: binaries and libraries such as
validator-engine,create-state,tonlibjson, andtolk - Work directory: per-run state, logs, configs, and emitted artifacts
Do not assume the skill directory and the repo are the same thing. The scripts live in the skill. They operate on the repo and build you pass in.
wallet-env.txt is the main handoff artifact between the launcher and follow-up helpers.
These helpers depend on tontester internals and private APIs. If tontester changes, expect to adjust helper behavior, generated bindings, or command assumptions.
Workflow Selection
Choose one workflow before running anything:
Workflow A — trigger via transaction
Use this when the bug is reached by deploying a contract, sending an internal message, or delivering a malformed/custom payload to a contract account.
Workflow B — trigger via validator behavior
Use this when the bug requires patched validator code, mixed builds, consensus interference, malformed protocol packets, reordered traffic, or deliberately invalid block behavior.
If a repro touches both, ask one question first: can you trigger it on an unmodified network after a normal deploy/send path? If yes, start with Workflow A. If no, treat it as Workflow B.
Core Rules
1. Start with the smallest network that can answer the question. Use one validator unless the path needs peers, consensus, or mixed builds.
2. Prefer ordinary deploy/send flow over zerostate edits. If the bug only reproduces after zerostate mutation, say that clearly.
3. Keep baseline and probing builds separate. Usually vanilla-build/ is baseline and build/ or build-probing/ is the modified build.
4. Decide the success condition before running. Examples: target mc_seqno, explicit crash marker, process death, active contract account, inspected transaction, or honest-node rejection of a malformed packet.
5. For Workflow B, make the probing node self-immune. Operational meaning: the probing node may emit malformed or adversarial behavior, but it must stay alive until the target effect is observed on the honest nodes. A run is invalid if the probing node dies first and that death could explain the outcome.
6. Record enough evidence to rerun the exact scenario. Keep the run directory, the build directories, the commands, the relevant addresses, and the exported artifacts.
7. Before calling something maintainer-ready, rerun it from a clean checkout or detached worktree with only the intended artifacts.
Core Scripts
Prefer the bundled scripts over one-off shell sequences.
scripts/run_basic_network.py
Launch one or more validators from a single build. Supports --emit-wallet-env, --base-port, --validators, and --keep-alive.
scripts/run_mixed_network.py
Launch baseline and probing validators from different build directories. Use this for malicious-vs-honest experiments, log-based crash detection, and probing-node survival checks.
scripts/compile_tolk.py
Compile a .tolk source to .fif and materialize the contract code BoC. It hard-fails if the tolk binary is stale relative to repo HEAD.
scripts/build_stateinit.py
Build a deployable StateInit BoC from code plus optional data and library-dictionary BoCs.
scripts/run_fift_script.py
Run a .fif script with the correct include paths.
scripts/wallet_send.py
Build and optionally send a wallet-signed message. Use --init-boc for deployment and --body-boc for arbitrary internal payloads.
scripts/send_boc.py
Send a prebuilt serialized external message BoC through tonlib without rebuilding it via wallet_send.py.
scripts/address_info.py
Normalize and inspect raw and friendly TON address forms when helper inputs need to be cross-checked.
scripts/account_state.py
Fetch raw account state and dump code/data BoCs for inspection.
scripts/get_method.py
Run a get-method through tonlib and print JSON.
scripts/inspect_latest_transaction.py
Fetch the latest account transaction, export raw transaction data, and save message body/init-state BoCs when tonlib returns them as msg.dataRaw.
scripts/run_liteclient.py
Run a lite-client command using wallet-env.txt or explicit repo/build/config inputs.
scripts/dump_boc.py
Print a BoC cell tree through Fift. Use this for payloads, StateInit, exported transaction data, and dumped account code/data.
scripts/summarize_run.py
Summarize node liveness and crash markers for a finished or live run directory.
scripts/demo_wallet_flow.py
Known-good end-to-end verifier for the simple wallet path.
Workflow A
Use Workflow A when the trigger is “deploy contract, then send message”.
1. Launch a small network with --emit-wallet-env. 2. Compile your contract with scripts/compile_tolk.py, or provide a hand-built code BoC from Fift assembly if you are not using Tolk. 3. Build deployable StateInit with scripts/build_stateinit.py. Pass --data-boc and --library-boc only when the contract actually needs them. 4. Deploy from the funded built-in main-wallet with scripts/wallet_send.py --init-boc. A deploy message that uses --init-boc may also invoke the contract's internal message handler. Check whether the deploy transaction itself changed contract state before sending a separate trigger. 5. Wait for activation. Prefer account_state.py plus a contract-specific get-method over assuming one masterchain advance is enough. 6. Build the trigger body as a BoC when the payload is custom or binary. run_fift_script.py is the easiest way to emit a one-off body.boc. 7. Send the trigger with scripts/wallet_send.py --body-boc. --body-boc is the authoritative payload path and overrides --comment. If you already have a signed external message BoC, use scripts/send_boc.py --boc instead of rebuilding it with wallet_send.py. --wait-mc-advance proves the network is alive. It does NOT prove the transaction succeeded, the deploy activated, or the contract state changed. Always inspect the target account directly. 8. Observe with inspect_latest_transaction.py --out-dir and dump_boc.py. The exported transaction-data.boc and in-msg-body.boc are the starting point for cell-level debugging. If the tonlib-based helpers crash (common symptom: KeyError: '@extra'), use run_liteclient.py as the fallback for all observation steps. See references/liteclient_fallback.md.
For the full worked sequence, read references/contract_deploy_flow.md.
For the simple wallet smoke path only, read references/wallet_and_deploy_helpers.md.
Workflow B
Use Workflow B when the trigger is “modify validator behavior, then observe honest-node reaction”.
1. Snapshot the baseline build before probing changes. 2. Patch the probing build only, and gate every behavioral change behind explicit TON_PROBING_* environment variables. 3. Choose a mixed topology and explicit success and failure conditions. Use --success-log, --failure-log, --require-node-alive, and --require-node-dead deliberately. 4. Run the mixed network with scripts/run_mixed_network.py. 5. Confirm both sides of the claim: probing markers were hit, and the honest-node effect happened or did not happen. 6. Summarize the run with scripts/summarize_run.py, then inspect specific logs manually only where the summary points.
For common patch shapes, read references/probing_patterns.md.
For an end-to-end example, read references/bug_hunt_example.md.
Negative Result
A valid “did not reproduce” is still useful evidence. Treat a run as a real negative result only if all of the following are true:
- the selected workflow actually executed its trigger path
- the relevant probing or trigger markers were present
- the network stayed healthy enough that the absence of the bug is meaningful
- the observation step looked at the right account, transaction, or node logs
- the timeout or seqno target was reached without the target effect
Stop iterating and report a negative result when a clean rerun gives the same outcome and the remaining changes are only minor parameter churn.
Iteration And Debugging
- If
compile_tolk.pyfails with a stale-build error, rebuildtolkfirst.
The exact fix is usually ninja -C <build-dir> tolk.
- If a deploy appears to succeed but the destination still looks empty, check too-early observation first.
Wait for activation instead of assuming the deploy path is broken.
- If a contract is active but the expected get-method fails, verify the method id and the initial data layout before changing the network topology.
- If
inspect_latest_transaction.pyshows the wrong payload, dump both the original trigger BoC and the exportedin-msg-body.bocand compare their cell trees.
- If probing markers are missing in Workflow B, the environment variables did not reach the node or the patched code path never executed.
- If the probing node dies before the target effect is observed, the run is invalid.
Evidence Standard
Record enough to support the claim:
- exact run directory
- build directories used
- success condition used
- relevant launcher and helper commands
- target addresses or node names
- exported transaction and BoC artifacts for Workflow A
- node liveness and log markers for Workflow B
For Workflow A, remember that --wait-mc-advance is only a liveness hint; confirm success from the target account or transaction.
Troubleshooting
- If
pythonis missing, usepython3. - If
tonapibindings are missing, the helpers usually generate them fromtest/tontester/generate_tl.py. - If a helper is copied outside the repo, keep passing the real repo as
--repo-root; includes and generated artifacts come from the repo, not the skill folder. - If tonlib-based scripts (
account_state.py,get_method.py,inspect_latest_transaction.py,wallet_send.py --auto-seqno) crash withKeyError: '@extra'or similar tonlib wrapper errors, the local tonlib build has a compatibility issue. Fall back tolite-clientfor inspection and usewallet_send.pywith manual--seqnoinstead of--auto-seqno.
See references/liteclient_fallback.md for the shared fallback flow.
- If
vanilla-build/CMakeCache.txtpoints atbuild/, treatvanilla-buildas invalid and recreate it. Do not trust a build tree that reconfigures into the wrong directory. - Do not assume
lite-clientis present in the passed build directory. Some checkouts only havecreate-state,tonlibjson, andtolkbuilt. - If disk is full, clear old
run-*directories before retrying.
References
references/wallet_and_deploy_helpers.md
Read this for the proven simple-wallet smoke path.
references/contract_deploy_flow.md
Read this for the generic Workflow A compile, deploy, trigger, inspect path.
references/liteclient_fallback.md
Read this when tonlib-based helpers fail and Workflow A needs lite-client inspection or manual wallet seqno handling.
references/probing_patterns.md
Read this when designing Workflow B code changes.
references/diagnostic_checklist.md
Read this when a run completed but the outcome is unclear.
references/bug_hunt_example.md
Read this for a concrete mixed-network negative-result example.
__pycache__/
*.pyc
*.pyo
.DS_Store
__MACOSX/
interface:
display_name: "TON Bug Triage"
short_description: "Triage TON local repros through contract txs or validator probes"
default_prompt: "Use $ton-bug-triage to verify a TON local-network repro through either Workflow A (contract transaction trigger) or Workflow B (mixed-build validator behavior)."
Workflow B Example: Mixed Network Negative Result
Use this example when validating a potential consensus bug with a mixed network and you need a concrete model for a valid negative result.
Scenario Summary
- Workflow:
Workflow B — trigger via validator behavior - Potential bug:
skip_intervals_dereference onlower_bound() == end()in the Simplex pool path - Goal: construct conditions where the probing node reorders SkipCerts and withholds proposals inside a leader window
- Observed result: the network continued to produce blocks to masterchain seqno
300; no liveness failure was observed in this setup
This is a useful outcome because the probing path executed and the honest nodes stayed healthy.
The exact node counts, ports, seqno targets, and probing windows here are investigation-specific. Reuse the structure of the experiment, not the exact numbers.
Setup
1. Build the repo. 2. Snapshot the clean baseline build: copy build/ to vanilla-build/ before probing changes. 3. Patch the probing build only:
validator/consensus/simplex/pool.cpp
add SkipCert reordering logic and probing markers.
validator/consensus/simplex/consensus.cpp
log consensus config and leader-window transitions.
validator/consensus/block-producer.cpp
add proposal withholding gated by env vars. 4. Rebuild the probing build.
Probing Inputs
The verified env-var shape was:
TON_PROBING_REORDER_SKIP_CERTS=1TON_PROBING_WITHHOLD_PROPOSALS=1TON_PROBING_WITHHOLD_START=3TON_PROBING_WITHHOLD_END=7
The probing node was treated as self-immune:
- the run was only considered valid if the probing node stayed alive until the honest-node effect was ruled in or ruled out
Example Run
python3 /path/to/skill/scripts/run_mixed_network.py \
--repo-root /path/to/repo \
--build /path/to/repo/vanilla-build \
--probing-build /path/to/repo/build \
--workdir /path/to/repo/tmp/tontester-mixed-probe-lw10 \
--base-port 2201 \
--normal-nodes 3 \
--probing-nodes 1 \
--dht-nodes 1 \
--enable-simplex \
--simplex-slots-per-leader-window 10 \
--simplex-first-block-timeout-ms 200 \
--simplex-target-block-rate-ms 300 \
--probing-env TON_PROBING_REORDER_SKIP_CERTS=1 \
--probing-env TON_PROBING_WITHHOLD_PROPOSALS=1 \
--probing-env TON_PROBING_WITHHOLD_START=3 \
--probing-env TON_PROBING_WITHHOLD_END=7 \
--mc-seqno 300 \
--require-node-alive node4If you were testing for a crash instead of a liveness negative result, add explicit stop conditions such as:
--success-log 'any:Signal: 6' \
--require-node-alive node4Use --require-node-dead nodeX only when the victim is known in advance.
Evidence Collected
- probing-node logs confirmed the reordering and withholding markers fired
- honest-node logs showed the network continued to process blocks
- the mixed run reached masterchain seqno
300 - the probing node stayed alive long enough to make the result meaningful
This combination is a valid negative result.
What Made The Result Valid
- the trigger path was real, not hypothetical
- the probing node did not die first
- the success condition was chosen before the run
- the absence of a crash or stall was observed over a meaningful interval
Common Failure Modes In This Pattern
- probing markers missing:
env vars did not reach the node or the patch never executed
- probing node dies first:
invalid run; fix self-immunity
- relying only on
mc_seqnofor a crash claim:
insufficient; pair it with explicit log or process conditions
For a broader list of patch shapes, read probing_patterns.md.
For post-run triage, read diagnostic_checklist.md.
Contract Deploy Flow
Use this reference for Workflow A — trigger via transaction.
This is the full path:
1. launch a small network 2. compile a contract 3. build StateInit 4. deploy with wallet_send.py --init-boc 5. send a trigger body 6. inspect the resulting transaction and dump the exported BoCs
The commands below were smoke-tested against a local repo with a rebuilt build/tolk/tolk.
Fastest Verified Example
The fastest known-good contract for this flow is the existing repo source:
/path/to/repo/tolk-tester/tests/handle-msg-5.tolk
It has:
- an internal-message handler
- a get-method with method id
101 - no special deployment dependencies
Once this path works, replace that source file with your real contract source.
1. Launch The Network
python3 /path/to/skill/scripts/run_basic_network.py \
--repo-root /path/to/repo \
--build /path/to/repo/build \
--validators 2 \
--mc-seqno 3 \
--emit-wallet-env \
--keep-alive 240Capture the printed wallet-env.txt path. The rest of the flow uses it.
2. Compile The Contract
python3 /path/to/skill/scripts/compile_tolk.py \
--wallet-env /path/to/run/wallet-env.txt \
--source /path/to/repo/tolk-tester/tests/handle-msg-5.tolk \
--out-dir /tmp/contractExpected outputs:
/tmp/contract/handle-msg-5.fif/tmp/contract/handle-msg-5.code.boc
If this fails with a stale-build error, rebuild tolk first:
ninja -C /path/to/repo/build tolk3. Build StateInit
python3 /path/to/skill/scripts/build_stateinit.py \
--wallet-env /path/to/run/wallet-env.txt \
--code-boc /tmp/contract/handle-msg-5.code.boc \
--out-dir /tmp/contract-stateinitThis prints:
- raw address
- non-bounceable address
- bounceable address
Expected outputs:
/tmp/contract-stateinit/contract-stateinit.boc/tmp/contract-stateinit/contract.addr
If your contract needs initial data or a library dictionary, add:
--data-boc /path/to/data.boc--library-boc /path/to/libs.boc
4. Deploy The Contract
Use the raw address printed by build_stateinit.py.
python3 /path/to/skill/scripts/wallet_send.py \
--wallet-env /path/to/run/wallet-env.txt \
--dest-addr=0:<contract-addr-hex> \
--auto-seqno \
--amount 1 \
--init-boc /tmp/contract-stateinit/contract-stateinit.boc \
--show-seqno \
--wait-mc-advance \
--wait-timeout 20wallet_send.py uses the funded built-in main-wallet by default when wallet-env.txt is present.
A deploy message sent with wallet_send.py --init-boc can execute the contract's internal message handler in the same transaction as the deploy. For stateful contracts, "deploy" and "first trigger" are not always separate events. If your contract increments state on any internal message, the deploy transaction itself will be the first state change.
--wait-mc-advance proves the network is alive. It does NOT prove the transaction succeeded, the deploy activated, or the contract state changed. Always inspect the target account directly.
5. Wait For Activation
Do not trigger the contract until it is active.
python3 /path/to/skill/scripts/account_state.py \
--wallet-env /path/to/run/wallet-env.txt \
--address 0:<contract-addr-hex> \
--out-dir /tmp/contract-accountFor the verified example, a stronger activation check is:
python3 /path/to/skill/scripts/get_method.py \
--wallet-env /path/to/run/wallet-env.txt \
--address 0:<contract-addr-hex> \
--method 101Expected result:
exit_code = 0top_number = 0
6. Build An Arbitrary Trigger Body
Create a tiny Fift script such as:
"TonUtil.fif" include
x{DEADBEEF} s>c 2 boc+>B "body.boc" B>fileThen run it:
python3 /path/to/skill/scripts/run_fift_script.py \
--wallet-env /path/to/run/wallet-env.txt \
--script /tmp/body.fif \
--cwd /tmp/contract-bodyThis emits:
/tmp/contract-body/body.boc
Use this path whenever the trigger message must contain binary payload instead of a wallet comment.
7. Send The Trigger
python3 /path/to/skill/scripts/wallet_send.py \
--wallet-env /path/to/run/wallet-env.txt \
--dest-addr=0:<contract-addr-hex> \
--auto-seqno \
--amount 0.1 \
--body-boc /tmp/contract-body/body.boc \
--show-seqno \
--wait-mc-advance \
--wait-timeout 20 \
--out-dir /tmp/trigger-sendImportant:
--body-bocis the internal payload path.--body-bocoverrides--comment.- Remember:
--wait-mc-advanceis only a liveness hint. Verify the target account or transaction directly.
8. Inspect The Result
python3 /path/to/skill/scripts/inspect_latest_transaction.py \
--wallet-env /path/to/run/wallet-env.txt \
--address 0:<contract-addr-hex> \
--out-dir /tmp/contract-inspectLook for:
- the latest transaction id
- the inbound source and destination
- the transferred value
- the exported
transaction-data.boc - the exported
in-msg-body.bocwhen the inbound payload ismsg.dataRaw
Expected artifacts in the verified binary-payload example:
/tmp/contract-inspect/latest-transaction.json/tmp/contract-inspect/transaction-data.boc/tmp/contract-inspect/in-msg-body.boc
9. Dump The BoCs
python3 /path/to/skill/scripts/dump_boc.py \
--wallet-env /path/to/run/wallet-env.txt \
--boc /tmp/contract-inspect/in-msg-body.bocFor the verified example, this prints:
x{DEADBEEF}You can also dump the raw transaction blob:
python3 /path/to/skill/scripts/dump_boc.py \
--wallet-env /path/to/run/wallet-env.txt \
--boc /tmp/contract-inspect/transaction-data.bocLite-Client Fallback
If the tonlib-based helpers crash, use the shared fallback guide in liteclient_fallback.md.
For this flow, use it for getaccount, runmethodfull 0:<contract-addr-hex> 101, lasttransdump, and post-trigger state checks.
What To Change For Your Real Repro
- Replace
handle-msg-5.tolkwith your contract source. - Replace
x{DEADBEEF}with the actual trigger payload. - Replace get-method
101with your contract-specific activation or observation method if one exists.
If the deployment path works but the bug does not reproduce, use references/diagnostic_checklist.md before changing the network topology.
Diagnostic Checklist
Use this reference when a run finished but the result is unclear.
Start by deciding which workflow you were actually exercising.
Workflow A
- If any tonlib-based script crashes with
KeyErroror similar wrapper errors:
do not debug the wrapper. Switch to lite-client for the rest of the session. Use run_liteclient.py or call lite-client directly.
- If
--auto-seqnofails:
use manual --seqno. For the built-in main-wallet, seqno starts at 0 and increments by 1 per send.
Compile Step
- If
compile_tolk.pyfails with a stale-build error:
rebuild tolk first and rerun the exact same command.
- If
compile_tolk.pysucceeds but no code BoC exists:
inspect the generated .fif and rerun the helper before changing the contract source.
Deploy Step
- If
wallet_send.py --init-bocsucceeded but the account still has empty code/data:
check too-early observation first. Use account_state.py after another masterchain advance.
- If the address in
account_state.pyis not the address frombuild_stateinit.py:
stop and fix the address mismatch before continuing.
Activation Step
- If the account is active but the expected get-method fails:
verify the method id and the initial data layout.
- If the contract needs data or libraries and you skipped them in
build_stateinit.py:
rebuild StateInit with --data-boc or --library-boc.
Trigger Step
- If the trigger transaction never appears:
verify the source wallet seqno, the destination address, and the trigger amount.
- If the latest transaction exists but the payload looks wrong:
dump both: dump_boc.py --boc <original-body.boc> dump_boc.py --boc <exported-in-msg-body.boc>
- If
inspect_latest_transaction.pyexportsmsg.dataTextinstead ofmsg.dataRaw:
tonlib decoded the payload as text. Use the JSON text fields instead of expecting in-msg-body.boc.
Observation Step
- If
transaction-data.bocdumps cleanly but nothing interesting happened:
the trigger landed, but the bug did not reproduce.
- If the contract state changed unexpectedly:
dump account_state.py code and data BoCs and compare them to the expected post-state.
Workflow B
Before The Effect
- If probing markers are missing entirely:
the env vars did not reach the node or the patched code path never executed.
- If the probing node died before any honest-node effect:
the run is invalid. Fix self-immunity before drawing conclusions.
During The Effect
- If honest nodes stayed live and
mc_seqnokept advancing:
this is a negative result unless your success condition was something else.
- If a log marker fired but no observable effect followed:
the patch executed, but it was not sufficient to trigger the bug.
- If an honest node died or stopped producing blocks:
capture that node’s log, the probing-node marker log, and the summarize_run.py output before rerunning.
After The Effect
- If
summarize_run.pyreportsrunningfor every validator and no crash markers:
you do not have a crash repro yet.
- If you need block-level evidence:
do not assume lite-client already exists in the build directory. Build it separately or use alternate tooling.
Valid Negative Result
A negative result is valid when:
- the intended trigger path really executed
- the observation step looked at the correct artifacts
- the network remained healthy enough to make the absence meaningful
- a clean rerun reproduces the same absence
Report that result directly instead of continuing with random parameter churn.
Lite-Client Fallback
Use this reference when tonlib-based helpers crash with KeyError: '@extra' or similar wrapper errors.
Switch observation steps to run_liteclient.py, and use manual --seqno if wallet_send.py --auto-seqno is unavailable.
Generic Account Check
python3 /path/to/skill/scripts/run_liteclient.py \
--wallet-env /path/to/run/wallet-env.txt \
-- getaccount <address>Look for:
- non-empty
code - non-empty
data last transaction lt/hash
Use this as the activation check for both contract accounts and generated wallets.
Generic Get-Method Check
python3 /path/to/skill/scripts/run_liteclient.py \
--wallet-env /path/to/run/wallet-env.txt \
-- runmethodfull <address> <method-id>This prints the return stack and the get-method exit code.
Examples:
- use method id
101for the verified contract example incontract_deploy_flow.md - use method id
85143for the generated-walletseqnocheck inwallet_and_deploy_helpers.md
Latest Transaction Dump
First read the latest transaction id from getaccount:
last transaction lt = <lt> hash = <hash>Then dump the transaction:
python3 /path/to/skill/scripts/run_liteclient.py \
--wallet-env /path/to/run/wallet-env.txt \
-- lasttransdump <address> <lt> <hash> 1This prints the full transaction, including inbound source, destination, transferred value, and message body or comment when visible in the dump.
Wallet Seqno
If tonlib-backed --auto-seqno is unavailable, read the wallet seqno with runmethodfull and pass manual --seqno to wallet_send.py.
For the generated simple-wallet flow, the reliable numeric method id is 85143.
State After A Trigger
After a failed or successful trigger, rerun getaccount on the target address to inspect the current account state directly.
Probing Patterns
Use this reference for Workflow B — trigger via validator behavior.
The rule for every probing patch is the same:
- gate it behind explicit
TON_PROBING_*environment variables - log a single-line marker when it fires
- make the probing node self-immune so the run is not invalidated by the probe crashing first
Shared Pattern
Use this structure no matter which file you patch:
1. Read one or more TON_PROBING_* env vars at process startup or at the decision point. 2. Exit early unless the env var is explicitly enabled. 3. Log a stable marker that is easy to grep. 4. Skip or neutralize the mutation when the target is the probing node itself.
Self-immunity can be based on:
- local validator id
- local ADNL id
- local node role or index
- a target address or peer id from
TON_PROBING_TARGET_ADDRor similar env vars
Do not hide the guard. Log when the guard causes the node to skip the mutation.
Withhold Messages Or Proposals
Use this when you want to drop or suppress a message that would normally be sent.
Likely source files:
validator/consensus/block-producer.cppvalidator/consensus/simplex/*.cppvalidator/full-node*.cpp
Useful env vars:
TON_PROBING_WITHHOLD_PROPOSALS=1TON_PROBING_WITHHOLD_START=<slot-offset>TON_PROBING_WITHHOLD_END=<slot-offset>
Log shape:
TON_PROBING withholding proposal slot=<n> target=<peer> reason=<...>
Self-immunity:
- do not withhold the messages that keep the probing node alive or synchronized
- if a slot range would starve the probing node first, skip the mutation and log that skip
Send Malformed Data Or Packets
Use this when the bug depends on a malformed TL object, invalid BoC, corrupted proof, or broken serialization.
Likely source files:
validator/impl/*.cppvalidator/full-node-*.cppadnl/*.cpp
Useful env vars:
TON_PROBING_SEND_BAD_PACKET=1TON_PROBING_BAD_PACKET_KIND=<name>
Log shape:
TON_PROBING send bad packet kind=<name> peer=<peer>
Self-immunity:
- never feed the malformed object back into the probing node’s own validation path
- if the same packet is reflected locally, short-circuit that reflection in probing mode
Reorder Protocol Messages
Use this when the bug depends on out-of-order processing.
Likely source files:
validator/consensus/simplex/pool.cppvalidator/consensus/simplex/consensus.cpp- queue or broadcast handlers near the message type you are targeting
Useful env vars:
TON_PROBING_REORDER_SKIP_CERTS=1TON_PROBING_REORDER_BUFFER=<count>
Log shape:
TON_PROBING reorder <message-kind> buffered=<n> released=<n>
Self-immunity:
- do not reorder the probing node’s own critical recovery or catch-up path
- if reordering would deadlock the probing node before the honest nodes see the mutation, skip it
Delay Responses
Use this when the bug depends on timeouts, races, or delayed votes.
Likely source files:
- consensus handlers
- block download or catch-up paths
- message dispatch or retry logic
Useful env vars:
TON_PROBING_DELAY_MS=<milliseconds>TON_PROBING_DELAY_KIND=<message-kind>
Log shape:
TON_PROBING delay kind=<message-kind> ms=<value>
Self-immunity:
- do not delay the probing node’s own minimum liveness path
- if the delay would obviously kill the probing node first, clamp or skip it
Produce Invalid Blocks Or Proofs
Use this when the goal is to test honest-node rejection or crash handling for invalid consensus artifacts.
Likely source files:
validator/consensus/block-producer.cppvalidator/impl/block*.cpp- proof construction or acceptance paths
Useful env vars:
TON_PROBING_INVALID_BLOCK=1TON_PROBING_INVALID_FIELD=<name>
Log shape:
TON_PROBING invalid block field=<name> block=<id>
Self-immunity:
- do not let the probing node accept or execute the invalid artifact as if it were honest input
- keep the probing node alive long enough to see honest-node rejection, crash, or fork symptoms
Recommended Run Conditions
When using run_mixed_network.py, pair the patch with explicit conditions:
--success-logfor the expected probing marker or honest-node effect--failure-logfor invalid-run markers--require-node-alive nodeNfor the probing node when self-immunity matters--require-node-dead nodeNonly when the target effect is honest-node death and the victim is known
Use scripts/summarize_run.py after the run even if you already tailed logs manually.
Wallet Smoke Path
Use this reference when you need the proven simple-wallet deploy/send flow on a local tontester network.
This document is intentionally narrow.
- For generic contract deployment, read
contract_deploy_flow.md. - For binary trigger payloads, prefer
wallet_send.py --body-boc. - If you already have a serialized external message BoC, use
send_boc.py --boc. --body-bocoverrides--comment.
Fastest Verified Path
If you want a known-good smoke test for the full flow, run:
python3 /path/to/skill/scripts/demo_wallet_flow.py \
--repo-root /path/to/repo \
--build /path/to/repo/vanilla-buildThat verifier does all of the following on a 2-validator network:
- launch validators and emit
wallet-env.txt - create two workchain wallets
- deploy both wallets from the funded built-in
main-wallet - wait until both deployed wallets become active
- send TON from wallet A to wallet B with a comment
- inspect wallet B's latest transaction and confirm sender, value, and comment
Use it when you need a quick proof that the local helper stack is healthy before doing a more specialized experiment.
Manual Flow
1. Launch The Network
python3 /path/to/skill/scripts/run_basic_network.py \
--repo-root /path/to/repo \
--build /path/to/repo/vanilla-build \
--validators 2 \
--mc-seqno 3 \
--emit-wallet-envThis prints the run directory and writes wallet-env.txt.
2. Build Wallet StateInit
Use the bundled Fift helper that saves -stateinit.boc:
python3 /path/to/skill/scripts/run_fift_script.py \
--wallet-env /path/to/run/wallet-env.txt \
--script /path/to/skill/scripts/fift/new-wallet-save-stateinit.fif \
--cwd /tmp/wallet-a \
-- 0 wallet-aRepeat for wallet-b.
Expected outputs:
wallet-a.pkwallet-a.addrwallet-a-query.bocwallet-a-stateinit.boc
3. Deploy From The Built-In Main Wallet
Fresh tontester zerostates include a funded simple wallet at STATE_DIR/main-wallet.{pk,addr}. wallet_send.py uses it by default when wallet-env.txt is present.
python3 /path/to/skill/scripts/wallet_send.py \
--wallet-env /path/to/run/wallet-env.txt \
--dest-addr <wallet-a-raw-or-friendly-address> \
--auto-seqno \
--amount 1 \
--init-boc /tmp/wallet-a/wallet-a-stateinit.boc \
--show-seqno \
--wait-mc-advanceRepeat for wallet B.
Important:
--wait-mc-advanceis only a liveness hint.- A newly deployed workchain wallet may still be inactive immediately after that, so confirm activation before using it.
4. Wait For Activation
Do not use the new wallet as a sender until it is actually active.
At minimum, confirm:
- non-empty code
- non-empty data
last_transaction_idpresent
You can check with:
python3 /path/to/skill/scripts/account_state.py \
--wallet-env /path/to/run/wallet-env.txt \
--address <wallet-a-address> \
--out-dir /tmp/wallet-a-stateIf you need a stronger wallet-specific check, run seqno.
5. Confirm Wallet Seqno
For these generated wallets, numeric method id 85143 is reliable:
python3 /path/to/skill/scripts/get_method.py \
--wallet-env /path/to/run/wallet-env.txt \
--address <wallet-a-address> \
--method 85143Expected initial result: top_number = 0.
wallet_send.py --auto-seqno already falls back to the numeric method when needed, so you usually do not need to pass a hardcoded --seqno.
6. Send From Wallet A To Wallet B With A Comment
python3 /path/to/skill/scripts/wallet_send.py \
--wallet-env /path/to/run/wallet-env.txt \
--wallet-base /tmp/wallet-a/wallet-a \
--dest-addr <wallet-b-address> \
--auto-seqno \
--amount 0.1 \
--comment "skill demo payment" \
--show-seqno \
--wait-mc-advanceAfter sending, wait until either:
- wallet A
seqnoincrements, or - wallet B's latest transaction changes
Replaying A Prebuilt External Message
If you want to replay the same payment later, first build it without sending it:
python3 /path/to/skill/scripts/wallet_send.py \
--wallet-env /path/to/run/wallet-env.txt \
--wallet-base /tmp/wallet-a/wallet-a \
--dest-addr <wallet-b-address> \
--auto-seqno \
--amount 0.1 \
--comment "skill demo payment" \
--dry-run \
--out-dir /tmp/wallet-a-sendThen send the serialized external message exactly as produced:
python3 /path/to/skill/scripts/send_boc.py \
--wallet-env /path/to/run/wallet-env.txt \
--boc /tmp/wallet-a-send/wallet-query.boc \
--show-seqno \
--wait-mc-advanceUse this path for replay and transport debugging. It sends the serialized message exactly as provided.
7. Inspect The Resulting Transaction
Use the dedicated inspection helper instead of relying only on account_state.py:
python3 /path/to/skill/scripts/inspect_latest_transaction.py \
--wallet-env /path/to/run/wallet-env.txt \
--address <wallet-b-address>This prints JSON including:
- transaction lt/hash
- inbound source and destination
- transferred value
- fees
- decoded comment text when tonlib exposes it as
msg.dataTextor raw comment bytes
For a successful comment transfer, expect the latest transaction to show:
- source = wallet A
- destination = wallet B
- value = transfer amount in nanotons
- comment = expected text
Lite-Client Fallback
If the tonlib-based helpers crash, use the shared fallback guide in liteclient_fallback.md.
For this flow, use it for wallet activation checks, manual wallet seqno, and lasttransdump on wallet B.
Address Handling
Helpers accept either raw workchain:hex or tonlib-serialized/base64 forms.
If a raw address begins with -, prefer the --flag=value form:
--address=-1:...--dest-addr=-1:...
Use address_info.py to normalize between forms when needed.
Common Failure Modes
- Deploy seemed to work, but the new wallet still has empty code/data.
Most often this means you checked too early. Wait for activation before declaring the deploy broken.
- Sending from the new wallet fails with unpack-state errors.
The wallet usually is not active yet, or you signed with the wrong seqno.
- The network advanced, but you still do not see the expected transfer.
mc_seqno movement is not transaction proof. Inspect the latest account transaction directly.
"""Fetch raw account state through tonlib and optionally dump code/data BoCs.
Use this to confirm activation, inspect current balance and last transaction,
or export code/data artifacts for follow-up debugging.
"""
from __future__ import annotations
import argparse
import asyncio
import json
from pathlib import Path
from ton_triage_lib import (
build_tonlib_client,
normalize_account_address,
raw_get_account_state_with_timeout,
resolve_path,
runtime_from_args,
)
async def _run(args: argparse.Namespace) -> None:
runtime = runtime_from_args(args)
out_dir = resolve_path(Path.cwd(), args.out_dir) if args.out_dir else None
client = await build_tonlib_client(runtime, verbosity=args.verbosity, request_timeout=args.request_timeout)
try:
normalized = await normalize_account_address(client, args.address, request_timeout=args.request_timeout)
state = await raw_get_account_state_with_timeout(
client,
normalized.serialized,
args.request_timeout,
)
finally:
await client.aclose()
code = state.code or b""
data = state.data or b""
result = {
"address_input": args.address,
"address_serialized": normalized.serialized,
"address_raw": normalized.raw,
"code_len": len(code),
"data_len": len(data),
}
if out_dir is not None:
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "account-state.json").write_text(state.to_json())
(out_dir / "code.boc").write_bytes(code)
(out_dir / "data.boc").write_bytes(data)
result["out_dir"] = str(out_dir)
print(json.dumps(result, indent=2))
def main() -> None:
parser = argparse.ArgumentParser(description="Fetch raw account state through tonlib")
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing tonlibjson")
parser.add_argument("--config", help="Lite-client config JSON path")
parser.add_argument("--state-dir", help="Optional state dir for helper defaults")
parser.add_argument(
"--address",
required=True,
help="TON account address accepted by tonlib; use --address=<value> for raw forms like -1:...",
)
parser.add_argument("--out-dir", help="Optional output directory for dumped code/data/state")
parser.add_argument("--verbosity", type=int, default=0, help="Tonlib verbosity level")
parser.add_argument(
"--request-timeout",
type=float,
default=5.0,
help="Per-request timeout for tonlib address/state queries",
)
args = parser.parse_args()
asyncio.run(_run(args))
if __name__ == "__main__":
main()
"""Normalize a TON account address through tonlib.
Use this when you need to convert between raw and serialized forms or inspect
address metadata such as bounceability and testnet bits.
"""
from __future__ import annotations
import argparse
import asyncio
import json
from ton_triage_lib import build_tonlib_client, normalize_account_address, runtime_from_args
async def _run(args: argparse.Namespace) -> None:
runtime = runtime_from_args(args)
client = await build_tonlib_client(runtime, verbosity=args.verbosity, request_timeout=args.request_timeout)
try:
normalized = await normalize_account_address(client, args.address, request_timeout=args.request_timeout)
finally:
await client.aclose()
print(
json.dumps(
{
"address_input": normalized.input_value,
"address_serialized": normalized.serialized,
"address_raw": normalized.raw,
"workchain_id": normalized.workchain_id,
"bounceable": normalized.bounceable,
"testnet": normalized.testnet,
"addr_hex": normalized.addr_hex,
},
indent=2,
)
)
def main() -> None:
parser = argparse.ArgumentParser(description="Normalize and inspect a TON account address through tonlib")
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing tonlibjson")
parser.add_argument("--config", help="Lite-client config JSON path")
parser.add_argument("--state-dir", help="Optional state dir for helper defaults")
parser.add_argument(
"--address",
required=True,
help="TON account address in any tonlib-accepted form; use --address=<value> for raw forms like -1:...",
)
parser.add_argument("--verbosity", type=int, default=0, help="Tonlib verbosity level")
parser.add_argument(
"--request-timeout",
type=float,
default=5.0,
help="Per-request timeout for tonlib address normalization",
)
args = parser.parse_args()
asyncio.run(_run(args))
if __name__ == "__main__":
main()
"""Build a deployable StateInit BoC from code, data, and optional libraries.
Use this after compiling contract code and before deployment or zerostate-style
address calculation.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from ton_triage_lib import (
resolve_path,
run_fift_script_command,
runtime_from_args,
wallet_address_from_base,
)
SCRIPT_DIR = Path(__file__).resolve().parent
FIFT_DIR = SCRIPT_DIR / "fift"
def _parse_marker(output: str, marker: str) -> str | None:
prefix = f"{marker}: "
for raw_line in output.splitlines():
line = raw_line.strip()
if line.startswith(prefix):
return line.removeprefix(prefix).strip()
return None
def _validate_name(value: str) -> str:
if value in {"", ".", ".."}:
raise argparse.ArgumentTypeError("name must be a simple non-empty filename base")
path = Path(value)
if path.name != value:
raise argparse.ArgumentTypeError("name must not contain directory separators")
return value
def main() -> None:
parser = argparse.ArgumentParser(
description="Build a deployable StateInit BoC from code plus optional data and libraries",
epilog=(
"Examples:\n"
" python3 build_stateinit.py --wallet-env /tmp/run/wallet-env.txt \\\n"
" --code-boc /tmp/contract/code.boc --out-dir /tmp/contract-build\n"
" python3 build_stateinit.py --repo-root /path/to/repo --build /path/to/repo/build \\\n"
" --code-boc code.boc --data-boc data.boc --library-boc libs.boc \\\n"
" --workchain 0 --name target --out-dir /tmp/target-stateinit"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing create-state")
parser.add_argument("--config", help="Unused here; accepted for wallet-env compatibility")
parser.add_argument("--state-dir", help="Unused here; accepted for wallet-env compatibility")
parser.add_argument("--code-boc", required=True, help="Compiled contract code BoC")
parser.add_argument("--data-boc", help="Optional initial data BoC")
parser.add_argument(
"--library-boc",
help="Optional library-dictionary root cell BoC; pass a prebuilt HashmapE root, not raw library code",
)
parser.add_argument("--workchain", type=int, default=0, help="Destination workchain id")
parser.add_argument("--name", type=_validate_name, default="contract", help="Filename base for emitted artifacts")
parser.add_argument("--out-dir", required=True, help="Output directory for the emitted StateInit and address files")
args = parser.parse_args()
runtime = runtime_from_args(args, require_config=False)
out_dir = resolve_path(Path.cwd(), args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
code_boc = resolve_path(Path.cwd(), args.code_boc)
data_boc = resolve_path(Path.cwd(), args.data_boc) if args.data_boc else None
library_boc = resolve_path(Path.cwd(), args.library_boc) if args.library_boc else None
completed = run_fift_script_command(
runtime.repo_root,
runtime.build_dir,
FIFT_DIR / "build_stateinit.fif",
cwd=out_dir,
script_args=[
str(args.workchain),
str(code_boc),
str(data_boc) if data_boc is not None else "-",
str(library_boc) if library_boc is not None else "-",
args.name,
],
capture_output=True,
)
if completed.stdout:
print(completed.stdout, end="" if completed.stdout.endswith("\n") else "\n")
contract_base = out_dir / args.name
stateinit_boc = contract_base.with_name(f"{args.name}-stateinit.boc")
address_file = contract_base.with_suffix(".addr")
result = {
"workchain": args.workchain,
"code_boc": str(code_boc),
"data_boc": str(data_boc) if data_boc is not None else None,
"library_boc": str(library_boc) if library_boc is not None else None,
"stateinit_boc": str(stateinit_boc),
"address_file": str(address_file),
"address_raw": wallet_address_from_base(contract_base),
"address_non_bounceable": _parse_marker(completed.stdout, "contract address non-bounceable"),
"address_bounceable": _parse_marker(completed.stdout, "contract address bounceable"),
}
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
"""Compile a Tolk contract and materialize its code BoC.
Use this when Workflow A starts from a `.tolk` source and you want a checked
path that refuses to use a stale `tolk` binary.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from ton_triage_lib import (
ensure_tolk_matches_repo,
resolve_path,
run_fift_script_command,
run_subprocess,
runtime_from_args,
)
def _validate_name(value: str) -> str:
if value in {"", ".", ".."}:
raise argparse.ArgumentTypeError("name must be a simple non-empty filename base")
path = Path(value)
if path.name != value:
raise argparse.ArgumentTypeError("name must not contain directory separators")
return value
def main() -> None:
parser = argparse.ArgumentParser(
description="Compile a Tolk contract and materialize its code BoC",
epilog=(
"Examples:\n"
" python3 compile_tolk.py --repo-root /path/to/repo --build /path/to/repo/build \\\n"
" --source /tmp/counter.tolk --out-dir /tmp/counter-build\n"
" python3 compile_tolk.py --wallet-env /tmp/run/wallet-env.txt \\\n"
" --source contracts/trigger.tolk --name trigger --out-dir /tmp/trigger-build"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing tolk and create-state")
parser.add_argument("--config", help="Unused here; accepted for wallet-env compatibility")
parser.add_argument("--state-dir", help="Unused here; accepted for wallet-env compatibility")
parser.add_argument("--source", required=True, help="Path to the .tolk source file")
parser.add_argument("--out-dir", required=True, help="Output directory for compiler artifacts")
parser.add_argument("--name", type=_validate_name, help="Filename base for emitted artifacts; defaults to the source stem")
args = parser.parse_args()
runtime = runtime_from_args(args, require_config=False)
source = resolve_path(Path.cwd(), args.source)
out_dir = resolve_path(Path.cwd(), args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
name = args.name or source.stem
build_info = ensure_tolk_matches_repo(runtime.repo_root, runtime.build_dir)
fift_out = out_dir / f"{name}.fif"
code_boc = out_dir / f"{name}.code.boc"
completed = run_subprocess(
[
str(build_info.binary),
"-o",
str(fift_out),
"-b",
str(code_boc),
str(source),
],
cwd=out_dir,
capture_output=True,
)
if completed.stdout:
print(completed.stdout, end="" if completed.stdout.endswith("\n") else "\n")
if completed.stderr:
print(completed.stderr, end="" if completed.stderr.endswith("\n") else "\n")
fift_completed = run_fift_script_command(
runtime.repo_root,
runtime.build_dir,
fift_out,
cwd=out_dir,
capture_output=True,
)
if fift_completed.stdout:
print(fift_completed.stdout, end="" if fift_completed.stdout.endswith("\n") else "\n")
print(
json.dumps(
{
"source": str(source),
"tolk_binary": str(build_info.binary),
"tolk_version": build_info.version,
"build_commit": build_info.build_commit,
"build_date": build_info.build_date,
"fift_output": str(fift_out),
"code_boc": str(code_boc),
},
indent=2,
)
)
if __name__ == "__main__":
main()
"""Run the proven wallet-to-wallet smoke flow on a local tontester network.
Use this script when you want a known-good end-to-end helper check before a
more specialized deploy or validator-behavior repro.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import shlex
import subprocess
import sys
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from inspect_latest_transaction import inspect_latest_transaction
from ton_triage_lib import (
build_tonlib_client,
normalize_account_address,
raw_get_account_state_with_timeout,
resolve_wallet_seqno,
runtime_from_args,
wallet_address_from_base,
)
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(line_buffering=True)
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(line_buffering=True)
def _format_command(cmd: list[str]) -> str:
return " ".join(shlex.quote(part) for part in cmd)
def _run_command(cmd: list[str], *, cwd: Path | None = None) -> str:
print(f"running: {_format_command(cmd)}")
completed = subprocess.run(
cmd,
cwd=cwd,
check=True,
text=True,
capture_output=True,
)
if completed.stdout:
print(completed.stdout, end="" if completed.stdout.endswith("\n") else "\n")
if completed.stderr:
print(completed.stderr, end="" if completed.stderr.endswith("\n") else "\n", file=sys.stderr)
return completed.stdout
def _run_json_command(cmd: list[str], *, cwd: Path | None = None) -> dict[str, object]:
return json.loads(_run_command(cmd, cwd=cwd))
def _runtime_from_wallet_env(wallet_env: Path):
return runtime_from_args(
argparse.Namespace(
wallet_env=str(wallet_env),
run_dir=None,
repo_root=None,
build=None,
config=None,
state_dir=None,
wallet_base=None,
)
)
def _require_process_alive(process: subprocess.Popen[str], *, context: str) -> None:
if process.poll() is not None:
raise RuntimeError(f"network launcher exited during {context} with status {process.returncode}")
def _launch_network(args: argparse.Namespace) -> tuple[subprocess.Popen[str], Path, Path]:
if args.validators < 2:
raise SystemExit("--validators must be at least 2 for this demo")
cmd = [
sys.executable,
"-u",
str(SCRIPT_DIR / "run_basic_network.py"),
"--repo-root",
str(args.repo_root),
"--build",
str(args.build),
"--workdir",
args.workdir,
"--base-port",
str(args.base_port),
"--validators",
str(args.validators),
"--mc-seqno",
str(args.mc_seqno),
"--emit-wallet-env",
"--keep-alive",
str(args.keep_alive),
]
print(f"running: {_format_command(cmd)}")
process = subprocess.Popen(
cmd,
cwd=args.repo_root,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
if process.stdout is None:
raise RuntimeError("failed to capture launcher stdout")
run_dir: Path | None = None
wallet_env: Path | None = None
deadline = time.time() + args.launch_timeout
while True:
line = process.stdout.readline()
if not line:
if process.poll() is not None:
raise RuntimeError(f"network launcher exited early with status {process.returncode}")
if time.time() >= deadline:
raise TimeoutError("timed out waiting for network launcher output")
time.sleep(0.1)
continue
print(line, end="")
if line.startswith("run dir: "):
run_dir = Path(line.removeprefix("run dir: ").strip())
elif line.startswith("wallet env: "):
wallet_env = Path(line.removeprefix("wallet env: ").strip())
elif line.startswith("reached mc seqno >="):
break
if time.time() >= deadline:
raise TimeoutError("timed out waiting for the launcher to reach the target mc seqno")
if run_dir is None or wallet_env is None:
raise RuntimeError("launcher output did not include run dir and wallet env")
return process, run_dir, wallet_env
def _stop_process(process: subprocess.Popen[str]) -> None:
if process.poll() is not None:
return
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=10)
def _build_wallet(wallet_env: Path, output_dir: Path, name: str) -> Path:
wallet_dir = output_dir / name
wallet_dir.mkdir(parents=True, exist_ok=True)
_run_command(
[
sys.executable,
str(SCRIPT_DIR / "run_fift_script.py"),
"--wallet-env",
str(wallet_env),
"--script",
str(SCRIPT_DIR / "fift" / "new-wallet-save-stateinit.fif"),
"--cwd",
str(wallet_dir),
"--",
"0",
name,
]
)
return wallet_dir / name
def _deploy_wallet(wallet_env: Path, dest_addr: str, init_boc: Path, amount: str) -> None:
_run_command(
[
sys.executable,
str(SCRIPT_DIR / "wallet_send.py"),
"--wallet-env",
str(wallet_env),
f"--dest-addr={dest_addr}",
"--auto-seqno",
"--amount",
amount,
"--init-boc",
str(init_boc),
"--show-seqno",
"--wait-mc-advance",
"--wait-timeout",
"20",
]
)
def _send_from_wallet(
wallet_env: Path,
wallet_base: Path,
dest_addr: str,
amount: str,
comment: str,
) -> None:
_run_command(
[
sys.executable,
str(SCRIPT_DIR / "wallet_send.py"),
"--wallet-env",
str(wallet_env),
"--wallet-base",
str(wallet_base),
f"--dest-addr={dest_addr}",
"--auto-seqno",
"--amount",
amount,
"--comment",
comment,
"--show-seqno",
"--wait-mc-advance",
"--wait-timeout",
"20",
]
)
def _verify_seqno_helper(wallet_env: Path, address: str, expected: int) -> dict[str, object]:
result = _run_json_command(
[
sys.executable,
str(SCRIPT_DIR / "get_method.py"),
"--wallet-env",
str(wallet_env),
f"--address={address}",
"--method",
"85143",
]
)
if result.get("exit_code") != 0 or result.get("top_number") != expected:
raise RuntimeError(f"unexpected seqno helper result for {address}: {result}")
return result
async def _wait_for_active(
client,
address: str,
*,
label: str,
wait_timeout: float,
request_timeout: float,
):
normalized = await normalize_account_address(client, address, request_timeout=request_timeout)
deadline = asyncio.get_running_loop().time() + wait_timeout
while True:
state = await raw_get_account_state_with_timeout(client, normalized.serialized, request_timeout)
if state.code and state.data and state.last_transaction_id is not None:
print(
f"{label} active:"
f" balance={state.balance}"
f" last_lt={state.last_transaction_id.lt}"
)
return normalized, state
if asyncio.get_running_loop().time() >= deadline:
raise TimeoutError(f"timed out waiting for {label} activation")
await asyncio.sleep(0.5)
async def _wait_for_seqno(
client,
address: str,
*,
expected: int,
label: str,
wait_timeout: float,
request_timeout: float,
) -> int:
deadline = asyncio.get_running_loop().time() + wait_timeout
while True:
seqno, _method = await resolve_wallet_seqno(client, address, request_timeout=request_timeout)
if seqno >= expected:
print(f"{label} seqno now {seqno}")
return seqno
if asyncio.get_running_loop().time() >= deadline:
raise TimeoutError(f"timed out waiting for {label} seqno {expected}")
await asyncio.sleep(0.5)
async def _wait_for_recipient_transaction(
client,
*,
address: str,
previous_lt: int,
expected_source: str,
expected_comment: str,
wait_timeout: float,
request_timeout: float,
) -> dict[str, object]:
deadline = asyncio.get_running_loop().time() + wait_timeout
while True:
tx = await inspect_latest_transaction(client, address, request_timeout=request_timeout)
transaction = tx["transaction"]
transaction_id = transaction["transaction_id"]
current_lt = transaction_id["lt"]
in_msg = transaction["in_msg"]
if (
isinstance(current_lt, int)
and current_lt > previous_lt
and isinstance(in_msg, dict)
and in_msg.get("source") == expected_source
and in_msg.get("comment") == expected_comment
):
print(
"recipient transfer observed:"
f" lt={current_lt}"
f" comment={expected_comment!r}"
)
return tx
if asyncio.get_running_loop().time() >= deadline:
raise TimeoutError("timed out waiting for recipient transfer transaction")
await asyncio.sleep(0.5)
async def _run(args: argparse.Namespace) -> None:
process: subprocess.Popen[str] | None = None
client = None
try:
process, run_dir, wallet_env = _launch_network(args)
artifact_dir = run_dir / "wallet-demo"
artifact_dir.mkdir(parents=True, exist_ok=True)
wallet_a_base = _build_wallet(wallet_env, artifact_dir, "wallet-a")
wallet_b_base = _build_wallet(wallet_env, artifact_dir, "wallet-b")
wallet_a_raw = wallet_address_from_base(wallet_a_base)
wallet_b_raw = wallet_address_from_base(wallet_b_base)
print(f"wallet A raw address: {wallet_a_raw}")
print(f"wallet B raw address: {wallet_b_raw}")
_require_process_alive(process, context="wallet deployment setup")
_deploy_wallet(
wallet_env,
wallet_a_raw,
wallet_a_base.with_name(wallet_a_base.name + "-stateinit.boc"),
args.deploy_amount,
)
_require_process_alive(process, context="wallet A deployment")
_deploy_wallet(
wallet_env,
wallet_b_raw,
wallet_b_base.with_name(wallet_b_base.name + "-stateinit.boc"),
args.deploy_amount,
)
runtime = _runtime_from_wallet_env(wallet_env)
client = await build_tonlib_client(
runtime,
verbosity=args.verbosity,
request_timeout=args.request_timeout,
)
wallet_a_normalized, wallet_a_state = await _wait_for_active(
client,
wallet_a_raw,
label="wallet A",
wait_timeout=args.wait_timeout,
request_timeout=args.request_timeout,
)
wallet_b_normalized, wallet_b_state = await _wait_for_active(
client,
wallet_b_raw,
label="wallet B",
wait_timeout=args.wait_timeout,
request_timeout=args.request_timeout,
)
wallet_a_seqno_before = _verify_seqno_helper(wallet_env, wallet_a_raw, expected=0)
wallet_b_seqno_before = _verify_seqno_helper(wallet_env, wallet_b_raw, expected=0)
_require_process_alive(process, context="wallet-to-wallet send")
_send_from_wallet(
wallet_env,
wallet_a_base,
wallet_b_raw,
args.transfer_amount,
args.comment,
)
sender_seqno_after = await _wait_for_seqno(
client,
wallet_a_raw,
expected=1,
label="wallet A",
wait_timeout=args.wait_timeout,
request_timeout=args.request_timeout,
)
recipient_tx = await _wait_for_recipient_transaction(
client,
address=wallet_b_raw,
previous_lt=wallet_b_state.last_transaction_id.lt,
expected_source=wallet_a_normalized.serialized,
expected_comment=args.comment,
wait_timeout=args.wait_timeout,
request_timeout=args.request_timeout,
)
recipient_tx_from_helper = _run_json_command(
[
sys.executable,
str(SCRIPT_DIR / "inspect_latest_transaction.py"),
"--wallet-env",
str(wallet_env),
f"--address={wallet_b_raw}",
]
)
wallet_a_seqno_after = _verify_seqno_helper(wallet_env, wallet_a_raw, expected=1)
final_wallet_a_state = await raw_get_account_state_with_timeout(
client,
wallet_a_normalized.serialized,
args.request_timeout,
)
final_wallet_b_state = await raw_get_account_state_with_timeout(
client,
wallet_b_normalized.serialized,
args.request_timeout,
)
summary = {
"run_dir": str(run_dir),
"wallet_env": str(wallet_env),
"wallet_a": {
"raw": wallet_a_raw,
"serialized": wallet_a_normalized.serialized,
"deploy_balance": wallet_a_state.balance,
"deploy_last_lt": wallet_a_state.last_transaction_id.lt,
"seqno_before_send": wallet_a_seqno_before["top_number"],
"seqno_after_send": wallet_a_seqno_after["top_number"],
"seqno_after_send_polled": sender_seqno_after,
"final_balance": final_wallet_a_state.balance,
"final_last_lt": (
final_wallet_a_state.last_transaction_id.lt
if final_wallet_a_state.last_transaction_id is not None
else None
),
},
"wallet_b": {
"raw": wallet_b_raw,
"serialized": wallet_b_normalized.serialized,
"deploy_balance": wallet_b_state.balance,
"deploy_last_lt": wallet_b_state.last_transaction_id.lt,
"seqno_before_receive": wallet_b_seqno_before["top_number"],
"final_balance": final_wallet_b_state.balance,
"final_last_lt": (
final_wallet_b_state.last_transaction_id.lt
if final_wallet_b_state.last_transaction_id is not None
else None
),
},
"transfer": {
"amount": args.transfer_amount,
"comment": args.comment,
"recipient_transaction": recipient_tx,
"recipient_transaction_helper": recipient_tx_from_helper,
},
}
summary_path = run_dir / "wallet-demo-summary.json"
summary_path.write_text(json.dumps(summary, indent=2) + "\n")
print(f"summary json: {summary_path}")
print(json.dumps(summary, indent=2))
finally:
if client is not None:
await client.aclose()
if process is not None:
_stop_process(process)
def main() -> None:
parser = argparse.ArgumentParser(
description="Run a 2-validator local TON network, deploy two wallets, send a comment transfer, and inspect it",
)
parser.add_argument("--repo-root", required=True, help="Path to the TON repo root")
parser.add_argument("--build", required=True, help="Build directory to run")
parser.add_argument(
"--workdir",
default="tmp/tontester-demo-wallet",
help="Base working directory for run artifacts",
)
parser.add_argument("--base-port", type=int, default=3201, help="First TCP/UDP port to allocate")
parser.add_argument("--validators", type=int, default=2, help="Number of validator nodes")
parser.add_argument("--mc-seqno", type=int, default=3, help="Initial masterchain seqno target")
parser.add_argument("--keep-alive", type=int, default=240, help="Seconds to keep the network alive")
parser.add_argument(
"--launch-timeout",
type=float,
default=90.0,
help="Timeout while waiting for the launcher to reach the initial mc seqno",
)
parser.add_argument(
"--wait-timeout",
type=float,
default=60.0,
help="Timeout for wallet activation and post-send transaction observation",
)
parser.add_argument("--deploy-amount", default="1", help="Amount sent to each wallet on deploy")
parser.add_argument("--transfer-amount", default="0.1", help="Amount sent from wallet A to wallet B")
parser.add_argument("--comment", default="skill demo payment", help="Transfer comment payload")
parser.add_argument("--verbosity", type=int, default=0, help="Tonlib verbosity level")
parser.add_argument(
"--request-timeout",
type=float,
default=5.0,
help="Per-request timeout for tonlib calls made by the verifier",
)
args = parser.parse_args()
args.repo_root = Path(args.repo_root).resolve()
build_path = Path(args.build)
if not build_path.is_absolute():
build_path = (args.repo_root / build_path).resolve()
args.build = build_path
asyncio.run(_run(args))
if __name__ == "__main__":
main()
"""Render a BoC cell tree through Fift for human inspection.
Use this when you need to inspect payload, StateInit, or transaction BoCs at
the cell level instead of treating them as opaque bytes.
"""
from __future__ import annotations
import argparse
import tempfile
from pathlib import Path
from ton_triage_lib import resolve_path, run_fift_script_command, runtime_from_args
SCRIPT_DIR = Path(__file__).resolve().parent
FIFT_DIR = SCRIPT_DIR / "fift"
def main() -> None:
parser = argparse.ArgumentParser(
description="Dump a BoC cell tree through Fift",
epilog=(
"Examples:\n"
" python3 dump_boc.py --wallet-env /tmp/run/wallet-env.txt --boc /tmp/contract/code.boc\n"
" python3 dump_boc.py --repo-root /path/to/repo --build /path/to/repo/build \\\n"
" --boc-hex b5ee9c72410101010004000000"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing create-state")
parser.add_argument("--config", help="Unused here; accepted for wallet-env compatibility")
parser.add_argument("--state-dir", help="Unused here; accepted for wallet-env compatibility")
parser.add_argument("--boc", help="Path to a BoC file")
parser.add_argument("--boc-hex", help="BoC bytes as a hex string")
parser.add_argument("--out-file", help="Optional path to also save the dump text")
args = parser.parse_args()
if bool(args.boc) == bool(args.boc_hex):
raise SystemExit("pass exactly one of --boc or --boc-hex")
runtime = runtime_from_args(args, require_config=False)
if args.boc:
boc_path = resolve_path(Path.cwd(), args.boc)
with tempfile.TemporaryDirectory(prefix="dump-boc-") as tmp_dir:
completed = run_fift_script_command(
runtime.repo_root,
runtime.build_dir,
FIFT_DIR / "dump_boc.fif",
cwd=Path(tmp_dir),
script_args=[str(boc_path)],
capture_output=True,
)
else:
boc_bytes = bytes.fromhex(args.boc_hex)
with tempfile.TemporaryDirectory(prefix="dump-boc-") as tmp_dir:
tmp_path = Path(tmp_dir)
boc_path = tmp_path / "inline.boc"
boc_path.write_bytes(boc_bytes)
completed = run_fift_script_command(
runtime.repo_root,
runtime.build_dir,
FIFT_DIR / "dump_boc.fif",
cwd=tmp_path,
script_args=[str(boc_path)],
capture_output=True,
)
dump_text = completed.stdout
if args.out_file:
out_file = resolve_path(Path.cwd(), args.out_file)
out_file.parent.mkdir(parents=True, exist_ok=True)
out_file.write_text(dump_text)
print(dump_text, end="" if dump_text.endswith("\n") else "\n")
if __name__ == "__main__":
main()
#!/usr/bin/fift -s
"TonUtil.fif" include
{ ."usage: " @' $0 type ." <workchain-id> <code-boc> <data-boc-or--> <library-boc-or--> [<filename-base>]" cr
."Builds a deployable StateInit BoC from code plus optional data and library dictionary cells." cr
."Saves <filename-base>-stateinit.boc and <filename-base>.addr ('contract' by default)." cr
1 halt
} : usage
$# dup 4 < swap 5 > or ' usage if
5 :$1..n
$1 parse-workchain-id =: wc
$2 =: code-boc-file
$3 =: data-boc-file
$4 =: library-boc-file
$5 "contract" replace-if-null =: file-base
code-boc-file dup ."Loading code cell from file `" type ."`" cr
file>B B>boc =: code
data-boc-file "-" $=
{ null }
{ data-boc-file dup ."Loading data cell from file `" type ."`" cr file>B B>boc }
cond =: data
library-boc-file "-" $=
{ null }
{ library-boc-file dup ."Loading library dictionary cell from file `" type ."`" cr file>B B>boc }
cond =: libraries
data null? {
libraries null? {
<b b{00100} s, code ref, b>
} {
<b b{00101} s, code ref, libraries ref, b>
} cond
} {
libraries null? {
<b b{00110} s, code ref, data ref, b>
} {
<b b{00111} s, code ref, data ref, libraries ref, b>
} cond
} cond
dup =: state-init
."StateInit cell: " state-init <s csr. cr
state-init 2 boc+>B file-base +"-stateinit.boc" tuck B>file drop
state-init hashu wc swap 2dup 2constant contract_addr
."contract address raw: " 2dup .addr cr
2dup file-base +".addr" save-address-verbose
."contract address non-bounceable: " 2dup 7 .Addr cr
."contract address bounceable: " 6 .Addr cr
."stateinit boc path: " file-base +"-stateinit.boc" type cr
."address file path: " file-base +".addr" type cr
#!/usr/bin/fift -s
"TonUtil.fif" include
{ ."usage: " @' $0 type ." <boc-file>" cr
."Loads a single-root BoC from file and prints the cell tree." cr
1 halt
} : usage
$# 1 <> ' usage if
$1 dup ."Loading BoC from file `" type ."`" cr
file>B B>boc <s csr. cr
#!/usr/bin/fift -s
"TonUtil.fif" include
"Asm.fif" include
{ ."usage: " @' $0 type ." <workchain-id> [<filename-base>]" cr
."Creates a new wallet in specified workchain, with private key saved to or loaded from <filename-base>.pk" cr
."('new-wallet.pk' by default)" cr 1 halt
} : usage
$# 1- -2 and ' usage if
$1 parse-workchain-id =: wc
def? $2 { @' $2 } { "new-wallet" } cond constant file-base
."Creating new wallet in workchain " wc . cr
<{ SETCP0 DUP IFNOTRET
DUP 85143 INT EQUAL OVER 78748 INT EQUAL OR IFJMP:<{
1 INT AND c4 PUSHCTR CTOS 32 LDU 256 PLDU CONDSEL
}>
INC 32 THROWIF
512 INT LDSLICEX DUP 32 PLDU
c4 PUSHCTR CTOS 32 LDU 256 LDU ENDS
s1 s2 XCPU
EQUAL 33 THROWIFNOT
s2 PUSH HASHSU
s0 s4 s4 XC2PU
CHKSIGNU
34 THROWIFNOT
ACCEPT
SWAP 32 LDU NIP
DUP SREFS IF:<{
8 LDU LDREF
s0 s2 XCHG SENDRAWMSG
}>
ENDS
INC NEWC 32 STU 256 STU ENDC c4 POPCTR
}>c
<b 0 32 u,
file-base +".pk" load-generate-keypair
constant wallet_pk
B,
b>
null
<b b{0011} s, 3 roll ref, rot ref, swap dict, b>
dup ."StateInit: " <s csr. cr
dup 2 boc+>B file-base +"-stateinit.boc" tuck B>file drop
dup hashu wc swap 2dup 2constant wallet_addr
."new wallet address = " 2dup .addr cr
2dup file-base +".addr" save-address-verbose
."Non-bounceable address (for init): " 2dup 7 .Addr cr
."Bounceable address (for later access): " 6 .Addr cr
<b 0 32 u, b>
dup ."signing message: " <s csr. cr
dup hashu wallet_pk ed25519_sign_uint rot
<b b{1000100} s, wallet_addr addr, b{000010} s, swap <s s, b{0} s, swap B, swap <s s, b>
dup ."External message for initialization is " <s csr. cr
2 boc+>B dup Bx. cr
file-base +"-query.boc" tuck B>file
."(Saved wallet creating query to file " type .")" cr
"""Run a get-method through tonlib and print the decoded result.
Use this for activation checks, wallet seqno inspection, and contract-specific
state reads after deploy or trigger transactions.
"""
from __future__ import annotations
import argparse
import asyncio
import json
from ton_triage_lib import (
build_tonlib_client,
first_stack_number,
run_get_method,
runtime_from_args,
stack_entry_to_json,
)
async def _run(args: argparse.Namespace) -> None:
runtime = runtime_from_args(args)
client = await build_tonlib_client(runtime, verbosity=args.verbosity, request_timeout=args.request_timeout)
try:
normalized, _info, result = await run_get_method(
client,
args.address,
args.method,
stack_numbers=args.stack_number,
request_timeout=args.request_timeout,
)
finally:
await client.aclose()
output = {
"address_input": args.address,
"address_serialized": normalized.serialized,
"address_raw": normalized.raw,
"method": args.method,
"gas_used": result.gas_used,
"exit_code": result.exit_code,
"stack": [stack_entry_to_json(entry) for entry in result.stack],
}
top_number = first_stack_number(result)
if top_number is not None:
output["top_number"] = top_number
print(json.dumps(output, indent=2))
def main() -> None:
parser = argparse.ArgumentParser(description="Run a get-method through tonlib and print JSON")
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing tonlibjson")
parser.add_argument("--config", help="Lite-client config JSON path")
parser.add_argument("--state-dir", help="Optional state dir for helper defaults")
parser.add_argument(
"--address",
required=True,
help="TON account address accepted by tonlib; use --address=<value> for raw forms like -1:...",
)
parser.add_argument("--method", required=True, help="Get-method name or numeric id")
parser.add_argument(
"--stack-number",
type=int,
action="append",
default=[],
help="Integer stack argument; repeat as needed",
)
parser.add_argument("--verbosity", type=int, default=0, help="Tonlib verbosity level")
parser.add_argument(
"--request-timeout",
type=float,
default=5.0,
help="Per-request timeout for tonlib address loading and get-method execution",
)
args = parser.parse_args()
asyncio.run(_run(args))
if __name__ == "__main__":
main()
"""Fetch the latest account transaction and export useful artifacts.
Use this after deploy or trigger messages when you need transaction details,
decoded comments, or raw BoCs for deeper inspection.
"""
from __future__ import annotations
import argparse
import asyncio
import json
from pathlib import Path
from ton_triage_lib import (
build_tonlib_client,
normalize_account_address,
raw_get_account_state_with_timeout,
resolve_path,
runtime_from_args,
tonlib_call,
)
def _message_data_type(msg_data) -> str:
data = msg_data.to_dict()
raw_type = data.get("@type", msg_data.__class__.__name__)
return str(raw_type)
def _decode_comment_body(body: bytes) -> str | None:
if len(body) < 4 or body[:4] != b"\x00\x00\x00\x00":
return None
payload = body[4:].rstrip(b"\x00")
if not payload:
return ""
try:
return payload.decode("utf-8")
except UnicodeDecodeError:
return None
def _write_bytes_artifact(out_dir: Path | None, filename: str, data: bytes) -> str | None:
if out_dir is None or not data:
return None
path = out_dir / filename
path.write_bytes(data)
return str(path)
def _message_data_to_json(
msg_data,
*,
out_dir: Path | None,
artifact_prefix: str,
) -> dict[str, object]:
result: dict[str, object] = {"type": _message_data_type(msg_data)}
text = getattr(msg_data, "text", None)
if isinstance(text, bytes):
result["text_hex"] = text.hex()
try:
result["text_utf8"] = text.decode("utf-8")
except UnicodeDecodeError:
pass
else:
result["comment"] = result["text_utf8"]
body = getattr(msg_data, "body", None)
if isinstance(body, bytes):
result["body_len"] = len(body)
result["body_hex"] = body.hex()
body_file = _write_bytes_artifact(out_dir, f"{artifact_prefix}-body.boc", body)
if body_file is not None:
result["body_file"] = body_file
comment = _decode_comment_body(body)
if comment is not None:
result["comment"] = comment
init_state = getattr(msg_data, "init_state", None)
if isinstance(init_state, bytes):
result["init_state_len"] = len(init_state)
init_state_file = _write_bytes_artifact(out_dir, f"{artifact_prefix}-init-state.boc", init_state)
if init_state_file is not None:
result["init_state_file"] = init_state_file
return result
def _account_address_value(account_address) -> str | None:
if account_address is None:
return None
return account_address.account_address
def _message_to_json(
message,
*,
out_dir: Path | None,
artifact_prefix: str,
) -> dict[str, object] | None:
if message is None:
return None
result: dict[str, object] = {
"hash_hex": message.hash.hex(),
"source": _account_address_value(message.source),
"destination": _account_address_value(message.destination),
"value": message.value,
"fwd_fee": message.fwd_fee,
"ihr_fee": message.ihr_fee,
"created_lt": message.created_lt,
"body_hash_hex": message.body_hash.hex(),
}
if message.msg_data is not None:
result["msg_data"] = _message_data_to_json(
message.msg_data,
out_dir=out_dir,
artifact_prefix=artifact_prefix,
)
comment = result["msg_data"].get("comment")
if comment is not None:
result["comment"] = comment
return result
def _transaction_to_json(transaction, *, out_dir: Path | None) -> dict[str, object]:
transaction_id = transaction.transaction_id
result = {
"address": _account_address_value(transaction.address),
"utime": transaction.utime,
"transaction_id": {
"lt": transaction_id.lt if transaction_id is not None else None,
"hash_hex": transaction_id.hash.hex() if transaction_id is not None else None,
},
"fee": transaction.fee,
"storage_fee": transaction.storage_fee,
"other_fee": transaction.other_fee,
"data_len": len(transaction.data),
"in_msg": _message_to_json(
transaction.in_msg,
out_dir=out_dir,
artifact_prefix="in-msg",
),
"out_msgs": [
_message_to_json(
message,
out_dir=out_dir,
artifact_prefix=f"out-msg-{index}",
)
for index, message in enumerate(transaction.out_msgs, start=1)
],
}
if isinstance(transaction.data, bytes):
result["data_hex"] = transaction.data.hex()
data_file = _write_bytes_artifact(out_dir, "transaction-data.boc", transaction.data)
if data_file is not None:
result["data_file"] = data_file
return result
async def inspect_latest_transaction(
client,
address: str,
*,
request_timeout: float = 5.0,
out_dir: Path | None = None,
) -> dict[str, object]:
normalized = await normalize_account_address(client, address, request_timeout=request_timeout)
state = await raw_get_account_state_with_timeout(
client,
normalized.serialized,
request_timeout,
)
if state.last_transaction_id is None:
raise RuntimeError(f"account {normalized.raw} has no transactions yet")
transactions = await tonlib_call(
client.raw_get_transactions(
normalized.serialized,
state.last_transaction_id.lt,
state.last_transaction_id.hash,
),
timeout=request_timeout,
label="raw_get_transactions()",
)
latest = None
for transaction in transactions.transactions:
transaction_id = transaction.transaction_id
if transaction_id is None:
continue
if (
transaction_id.lt == state.last_transaction_id.lt
and transaction_id.hash == state.last_transaction_id.hash
):
latest = transaction
break
if latest is None:
raise RuntimeError(
"latest transaction id was not present in raw_get_transactions() result"
)
return {
"address_input": address,
"address_serialized": normalized.serialized,
"address_raw": normalized.raw,
"balance": state.balance,
"last_transaction_id": {
"lt": state.last_transaction_id.lt,
"hash_hex": state.last_transaction_id.hash.hex(),
},
"transaction": _transaction_to_json(latest, out_dir=out_dir),
}
async def _run(args: argparse.Namespace) -> None:
runtime = runtime_from_args(args)
out_dir = resolve_path(Path.cwd(), args.out_dir) if args.out_dir else None
if out_dir is not None:
out_dir.mkdir(parents=True, exist_ok=True)
client = await build_tonlib_client(runtime, verbosity=args.verbosity, request_timeout=args.request_timeout)
try:
result = await inspect_latest_transaction(
client,
args.address,
request_timeout=args.request_timeout,
out_dir=out_dir,
)
finally:
await client.aclose()
if out_dir is not None:
result["out_dir"] = str(out_dir)
(out_dir / "latest-transaction.json").write_text(json.dumps(result, indent=2) + "\n")
print(json.dumps(result, indent=2))
def main() -> None:
parser = argparse.ArgumentParser(description="Fetch the latest account transaction through tonlib")
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing tonlibjson")
parser.add_argument("--config", help="Lite-client config JSON path")
parser.add_argument("--state-dir", help="Optional state dir for helper defaults")
parser.add_argument(
"--address",
required=True,
help="TON account address accepted by tonlib; use --address=<value> for raw forms like -1:...",
)
parser.add_argument("--out-dir", help="Optional output directory for raw transaction and message artifacts")
parser.add_argument("--verbosity", type=int, default=0, help="Tonlib verbosity level")
parser.add_argument(
"--request-timeout",
type=float,
default=5.0,
help="Per-request timeout for tonlib address, state, and transaction queries",
)
args = parser.parse_args()
asyncio.run(_run(args))
if __name__ == "__main__":
main()
"""Launch a small local TON network from a single build directory.
Use this for single-build smoke tests, contract deploy flows, and any repro
that does not need mixed honest/probing validator binaries.
"""
from __future__ import annotations
import argparse
import asyncio
import time
from pathlib import Path
from ton_triage_lib import (
add_tontester_to_syspath,
load_install,
new_run_dir,
resolve_path,
write_wallet_env,
)
def _node_process(node):
# tontester exposes no public process handle, so liveness checks use the
# current name-mangled private attribute.
return getattr(node, "_Node__process", None)
def _node_is_alive(node) -> bool:
process = _node_process(node)
return process is not None and process.returncode is None
async def _run(args: argparse.Namespace) -> None:
if args.validators < 1:
raise SystemExit("--validators must be at least 1")
repo_root = resolve_path(Path.cwd(), args.repo_root)
add_tontester_to_syspath(repo_root)
from tontester.network import Network
from tontester.zerostate import SimplexConsensusConfig
build_dir = resolve_path(repo_root, args.build)
workdir_base = resolve_path(repo_root, args.workdir)
workdir = new_run_dir(workdir_base)
install = load_install(repo_root, build_dir)
async with Network(install, workdir) as net:
# tontester does not expose a public base-port setter; this private
# field controls the first allocated TCP/UDP port for the run.
net._port = args.base_port - 1 # pylint: disable=protected-access
if args.enable_simplex:
simplex = SimplexConsensusConfig()
net.config.mc_consensus = simplex
net.config.shard_consensus = simplex
if args.activate_spam:
net.config.spam = True
dht_nodes = [net.create_dht_node() for _ in range(args.dht_nodes)]
full_nodes = [net.create_full_node() for _ in range(args.validators)]
for node in full_nodes:
node.make_initial_validator()
for dht in dht_nodes:
node.announce_to(dht)
for dht in dht_nodes:
await dht.run()
for node in full_nodes:
await node.run()
print(f"run dir: {workdir}")
print(f"repo root: {repo_root}")
print(f"build dir: {build_dir}")
print(f"base port: {args.base_port}")
if args.enable_simplex:
print(
"simplex config:"
f" target_block_rate_ms={simplex.target_block_rate_ms}"
f" slots_per_leader_window={simplex.slots_per_leader_window}"
f" first_block_timeout_ms={simplex.first_block_timeout_ms}"
f" max_leader_window_desync={simplex.max_leader_window_desync}"
)
for index, dht in enumerate(dht_nodes, start=1):
print(f"dht{index} log: {dht.log_path}")
for index, node in enumerate(full_nodes, start=1):
print(f"node{index} log: {node.log_path}")
if args.emit_wallet_env:
lite_config = workdir / args.liteclient_config
lite_db = workdir / args.lite_db_dir
lite_db.mkdir(parents=True, exist_ok=True)
# tontester has no public serializer for the generated liteserver
# config, so the helper uses the node's private config object here.
lite_config.write_text(full_nodes[0]._liteserver_config.to_json()) # pylint: disable=protected-access
env_file = workdir / "wallet-env.txt"
write_wallet_env(
env_file,
repo_root=repo_root,
build_dir=build_dir,
tonlibjson=install.tonlibjson,
workdir=workdir,
state_dir=workdir / "state",
main_wallet_base=workdir / "state" / "main-wallet",
liteclient_config=lite_config,
lite_db=lite_db,
extra={
"RUNTIME_NODE": "node1",
"RUNTIME_ROLE": "baseline",
},
)
print(f"wallet env: {env_file}")
deadline = time.time() + args.wait_timeout
last_error: Exception | None = None
while True:
if all(not _node_is_alive(node) for node in full_nodes):
status = ", ".join(f"node{index}:{_node_process(node).returncode}" for index, node in enumerate(full_nodes, start=1))
raise RuntimeError(f"all validator nodes exited before mc seqno {args.mc_seqno} was reached: {status}")
try:
await net.wait_mc_block(seqno=args.mc_seqno)
break
except Exception as exc:
last_error = exc
if time.time() >= deadline:
message = f"timed out waiting for mc seqno {args.mc_seqno}"
if last_error is not None:
message += f"; last error: {last_error}"
raise TimeoutError(message) from last_error
await asyncio.sleep(0.5)
print(f"reached mc seqno >= {args.mc_seqno}")
keep_alive = args.keep_alive
if keep_alive is None:
keep_alive = 300 if args.emit_wallet_env else 0
if keep_alive > 0:
print(f"keeping network alive for {keep_alive} seconds")
await asyncio.sleep(keep_alive)
def main() -> None:
parser = argparse.ArgumentParser(description="Run a simple local TON test network with tontester")
parser.add_argument("--repo-root", required=True, help="Path to the TON repo root")
parser.add_argument("--build", required=True, help="Build directory to run")
parser.add_argument(
"--workdir",
default="tmp/tontester-basic",
help="Base working directory for node data",
)
parser.add_argument(
"--base-port",
type=int,
default=2001,
help="First TCP/UDP port to allocate inside the run",
)
parser.add_argument("--validators", type=int, default=1, help="Number of validator nodes")
parser.add_argument("--dht-nodes", type=int, default=1, help="Number of DHT nodes")
parser.add_argument("--mc-seqno", type=int, default=3, help="Masterchain seqno target")
parser.add_argument(
"--wait-timeout",
type=int,
default=60,
help="Timeout in seconds while waiting for the target seqno",
)
parser.add_argument(
"--keep-alive",
type=int,
default=None,
help="Seconds to keep the network running after the target seqno is reached; defaults to 300 with --emit-wallet-env, else 0",
)
parser.add_argument(
"--emit-wallet-env",
action="store_true",
help="Write wallet-env.txt and liteclient.config.json into the run directory",
)
parser.add_argument(
"--liteclient-config",
default="liteclient.config.json",
help="Filename for lite-client config in the run directory",
)
parser.add_argument(
"--lite-db-dir",
default="lite-db",
help="Lite-client DB directory name under the run directory",
)
parser.add_argument(
"--enable-simplex",
action="store_true",
help="Enable simplex consensus in the generated zerostate",
)
parser.add_argument(
"--activate-spam",
action="store_true",
help="Include the spammer contract in the generated zerostate",
)
args = parser.parse_args()
try:
asyncio.run(_run(args))
except KeyboardInterrupt:
print("interrupted")
if __name__ == "__main__":
main()
"""Run a Fift script with the skill's standard TON include paths.
Use this for one-off Fift helpers that emit BoCs or other artifacts without
having to hand-assemble the create-state invocation each time.
"""
from __future__ import annotations
import argparse
import subprocess
from pathlib import Path
from ton_triage_lib import load_install, resolve_path, runtime_from_args
def main() -> None:
parser = argparse.ArgumentParser(description="Run a Fift script with standard TON include paths")
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing create-state")
parser.add_argument("--script", required=True, help="Path to the Fift script to execute")
parser.add_argument(
"--cwd",
help="Working directory for outputs created by the Fift script; defaults to WORKDIR from wallet-env",
)
parser.add_argument(
"script_args",
nargs=argparse.REMAINDER,
help="Arguments passed through to the Fift script after '--'",
)
args = parser.parse_args()
runtime = runtime_from_args(args, require_config=False)
repo_root = runtime.repo_root
build_dir = runtime.build_dir
script_path = resolve_path(Path.cwd(), args.script)
if args.cwd:
cwd = resolve_path(Path.cwd(), args.cwd)
elif runtime.workdir is not None:
cwd = runtime.workdir
else:
cwd = Path.cwd().resolve()
cwd.mkdir(parents=True, exist_ok=True)
install = load_install(repo_root, build_dir)
passthrough = args.script_args
if passthrough[:1] == ["--"]:
passthrough = passthrough[1:]
cmd = [str(install.fift_exe)]
for include_dir in install.fift_include_dirs:
cmd += ["-I", str(include_dir)]
cmd += ["-s", str(script_path), *passthrough]
print("running:", " ".join(cmd))
print(f"cwd: {cwd}")
subprocess.run(cmd, cwd=cwd, check=True)
if __name__ == "__main__":
main()
"""Run a lite-client command using wallet-env.txt or explicit runtime paths.
Use this as the fallback observation path when tonlib-based helpers crash or
when you need a raw lite-client response for debugging.
"""
from __future__ import annotations
import argparse
import subprocess
from pathlib import Path
from ton_triage_lib import format_command, resolve_path, runtime_from_args
def main() -> None:
parser = argparse.ArgumentParser(
description="Run a lite-client command using wallet-env.txt or explicit repo/build/config inputs"
)
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing lite-client")
parser.add_argument("--config", help="Lite-client config JSON path")
parser.add_argument("--state-dir", help="Unused here; accepted for wallet-env compatibility")
parser.add_argument(
"--lite-db",
help="Optional lite-client DB directory override; defaults to LITECLIENT_DB from wallet-env when present",
)
parser.add_argument(
"--timeout",
type=int,
default=10,
help="lite-client batch timeout in seconds",
)
parser.add_argument(
"liteclient_args",
nargs=argparse.REMAINDER,
help="lite-client command tokens passed after '--'",
)
args = parser.parse_args()
passthrough = args.liteclient_args
if passthrough[:1] == ["--"]:
passthrough = passthrough[1:]
if not passthrough:
raise SystemExit("pass a lite-client command after '--'")
runtime = runtime_from_args(args)
if runtime.config_path is None:
raise SystemExit("lite-client config was not resolved; pass --config or use --wallet-env/--run-dir")
lite_client = runtime.build_dir / "lite-client" / "lite-client"
if not lite_client.exists():
raise SystemExit(
f"lite-client not found in build dir; build it with ninja -C {runtime.build_dir} lite-client"
)
if args.lite_db:
lite_db = resolve_path(Path.cwd(), args.lite_db)
else:
lite_db = runtime.lite_db
if lite_db is not None:
lite_db.mkdir(parents=True, exist_ok=True)
command = " ".join(passthrough)
cmd = [str(lite_client), "-C", str(runtime.config_path), "-t", str(args.timeout), "-c", command]
if lite_db is not None:
cmd[1:1] = ["-D", str(lite_db)]
cwd = runtime.workdir if runtime.workdir is not None else Path.cwd().resolve()
print(f"running: {format_command(cmd)}", flush=True)
print(f"cwd: {cwd}", flush=True)
subprocess.run(cmd, cwd=cwd, check=True)
if __name__ == "__main__":
main()
"""Launch a mixed-build local TON network with baseline and probing nodes.
Use this for Workflow B experiments where honest and patched validator binaries
must coexist in one tontester run.
"""
from __future__ import annotations
import argparse
import asyncio
import re
import time
from dataclasses import dataclass
from pathlib import Path
from ton_triage_lib import (
add_tontester_to_syspath,
load_install,
new_run_dir,
resolve_path,
write_wallet_env,
)
@dataclass(frozen=True)
class LogSpec:
target: str
pattern: str
def _probing_env_from_args(
probing_target_addr: str | None,
raw_assignments: list[str],
) -> dict[str, str]:
env: dict[str, str] = {}
if probing_target_addr:
env["TON_PROBING_TARGET_ADDR"] = probing_target_addr
for raw in raw_assignments:
key, sep, value = raw.partition("=")
if not sep:
raise SystemExit(f"invalid --probing-env assignment {raw!r}; expected KEY=VALUE")
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
raise SystemExit(f"invalid probing env key {key!r}")
env[key] = value
return env
def _parse_log_spec(raw: str) -> LogSpec:
selector, sep, pattern = raw.partition(":")
if sep and (selector == "any" or re.fullmatch(r"node\d+", selector)):
target = selector
elif sep and (selector == "any" or selector.startswith("node")):
raise SystemExit(f"invalid log selector {selector!r}; expected any or nodeN")
else:
target = "any"
pattern = raw
if not pattern:
raise SystemExit(f"invalid log selector: {raw!r}")
return LogSpec(target=target, pattern=pattern)
def _parse_node_name(raw: str, node_count: int) -> str:
if not re.fullmatch(r"node\d+", raw):
raise SystemExit(f"invalid node name {raw!r}; expected node1, node2, ...")
index = int(raw[4:])
if index < 1 or index > node_count:
raise SystemExit(f"node {raw!r} is out of range for {node_count} validator nodes")
return raw
def _validate_log_specs(specs: list[LogSpec], node_count: int) -> None:
for spec in specs:
if spec.target != "any":
_parse_node_name(spec.target, node_count)
def _node_process(node):
# tontester exposes no public process handle, so liveness checks use the
# current name-mangled private attribute.
return getattr(node, "_Node__process", None)
def _node_is_alive(node) -> bool:
process = _node_process(node)
return process is not None and process.returncode is None
def _node_status_line(name: str, node) -> str:
process = _node_process(node)
if process is None:
return f"{name}: not-started"
if process.returncode is None:
return f"{name}: alive"
return f"{name}: exited({process.returncode})"
def _poll_log_updates(
node_logs: dict[str, Path],
offsets: dict[str, int],
tails: dict[str, str],
tail_size: int,
) -> dict[str, str]:
updates: dict[str, str] = {}
for name, path in node_logs.items():
if not path.exists():
updates[name] = ""
continue
size = path.stat().st_size
if size < offsets[name]:
offsets[name] = 0
tails[name] = ""
with path.open("rb") as handle:
handle.seek(offsets[name])
chunk = handle.read()
offsets[name] = size
text = chunk.decode("utf-8", errors="ignore")
combined = tails[name] + text
tails[name] = combined[-tail_size:]
updates[name] = combined
return updates
def _match_log_spec(spec: LogSpec, updates: dict[str, str]) -> bool:
if spec.target == "any":
return any(spec.pattern in text for text in updates.values())
return spec.pattern in updates.get(spec.target, "")
async def _wait_for_log_conditions(
*,
node_map: dict[str, object],
node_logs: dict[str, Path],
success_specs: list[LogSpec],
failure_specs: list[LogSpec],
require_alive: list[str],
require_dead: list[str],
wait_timeout: float,
poll_interval: float,
) -> None:
deadline = time.time() + wait_timeout
offsets = {name: 0 for name in node_logs}
max_pattern = max((len(spec.pattern) for spec in success_specs + failure_specs), default=1)
tails = {name: "" for name in node_logs}
matched_success = [False] * len(success_specs)
tail_size = max(max_pattern + 128, 256)
while True:
updates = _poll_log_updates(node_logs, offsets, tails, tail_size)
for spec in failure_specs:
if _match_log_spec(spec, updates):
raise RuntimeError(f"observed failure marker {spec.target}:{spec.pattern}")
for index, spec in enumerate(success_specs):
if not matched_success[index] and _match_log_spec(spec, updates):
matched_success[index] = True
dead_ready = all(not _node_is_alive(node_map[name]) for name in require_dead)
success_ready = all(matched_success) if success_specs else True
if require_dead and dead_ready and success_ready:
return
if success_specs and success_ready and not require_dead:
return
for name in require_alive:
if not _node_is_alive(node_map[name]):
raise RuntimeError(f"{name} died before the success condition was reached")
if time.time() >= deadline:
status = ", ".join(_node_status_line(name, node_map[name]) for name in sorted(node_map))
raise TimeoutError(f"timed out waiting for log conditions; node status: {status}")
await asyncio.sleep(poll_interval)
async def _wait_for_seqno_or_fail(
*,
reference_node,
target_seqno: int,
node_map: dict[str, object],
node_logs: dict[str, Path],
failure_specs: list[LogSpec],
require_alive: list[str],
wait_timeout: float,
poll_interval: float,
) -> None:
deadline = time.time() + wait_timeout
offsets = {name: 0 for name in node_logs}
max_pattern = max((len(spec.pattern) for spec in failure_specs), default=1)
tails = {name: "" for name in node_logs}
tail_size = max(max_pattern + 128, 256)
client = await reference_node.tonlib_client()
last_error: Exception | None = None
while True:
updates = _poll_log_updates(node_logs, offsets, tails, tail_size)
if not _node_is_alive(reference_node):
status = ", ".join(_node_status_line(name, node_map[name]) for name in sorted(node_map))
raise RuntimeError(f"reference node died before masterchain seqno {target_seqno} was reached; {status}")
for name in require_alive:
if not _node_is_alive(node_map[name]):
raise RuntimeError(f"{name} died before masterchain seqno {target_seqno} was reached")
for spec in failure_specs:
if _match_log_spec(spec, updates):
raise RuntimeError(f"observed failure marker {spec.target}:{spec.pattern}")
remaining = deadline - time.time()
if remaining <= 0:
status = ", ".join(_node_status_line(name, node_map[name]) for name in sorted(node_map))
message = f"timed out waiting for mc seqno {target_seqno}; node status: {status}"
if last_error is not None:
message += f"; last tonlib error: {last_error}"
raise TimeoutError(message)
request_timeout = min(max(poll_interval * 2, 1.0), remaining)
request = asyncio.create_task(client.get_masterchain_info())
try:
info = await asyncio.wait_for(request, timeout=request_timeout)
if info.last is not None and info.last.seqno >= target_seqno:
return
last_error = None
except asyncio.TimeoutError:
request.cancel()
try:
await request
except asyncio.CancelledError:
pass
last_error = TimeoutError(f"get_masterchain_info() timed out after {request_timeout:.1f}s")
except Exception as exc:
last_error = exc
await asyncio.sleep(poll_interval)
def _simplex_config(args: argparse.Namespace, SimplexConsensusConfig):
return SimplexConsensusConfig(
target_block_rate_ms=args.simplex_target_block_rate_ms,
slots_per_leader_window=args.simplex_slots_per_leader_window,
first_block_timeout_ms=args.simplex_first_block_timeout_ms,
max_leader_window_desync=args.simplex_max_leader_window_desync,
)
async def _run(args: argparse.Namespace) -> None:
total_nodes = args.normal_nodes + args.probing_nodes
if total_nodes < 1:
raise SystemExit("the mixed network needs at least one full node")
if args.probing_nodes < 1 and (args.probing_env or args.probing_target_addr):
raise SystemExit("--probing-env/--probing-target-addr require at least one probing node")
success_specs = [_parse_log_spec(spec) for spec in args.success_log]
failure_specs = [_parse_log_spec(spec) for spec in args.failure_log]
_validate_log_specs(success_specs, total_nodes)
_validate_log_specs(failure_specs, total_nodes)
require_alive = [_parse_node_name(name, total_nodes) for name in args.require_node_alive]
require_dead = [_parse_node_name(name, total_nodes) for name in args.require_node_dead]
repo_root = resolve_path(Path.cwd(), args.repo_root)
add_tontester_to_syspath(repo_root)
from tontester.network import Network
from tontester.zerostate import SimplexConsensusConfig
baseline_build_dir = resolve_path(repo_root, args.build)
probing_build_dir = resolve_path(repo_root, args.probing_build)
workdir_base = resolve_path(repo_root, args.workdir)
workdir = new_run_dir(workdir_base)
baseline = load_install(repo_root, baseline_build_dir)
async with Network(baseline, workdir) as net:
# tontester does not expose a public base-port setter; this private
# field controls the first allocated TCP/UDP port for the run.
net._port = args.base_port - 1 # pylint: disable=protected-access
if args.enable_simplex:
simplex = _simplex_config(args, SimplexConsensusConfig)
net.config.mc_consensus = simplex
net.config.shard_consensus = simplex
if args.activate_spam:
net.config.spam = True
dht_nodes = [net.create_dht_node() for _ in range(args.dht_nodes)]
full_nodes = []
full_node_installs = []
full_node_roles = []
for _ in range(args.normal_nodes):
full_nodes.append(net.create_full_node())
full_node_installs.append(baseline)
full_node_roles.append("baseline")
if args.probing_nodes > 0:
probing = load_install(repo_root, probing_build_dir)
probing_env = _probing_env_from_args(args.probing_target_addr, args.probing_env)
for _ in range(args.probing_nodes):
full_nodes.append(
net.create_full_node(
install=probing,
env=probing_env,
)
)
full_node_installs.append(probing)
full_node_roles.append("probing")
for node in full_nodes:
node.make_initial_validator()
for dht in dht_nodes:
node.announce_to(dht)
for dht in dht_nodes:
await dht.run()
for node in full_nodes:
await node.run()
print(f"run dir: {workdir}")
print(f"repo root: {repo_root}")
print(f"baseline build: {baseline_build_dir}")
print(f"probing build: {probing_build_dir}")
print(f"base port: {args.base_port}")
if args.enable_simplex:
print(
"simplex config:"
f" target_block_rate_ms={simplex.target_block_rate_ms}"
f" slots_per_leader_window={simplex.slots_per_leader_window}"
f" first_block_timeout_ms={simplex.first_block_timeout_ms}"
f" max_leader_window_desync={simplex.max_leader_window_desync}"
)
active_probing_env = _probing_env_from_args(args.probing_target_addr, args.probing_env)
if active_probing_env:
summary = ", ".join(f"{key}={value}" for key, value in sorted(active_probing_env.items()))
print(f"probing env: {summary}")
for index, dht in enumerate(dht_nodes, start=1):
print(f"dht{index} log: {dht.log_path}")
node_logs = {}
node_map = {}
for index, node in enumerate(full_nodes, start=1):
name = f"node{index}"
node_logs[name] = node.log_path
node_map[name] = node
print(f"{name} log: {node.log_path}")
if args.emit_wallet_env:
lite_config = workdir / args.liteclient_config
lite_db = workdir / args.lite_db_dir
lite_db.mkdir(parents=True, exist_ok=True)
# tontester has no public serializer for the generated liteserver
# config, so the helper uses the node's private config object here.
lite_config.write_text(full_nodes[0]._liteserver_config.to_json()) # pylint: disable=protected-access
env_install = full_node_installs[0]
env_role = full_node_roles[0]
env_file = workdir / "wallet-env.txt"
write_wallet_env(
env_file,
repo_root=repo_root,
build_dir=env_install.build_dir,
tonlibjson=env_install.tonlibjson,
workdir=workdir,
state_dir=workdir / "state",
main_wallet_base=workdir / "state" / "main-wallet",
liteclient_config=lite_config,
lite_db=lite_db,
extra={
"RUNTIME_NODE": "node1",
"RUNTIME_ROLE": env_role,
},
)
print(f"wallet env: {env_file}")
if success_specs or require_dead:
await _wait_for_log_conditions(
node_map=node_map,
node_logs=node_logs,
success_specs=success_specs,
failure_specs=failure_specs,
require_alive=require_alive,
require_dead=require_dead,
wait_timeout=args.wait_timeout,
poll_interval=args.poll_interval,
)
print("success condition reached")
else:
await _wait_for_seqno_or_fail(
reference_node=full_nodes[0],
target_seqno=args.mc_seqno,
node_map=node_map,
node_logs=node_logs,
failure_specs=failure_specs,
require_alive=require_alive,
wait_timeout=args.wait_timeout,
poll_interval=args.poll_interval,
)
print(f"reached mc seqno >= {args.mc_seqno}")
keep_alive = args.keep_alive
if keep_alive is None:
keep_alive = 300 if args.emit_wallet_env else 0
if keep_alive > 0:
print(f"keeping network alive for {keep_alive} seconds")
await asyncio.sleep(keep_alive)
def main() -> None:
parser = argparse.ArgumentParser(
description="Run a mixed-build local TON network with tontester",
)
parser.add_argument("--repo-root", required=True, help="Path to the TON repo root")
parser.add_argument("--build", default="build", help="Baseline build directory")
parser.add_argument(
"--probing-build",
default="build-probing",
help="Probing build directory (can be the same as --build)",
)
parser.add_argument(
"--workdir",
default="tmp/tontester-mixed-builds",
help="Base working directory for node data",
)
parser.add_argument(
"--base-port",
type=int,
default=2001,
help="First TCP/UDP port to allocate inside the run",
)
parser.add_argument("--normal-nodes", type=int, default=3, help="Number of baseline full nodes")
parser.add_argument("--probing-nodes", type=int, default=1, help="Number of probing full nodes")
parser.add_argument("--dht-nodes", type=int, default=1, help="Number of DHT nodes")
parser.add_argument("--mc-seqno", type=int, default=100, help="Masterchain seqno target")
parser.add_argument(
"--wait-timeout",
type=int,
default=120,
help="Timeout in seconds while waiting for seqno or log-based success conditions",
)
parser.add_argument(
"--poll-interval",
type=float,
default=0.2,
help="Polling interval for log-based success conditions",
)
parser.add_argument(
"--success-log",
action="append",
default=[],
metavar="NODE:TEXT",
help="Stop successfully when this substring appears; use node1:..., node2:..., or any:...",
)
parser.add_argument(
"--failure-log",
action="append",
default=[],
metavar="NODE:TEXT",
help="Fail immediately when this substring appears; use node1:..., node2:..., or any:...",
)
parser.add_argument(
"--require-node-alive",
action="append",
default=[],
metavar="NODE",
help="Fail if this node dies before success; repeat as needed",
)
parser.add_argument(
"--require-node-dead",
action="append",
default=[],
metavar="NODE",
help="Require this node to die before success; repeat as needed",
)
parser.add_argument(
"--enable-simplex",
action="store_true",
help="Enable simplex (new consensus) in genesis config (config param 30).",
)
parser.add_argument(
"--simplex-target-block-rate-ms",
type=int,
default=1000,
help="Simplex target_block_rate_ms; used with --enable-simplex",
)
parser.add_argument(
"--simplex-slots-per-leader-window",
type=int,
default=4,
help="Simplex slots_per_leader_window; used with --enable-simplex",
)
parser.add_argument(
"--simplex-first-block-timeout-ms",
type=int,
default=1000,
help="Simplex first_block_timeout_ms; used with --enable-simplex",
)
parser.add_argument(
"--simplex-max-leader-window-desync",
type=int,
default=2,
help="Simplex max_leader_window_desync; used with --enable-simplex",
)
parser.add_argument(
"--activate-spam",
action="store_true",
help="Include the spammer smart contract in the generated zerostate",
)
parser.add_argument(
"--probing-target-addr",
dest="probing_target_addr",
help="Target account address (base64 or 0:HEX) used by probing logic",
)
parser.add_argument(
"--keep-alive",
type=int,
default=None,
help="Seconds to keep the network running after reaching the success condition; defaults to 300 with --emit-wallet-env, else 0",
)
parser.add_argument(
"--probing-env",
action="append",
default=[],
metavar="KEY=VALUE",
help="Extra environment variable to pass to probing nodes; repeat as needed",
)
parser.add_argument(
"--liteclient-config",
default="liteclient.config.json",
help="Filename for liteclient config in the run directory",
)
parser.add_argument(
"--emit-wallet-env",
action="store_true",
help="Write wallet-env.txt and liteclient.config.json in the run workdir",
)
parser.add_argument(
"--lite-db-dir",
default="lite-db",
help="Lite-client DB directory name (under workdir)",
)
args = parser.parse_args()
try:
asyncio.run(_run(args))
except KeyboardInterrupt:
print("interrupted")
if __name__ == "__main__":
main()
"""Send an already-built external message BoC through tonlib.
Use this when another tool already produced the serialized external message and
you only need transport, seqno observation, or replay.
"""
from __future__ import annotations
import argparse
import asyncio
import hashlib
from pathlib import Path
from ton_triage_lib import (
build_tonlib_client,
get_masterchain_info_with_timeout,
raw_send_message_with_timeout,
resolve_path,
runtime_from_args,
wait_for_mc_advance,
)
async def _run(args: argparse.Namespace) -> None:
runtime = runtime_from_args(args)
boc_path = resolve_path(Path.cwd(), args.boc)
body = boc_path.read_bytes()
print(f"repo root: {runtime.repo_root}")
print(f"build dir: {runtime.build_dir}")
print(f"config: {runtime.config_path}")
print(f"boc: {boc_path} ({len(body)} bytes)")
print(f"boc sha256: {hashlib.sha256(body).hexdigest()}")
client = await build_tonlib_client(runtime, verbosity=args.verbosity, request_timeout=args.request_timeout)
try:
before_seqno = None
if args.show_seqno:
info = await get_masterchain_info_with_timeout(client, args.request_timeout)
if info.last is not None:
before_seqno = info.last.seqno
print(f"mc seqno before: {before_seqno}")
elif args.wait_mc_advance:
info = await get_masterchain_info_with_timeout(client, args.request_timeout)
if info.last is not None:
before_seqno = info.last.seqno
await raw_send_message_with_timeout(client, body, args.request_timeout)
print("raw_send_message: ok")
if args.wait_mc_advance and before_seqno is not None:
advanced = await wait_for_mc_advance(
client,
before_seqno=before_seqno,
wait_timeout=args.wait_timeout,
request_timeout=args.request_timeout,
)
print(f"mc seqno advanced to: {advanced}")
elif args.wait_seconds > 0:
await asyncio.sleep(args.wait_seconds)
if args.show_seqno:
info = await get_masterchain_info_with_timeout(client, args.request_timeout)
if info.last is not None:
print(f"mc seqno after: {info.last.seqno}")
finally:
await client.aclose()
def main() -> None:
parser = argparse.ArgumentParser(description="Send a raw BOC message through tonlib")
parser.add_argument("--wallet-env", help="Path to wallet-env.txt emitted by the network runner")
parser.add_argument("--run-dir", help="Run directory containing wallet-env.txt")
parser.add_argument("--repo-root", help="Path to the TON repo root")
parser.add_argument("--build", help="Build directory containing tonlibjson")
parser.add_argument("--config", help="Lite-client config JSON path")
parser.add_argument("--state-dir", help="Optional state dir for helper defaults")
parser.add_argument("--boc", required=True, help="Path to the serialized BOC message to send")
parser.add_argument(
"--wait-seconds",
type=float,
default=0.0,
help="Optional delay after sending before a final seqno read",
)
parser.add_argument(
"--wait-mc-advance",
action="store_true",
help="Wait until masterchain seqno advances after the send; useful as a liveness hint, not proof of inclusion",
)
parser.add_argument(
"--wait-timeout",
type=float,
default=10.0,
help="Timeout for --wait-mc-advance",
)
parser.add_argument(
"--request-timeout",
type=float,
default=5.0,
help="Per-request timeout for tonlib calls such as getMasterchainInfo and raw_send_message",
)
parser.add_argument(
"--verbosity",
type=int,
default=0,
help="Tonlib verbosity level",
)
parser.add_argument(
"--show-seqno",
action="store_true",
help="Print masterchain seqno before and after the send",
)
args = parser.parse_args()
asyncio.run(_run(args))
if __name__ == "__main__":
main()