Hardforks
A hardfork is a network upgrade that changes consensus rules. All nodes must upgrade before the scheduled activation timestamp; nodes running old software will diverge from the network after activation.
Activation Mechanism
Each hardfork has:
- A unique number (N).
- A Unix timestamp — the earliest wall-clock time at which the hardfork can activate.
- A validator vote supermajority — >80% of the current validator set must signal the new hardfork version via
validator_update_operation.
Both conditions must be satisfied simultaneously. Validators can block an unwanted hardfork by withholding their version vote even after the scheduled timestamp.
Hardfork History
| # | Version | Key changes |
|---|---|---|
| 1–10 | 1.x – 2.x | Foundation, social graph, energy system, committee, subscriptions |
| 11 | 3.0.0 | — |
| 12 | 3.1.0 | Fork collision metrics, vote-weighted fork comparison, emergency consensus mode, NTP improvements |
| 13 | 3.2.0 | Validator reward sharing with vote-proportional distribution |
HF12 Summary
HF12 (version 3.1.0) introduced:
- Fork collision counter —
fork_collision_countandlast_fork_collision_block_numadded todynamic_global_property_object. Observable viaget_dynamic_global_properties. - Vote-weighted fork comparison (
compare_fork_branches()) — fork selection uses total delegated SHARES per validator branch + 10% bonus for longer chain. - Emergency consensus mode — activates automatically after 1 hour with no blocks; "committee" account takes all 21 slots. See Emergency Consensus.
- Minority fork auto-resync — validator plugin detects node isolation (21 consecutive own-blocks) and rolls back to LIB.
- NTP improvements — dedicated NTP client with configurable servers, interval, and round-trip threshold.
HF13 Summary
HF13 (version 3.2.0) introduced:
Validator reward sharing: part of each block's validator reward is redistributed proportionally to the accounts that voted for that validator (by their SHARES vote weight).
- New field on
validator_object:reward_percent— fraction of block reward shared with voters (0–10000 basis points). - New virtual operation:
validator_reward_virtual_operation— fired once per reward distribution. - Set via
validator_update_operation.
Implementing a New Hardfork
Step 1: Create hardfork definition file
libraries/chain/hardfork.d/N.hf:
#ifndef CHAIN_HARDFORK_N
#define CHAIN_HARDFORK_N N
#define CHAIN_HARDFORK_N_TIME 1234567890 // Unix timestamp — must be in the future
#define CHAIN_HARDFORK_N_VERSION hardfork_version(3, N, 0)
#endifStep 2: Bump constants
libraries/chain/hardfork.d/0-preamble.hf:
#define CHAIN_NUM_HARDFORKS Nlibraries/protocol/include/graphene/protocol/config.hpp (if protocol-visible):
#define CHAIN_VERSION (version(3, N, 0))Step 3: Schema version
If any chainbase object layout changes (new fields, removed fields, resized types), increment CHAIN_SCHEMA_VERSION in config.hpp:
#define CHAIN_SCHEMA_VERSION uint32_t(N)The chain plugin checks this at startup. A mismatch wipes shared_memory.bin before opening, preventing corrupt reads from old layouts.
New fields should always have zero-value defaults to avoid migration code:
uint16_t my_new_field = 0;Step 4: Wire into database.cpp
init_hardforks():
FC_ASSERT(CHAIN_HARDFORK_N == N);
_hardfork_times[N] = fc::time_point_sec(CHAIN_HARDFORK_N_TIME);
_hardfork_versions[N] = hardfork_version(CHAIN_HARDFORK_N_VERSION);apply_hardfork() case:
case CHAIN_HARDFORK_N: {
// Migration if any. Leave empty with a comment if zero defaults cover it.
break;
}Step 5: Operation and evaluator (if new op)
- Add struct to
chain_operations.hppwithvalidate()and authority getters. - Add to the
static_variantinoperations.hpp. - Declare
DEFINE_EVALUATOR(my_new_op)inchain_evaluator.hpp. - Implement
do_apply()in a.cppevaluator file — always checkASSERT_REQ_HF(CHAIN_HARDFORK_N, ...)first. - Register in
initialize_evaluators()indatabase.cpp.
Step 6: Plugin updates
| Plugin | What to update |
|---|---|
account_history | Add impact extractor for any new virtual operation |
validator_api | Add new fields from validator_object to validator_api_object |
snapshot | Add new chainbase objects to serialize_state / load_snapshot |
Schema Version Lifecycle
Fresh node (no existing data):
stored = 0, compiled = N → mismatch
wipe shared_memory (no-op if absent)
write schema_version = N
genesis → normal startup
Upgrade (old binary had version M < N):
stored = M, compiled = N → mismatch
wipe shared_memory.bin
write schema_version = N
db.open() → revision mismatch exception
→ auto-recovery: snapshot import + dlt_block_log replay
Normal restart:
stored = N, compiled = N → match
db.open() proceeds normallyKey files:
config.hpp—CHAIN_SCHEMA_VERSIONplugins/chain/plugin.cpp— schema check and wipe logic<data_dir>/schema_version— plain text file with current version
Deployment Checklist
- [ ]
CHAIN_NUM_HARDFORKSincremented - [ ]
CHAIN_VERSIONbumped (if protocol-visible) - [ ]
CHAIN_SCHEMA_VERSIONincremented (if any chainbase object layout changed) - [ ] Hardfork
.hffile created with future activation timestamp - [ ] All new fields have zero defaults;
apply_hardforkcomment explains why no migration needed - [ ] New evaluator registered in
initialize_evaluators() - [ ] New virtual op registered in
account_historyplugin - [ ]
validator_api_objectupdated ifvalidator_objectchanged - [ ] Snapshot plugin updated if new chainbase objects added
See also: Fair-DPOS, Emergency Consensus, Snapshots.