
Thor
- 62 installs
- 9 repo stars
- Updated June 11, 2026
- vechain/vechain-ai-skills
Helps with ai & agent building tasks.
About
thor is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- thor
- AI & Agent Building
- AI-coding skill
Thor by the numbers
- 62 all-time installs (skills.sh)
- +2 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #6,310 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vechain/vechain-ai-skills --skill thorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 62 |
|---|---|
| repo stars | ★ 9 |
| Last updated | June 11, 2026 |
| Repository | vechain/vechain-ai-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Thor Skill
CRITICAL RULES
1. Read reference files FIRST. When the user's request involves any topic in the reference table below, read those files before doing anything else. Briefly mention which files you are reading so the user can confirm the skill is active. 2. Information priority: (a) Reference files in this skill — always the primary source. (b) The thor repo source code at github.com/vechain/thor/v2 for implementation details. (c) Web search — only as a last resort for topics NOT covered in references. 3. Prefer working directly in the main conversation. Plan mode and subagents do not inherit skill context and may produce stale answers. 4. After compaction or context loss, re-read this SKILL.md to restore awareness of the reference table.
Scope
Use this Skill for anything related to the VeChainThor node (thor):
- Architecture and package structure of the Go codebase
- Consensus: PoA v1/v2, PoS (Galactica), BFT finality
- Built-in contracts: Authority, Energy, Staker, Params, Prototype, Executor, Extension
- REST API endpoints and WebSocket subscriptions
- Storage: LevelDB, SQLite logdb, trie, pruning, node types
- P2P networking: discovery, block/tx propagation
- Cross-cutting flows: block production, transaction lifecycle, reward distribution, staking/delegation, chain sync
- Solo mode for local development
- Contributing to the thor codebase: build, test, add endpoints, fork config
For application-level VeChain development, see the companion skills:
- vechain-core — SDK usage, fee delegation, multi-clause transactions, dual-token model
- vechain-kit — VeChain Kit hooks, components, wallet connection, social login
- smart-contract-development — Solidity, Hardhat, testing, security
- stargate — NFT staking, validator delegation, VTHO rewards
Operating procedure
1. Identify the question type
- PM/architecture question → read
architecture.md+ relevant flow files - Contributor question → read
contributing.md+ relevant package reference - "How does X work?" → read the matching flow file(s)
- API question → read
api.md - Node operations → read
architecture.md(node types, flags, ports)
2. Read before answering
Load the matching reference files from the table below. Cross-cutting questions may need multiple files.
3. Answer with context
- Reference specific packages, types, and files from the thor codebase
- For flows, trace through the packages step-by-step
- For PM audiences, lead with the high-level summary before diving into implementation
- For contributors, include package paths and key type names
Reference files
Read the matching files BEFORE doing anything else. See Critical Rules above.
| Topic | File | Read when user mentions... |
|---|---|---|
| Architecture | references/architecture.md | overview, packages, node types, ports, flags, fork config, tech stack |
| Consensus | references/consensus.md | PoA, PoS, BFT, finality, proposer, validator selection, VRF, Galactica |
| Built-in contracts | references/built-in-contracts.md | Authority, Energy, VTHO, Staker, Params, Prototype, Executor, native contracts |
| REST API | references/api.md | API, endpoints, routes, subscriptions, WebSocket, Swagger |
| Storage | references/storage.md | database, LevelDB, SQLite, logdb, trie, pruning, archive, disk |
| P2P networking | references/p2p.md | peers, discovery, discv5, propagation, sync protocol, bootstrap |
| Contributing | references/contributing.md | build, Makefile, tests, lint, add endpoint, fork config, PR, Go conventions |
| Solo mode | references/solo.md | solo, local dev, test chain, pre-funded accounts, auto-mine |
| Flow: block production | references/flow-block-production.md | block packing, scheduler, proposer, how blocks are created |
| Flow: transaction lifecycle | references/flow-transaction-lifecycle.md | tx flow, txpool, clause execution, receipts, tx validation |
| Flow: reward distribution | references/flow-reward-distribution.md | rewards, VTHO generation, validator rewards, gas rewards, burning |
| Flow: staking & delegation | references/flow-staking-delegation.md | staking, delegation, unstaking, Staker contract, validator lifecycle |
| Flow: chain sync | references/flow-sync.md | sync, catch-up, peer download, fork handling, reorg, propagation |
REST API
Thor exposes an HTTP API (default localhost:8669) plus a separate admin server (localhost:2113). Uses gorilla/mux router. WebSocket endpoints for real-time subscriptions.
Route Organization
api/
├── accounts/ # Account state, code, storage, contract calls
├── blocks/ # Block retrieval
├── transactions/ # Submit and fetch transactions
├── events/ # Event log queries (POST /logs/event)
├── transfers/ # Transfer log queries (POST /logs/transfer)
├── fees/ # Fee history and priority
├── node/ # Peers, txpool
├── debug/ # EVM tracers, storage range
├── subscriptions/ # WebSocket subscriptions
├── doc/ # OpenAPI spec, Swagger/Stoplight UI
├── middleware/ # CORS, metrics, logging, panic recovery
├── restutil/ # Shared HTTP helpers
└── admin/ # Separate admin server (health, loglevel, apilogs)Endpoint Groups
Accounts — /accounts
| Method | Path | Description |
|---|---|---|
| GET | /accounts/{address} | Balance, energy, hasCode |
| GET | /accounts/{address}/code | Contract bytecode |
| GET | /accounts/{address}/storage/{key} | Storage slot value |
| GET | /accounts/{address}/storage/raw/{key} | Raw storage bytes |
| POST | /accounts/* | Batch contract call (multiple clauses) |
Query param revision selects block (ID, number, or best).
Deprecated (requires --api-enable-deprecated): POST /accounts, POST /accounts/{address} for single calls.
Blocks — /blocks
| Method | Path | Description |
|---|---|---|
| GET | /blocks/{revision} | Block by ID or number. Query: raw, expanded |
Transactions — /transactions
| Method | Path | Description |
|---|---|---|
| POST | /transactions | Submit raw signed transaction |
| GET | /transactions/{id} | Fetch transaction. Query: head, raw, pending |
| GET | /transactions/{id}/receipt | Transaction receipt |
Events — /logs/event
| Method | Path | Description |
|---|---|---|
| POST | /logs/event | Filter event logs by criteria, block range, pagination |
Transfers — /logs/transfer
| Method | Path | Description |
|---|---|---|
| POST | /logs/transfer | Filter transfer logs by criteria, block range, pagination |
Both log endpoints are disabled when --skip-logs is set.
Fees — /fees
| Method | Path | Description |
|---|---|---|
| GET | /fees/history | Fee history. Query: blockCount, newestBlock, rewardPercentiles |
| GET | /fees/priority | Suggested max priority fee per gas |
Node — /node
| Method | Path | Description |
|---|---|---|
| GET | /node/network/peers | Connected peer stats |
| GET | /node/txpool | Pending txs (requires --api-enable-txpool) |
| GET | /node/txpool/status | Txpool count (requires --api-enable-txpool) |
Debug — /debug
| Method | Path | Description |
|---|---|---|
| POST | /debug/tracers | Trace existing clause (target: blockID/txID/clauseIndex) |
| POST | /debug/tracers/call | Trace a simulated call |
| POST | /debug/storage-range | Storage range at clause execution point |
Optional pprof endpoints at /debug/pprof/* when --api-pprof is set.
Subscriptions — /subscriptions (WebSocket)
| Path | Query Params | Payload |
|---|---|---|
/subscriptions/block | pos | New blocks |
/subscriptions/event | pos, addr, t0–t4 | Event logs |
/subscriptions/transfer | pos, txOrigin, sender, recipient | Transfers |
/subscriptions/beat2 | pos | Beat2 heartbeat messages |
/subscriptions/txpool | — | Pending transaction IDs |
pos (block ID) must be within backtrace limit (default 1000 blocks).
Deprecated: /subscriptions/beat (requires --api-enable-deprecated).
Documentation — /doc
| Path | Description |
|---|---|
/ | Redirects to Stoplight UI |
/doc/thor.yaml | OpenAPI YAML spec |
/doc/* | Static assets (Swagger UI, Stoplight UI) |
Embedded via //go:embed.
Admin API (Separate Server)
Runs on --admin-addr (default localhost:2113):
| Method | Path | Description |
|---|---|---|
| GET | /admin/health | Health status (block lag tolerance, peer count) |
| GET/POST | /admin/loglevel | Get/set log level (debug/info/warn/error/trace/crit) |
| GET/POST | /admin/apilogs | Get/toggle API request logging |
Middleware Stack
Applied in order:
1. RequestBodyLimit — 200 KB max body 2. APITimeout — configurable, default 10s 3. RequestLogger — logs slow queries and 5xx responses 4. Metrics — counters and histograms (Prometheus) 5. PanicRecovery — recovers panics, optional stack trace logging 6. XGenesisID — validates/sets x-genesis-id header 7. XThorestVersion — sets x-thorest-ver header 8. Compress — gzip compression 9. CORS — configurable allowed origins (--api-cors), allows content-type and x-genesis-id headers
Server Configuration
| Flag | Default | Description |
|---|---|---|
--api-addr | localhost:8669 | API listen address |
--api-cors | — | CORS allowed origins |
--api-timeout | 10s | Request timeout |
--api-pprof | false | Enable pprof endpoints |
--api-enable-txpool | false | Enable txpool endpoints |
--api-enable-deprecated | false | Enable deprecated endpoints |
--admin-addr | localhost:2113 | Admin server address |
--metrics-addr | localhost:2112 | Prometheus metrics address |
Thor Node Architecture
Module path: github.com/vechain/thor/v2
Tech Stack
| Component | Technology |
|---|---|
| Language | Go (1.25+) |
| State DB | LevelDB via muxdb (multiplexed) |
| Log DB | SQLite (logdb) |
| EVM | Fork of go-ethereum, Shanghai hardfork compatible |
| Trie | Modified Merkle Patricia Trie with versioned nodes |
| P2P | devp2p (Ethereum-derived), RLPx |
| Hashing | Blake2b (not Keccak for most chain ops) |
| VRF | ECVRF-SECP256K1-SHA256-TAI |
| CLI | urfave/cli/v3 |
Package Map
| Package | Purpose |
|---|---|
cmd/thor | CLI entry point — main.go (network node), solo subcommand, master-key, reprocess |
thor | Core types (Address, Bytes32), constants, fork config, governance param keys |
block | Block header/body structures, signature handling, VRF alpha/beta fields |
tx | Transaction types (legacy + typed after Galactica), multi-clause model, fee delegation |
state | State trie access — account balances, storage, code; Stater creates state snapshots from root |
muxdb | Multiplexed DB layer over LevelDB — main state storage with optional metrics |
logdb | SQLite-based event/transfer log storage (skippable via --skip-logs) |
chain | Chain repository — block summaries, chain forks, tx lookups, chain tag |
consensus | Block validation — header checks, proposer validation (PoA or PoS), tx execution, state root verification |
bft | BFT finality engine — vote tracking, epoch quality, justified/finalized checkpoints |
scheduler | Block proposer scheduling — PoASchedulerV1, PoASchedulerV2, PoSScheduler |
vrf | Verifiable Random Function — seed generation for proposer shuffling (post-VIP-214) |
packer | Block construction — assembles blocks with transactions for proposers |
runtime | EVM execution environment — runs transactions, handles built-in contract calls |
builtin | Built-in contracts: Authority, Energy, Params, Prototype, Extension, Executor, Staker, Measure |
txpool | Transaction pool management with per-account limits and lifetime expiry |
api | RESTful API server (OpenAPI spec in api/doc/thor.yaml) |
p2p / p2psrv | P2P networking layer |
comm | P2P communication — block/tx propagation between peers |
genesis | Genesis block generation for mainnet, testnet, devnet |
vm | Modified EVM (forked from go-ethereum) |
tracers | Transaction tracers — JS, native, and logger tracers (forked from geth) |
trie | Versioned Merkle Patricia Trie implementation |
kv | Key-value store abstraction |
xenv | Execution environment for built-in contract native calls |
metrics | Prometheus metrics collection |
cache | LRU and priority caches |
stackedmap | Copy-on-write map used in state management |
Data Flow: Block Sync & Validation
P2P peers → comm (download blocks)
→ consensus.Process()
→ validateBlockHeader (timestamp, gas, score, VRF alpha/beta, baseFee)
→ staker.SyncPOS() (check if PoS transition happened)
→ validateAuthorityProposer OR validateStakingProposer
→ scheduler picks correct Scheduler impl
→ verify signer is scheduled for this timeslot
→ verify score matches
→ validateBlockBody (tx roots, chain tag, expiry, blocklist)
→ verifyBlock (execute all txs, compare state root, receipts root)
→ if PoS active: distribute rewards via Energy.DistributeRewards
→ bft.CommitBlock (track votes, update quality, finalize checkpoints)
→ chain.Repository saves block
→ logdb records events/transfers (unless --skip-logs)
→ pruner trims old state (unless --disable-pruner)Package Dependencies (Simplified)
cmd/thor
├── consensus (uses: chain, state, scheduler, builtin, runtime, block, tx)
├── bft (uses: chain, state, builtin, block)
├── packer (uses: chain, state, builtin, runtime, txpool)
├── txpool (uses: chain, state, tx)
├── api (uses: chain, state, logdb, txpool, bft)
├── comm (uses: chain, txpool, p2p)
├── logdb (standalone SQLite)
└── muxdb (standalone LevelDB)
builtin → state, thor, xenv, abi
├── authority, energy, params, prototype (state-backed linked lists / storage)
└── staker (validation, delegation, aggregation, globalstats sub-packages)
consensus → scheduler → thor (for constants, Blake2b)
bft → chain, builtin.Staker (for PoS weight queries)Node Types
| Type | Flags | Storage | Notes |
|---|---|---|---|
| Full node | --network mainnet | ~200 GB (Apr 2024), 1 TB SSD recommended | Logs + pruner enabled |
| Full without logs | --skip-logs | ~100 GB | Recommended for validators; /logs API disabled |
| Archive node | --disable-pruner | 400+ GB | Full state history preserved |
| Validator | --skip-logs + master key | ~100 GB, 500 GB NVMe SSD | Must be voted in via Authority contract, needs endorsement |
| Solo | thor solo | In-memory (or --persist) | Local dev/test; single node, no P2P; --on-demand for tx-triggered blocks |
Key Configuration
CLI Flags
| Flag | Default | Description |
|---|---|---|
--network | — | mainnet/testnet or path/URL to genesis file |
--api-addr | localhost:8669 | REST API listen address |
--p2p-port | 11235 | P2P network port |
--max-peers | 25 | Max P2P peers (0 disables P2P) |
--skip-logs | false | Skip event/transfer log writes |
--disable-pruner | false | Keep all state history (archive mode) |
--cache | 4096 | MB of RAM for trie node cache |
--beneficiary | — | Address for block rewards |
--target-gas-limit | 0 (adaptive) | Target block gas limit |
--enable-metrics | false | Prometheus metrics |
--metrics-addr | localhost:2112 | Metrics endpoint |
--enable-admin | false | Admin server |
--admin-addr | localhost:2113 | Admin endpoint |
--min-effective-priority-fee | 0 | Min priority fee for packing txs |
All flags support THOR_ prefixed env vars (e.g., THOR_NETWORK, THOR_API_ADDR).
Ports
| Port | Protocol | Purpose |
|---|---|---|
| 8669 | TCP (HTTP) | REST API |
| 11235 | TCP + UDP | P2P network |
| 2112 | TCP (HTTP) | Prometheus metrics (opt-in) |
| 2113 | TCP (HTTP) | Admin server (opt-in) |
Fork Config
Fork activation is defined per-network by genesis block ID in thor/fork_config.go. Each fork activates at a specific block number.
| Fork | Mainnet Block | What Changed |
|---|---|---|
VIP191 | 3,337,300 | Fee delegation (tx feature flag) |
ETH_CONST | 3,337,300 | Ethereum-compatible constants |
BLOCKLIST | 4,817,300 | Origin/delegator address blocklist |
ETH_IST | 9,254,300 | Ethereum Istanbul EVM changes |
VIP214 | 10,653,500 | PoA2 — VRF-based proposer shuffling, complex block signatures |
FINALITY | 13,815,000 | BFT finality engine — justified/finalized checkpoints |
GALACTICA | 22,084,200 | EIP-1559 base fee, typed transactions, staker contract |
HAYABUSA | 23,414,400 | PoA-to-PoS transition period begins; weight-based BFT |
Block Constants
| Parameter | Value |
|---|---|
| Block interval | 10 seconds |
| Initial gas limit | 10,000,000 |
| Min gas limit | 1,000,000 |
| Tx base gas | 5,000 |
| Clause gas | 16,000 |
| Max state history | 65,535 blocks |
| Epoch length | Used for BFT rounds |
| Max block proposers | 101 (initial) |
| Energy growth rate | ~0.000432 VTHO/VET/day |
Built-in Contracts
Built-in contracts are native to the VeChainThor blockchain. Unlike regular Solidity contracts, they:
- Have deterministic addresses derived from their name:
thor.BytesToAddress([]byte("ContractName")) - Are not deployed via transactions — their bytecode is loaded at genesis
- Have native method implementations in Go that bypass the EVM for core operations
- Solidity ABI is still used for encoding/decoding — callers interact via normal contract calls
- Some older contracts (compiled with Solidity 0.4.24) return unused gas to the caller
Package: builtin/ — builtin.go defines all contract bindings, each *_native.go file registers native method handlers.
Contract Overview
| Contract | Address | Purpose |
|---|---|---|
| Authority | 0x...Authority | PoA authority node registry |
| Energy | 0x...Energy | VTHO token (ERC20-like) |
| Params | 0x...Params | On-chain governance parameters |
| Prototype | 0x...Prototype | Account metadata: master, sponsors, credit plans |
| Extension | 0x...Extension | Block/tx introspection from Solidity |
| Executor | 0x...Executor | On-chain governance executor |
| Staker | 0x...Staker | PoS staking, delegation, rewards (post-GALACTICA) |
| Measure | 0x...Measure | Block measurement contract |
Addresses are computed asthor.BytesToAddress([]byte(name)). For example, Authority's address =BytesToAddress([]byte("Authority")).
Authority
File: builtin/authority_native.go, builtin/authority/authority.go
Manages the list of PoA authority nodes. Each entry has a master address, endorser, identity hash, and active flag. Stored as a linked list in contract storage.
Native Methods
| Method | Description |
|---|---|
native_executor | Returns the executor address from Params |
native_add(nodeMaster, endorsor, identity) | Register a new authority node |
native_revoke(nodeMaster) | Remove an authority node |
native_get(nodeMaster) | Get node info: listed, endorser, identity, active |
native_first | First node in the linked list |
native_next(nodeMaster) | Next node in the linked list |
native_isEndorsed(nodeMaster) | Check if endorser meets the VET endorsement requirement |
After HAYABUSA, native_isEndorsed also considers queued VET in the Staker contract (transition period logic).
Energy (VTHO)
File: builtin/energy_native.go, builtin/energy/energy.go
VTHO is the gas/fee token. It is generated automatically from VET holdings at a fixed growth rate (~0.000432 VTHO per VET per day). Energy also handles total supply tracking and reward distribution.
Native Methods
| Method | Description |
|---|---|
native_totalSupply | Total VTHO supply |
native_totalBurned | Total VTHO burned |
native_get(addr) | VTHO balance of an address (includes accrued growth) |
native_add(addr, amount) | Credit VTHO to an address |
native_sub(addr, amount) | Debit VTHO from an address |
native_master(addr) | Get the master address of an account |
Post-GALACTICA, the Energy contract also handles DistributeRewards — splitting block rewards between the proposer, validator beneficiary, and delegator reward pools.
VTHO Generation
VTHO is not minted per-block. Instead, each account's VTHO balance is calculated on-the-fly based on:
- Last recorded energy balance
- VET balance × growth rate × elapsed time since last update
Growth rate: 5,000,000,000 wei VTHO per VET per second ≈ 0.000432 VTHO/VET/day.
Params
File: builtin/params_native.go, builtin/params/params.go
Generic key-value store for on-chain governance parameters. Only the Executor contract can write to it.
Native Methods
| Method | Description |
|---|---|
native_executor | Returns the executor address |
native_get(key) | Read a parameter value |
native_set(key, value) | Write a parameter value (executor only) |
Key Parameters
| Key | Purpose | Initial Value |
|---|---|---|
executor | Governance executor address | Set at genesis |
reward-ratio | Block reward ratio | 30% (3e17) |
validator-reward-percentage | Validator share of rewards | 30% |
base-gas-price | Legacy tx default gas price | 1e15 wei |
proposer-endorsement | VET required to endorse a proposer | 25,000,000 VET |
max-block-proposers | Max authority/validator count | 101 |
curve-factor | VTHO issuance curve factor (post-PoS) | 76,800 |
delegator-contract-address | Delegator contract address | — |
staker-switches | Bit flags to pause staker/stargate | — |
Prototype
File: builtin/prototype_native.go, builtin/prototype/prototype.go
Provides per-account metadata: master address, credit plans, user lists, and sponsor mechanism. Central to VeChain's multi-party payment protocol (MPP).
Native Methods
| Method | Description |
|---|---|
native_master(self) | Get account's master address |
native_setMaster(self, newMaster) | Set account's master |
native_balanceAtBlock(self, blockNum) | Historical VET balance (within MaxStateHistory) |
native_energyAtBlock(self, blockNum) | Historical VTHO balance |
native_hasCode(self) | Check if account has contract code |
native_storageFor(self, key) | Read account's storage slot |
native_creditPlan(self) | Get credit plan (credit, recoveryRate) |
native_setCreditPlan(self, credit, rate) | Set credit plan |
native_isUser(self, user) | Check if address is a user of the contract |
native_addUser(self, user) | Add user |
native_removeUser(self, user) | Remove user |
native_sponsor(self, sponsor) | Register as sponsor |
native_unsponsor(self, sponsor) | Unregister as sponsor |
native_isSponsor(self, sponsor) | Check sponsor status |
native_selectSponsor(self, sponsor) | Select active sponsor |
native_currentSponsor(self) | Get current sponsor |
Multi-Party Payment (MPP)
A contract can set a credit plan so users get free gas up to a credit limit, with a recovery rate. Sponsors can volunteer to pay for a contract's users. This enables gasless UX.
Extension
File: builtin/extension_native.go
Provides Solidity-accessible introspection into block and transaction context. Has three versions (Extension, ExtensionV2, ExtensionV3) — V3 is the active native ABI.
Native Methods
| Method | Description |
|---|---|
native_blake2b256(data) | Blake2b hash (VeChain's native hash) |
native_blockID(blockNum) | Block ID by number |
native_blockTotalScore(blockNum) | Total score at block |
native_blockTime(blockNum) | Timestamp at block |
native_blockSigner(blockNum) | Signer of a block |
native_totalSupply | VET total supply |
native_txProvedWork | PoW proved work of current tx |
native_txID | Current transaction ID |
native_txBlockRef | Current tx block reference |
native_txExpiration | Current tx expiration |
native_txGasPayer | Gas payer of current tx |
native_txClauseIndex | Current clause index within multi-clause tx |
native_txClauseCount | Total clause count of current tx |
Executor
File: builtin/builtin.go (binding only — executor_test.go exists)
The on-chain governance contract. Proposals can be voted on by authority nodes to change governance parameters, add/remove authorities, or execute arbitrary calls. It is the only entity authorized to call Params.set().
Staker
File: builtin/staker_native.go, builtin/staker/staker.go
Introduced with GALACTICA. Manages PoS validator registration, staking, delegation, and rewards.
Architecture
The Staker contract delegates to sub-services:
| Sub-package | Purpose |
|---|---|
staker/validation | Validator registry — linked list of active/queued/exited validators |
staker/delegation | Delegation records — delegators staking against validators |
staker/aggregation | Per-validator aggregate stats (locked/pending VET + weight) |
staker/globalstats | Global totals: locked, queued, withdrawable, cooldown |
staker/stakes | Weighted stake calculation (stake × multiplier) |
Native Methods — Validation
| Method | Description |
|---|---|
native_addValidation(validator, endorser, period, stake) | Register a new validator (25M–600M VET) |
native_signalExit(validator, endorser) | Signal intent to exit (calculates exit block) |
native_increaseStake(validator, endorser, amount) | Increase active validator's stake |
native_decreaseStake(validator, endorser, amount) | Decrease stake (must stay ≥ 25M VET) |
native_withdrawStake(validator, endorser) | Withdraw after exit |
native_setBeneficiary(validator, endorser, beneficiary) | Set reward recipient address |
native_getValidation(validator) | Get validator info: endorser, locked/queued VET, weight, status, period, blocks |
native_getWithdrawable(validator) | Get withdrawable amount |
native_firstActive / native_firstQueued | Linked list traversal |
native_next(prev) | Next validator in list |
native_getValidationsNum | Count of active + queued validators |
native_getValidationTotals(validator) | Total locked/queued/exiting stake + weight for a validator |
Native Methods — Delegation
| Method | Description |
|---|---|
native_addDelegation(validator, stake, multiplier) | Delegate VET to a validator |
native_signalDelegationExit(delegationID) | Signal delegation exit |
native_withdrawDelegation(delegationID) | Withdraw delegated stake |
native_getDelegation(delegationID) | Get delegation info |
native_getDelegatorsRewards(validator, period) | Accumulated delegator rewards |
native_getDelegatorContract | Reads delegator contract address from Params |
Native Methods — Global
| Method | Description |
|---|---|
native_totalStake | Total locked VET and weight across all validators |
native_queuedStake | Total queued VET |
native_issuance | Current block reward issuance amount |
native_getControlSwitches | Staker/stargate pause flags |
Staking Rules
| Rule | Value |
|---|---|
| Min stake per validator | 25,000,000 VET |
| Max stake per validator | 600,000,000 VET (including delegations) |
| Staking periods | Low / Medium / High (governance-configurable) |
| Delegation multiplier | 1–N; affects weight calculation: stake × multiplier |
| PoS activation | ≥ 2/3 of maxBlockProposers queued → auto-activates at epoch boundary |
| Exit | Signal → wait for period end → exit block → withdraw |
Validator States
StatusQueued → (transition when 2/3 threshold met) → StatusActive → (signalExit) → StatusExit- Queued: registered but not yet producing blocks
- Active: producing blocks, earning rewards, stake is locked
- Exit: waiting for/past exit block, stake can be withdrawn
Rewards
Post-GALACTICA, block rewards are distributed via Energy.DistributeRewards():
validator-reward-percentage(default 30%) goes to the block proposer's beneficiary- Remaining goes to the delegator reward pool for that validator
- VTHO issuance is calculated using the
curve-factorparameter
Transition Period (HAYABUSA)
During the HAYABUSA transition period:
- Authority nodes can register in the Staker contract (
AddValidationrequires existing authority listing) - Queued VET counts toward endorsement checks in the Authority contract
- PoA remains active until ≥ 2/3 of maxBlockProposers have queued
- Once threshold is met, all queued validators activate and PoS takes over
How Native Calls Work
1. When the EVM encounters a CALL to a built-in contract address, FindNativeCall in builtin.go matches the method selector 2. If a native method is found, the Go implementation runs directly (no EVM bytecode execution) 3. Native methods receive an xenv.Environment providing access to state, block context, gas metering, and event logging 4. For older contracts (Authority, Energy, Params, Prototype, Extension), unused gas is returned to the caller 5. The Staker contract uses a different pattern: errors that are Revert type cause the EVM to revert with a reason string; unexpected errors panic
Consensus Mechanisms
Thor uses a hybrid consensus model that evolved through several forks: PoA v1 → PoA v2 (VIP-214) → BFT finality (FINALITY fork) → PoS (HAYABUSA/GALACTICA).
Proof of Authority (PoA)
Authority nodes are registered on-chain via the Authority built-in contract. Each authority node has a master address, an endorser (who stakes VET as collateral), and an identity hash.
Endorsement Requirement
A proposer is only eligible if its endorser's VET balance meets the proposer-endorsement governance parameter (initially 25M VET). After HAYABUSA, queued stake in the Staker contract also counts toward this balance check.
PoA v1 (Pre-VIP-214)
- Packages:
scheduler/poa_v1.go,consensus/poa_validator.go - Active authority nodes are collected from the
Authoritycontract - Proposer for each time slot determined by DPRP:
H(parentBlockNumber, blockTime)[:8] % len(actives) H= Blake2b hash- Each block time slot is
T= 10 seconds apart - If a scheduled proposer misses its slot, it gets deactivated; the producing proposer gets a score equal to the count of remaining active proposers
- No seed or VRF involved — deterministic from block number + time
PoA v2 (Post-VIP-214)
- Packages:
scheduler/poa_v2.go - Proposer list is shuffled using a VRF-derived seed
- Shuffle key:
Blake2b(seed, parentBlockNumber, proposerAddress)— sorted to create a deterministic sequence - Seed comes from the
Betaoutput of a previous block's VRF proof (viascheduler/seed.go) - Time slot assignment: proposer at index
(offset) % len(shuffled)where offset is derived from elapsed slots - Missed proposers are still deactivated; score = remaining active count
PoA → PoS: Key Differences
| Aspect | PoA v1 | PoA v2 | PoS |
|---|---|---|---|
| Proposer selection | DPRP hash | VRF-seeded shuffle | Weighted random sampling |
| Score model | Active count | Active count | Active weight / total weight × 10000 |
| Validator source | Authority contract | Authority contract | Staker contract (leader group) |
| Weight | Equal (1 per node) | Equal (1 per node) | Proportional to stake |
Proof of Stake (PoS) — HAYABUSA / GALACTICA
Transition to PoS
The transition is managed by builtin/staker/transition.go:
1. After HAYABUSA fork, authority nodes can register in the Staker contract (requires existing authority listing during transition period) 2. At each epoch boundary (every EpochLength blocks), staker.SyncPOS() checks if ≥ 2/3 of maxBlockProposers are queued in the Staker contract 3. When threshold is met, queued validators are activated and PoS takes over proposer selection 4. Once active, the Authority contract is no longer used for proposer selection
PoS Scheduler
- Package:
scheduler/pos.go - Uses A-Res reservoir sampling for weighted random proposer ordering:
- For each validator:
score = -ln(random) / weight - Sort by score ascending — lower score = higher priority
randomis from a ChaCha8 PRNG seeded withBlake2b(vrfSeed, parentBlockNumber)- Higher stake → higher weight → statistically more likely to get earlier slots
- Time slot and missed-proposer logic identical to PoA v2
Validator Requirements
| Parameter | Value |
|---|---|
| Min stake | 25,000,000 VET |
| Max stake | 600,000,000 VET |
| Staking periods | Low / Medium / High (governance-defined) |
| Endorser | Required (same address manages the validation) |
| Beneficiary | Optional; directs block rewards to a different address |
Validator Lifecycle
AddValidation (queued)
→ transition() activates when 2/3 threshold met
→ active (producing blocks, earning rewards)
→ SignalExit → ExitBlock calculated based on staking period
→ exit status → WithdrawStakeValidators can also: IncreaseStake, DecreaseStake, SetBeneficiary, SetOnline/offline.
Delegation
Delegators stake VET against an active or queued validator:
AddDelegation(validator, stake, multiplier)— multiplier affects weight calculationSignalDelegationExit→ waiting for period end →WithdrawDelegation- Delegator rewards accumulate per-validator and per-staking-period
BFT Finality Engine
- Package:
bft/ - Activated at the
FINALITYfork - Implements Casper-like justified/finalized checkpoint logic (described in VIP-220)
- Epoch: a fixed number of blocks (
EpochLength)
Core Concepts
| Term | Meaning |
|---|---|
| Checkpoint | First block of an epoch |
| Quality | Accumulated count of justified epochs |
| Justified | An epoch where >2/3 of validators (by count pre-HAYABUSA, by weight post-HAYABUSA) participated |
| Committed | An epoch where >2/3 voted COM (commit) |
| Finalized | A checkpoint whose quality is current_quality - 1 after a committed epoch |
| COM bit | Block header flag — proposer votes to commit the current epoch's checkpoint |
Justification & Finality Flow
Epoch N blocks produced
→ at store point (last block of epoch): computeState()
→ justifier collects votes from all blocks in the epoch
→ if unique signers > 2/3 of maxBlockProposers → epoch is JUSTIFIED (quality++)
→ if COM voters > 2/3 → epoch is COMMITTED
→ if committed AND quality > 1 → checkpoint at quality-1 becomes FINALIZED
→ finalized block ID persisted to DBVote Safety (VIP-220)
ShouldVote() ensures a proposer never votes COM on conflicting checkpoints:
- Tracks past votes via
casts - Will not vote COM if a recent vote (within
quality ± 1) conflicts with the current chain's justified checkpoint - Prevents equivocation across forks
Pre-HAYABUSA vs Post-HAYABUSA BFT
- Pre-HAYABUSA: threshold =
maxBlockProposers * 2/3(vote count) - Post-HAYABUSA: threshold =
totalWeight * 2/3(stake-weighted); each validator's vote carries its staking weight
Solo Mode
Solo uses a soloMockedEngine that simulates finality:
- Every epoch assumed committed
finalized = currentCheckpoint - 2 * EpochLengthjustified = currentCheckpoint - 1 * EpochLength- Enables pruner to work in solo mode
VRF (Verifiable Random Function)
- Package:
vrf/ - Algorithm: ECVRF-SECP256K1-SHA256-TAI (suite string
0xfe) - Uses
github.com/vechain/go-ecvrf Prove(sk, alpha)→(beta, pi)— beta is the random output, pi is the proofVerify(pk, alpha, pi)→beta— anyone can verify without the private key
Chained VRF in Blocks (Post-VIP-214)
- Block header contains
Alpha(VRF input) and a VRF proof in the signature Alpha= parent block'sBeta(VRF output), or parent'sStateRootfor the initial valueBetais extracted from the block's complex signature- The
Seeder(scheduler/seed.go) uses Beta from a block one epoch behind as the seed for proposer shuffling
Fork History
| Fork | Block (Mainnet) | Key Changes |
|---|---|---|
| VIP-191 | 3,337,300 | Fee delegation — transactions can designate a gas payer |
| ETH_CONST | 3,337,300 | Align gas constants with Ethereum |
| BLOCKLIST | 4,817,300 | Blocked addresses cannot originate or delegate transactions |
| ETH_IST | 9,254,300 | Ethereum Istanbul EVM opcodes |
| VIP-214 | 10,653,500 | PoA v2: VRF-seeded proposer shuffling, complex signatures (65→162 bytes), chained VRF |
| FINALITY | 13,815,000 | BFT finality engine: justified/finalized checkpoints, COM voting, epoch-based quality tracking |
| GALACTICA | 22,084,200 | EIP-1559 base fee, typed transactions, Staker built-in contract, reward distribution changes |
| HAYABUSA | 23,414,400 | PoA-to-PoS transition period; stake-weighted BFT (weight replaces vote count); authority nodes can migrate to Staker contract |
Contributing to Thor
Prerequisites
- Go 1.25+ (enforced by Makefile version check)
golangci-lint— install from <https://golangci-lint.run/usage/install/>- Docker — needed for Solidity compilation (
builtin/gen) and license checks - GPG key — all commits must be GPG-signed
Build System
| Target | Description |
|---|---|
make / make thor | Build bin/thor (blockchain node) |
make disco | Build bin/disco (discovery bootnode) |
make all | Build both binaries |
make test | Run unit tests with coverage |
make fuzz | Fuzz test tx/block encoding (default 1 min per target) |
make test-coverage | Tests with race detector + HTML coverage report |
make lint | Run golangci-lint + gopls/modernize |
make lint-fix | Auto-fix lint issues + regenerate builtins |
make generate | Regenerate builtin package from Solidity sources |
make license-check | Check license headers via Docker (Apache SkyWalking Eyes) |
make install-hooks | Install pre-commit hook (private key detection) |
make clean | Remove binaries and purge build/test caches |
Binaries are output to bin/.
Version strings are injected via -ldflags from cmd/thor/VERSION (currently 2.4.2) and cmd/disco/VERSION.
Running the Node Locally
make thor
bin/thor --network main # mainnet
bin/thor --network test # testnet
bin/thor solo # solo (dev) mode — instant blocks, all forks enabled at genesisSolo mode activates all forks at block 0 (SoloFork in thor/fork_config.go), making it ideal for local development.
Directory Organization
cmd/
thor/ Main node binary (flags, solo mode, pruner, sync, p2p)
disco/ Bootnode discovery tool
api/ REST API layer (gorilla/mux)
accounts/ Account/contract endpoints
blocks/ Block query endpoints
transactions/ Tx submission/query
events/ Event log filtering
transfers/ Transfer log filtering
subscriptions/ WebSocket subscriptions
debug/ Debug/tracer endpoints
admin/ Admin endpoints
fees/ Fee delegation endpoints
node/ Node info endpoints
middleware/ HTTP middleware (metrics, logging)
restutil/ Shared REST utilities
doc/ OpenAPI spec
thor/ Core types (Address, Bytes32, ForkConfig)
tx/ Transaction model
block/ Block model
chain/ Chain repository (block storage, indexing)
state/ State trie access
runtime/ EVM execution runtime
vm/ EVM implementation (go-ethereum fork)
consensus/ Block validation / consensus rules
packer/ Block packing / proposal
scheduler/ Block scheduling
bft/ Byzantine fault tolerance (finality)
builtin/ Built-in smart contracts (native bindings)
gen/ Solidity sources + code generation
genesis/ Genesis block construction
logdb/ Event/transfer log database
muxdb/ Multiplexed database layer
trie/ Merkle Patricia trie
txpool/ Transaction pool
p2p/ Low-level P2P networking
p2psrv/ P2P server
comm/ P2P communication protocol
tracers/ EVM tracing (js, logger, native)
thorclient/ Go client library for thor API
metrics/ Prometheus metrics
test/ Test utilities
testchain/ In-memory chain for testing
testnode/ In-memory node for API testing
datagen/ Test data generators
bindcontract/ Contract binding helpers
eventcontract/ Test event contractTesting
Tests use standard go test and github.com/stretchr/testify.
Test utilities (test/)
| Package | Purpose |
|---|---|
testchain | Spins up an in-memory chain with genesis, BFT engine, and block production helpers |
testnode | Builds a full in-memory node with REST API for integration tests |
datagen | Generates random addresses, hashes, bytes, and numbers for tests |
bindcontract | Helpers for deploying and binding test contracts |
eventcontract | Pre-built contract for event emission tests |
testchain.Chain provides MintBlock() / MintTransactions() to produce blocks programmatically without P2P.
Fuzz targets
Located in tx/ and block/ packages — test marshalling/unmarshalling roundtrips.
Code Style & Linting
Formatting
Enforced via golangci-lint formatters (.golangci.yml):
gofmt(with simplify)goimports(local prefix:github.com/vechain/thor)gofumptgolines(max line length: 160)
Linters enabled
bidichk, copyloopvar, durationcheck, gosec, govet, ineffassign, misspell, revive, staticcheck, unconvert, unused, whitespace
Paths excluded from linting: third_party, builtin, examples, p2p/*.
Pre-commit hook
make install-hooks installs a hook that scans for 64-char hex strings (potential private keys) in staged changes. Bypass with SAFE_TO_IGNORE_KEY=1.
CI Pipeline (Pull Requests)
PRs trigger these checks (.github/workflows/on-pull-request.yaml):
1. Unit tests — make test with codecov upload 2. License check — Apache SkyWalking Eyes header scan 3. Lint — golangci-lint via lint-go.yaml 4. Go module check — verifies go.mod / go.sum consistency 5. Workflow scan — security scan of GitHub Actions workflows 6. Docker test suite — integration tests via Docker 7. Rosetta tests — Coinbase Rosetta API compliance
Adding a REST API Endpoint
Each API domain follows the same pattern in api/:
1. Create a package under api/<domain>/ (e.g., api/fees/) 2. Define the handler struct with dependencies (repo, stater, etc.):
type Fees struct {
repo *chain.Repository
stater *state.Stater
}
func New(repo *chain.Repository, stater *state.Stater) *Fees { ... }3. Implement handler methods returning func(w http.ResponseWriter, req *http.Request) error:
func (f *Fees) handleGetFee(w http.ResponseWriter, req *http.Request) error { ... }4. Add a `Mount` method to register routes on a mux.Router:
func (f *Fees) Mount(root *mux.Router, pathPrefix string) {
sub := root.PathPrefix(pathPrefix).Subrouter()
sub.Path("/{id}").Methods(http.MethodGet).
Name("GET /fees/{id}").
HandlerFunc(restutil.WrapHandlerFunc(f.handleGetFee))
}5. Define request/response types in api/<domain>_types.go (at the api/ package level) 6. Wire it up in the main API router by calling Mount()
Key conventions:
- Use
restutil.WrapHandlerFuncto wrap handlers (handles error responses) - Parse path params with
mux.Vars(req) - Parse query params / block revision via shared utilities in
api/restutil/
Fork Configuration
Forks are defined in thor/fork_config.go as the ForkConfig struct:
type ForkConfig struct {
VIP191 uint32
ETH_CONST uint32
BLOCKLIST uint32
ETH_IST uint32
VIP214 uint32
FINALITY uint32
HAYABUSA uint32
GALACTICA uint32
}Each field is a block number at which the fork activates. math.MaxUint32 means disabled.
To add a new fork:
1. Add a new field to ForkConfig 2. Set it to math.MaxUint32 in NoFork 3. Set it to 0 in SoloFork (enables immediately in dev mode) 4. Add activation block numbers in the forkConfigs map for mainnet/testnet genesis IDs 5. Update String() to include the new fork name 6. Reference forkConfig.YourFork in consensus/runtime code to gate new behavior
Fork names follow VIP numbers (e.g., VIP191, VIP214) or codenames (e.g., HAYABUSA, GALACTICA).
Built-in Contracts
Built-in (native) contracts live in builtin/:
| Contract | File | Purpose |
|---|---|---|
| Authority | authority_native.go | Authority/validator management |
| Energy (VTHO) | energy_native.go | VTHO token operations |
| Params | params_native.go | On-chain governance parameters |
| Prototype | prototype_native.go | Account metadata/master |
| Extension | extension_native.go | Block/tx introspection helpers |
| Executor | (via Solidity) | On-chain governance execution |
| Staker | staker_native.go | PoS staking (Hayabusa+) |
Modifying built-in contracts
1. Edit Solidity source in builtin/gen/*.sol 2. Compile: docker run --rm -v ./builtin/gen:/solidity ghcr.io/argotorg/solc:0.4.24 ... (see gen.go for exact flags — solc 0.4.24 for legacy contracts, solc 0.8.20 for staker) 3. Regenerate Go bindings: make generate (runs go generate builtin/gen/gen.go) 4. Implement native call handlers in builtin/*_native.go 5. Wire into builtin/builtin.go and builtin/contract.go
Compiled ABI/bytecode lives in builtin/gen/compiled/ (embedded via //go:embed).
Replaced Dependencies
Two critical replace directives in go.mod:
| Original | Fork | Why |
|---|---|---|
github.com/ethereum/go-ethereum | github.com/vechain/go-ethereum | Customized EVM, crypto, RLP — diverged significantly from upstream |
github.com/syndtr/goleveldb | github.com/vechain/goleveldb | Custom patches for VeChain's storage layer |
When importing go-ethereum packages, the actual code comes from the VeChain fork. This affects vm/, abi/, crypto/, rlp/, trie/ and related packages.
PR Requirements
1. Fork repo → create feature branch (feature/your-feature-name) 2. Keep branch up-to-date with master 3. Run make test and make lint before pushing 4. All commits must be GPG-signed 5. PR targets master branch 6. PR description must clearly explain changes and rationale 7. Follow Effective Go guidelines 8. VIPs (protocol changes) require a separate proposal at vechain/VIPs first
Block Production Flow
How a new block is produced in VeChainThor, from scheduling through finality.
Overview
scheduler/ → packer/ → runtime/ → consensus/ → bft/ → chain/
│ │ │ │ │ │
Schedule Assemble Execute Validate Vote StoreStep 1: Scheduling — Who Proposes Next
Package: scheduler/, packer/pos_scheduler.go, packer/poa_scheduler.go
The node's packerLoop (in cmd/thor/node/packer_loop.go) continuously calls Packer.Schedule() to determine when this node should propose.
| Type | Scheduler | Selection Method |
|---|---|---|
| PoA (pre-Galactica) | PoASchedulerV1 / PoASchedulerV2 | Round-robin from Authority candidates with endorsement balance check |
| PoS (post-Galactica) | PoSScheduler | Weighted random sampling (A-Res reservoir) from Staker.LeaderGroup() |
Key types:
scheduler.Scheduler— interface:Schedule(nowTime) → newBlockTime,IsTheTime(),Updates()scheduler.Proposer—{Address, Active, Weight}scheduler.Seeder— generates deterministic VRF-based seeds per block
PoS scheduling detail: NewPoSScheduler generates a deterministic shuffle using ChaCha8 PRNG seeded with Blake2b(VRF_seed, blockNumber). Each validator gets score -ln(random)/weight (A-Res algorithm). Validators sorted by score determine the slot order.
Triggers next: Packer.Schedule() returns a *Flow with the target block time.
Step 2: Packing — Assembling the Block
Package: packer/
Packer.Schedule() determines consensus mode (PoA vs PoS) by calling Staker.SyncPOS(), then delegates to schedulePOS or schedulePOA. Both return (beneficiary, newBlockTime, score).
A Flow is created with a runtime.Runtime initialized with:
xenv.BlockContext{
Beneficiary, Signer, Number, Time,
GasLimit, TotalScore, BaseFee,
}Transaction adoption (Flow.Adopt): 1. Check blocklist, chain tag, block ref, expiration 2. Check gas capacity (gasUsed + tx.Gas ≤ GasLimit) 3. Post-Galactica: validate EffectiveGasPrice ≥ BaseFee and priority fee 4. Check tx not duplicate, dependency resolved 5. Create state checkpoint → runtime.ExecuteTransaction(tx) → on error, revert 6. Accumulate gasUsed, append to txs and receipts
The packerLoop calls flow.Adopt(tx) for each executable tx from the pool, stopping at errGasLimitReached.
Key types:
packer.Packer— holds repo, stater, nodeMaster, beneficiary, forkConfigpacker.Flow— accumulates txs/receipts, tracks gas, calls runtime
Triggers next: Flow.Pack(privateKey, conflicts, shouldVote) builds and signs the block.
Step 3: Runtime Execution
Package: runtime/
Runtime.ExecuteTransaction resolves the tx, buys gas (VTHO), then executes each clause sequentially.
Per-clause execution: 1. PrepareClause creates a statedb.StateDB and EVM instance 2. If clause.To() == nil → contract creation; else → evm.Call 3. Collects Output{Data, Events, Transfers, LeftOverGas, VMErr} 4. On VM error → revert all clauses to checkpoint, mark receipt.Reverted = true
Gas payment: ResolvedTransaction.BuyGas deducts VTHO from payer (delegator → sponsor → origin fallback).
Reward calculation (post-Galactica):
priorityFeePerGas = min(maxFeePerGas - baseFee, maxPriorityFeePerGas)receipt.Reward = priorityFeePerGas × gasUsed- Reward credited to
BeneficiaryviaEnergy.Add
Key types:
runtime.Runtime— wraps EVM, chain, state, block contextruntime.ResolvedTransaction— resolved origin, delegator, intrinsic gas, clausesruntime.TransactionExecutor— iterator over clauses withHasNextClause/PrepareNext/Finalizetx.Receipt—{Type, Reverted, Outputs, GasUsed, GasPayer, Paid, Reward}
Step 4: Consensus Validation
Package: consensus/
When receiving a block (not packing), Consensus.Process() validates it:
1. Header validation (validateBlockHeader):
- Timestamp > parent, aligned to
BlockInterval - Not future (≤ now + BlockInterval)
- GasUsed ≤ GasLimit, valid gas limit delta
- TotalScore > parent
- Signature length (65 bytes pre-VIP214, complex sig post-VIP214)
- BaseFee matches
galactica.CalcBaseFee(parent)post-Galactica
2. Proposer validation:
- PoA:
validateAuthorityProposer— checks signer is in Authority candidates, scheduled for this slot - PoS:
validateStakingProposer— checks signer is inStaker.LeaderGroup(), scheduled viaPoSScheduler - Both verify
TotalScore = parent.TotalScore + score
3. Body validation (validateBlockBody):
TxsRootmatches merkle root of txs- Each tx: valid origin, chain tag, block ref, expiration, features
4. Block verification (verifyBlock):
- Re-execute all txs via
runtime.ExecuteTransaction - Verify
GasUsed,ReceiptsRoot,StateRootall match header - Post-PoS: distribute rewards via
Energy.DistributeRewards
Key types:
consensus.Consensus— holds repo, stater, seeder, forkConfig, validatorsCache
Step 5: BFT Finality
Package: bft/
The bft.Engine implements VIP-220 finality (epoch-based voting).
Concepts:
- Epoch:
EpochLengthblocks (e.g., 100). Checkpoint = first block of epoch. - COM bit: Block header flag indicating the proposer votes for current checkpoint.
- Quality: Count of epochs with sufficient votes. Stored at
storePoint(last block of epoch). - Justified: Checkpoint with quality ≥ threshold for current epoch.
- Finalized: When quality > 1,
findCheckpointByQuality(quality-1)is finalized.
Key operations:
ShouldVote(parentID)— packer checks before setting COM bitCommitBlock(header, isPacking)— saves quality at epoch end, updates finalizedSelect(header)— fork choice: prefer higher quality, thenBetterThanAccepts(parentID)— rejects blocks not descending from finalized checkpoint
Key types:
bft.Committer— interface forEnginebft.Engine— tracks casts (votes), finalized/justified checkpointsbft.justifier— per-epoch vote accumulator
Step 6: Chain Storage
Package: chain/
Repository.AddBlock(block, receipts, conflicts, asBest):
1. Index block: Update index trie (block number → block ID mapping) 2. Save block:
- Header summary →
hdrStore - Tx blobs →
bodyStore - Receipt blobs →
bodyStore - Tx metadata (index, reverted) →
txIndexer - Chain head →
headStore
3. If asBest: update bestBlockID in propStore, broadcast via tick.Signal
Key types:
chain.Repository— thread-safe block/tx/receipt storage overmuxdb.MuxDBchain.Chain— linked chain view from genesis to a given head, backed by index triechain.BlockSummary—{Header, Txs []Bytes32, Size, Conflicts}chain.TxMeta—{BlockNum, BlockConflicts, Index, Reverted}
End-to-End Sequence (Packing Path)
1. packerLoop waits for sync, then loops:
2. Packer.Schedule(bestBlock, now) → Flow
3. Wait until flow.When() - BlockInterval/2
4. For each txPool.Executables(): flow.Adopt(tx)
5. bft.ShouldVote() → shouldVote
6. flow.Pack(privateKey, conflicts, shouldVote) → block, stage, receipts
7. stage.Commit() → persist state trie
8. repo.AddBlock(block, receipts) → persist to DB
9. bft.CommitBlock(header, isPacking) → update finality
10. comm.BroadcastBlock(block) → propagate to peersEnd-to-End Sequence (Receiving Path)
1. comm.Sync downloads blocks from peers → handleBlockStream
2. node.processBlock(block):
3. bft.Accepts(parentID) → reject if not on finalized branch
4. consensus.Process(parent, block, now, conflicts) → stage, receipts
5. bft.Select(header) → fork choice (becomeBest?)
6. node.commitBlock: stage.Commit(), repo.AddBlock(), bft.CommitBlock()
7. If becomeBest: processFork (re-add orphaned txs to pool)Reward Distribution Flow
How block rewards are calculated and distributed to validators and delegators.
Overview
runtime/ (per-tx reward) → energy/ (distribute) → staker/ (delegation split)
│ │ │
Calculate Credit VTHO Split validator/delegatorTwo Reward Eras
| Era | Condition | Mechanism |
|---|---|---|
| Pre-Galactica | blockNumber < forkConfig.GALACTICA | rewardRatio × gas price per tx |
| Post-Galactica (PoS) | blockNumber ≥ forkConfig.GALACTICA | Priority fee per tx + block reward from staking curve |
Per-Transaction Reward (Both Eras)
Package: runtime/ — in PrepareTransaction().Finalize()
Pre-Galactica (PoA)
rewardRatio := Params.Get(KeyRewardRatio) // governance parameter
overallGasPrice := gasPrice + baseGasPrice * provedWork / txGas
reward := gasUsed * overallGasPrice * rewardRatio / 1e18The reward is a fraction of gas cost, controlled by on-chain RewardRatio parameter. The remainder (gas cost minus reward) is burned.
Post-Galactica (PoS)
priorityFeePerGas := min(maxFeePerGas - baseFee, maxPriorityFeePerGas)
receipt.Reward = priorityFeePerGas * gasUsedPriority fee goes entirely to the block proposer. The baseFee portion is burned (never credited).
Crediting per-tx reward
In both eras, after computing receipt.Reward:
Energy.Native(state, blockTime).Add(beneficiary, receipt.Reward)This credits VTHO directly to the block's Beneficiary address.
Block-Level Reward Distribution (PoS Only)
Package: builtin/energy/energy.go — DistributeRewards()
Called at block finalization in two places:
- Packing path:
Flow.Pack()inpacker/flow.go - Validation path:
verifyBlock()inconsensus/validator.go
Both call:
energy.DistributeRewards(beneficiary, signer, staker, blockNumber)Reward Calculation (CalculateRewards)
totalStaked, _ := staker.LockedStake() // in VET (not wei)
sqrtStake := sqrt(totalStaked) * 1e18 // convert to wei precision
curveFactor := Params.Get(KeyCurveFactor) // governance parameter
reward := curveFactor * sqrtStake / blocksPerYearThe reward curve is sublinear (reward ∝ √(totalStaked)), incentivizing broad participation rather than stake concentration.
Validator vs Delegator Split
validatorRewardPerc := Params.Get(KeyValidatorRewardPercentage) // e.g., 30
hasDelegations := staker.HasDelegations(signer)
if hasDelegations && validatorRewardPerc < 100 {
proposerReward = reward * validatorRewardPerc / 100
delegationReward = reward - proposerReward
// Credit delegator reward to delegation contract address
delegatorAddr := Params.Get(KeyDelegatorContractAddress)
state.SetEnergy(delegatorAddr, existing + delegationReward)
staker.IncreaseDelegatorsReward(signer, delegationReward, blockNumber)
}
// Credit validator reward to beneficiary
state.SetEnergy(beneficiary, existing + proposerReward)
energy.addIssued(reward) // track total supplyKey points:
- If the validator has no delegations, they receive 100% of the block reward
- If delegated, the split is governed by
KeyValidatorRewardPercentage(on-chain parameter) - Delegator rewards are held in a contract address and tracked per-validator by
staker.IncreaseDelegatorsReward addIssued(reward)increases the tracked VTHO total supply (fortotalSupply()queries)
VTHO (Energy) Generation
Package: builtin/energy/, state/
VTHO is generated in two ways:
1. Growth from VET holdings (pre-Hayabusa)
Every VET holder earns VTHO proportional to their balance over time:
func (acc Account) CalcEnergy(blockTime, stopTime uint64) *big.Int {
// energy += balance * (min(blockTime, stopTime) - acc.BlockTime) * energyGrowthRate
}Rate: 5 × 10⁻⁸ VTHO per VET per second (≈ 0.000432 VTHO/VET/day).
Hayabusa fork calls Energy.StopEnergyGrowth() which freezes growth at the fork block time. After this, VTHO only comes from block rewards.
2. Block rewards (post-Galactica)
As described above — DistributeRewards mints new VTHO each block.
VTHO Burning
VTHO is burned in two places:
1. Gas payment: ResolvedTransaction.BuyGas deducts gas × effectiveGasPrice VTHO from payer. After execution, unused gas is refunded. The net burn = gasUsed × effectiveGasPrice - receipt.Reward.
2. BaseFee (post-Galactica): The baseFee portion of gas cost is implicitly burned — it is deducted from the payer but never credited to anyone. Only the priority fee goes to the proposer.
Burning is tracked via Energy.totalAddSub — TotalBurned = TotalSub - TotalAdd.
BaseFee Mechanism (EIP-1559 Adapted)
Package: consensus/upgrade/galactica/
func CalcBaseFee(parent, forkConfig) *big.Int {
parentGasTarget = parent.GasLimit * GasTargetPercentage / 100
if parentGasUsed > parentGasTarget:
baseFee increases (min delta = 1)
else:
baseFee decreases (floor = InitialBaseFee)
}Adjustment rate: baseFeeChangeDenominator (governance parameter, similar to Ethereum's 8).
Reward Flow Diagram
┌──────────────────────┐
│ Per-Tx Priority Fee │
│ (runtime/ Finalize) │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Block Staking Reward │
│ (energy/ Calculate) │
│ √(totalStaked)*curve │
└──────────┬───────────┘
│
┌────────────────┴────────────────┐
│ │
┌─────────▼─────────┐ ┌─────────▼─────────┐
│ Validator Share │ │ Delegator Share │
│ validatorRewardPerc│ │ (100 - validatorRP)│
│ → beneficiary VTHO│ │ → contract address │
└───────────────────┘ │ → per-validator │
│ reward tracking │
└───────────────────┘Key Parameters (On-Chain Governance)
| Parameter | Key | Purpose |
|---|---|---|
KeyRewardRatio | Pre-Galactica reward fraction | Fraction of gas cost given as reward |
KeyCurveFactor | Block reward curve multiplier | Controls block reward magnitude |
KeyValidatorRewardPercentage | Validator share of block reward | Split between validator and delegators |
KeyLegacyTxBaseGasPrice | Base gas price for legacy txs | Floor price for legacy transactions |
KeyDelegatorContractAddress | Delegator reward holding address | Where delegator rewards accumulate |
Staking and Delegation Flow
How validators stake, delegators participate, and the PoS transition works (Galactica upgrade).
Overview
builtin/staker/ ← scheduler/ ← consensus/
│ │ │
Stake mgmt PoS scheduling Proposer validationStaker Contract Architecture
Package: builtin/staker/
The Staker struct composes four services:
| Service | Package | Responsibility |
|---|---|---|
validationService | staker/validation/ | Validator lifecycle (add, activate, exit, renew) |
delegationService | staker/delegation/ | Delegation CRUD and exit signaling |
aggregationService | staker/aggregation/ | Per-validator delegation totals |
globalStatsService | staker/globalstats/ | System-wide stake counters |
The contract is deployed at builtin.Staker.Address and uses native calls (Go code, not Solidity EVM).
Step 1: Becoming a Validator
Adding a Validation
Staker.AddValidation(validator, endorser, period, stake)
Requirements:
- Stake:
25M VET ≤ stake ≤ 600M VET(MinStakeVET/MaxStakeVET) - Validator address must not already exist
- Period must be one of:
LowStakingPeriod,MediumStakingPeriod,HighStakingPeriod - VET transferred to staker contract address
State changes: 1. validationService.Add() — creates validation.Validation in queued list 2. globalStatsService.AddQueued(stake) — increment global queued counter 3. ContractBalanceCheck() — invariant: locked + queued + withdrawable + cooldown = contract balance
Validation States
Queued → Active → [Exit signaled] → Exit| Status | Meaning |
|---|---|
StatusQueued | Waiting for activation at next epoch boundary |
StatusActive | In the leader group, eligible to produce blocks |
StatusExit | Exited, stake withdrawable after cooldown |
Key Type: validation.Validation
type Validation struct {
Endorser thor.Address
LockedVET uint64 // currently locked stake
Weight uint64 // effective weight (may include multiplier)
QueuedVET uint64 // additional stake pending next period
PendingUnlockVET uint64 // stake pending unlock at period end
Status Status
Period uint32 // staking period in blocks
StartBlock uint32 // when activated
ExitBlock *uint32 // when exit takes effect (nil if not signaled)
OfflineBlock *uint32 // when marked offline (nil if online)
Beneficiary *thor.Address // reward recipient override
}Step 2: Activation — Queued to Active
Package: builtin/staker/housekeep.go
Activation happens during epoch transitions via Staker.Housekeep(currentBlock), called by Staker.SyncPOS() at every block.
Housekeep runs when currentBlock % EpochLength == 0:
1. Eviction check (every EvictionCheckInterval): validators offline for > ValidatorEvictionThreshold blocks get force-exited 2. Renewals (UpdateGroup): active validators at period end get their weight recalculated, queued stake promoted to locked 3. Exits: validators with ExitBlock == currentBlock are moved to exit status 4. Activations: queued validators promoted to leader group up to MaxBlockProposers
Activation detail (activateNextValidation):
validator, validation := validationService.NextToActivate(maxLeaderGroupSize)
aggregationService.Renew(validator) // consolidate delegation totals
validationService.ActivateValidator(validator, ...) // set status=Active, startBlock
globalStatsService.ApplyRenewal(renewal) // move queued→locked in countersStep 3: Delegation
Adding a Delegation
Staker.AddDelegation(validator, stake, multiplier, currentBlock) → delegationID
Requirements:
- Validator must be
StatusQueuedorStatusActive(and not signaled exit) stake > 0,multiplier > 0- Total TVL (validation + delegations) must not exceed
MaxStakeVET(600M)
State changes: 1. delegationService.Add() → assigns delegationID, stores delegation starting at next iteration 2. aggregationService.AddPendingVET() → adds weighted stake to per-validator pending totals 3. globalStatsService.AddQueued(stake) → increment system queued counter 4. If validator is active: validationService.AddToRenewalList() → ensure renewal at next epoch
Key Type: delegation.Delegation
type Delegation struct {
Validation thor.Address // validator this delegates to
StartIteration uint32 // iteration when delegation activates
LastIteration *uint32 // iteration when delegation exits (nil = auto-renew)
Stake uint64 // staked VET amount
Multiplier uint8 // weight multiplier (longer lock = higher weight)
}Delegation Exit
Staker.SignalDelegationExit(delegationID, currentBlock):
- Sets
LastIterationto current iteration - Updates aggregation to signal pending exit
- Delegation remains locked until iteration end, then becomes withdrawable
Delegation Withdrawal
Staker.WithdrawDelegation(delegationID, currentBlock) → amount:
- Only if not started yet, or ended (past LastIteration)
- Returns VET to the delegator
- Updates global stats (remove from queued or withdrawable)
Step 4: PoS Consensus Participation
Leader Group Formation
Active validators form the leader group, queried via Staker.LeaderGroup():
type Leader struct {
Address thor.Address
Endorser thor.Address
Active bool // online status
Weight uint64 // stake weight for scheduling
Beneficiary *thor.Address // reward recipient
}PoS Scheduling
Package: scheduler/pos.go
PoSScheduler uses the leader group for weighted random block assignment:
1. Seed from VRF chain: Blake2b(VRF_beta, blockNumber) 2. Each validator gets priority: -ln(random) / weight (A-Res weighted reservoir sampling) 3. Validators sorted by priority → deterministic slot order 4. Schedule advances in BlockInterval steps
PoS Consensus Validation
Package: consensus/pos_validator.go
validateStakingProposer(header, parent, staker): 1. Get signer from header 2. Get leader group (cached or from staker.LeaderGroup()) 3. Build PoSScheduler with leader group and VRF seed 4. Verify sched.IsTheTime(header.Timestamp()) — signer is scheduled for this slot 5. Compute updates, score = sched.Updates(timestamp) — mark skipped validators as inactive 6. Verify parent.TotalScore + score == header.TotalScore
Online/Offline Tracking
Staker.SetOnline(validator, blockNum, online):
- Called during scheduling — validators that miss their slot are marked
Active: false - Offline validators can be evicted after
ValidatorEvictionThresholdblocks - Coming back online (producing a block) resets the offline counter
Step 5: Reward Distribution for Stakers/Delegators
See flow-reward-distribution.md for full detail. Summary:
1. Block reward calculated: curveFactor × √(totalStaked) / blocksPerYear 2. If validator has delegations: split by ValidatorRewardPercentage 3. Validator share → beneficiary VTHO balance 4. Delegator share → delegation contract address, tracked per-validator via IncreaseDelegatorsReward
Step 6: Unstaking and Cooldown
Validator Exit
SignalExit(validator, endorser, currentBlock)
→ sets ExitBlock (at next period boundary)
→ at ExitBlock epoch: Housekeep moves to StatusExit
WithdrawStake(validator, endorser, currentBlock)
→ returns locked + queued + cooldown VET
→ updates global statsCooldown Period
When a validator exits, their stake may go through a cooldown period before being withdrawable. The globalStatsService tracks three counters that reflect the lifecycle:
| Counter | Meaning |
|---|---|
queued | Stake waiting to become active |
locked | Actively staked and earning rewards |
withdrawable | Exited and available for withdrawal |
cooldown | Exited but still in cooldown period |
Stake Increase/Decrease (Active Validators)
IncreaseStake: adds toQueuedVET(takes effect next period)DecreaseStake: reducesLockedVET - PendingUnlockVET(next period)- Both require endorser authorization and add validator to renewal list
PoA → PoS Transition
Package: builtin/staker/, consensus/upgrade/galactica/
The transition is managed by Staker.SyncPOS(), called at every block:
1. Hayabusa fork: Staker contract deployed, energy growth stopped 2. Transition period: Authorities can migrate to staker contract. Authority endorsement checked via TransitionPeriodBalanceCheck which accepts both PoA endorsement and staker contract balance 3. Galactica fork: BaseFee activated, DynamicFee tx type enabled, Shanghai EVM opcodes 4. PoS activation: When enough validators have staked, SyncPOS returns Active: true and scheduling switches from PoA to PoS
Contract Balance Invariant
ContractBalanceCheck(pendingWithdraw) is called after every state-changing operation:
locked + queued + withdrawable + cooldown + pendingWithdraw == contract VET balance
== effectiveVET (slot 0)Any mismatch is a consensus-level error.
Chain Synchronization Flow
How a node discovers peers, downloads blocks, validates them, and handles forks.
Overview
p2p/p2psrv/ → comm/ → consensus/ → chain/ → bft/
│ │ │ │ │
Discover Download Validate Store FinalizeStep 1: Peer Discovery and Connection
Package: p2p/, p2psrv/
The P2P layer uses a devp2p-compatible stack with Kademlia-based discovery (discv5).
Discovery topic: thor1@<last8bytes_of_genesisID> — ensures peers are on the same network.
Handshake (Communicator.runPeer): 1. 5-second timeout for status exchange 2. Verify GenesisBlockID matches 3. Verify system clock diff ≤ 2 × BlockInterval 4. Exchange {BestBlockID, TotalScore, SysTimestamp} 5. Add to PeerSet, track head block and total score
Key types:
comm.Communicator— orchestrates sync, tx relay, block announcementscomm.Peer— wrapsp2p.Peerwith block/tx knowledge trackingcomm.PeerSet— thread-safe peer collection with filtering
Step 2: Initial Sync
Package: comm/communicator.go, comm/sync.go
Communicator.Sync() runs a timer-based sync loop:
1. Find peer with TotalScore ≥ our best block's TotalScore
2. download(ctx, repo, peer, headNum, handler) → stream blocks
3. Repeat until synced (best block time + BlockInterval ≥ now, or >2 sync rounds)
4. Close syncedCh → triggers packer loop and tx syncDownload Pipeline (Three-Stage)
Stage 1: fetchRawBlockBatches → rawBatches channel (cap 10)
Stage 2: decodeAndWarmupBatches → warmedUp channel (cap 2048)
Stage 3: handler (processBlock) → commit to chainStage 1 — fetches raw RLP-encoded blocks from peer via proto.GetBlocksFromNumber:
- Requests blocks starting from
ancestor + 1 - Batches of raw bytes sent to channel
- Stops when peer returns empty result
Stage 2 — decodes and pre-warms caches in parallel:
- RLP decode each block
- Verify block number sequence
- Parallel cache warm-up:
header.ID(),header.Beta(),tx.ID(),tx.IntrinsicGas(),tx.Delegator() - Throttling: inserts nil blocks when buffer > 10% full to reduce memory pressure
Stage 3 — node.handleBlockStream processes each block through processBlock
Common Ancestor Discovery
findCommonAncestor(ctx, repo, peer, headNum):
1. Fast seek: Exponential backward scan (1, 2, 4, 8, ... blocks back) using proto.GetBlockIDByNumber 2. Binary search: Between fast-seek result and head, find exact fork point 3. Returns the highest block number both nodes agree on
Step 3: Block Processing During Sync
Package: cmd/thor/node/block_exec.go
Node.processBlock(block, stats):
1. Guard processing: Lock + conflict detection
- If
blockNum > maxBlockNum + 1→errBlockTemporaryUnprocessable - Otherwise, count existing blocks at same height (
ScanConflicts)
2. BFT acceptance: bft.Accepts(parentID) — reject if parent not on finalized branch
3. Consensus processing: consensus.Process(parentSummary, block, now, conflicts) → stage, receipts
- Full header, proposer, body, and execution validation (see block production flow)
4. Fork choice:
- Post-FINALITY:
bft.Select(header)— quality-based selection - Pre-FINALITY:
header.BetterThan(prevBest)— total score comparison
5. Commit: commitBlock(ctx):
- Write logs to
logdb(if becoming best and logs enabled) stage.Commit()— persist state trierepo.AddBlock(block, receipts, conflicts, becomeBest)bft.CommitBlock(header, isPacking=false)- If becoming best:
processForkto re-add orphaned txs
Error Handling
| Error | Action |
|---|---|
errKnownBlock | Ignore (already stored) |
errFutureBlock | Queue (block timestamp too far ahead) |
errParentMissing | Queue (parent not yet received) |
errBlockTemporaryUnprocessable | Queue (block number too far ahead) |
errBFTRejected | Discard (conflicts with finalized checkpoint) |
| Consensus critical error | Log error, discard block |
Step 4: Steady-State Block Propagation
Package: comm/communicator.go, comm/announcement_loop.go
Once synced, new blocks arrive via two mechanisms:
Direct Block Push
Peers call proto.NotifyNewBlock(peer, block) — sends full block to √N random peers:
p := int(math.Sqrt(float64(len(peers))))
toPropagate := peers[:p] // send full block
toAnnounce := peers[p:] // send just block IDBlock ID Announcement
Remaining peers receive just the block ID via proto.NotifyNewBlockID. The announcement loop processes these:
1. Receive announcement with {blockID, senderPeer} 2. If block unknown: fetch from peer via proto.GetBlockByID 3. Process via processBlock 4. If accepted as trunk: broadcast to our peers
Transaction Propagation
Package: comm/txs_loop.go
- After sync completes:
syncTxs(peer)— bulk fetch txs from connected peer - Ongoing:
txFeedsubscription relays new executable txs to peers - Peer tracking:
peer.MarkTransaction(hash)prevents duplicate sends
Step 5: Fork Handling
Fork Detection
Forks are detected when processBlock finds the new block becomes best but the previous best was at the same height on a different branch.
Fork Resolution
Node.processFork(newBlock, oldBestBlockID):
oldTrunk := repo.NewChain(oldBestBlockID)
newTrunk := repo.NewChain(newBlock.ParentID())
sideIDs := oldTrunk.Exclude(newTrunk) // blocks on old branch not on new
for _, id := range sideIDs {
block := repo.GetBlock(id)
for _, tx := range block.Transactions() {
txPool.Add(tx) // re-add orphaned txs to pool
}
}The Chain.Exclude(other) method walks backwards from the chain head, finding blocks present in one chain but not the other.
Log Rewriting on Fork
oldBranch := oldTrunk.Exclude(newTrunk)
if len(oldBranch) > 0 {
logDB.Writer.Truncate(forkPoint) // remove old branch logs
}
newBranch := newTrunk.Exclude(oldTrunk)
for _, id := range newBranch {
logDB.Writer.Write(block, receipts) // write new branch logs
}BFT Finality and Fork Prevention
Post-FINALITY fork, the BFT engine prevents deep reorgs:
bft.Accepts(parentID)rejects blocks whose parent is not a descendant of the finalized checkpointbft.Select(header)prefers higher quality (more epoch votes) over total score- Once a checkpoint is finalized, all blocks not descending from it are permanently rejected
Node Startup Sequence
Package: cmd/thor/node/node.go
func (n *Node) Run(ctx context.Context) error {
maxBlockNum := repo.GetMaxBlockNum()
txStash := newTxStash(db, 1000)
// Four concurrent goroutines:
go n.comm.Sync(ctx, n.handleBlockStream) // sync + block processing
go n.houseKeeping(ctx) // periodic maintenance
go n.txStashLoop(ctx, txStash) // persist non-executable txs
go n.packerLoop(ctx) // block production (waits for sync)
}The packerLoop blocks on comm.Synced() channel before starting block production.
Protocol Messages
Package: comm/proto/
| Message | Direction | Purpose |
|---|---|---|
GetStatus | Request | Exchange genesis ID, best block, total score |
GetBlocksFromNumber | Request | Batch download blocks for sync |
GetBlockIDByNumber | Request | Common ancestor discovery |
GetBlockByID | Request | Fetch single block |
NotifyNewBlock | Push | Propagate full block |
NotifyNewBlockID | Push | Announce new block (lightweight) |
GetTxs | Request | Bulk tx sync after initial sync |
NotifyNewTx | Push | Relay new executable transaction |
Transaction Lifecycle Flow
End-to-end journey of a transaction from submission to on-chain finality.
Overview
API/P2P → txpool/ → packer/ → runtime/ → chain/ → logdb/
│ │ │ │ │ │
Receive Validate Select Execute Store Index logsStep 1: Transaction Receipt
Transactions enter the node through two paths:
| Entry Point | Package | Method |
|---|---|---|
REST API POST /transactions | api/transactions/ | TxPool.AddLocal(tx) |
| P2P propagation | comm/handle_rpc.go | TxPool.Add(tx) or TxPool.StrictlyAdd(tx) |
| Sync from peer | comm/sync.go | TxPool.StrictlyAdd(tx) |
Step 2: Transaction Types
Package: tx/
Two transaction types (starting from Galactica fork):
| Field | TypeLegacy (0x00) | TypeDynamicFee (0x51) |
|---|---|---|
| Gas pricing | GasPriceCoef (0–255) over BaseGasPrice | MaxFeePerGas + MaxPriorityFeePerGas |
| Encoding | RLP list | Type-prefix + RLP |
| Work proofs | ProvedWork for priority boost | Not applicable |
Common fields (both types):
ChainTag— last byte of genesis IDBlockRef— first 8 bytes of a recent block hash (establishes tx validity window)Expiration— valid fromBlockRef.NumbertoBlockRef.Number + ExpirationClauses— array of{To, Value, Data}(multi-clause model)Gas— max gasDependsOn— optional tx dependencyNonce— replay protection (not sequential like Ethereum)Reserved— feature flags (e.g.,DelegationFeaturefor VIP-191)
Key types:
tx.Transaction— immutable, caches signingHash/origin/id/intrinsicGastx.Clause—{To *Address, Value *big.Int, Data []byte}tx.BlockRef— 8-byte block referencetx.Features— bit flags (delegation support)
ID calculation: tx.ID = Blake2b(signingHash, origin) — unique per sender.
Step 3: TxPool Validation and Storage
Package: txpool/
TxPool.add() performs staged validation:
Static validation (validateTxBasics)
1. Signature low-S check (EnforceSignatureLowS) 2. Chain tag matches repo.ChainTag() 3. Size ≤ MaxTxSize (64 KB)
Blocklist check
- Origin and delegator checked against both hardcoded blocklist and fetched blocklist
Resolution
ResolveTx(tx) → TxObject with computed origin, priorityGasPrice
Executability check (when chain is synced)
TxObject.Executable() verifies against current state:
- Tx not already on chain
- Block ref in valid range, not expired
- Dependencies resolved (not reverted)
- Sufficient VTHO balance for gas cost
- Post-Galactica: effective gas price ≥ baseFee
Pool management
- Pool limit:
Options.Limit(default ~10k) - Per-account limit:
Options.LimitPerAccount - Non-executable cap: 20% of pool limit
- Priority eviction: sorted by
priorityGasPricedescending
Housekeeping (every second)
The wash() routine: 1. Removes expired, settled, blocked, energy-depleted txs 2. Recalculates priorityGasPrice when baseFee changes 3. Produces sorted Executables() list for the packer
Key types:
txpool.TxPool—{all *txObjectMap, executables atomic.Value}txpool.TxObject— wrapstx.Transactionwithexecutable bool,priorityGasPricetxpool.TxEvent—{Tx, Executable *bool}— published to subscribers (comm layer for P2P broadcast)
Step 4: Transaction Selection for Block
Package: packer/
In proposeAndCommit (node packer loop):
txs := n.txPool.Executables() // sorted by priority gas price desc
for _, tx := range txs {
err := flow.Adopt(tx)
if packer.IsGasLimitReached(err) { break }
if packer.IsTxNotAdoptableNow(err) { continue }
// bad tx → mark for removal
}Flow.Adopt performs final checks (see block production flow) and calls runtime.ExecuteTransaction.
Error categories:
badTxError— permanent rejection (wrong chain tag, expired, blocked origin)errTxNotAdoptableNow— temporary (future block ref, gas too high for remaining space)errGasLimitReached— block full, stop adoptingerrKnownTx— already on chain
Step 5: Clause Execution
Package: runtime/
VeChain's multi-clause model executes multiple operations atomically in one tx.
Execution flow per transaction
ResolveTransaction(tx) → ResolvedTransaction
└─ BuyGas(state, blockTime, baseFee) → deduct VTHO from payer
└─ For each clause:
└─ PrepareClause → EVM.Call or EVM.Create
└─ Output{Events, Transfers, LeftOverGas, VMErr}
└─ Finalize → ReceiptGas payer resolution order (BuyGas)
1. Delegator (VIP-191) — if tx has delegation feature + delegator signature 2. Sponsor — if all clauses share same To and user has credit via Prototype.Bind 3. Contract `To` — if sponsor insufficient but user has credit 4. Origin — fallback
Clause execution detail
- Each clause gets its own EVM instance via
PrepareClause - Contract creation:
clause.To == nil→EVM.Create - Native contract calls intercepted via
InterceptContractCall(builtin contracts) - On any clause VM error: all clauses revert to checkpoint
Receipt generation
Receipt{
Type: tx.Type(),
Reverted: reverted,
Outputs: []{Events, Transfers}, // nil if reverted
GasUsed: tx.Gas() - leftOverGas,
GasPayer: payer,
Paid: gasUsed * effectiveGasPrice,
Reward: priorityFeePerGas * gasUsed, // post-Galactica
}Step 6: Storage and Indexing
Block storage (chain/)
When the block is committed via Repository.AddBlock:
- Tx blobs →
bodyStorekeyed by(blockNum, conflicts, index, txFlag) - Receipt blobs →
bodyStorekeyed by(blockNum, conflicts, index, receiptFlag) - Tx metadata →
txIndexerkeyed bytxID + varint(blockNum) + varint(conflicts) - Filter key →
txIndexerkeyed by first 8 bytes of txID (bloom-like fast lookup)
Log indexing (logdb/)
LogDB (SQLite) indexes events and transfers for query:
- Events table:
blockNum, txIndex, clauseIndex, address, topic0–4, data - Transfers table:
blockNum, txIndex, clauseIndex, sender, recipient, amount - Written by
node.writeLogs()→logdb.Writer.Write(block, receipts) - On fork:
Writer.Truncate(forkPoint)then re-write new branch
Step 7: Query
Transactions become queryable via:
| Endpoint | Source | Key |
|---|---|---|
GET /transactions/{id} | chain.Chain.GetTransaction(id) | txID → txMeta → tx blob |
GET /transactions/{id}/receipt | chain.Chain.GetTransactionReceipt(id) | txID → txMeta → receipt blob |
POST /logs/event | logdb.LogDB.FilterEvents() | SQLite query with address/topic filters |
POST /logs/transfer | logdb.LogDB.FilterTransfers() | SQLite query with sender/recipient filters |
Error Paths Summary
| Stage | Error | Result |
|---|---|---|
| TxPool add | Bad signature, wrong chain tag, too large | badTxError — rejected |
| TxPool add | Pool full, non-executable | txRejectedError — rejected |
| TxPool wash | Expired, settled, insufficient energy | Removed from pool |
| Packer adopt | Expired, duplicate, gas limit | Skip or stop packing |
| Runtime execute | Intrinsic gas > provided gas | Tx not included |
| Runtime clause | VM revert/error | Receipt with Reverted: true, all clauses rolled back |
| Consensus verify | Mismatched receipts root, state root | Block rejected |
P2P Networking
Thor uses Ethereum-derived devp2p networking: discv5 for peer discovery, RLPx for encrypted connections, and a custom thor/1 protocol for block and transaction propagation. Default P2P port: 11235.
Discovery — discv5
Located in p2p/discv5/. Ethereum-style v5 topic discovery over UDP.
- Kademlia-like DHT with XOR distance metric
- Topic-based: nodes register/search by
thor1@<genesisID[24:]> - 257 buckets, 16 nodes per bucket, concurrency factor (alpha) = 3
UDP Packet Types
| Packet | Purpose |
|---|---|
ping / pong | Liveness check |
findnode / neighbors | Node lookup |
findnodeHash | v5 lookup by hash |
topicRegister / topicQuery / topicNodes | Topic discovery |
Key Constants
| Constant | Value |
|---|---|
| Protocol version | 4 |
| Response timeout | 500ms |
| Packet expiration | 20s |
| Max packet size | 1280 bytes |
| Max neighbors per response | ~12 |
| Max findnode failures | 5 |
| Seed count | 30 |
| Seed max age | 5 days |
| Bucket refresh interval | 1 min |
| Auto-refresh interval | 1 hour |
ENR (Ethereum Node Records)
Located in p2p/enr/. EIP-778 style records, max 300 bytes.
| Entry Key | Type | Purpose |
|---|---|---|
id | "v4" | Identity scheme |
secp256k1 | compressed pubkey | Node identity |
ip | IPv4/IPv6 | Network address |
tcp | uint16 | TCP port |
udp | uint16 | UDP discovery port |
Signing uses secp256k1-keccak identity scheme (Keccak256 of RLP-encoded record).
NAT Traversal
Located in p2p/nat/. Interface for port mapping.
| Value | Mechanism |
|---|---|
none / off / "" | No mapping |
any / auto / on | Auto-detect (UPnP or NAT-PMP) |
upnp | UPnP |
pmp / natpmp | NAT-PMP |
extip:<IP> | Fixed external IP |
Mapping timeout: 20 min, update interval: 15 min. Maps both TCP (P2P) and UDP (discovery).
P2P Server
Two layers:
p2psrv.Server (p2psrv/)
High-level wrapper that integrates discv5 with the devp2p server:
- Starts discv5 UDP listener
- NAT mapping for UDP port
- Registers genesis topic (
thor1@<genesisID[24:]>) - Runs discovery loop → feeds discovered nodes to dial loop
- Manages peer caches:
discoveredNodes(RandCache, 128),knownNodes(PrioCache, 5)
p2p.Server (p2p/server.go)
Low-level devp2p server:
- RLPx encrypted TCP connections
- Protocol negotiation via capabilities
- Connection flags:
dynDialedConn,staticDialedConn,inboundConn,trustedConn
Connection Management
| Setting | Value |
|---|---|
--max-peers | 25 (default) |
--p2p-port | 11235 |
| Max active dial tasks | 16 |
| Default max pending peers | 50 |
| Dial timeout | 15s |
| Frame read timeout | 30s |
| Frame write timeout | 20s |
| DialRatio | sqrt(MaxPeers) |
Inbound limit: MaxPeers - maxDialedConns. Trusted peers bypass limits.
Dial pacing: 500ms for first 20 dials, 2s after, 10s when >50% peers connected.
Comm Protocol — Block & Transaction Propagation
Located in comm/. Protocol: thor/1, max message size 10 MB.
Message Types
| Code | Message | Type | Purpose |
|---|---|---|---|
| 0 | MsgGetStatus | Call | Handshake / status exchange |
| 1 | MsgNewBlockID | Notify | Announce new block (ID only) |
| 2 | MsgNewBlock | Notify | Broadcast full block |
| 3 | MsgNewTx | Notify | Broadcast transaction |
| 4 | MsgGetBlockByID | Call | Request block by ID |
| 5 | MsgGetBlockIDByNumber | Call | Request block ID by number |
| 6 | MsgGetBlocksFromNumber | Call | Request batch of blocks (sync) |
| 7 | MsgGetTxs | Call | Sync transactions |
Block Propagation
BroadcastBlock():sqrt(peers)receiveMsgNewBlock(full block), remaining peers getMsgNewBlockID- Announced block IDs trigger fetch via
MsgGetBlockByIDin the announcement loop
Transaction Propagation
txsLoopsubscribes to the tx poolMsgNewTxsent to peers that haven't seen the transaction- After sync completes:
syncTxs()viaMsgGetTxsfor catch-up
Sync
Sync() selects the best peer (highest total score) and runs download() using MsgGetBlocksFromNumber to fetch block batches.
Bootstrap Nodes
disco Command (cmd/disco/)
Standalone bootstrap node — UDP-only discv5 listener, no TCP, no thor protocol.
| Flag | Default | Purpose |
|---|---|---|
--addr | :55555 | Listen address |
--keyfile | auto | Private key file |
--keyhex | — | Private key as hex |
--nat | none | NAT mechanism |
--netrestrict | — | CIDR whitelist |
Node Sources
1. Hardcoded fallback: 11 bootstrap nodes in cmd/thor/p2p/bootstrap.go (port 55555) 2. Remote list: https://vechain.github.io/bootstraps/node.list (HTTP enode URLs) 3. Peer cache: peers.cache in instance dir — known nodes saved on shutdown
Default Ports
| Port | Protocol | Use |
|---|---|---|
| 11235 | TCP | P2P connections (RLPx) |
| 55555 | UDP | Bootstrap node discovery |
Component Wiring
cmd/thor/main.go
→ newP2PCommunicator()
→ p2psrv.New(opts) // wraps p2p.Server, manages discv5
→ comm.New(...) // block/tx propagation logic
→ p2pCommunicator.Start()
→ p2psrv.Start(comm.Protocols(), comm.DiscTopic())
→ p2p.Server.Start() // TCP listener
→ discv5.ListenUDP() // UDP discovery
→ RegisterTopic("thor1@<genesisID>")
→ discoverLoop() + dialLoop()
→ comm.Start() // sync, broadcast loopsSolo Mode
Solo mode runs a local single-node VeChain chain for development and testing. No P2P networking, no real consensus — just a local sandbox with pre-funded accounts.
Starting Solo
thor solo [flags]Solo-Specific Flags
| Flag | Default | Description |
|---|---|---|
--on-demand | false | Create blocks only when pending transactions exist |
--block-interval | 10 | Block interval in seconds |
--persist | false | Persist data to disk (otherwise in-memory) |
--gas-limit | 40,000,000 | Block gas limit (0 = adaptive) |
--genesis | — | Custom genesis file path/URL (default: builtin devnet) |
--txpool-limit | 10,000 | Transaction pool size |
Common Examples
thor solo # basic, in-memory, 10s blocks
thor solo --on-demand # blocks only when txs arrive
thor solo --persist --on-demand # persistent + on-demand
thor solo --api-addr 0.0.0.0:8669 # expose API externally
thor solo --gas-limit 0 --api-cors="*" # adaptive gas, open CORSMining Modes
Auto-Mining (Default)
- Loop runs every second
- Produces a block when
time.Now().Unix() % blockInterval == 0 - Creates empty blocks if no transactions
On-Demand (--on-demand)
- Uses
OnDemandTxPoolinstead of the standard tx pool - Block produced immediately when an executable transaction is submitted
- Empty blocks are skipped
- Can produce blocks with future timestamps if needed
Pre-Funded Accounts
10 accounts, each with 1,000,000,000 VET (1e27 wei) in both balance and energy.
Generated from fixed private keys in genesis/devnet.go:
| # | Private Key |
|---|---|
| 0 | 99f0500549792796c14fed62011a51081dc5b5e68fe8bd8a13b86be829c4fd36 |
| 1 | 7b067f53d350f1cf20ec13df416b7b73e88a1dc7331bc904b92108b1e76a08b1 |
| 2 | f4a1a17039216f535d42ec23732c79943ffb45a089fbb78a14daad0dae93e991 |
| 3 | 35b5cc144faca7d7f220fca7ad3420090861d5231d80eb23e1013426847371c4 |
| 4 | 10c851d8d6c6ed9e6f625742063f292f4cf57c2dbeea8099fa3aca53ef90aef1 |
| 5 | 2dd2c5b5d65913214783a6bd5679d8c6ef29ca9f2e2eae98b4add061d0b85ea0 |
| 6 | e1b72a1761ae189c10ec3783dd124b902ffd8c6b93cd9ff443d5490ce70047ff |
| 7 | 35cbc5ac0c3a2de0eb4f230ced958fd6a6c19ed36b5d2b1803a9f11978f96072 |
| 8 | b639c258292096306d2f60bc1a8da9bc434ad37f15cd44ee9a2526685f592220 |
| 9 | 9d68178cdc934178cca0a0051f40ed46be153cf23cb1805b59cc612c0ad2bbe0 |
Account #0 (0xf077b491b355E64048cE21E3A6Fc4751eEeA77fa) also serves as the executor and block signer (PoA validator).
Persistence
| Mode | Storage | Behavior |
|---|---|---|
Default (no --persist) | In-memory | Data lost on restart |
--persist | Disk | Written to --data-dir, survives restarts |
With --persist, instance directory follows the standard naming: instance-<genesisID[24:]>-<version>.
API Access
Same HTTP API as a full node, default localhost:8669. Differences in solo:
- Debug API: PoA checks skipped during trace replay (
skipPoA: true) - Admin health: always reports healthy (peer count check bypassed)
- No P2P:
solo.Communicator{}is a no-op stub
All standard flags work: --api-addr, --api-cors, --api-timeout, --api-pprof, etc.
Genesis Configuration
Default solo genesis is the devnet config (genesis/devnet.go):
- Genesis ID:
0x00000000bb55405beed90df9fea5acdb1cb7caba61b0d7513395f42efd30e558 - All forks enabled from block 0 (
SoloFork) - Initial gas limit: 10,000,000 (overridable via
--gas-limit)
SoloFork = ForkConfig{
VIP191: 0, ETH_CONST: 0, BLOCKLIST: 0, ETH_IST: 0,
VIP214: 0, FINALITY: 0, GALACTICA: 0, HAYABUSA: 0,
}Custom genesis can be provided via --genesis <path-or-url>.
Solo vs Testnet/Mainnet
| Aspect | Solo | Testnet/Mainnet |
|---|---|---|
| P2P networking | None | Full discv5 + RLPx |
| Consensus | Mocked BFT engine | Real BFT with PoA/PoS |
| Genesis | Devnet (all forks at block 0) | Network-specific fork schedule |
| Block production | Local packer (auto or on-demand) | Committee-based |
| Master key | Not required | Required for block signing |
| Persistence | Optional (in-memory default) | Always on disk |
| Pre-funded accounts | 10 × 1B VET | None |
Use Cases
- Smart contract development: deploy and test contracts with instant feedback
- dApp development: full API available locally, no sync delay
- Integration testing: deterministic blocks, predictable behavior
- CI pipelines: lightweight, fast startup, no external dependencies
- On-demand mode: block only when you transact, ideal for scripted tests
Key Source Files
| File | Purpose |
|---|---|
cmd/thor/solo/solo.go | Solo entry point and main loop |
cmd/thor/solo/core.go | Block packing logic |
cmd/thor/solo/txpool.go | On-demand transaction pool |
cmd/thor/solo/types.go | Solo communicator stub |
genesis/devnet.go | Devnet genesis, DevAccounts, SoloConfig |
thor/fork_config.go | SoloFork definition |
bft/engine.go | soloMockedEngine |
Storage Architecture
Thor uses a layered storage design: MuxDB (LevelDB) for blockchain state and chain data, LogDB (SQLite) for event/transfer logs, and a Merkle-Patricia trie for state management.
MuxDB — Main Database
Central storage abstraction in muxdb/. Wraps LevelDB and manages trie nodes + named KV stores.
Key Spaces
| Space | Byte Prefix | Purpose |
|---|---|---|
trieHistSpace | 0 | Historical trie nodes (versioned, prunable) |
trieDedupedSpace | 1 | Deduplicated trie nodes (checkpointed, permanent) |
namedStoreSpace | 2 | Named KV stores for chain data |
Named KV Stores
| Store | Contents |
|---|---|
chain.hdr | Block headers |
chain.body | Block bodies |
chain.props | Chain properties |
chain.heads | Chain head pointers |
chain.txi | Transaction index |
state.code | Contract bytecode |
muxdb.props | DB config (partition factors) |
pruner.props | Pruner checkpoint state |
Trie Names
| Name | Purpose |
|---|---|
"a" | Account trie (address → account + metadata) |
"i" | Index trie (block index) |
"s" + storageID | Per-contract storage tries |
LevelDB — Underlying Engine
Engine: syndtr/goleveldb via engine.LevelEngine.
| Setting | Default | Description |
|---|---|---|
| BlockCacheCapacity | ReadCacheMB (256 MB) | Read cache |
| WriteBuffer | WriteBufferMB (128 MB) | Write buffer |
| Filter | Bloom 10-bit | Block filter |
| BlockSize | 32 KB | Data block size |
| CompactionTableSize | 4 MB | SST file size |
| OverflowPrefix | trieHistSpace | Compaction hint (when pruning enabled) |
Configured via --cache flag (default 4096 MB total).
LogDB — Event and Transfer Logs (SQLite)
SQLite database (mattn/go-sqlite3) with WAL journal mode.
Schema
ref table — blob deduplication for blockID, txID, addresses, topics:
CREATE TABLE ref (id INTEGER PRIMARY KEY, data BLOB NOT NULL UNIQUE);event table:
CREATE TABLE event (
seq INTEGER PRIMARY KEY,
blockID, blockTime, txID, txOrigin, clauseIndex,
address, topic0, topic1, topic2, topic3, topic4,
data BLOB
);Indexes on address, topic combinations (6 indexes total, one optional via --logdb-additional-indexes).
transfer table:
CREATE TABLE transfer (
seq INTEGER PRIMARY KEY,
blockID, blockTime, txID, txOrigin, clauseIndex,
sender, recipient, amount BLOB(32)
);Indexes on txOrigin, sender, recipient.
Query API
FilterEvents(ctx, *EventFilter)— range (block from/to), criteria (address, topics), order, limitFilterTransfers(ctx, *TransferFilter)— same pattern
seq encodes block number + tx index + log index for ordering.
Journal size limit: 50 MB. Separate read-only and write connections.
Merkle-Patricia Trie
Located in trie/. Versioned Merkle-Patricia trie for state management.
Node Types
| Type | Description |
|---|---|
fullNode | 17 children (16 hex nibbles + value) |
shortNode | Compressed path key + single child |
refNode | Hash + version (database reference) |
valueNode | Leaf value + metadata |
Version
Version{Major, Minor uint32} — major is block number, minor is sub-index within block.
Read Path
cache → hist space → deduped space (with fallback handling for account/index roots).
Write Path
Commit() writes to hist space, optionally populates cache. Checkpoint() copies nodes from hist to deduped space for a version range.
Empty root: thor.Blake2b(rlp.EmptyString).
State Management
Located in state/. Connects tries to the EVM execution layer.
revert-able state → stacked map → journal → playback → updated trie
→ trie cache → read-only trieState struct holds:
- Account trie (
"a") - Per-address cached objects (account data, code, storage trie, storage map)
StackedMapfor revertable changes (EVM snapshots)
Stage/Commit: Stage(newVersion) computes state root, returns Stage with commit() that writes code and commits all tries.
Pruning
Located in cmd/thor/pruner/. Removes historical trie nodes to reclaim disk space.
How It Works
1. Runs periodically: every 65536 blocks (or 8192 when nearly synced) 2. Checkpoint: copies trie nodes from hist → deduped space for [base, target) range 3. Delete: removes hist nodes in that range via DeleteTrieHistoryNodes() 4. Preserves MaxStateHistory (65535 blocks) of recent history for EVM access
Configuration
| Flag | Effect |
|---|---|
--disable-pruner | Stops pruner, keeps all history (archive mode) |
When disabled: TrieHistPartitionFactor = 524288 (vs 256 for pruning), instance dir gets -full suffix.
Partition Factors
| Setting | Pruning | Archive |
|---|---|---|
TrieHistPartitionFactor | 256 | 524288 |
TrieDedupedPartitionFactor | MaxUint32 | MaxUint32 |
TrieCachedNodeTTL | 30 | 30 |
Node Types vs Storage
| Type | Flags | Disk Size | Notes |
|---|---|---|---|
| Full | (default) | ~200 GB | Pruned trie history + logs |
| Full without logs | --skip-logs | ~100 GB | No event/transfer logs, /logs API disabled |
| Archive | --disable-pruner | >400 GB | All trie history preserved |
Instance directory naming: instance-<genesisID[24:]>-<version>[-full].
Cache Layer
| Cache | Implementation | Size | Purpose |
|---|---|---|---|
| Trie node cache | directcache | 1/4 queried + 3/4 committed | Hot trie nodes |
| Root cache | Map + TTL eviction | — | Trie roots |
| Chain summaries | ARC | 512 | Block summaries |
| Chain txs | ARC | 2048 | Transactions |
| Chain receipts | ARC | 2048 | Receipts |
| Code cache | ARC | 512 | Contract bytecode |
| Future blocks | RandCache | 32 | Blocks ahead of head |
| Base fee | Custom | — | Cached base fee calculations |
| LevelDB | Block cache + write buffer | Configurable | Engine-level I/O cache |
Component Wiring
main.go
→ openMainDB() → muxdb.Open(path, opts) → LevelDB engine
→ openLogDB() → logdb.New(path) → SQLite
→ chain.NewRepository(mainDB, genesis) → named KV stores for headers/bodies/txindex
→ state.NewStater(mainDB) → account trie + storage tries
→ pruner.New(mainDB, repo, bftEngine) // unless --disable-pruner