There are already great resources explaining what social forking is, so I won’t rehash it here. If you’re unfamiliar with the concept, I highly recommend reading this article series as a starting point.
This article is for anyone who wants to understand how to execute a social or hard fork on a Cosmos SDK-based blockchain. I wrote this based on my experience setting up and testing forks of our chain, Layer. My goal is to walk you through the general process and address common pain points that arise during a fork.
Step 1: Exporting Chain State
The first step is to export the current state of the chain at a chosen height. If the chain is still running, coordinate a halt height with other validators. You can configure this in your node’s ‘app.toml’:
toml
Once your node is halted (either manually or via configuration), export the chain state:
halt-height= <chosen_height>
bash
./layerd export --home ~/.layer > exported_layer_state.json
This exported_layer_state.json will serve as your new genesis file. It’s what you will modify to reflect the new state of the forked chain.
A Word on Exported State Size
The export process runs every module’s ExportGenesis function and combines their state into one JSON file. If your chain has custom modules or large amounts of historical data (e.g., oracles, reports), the resulting file can be massive.
In our case, if we exported all of our oracle module data to the genesis file it would include millions of data entries. This caused issues with CometBFT, which has compression limits that prevents such a bloated genesis from initializing the chain and also made the genesis file unreadable in normal text editors.
Recommendations: – Only export what’s critical to continue chain operation. – For large non-critical datasets, consider exporting them separately and reintroducing them via a post-fork upgrade.
You can achieve this by modifying ExportGenesis to dump additional data into module-specific files and importing them later with a migration or upgrade handler. In order to make it as trustless and verifiable as possible, think about using a checksum or some other strategy to allow for other validators and nodes to validate the exported data.
Step 2: Editing the Genesis File
Every fork has unique reasons and requirements, but most require the same set of changes:
General Chain Metadata
- Change the chain id.
- Update the genesis time.
- If using vote extensions, set a new vote_extensions_enabled_height to prevent CometBFT from expecting validator votes at the genesis height
Removing Malicious or Inactive Validators
If you’re forking due to validator misbehavior (e.g., 1/3 of validators halt or collude), you may need to remove or slash validators.
Investigate Voting Behavior
To detect inactive validators:
while true; do
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
result=$(curl -s localhost:26657/consensus_state | jq)
echo "[$timestamp] $result"
sleep 1
done | tee -a consensus_log.txt
Check for nil-Vote entries at consistent validator indexes. Let this run for awhile so that you get lots of data to sort through and don’t punish someone who just happened to miss a consensus round. Once you have identified consistent indexes with a nil-Vote do the following to identify and get more information on the validators:
bash
# Get validator info by index
./layerd query tendermint-validator-set --node tcp://localhost:26657 -o json | jq '.validators[<index>]'
# Extract public key and get staking info
PUBKEY=$(./layerd query comet-validator-set -o json | jq -r '.validators[<index>].pub_key.key')
./layerd query staking validators -o json | jq --arg pubkey "$PUBKEY" '.validators[] | select(.consensus_pubkey.value == $pubkey)'
Take note of the: – Consensus address (e.g., tellorvalcons…) – Moniker – Validator operator address
Genesis Edits to Remove Validator
Validator’s state is kept across multiple modules so it is important to keep track of what you have changed as it probably has effects on other data. When removing a validator you have to first calculate the number of tokens delegated to them using their Here’s a general checklist of changes:
1. Consensus Module
Remove validator from .consensus.validators. Use the moniker to pick find the validators in this array unless their moniker is not unique, and in that case you can use the public key found above2.
2. Staking Module
- Remove delegations from .staking.delegations
- Update validator in .staking.validators: set jailed, status = BOND_STATUS_UNBONDED, and zero out tokens and delegator_shares
- Remove validator from .staking.last_validator_powers
- Recalculate .staking.last_total_power
TIP: When removing a validator you must first calculate how many tokens that validator has delegated to it using the following steps:
1) Calculate validator’s exchange rate using the same data from before that gave us the validators operator address and moniker: Exchange Rate = validator.tokens / validator.delegator_shares
2) Loop through all delegations to the validator in .staking.delegations using this equation to calculate the number of tokens in each delegation: # of tokens = Validator Exchange Rate * Delegator Shares
The sum of this process is the number of tokens that need to be removed from the bonded tokens pool
3. Distribution Module
- Remove delegations to validator in .distribution.delegator_starting_infos
- Remove .distribution.outstanding_rewards entries for the validator
- Subtract removed rewards from the distribution module account balance.
4. Bank Module
- Adjust the bonded_tokens_pool and total_supply accordingly
5. Slashing Module
-
- Remove validator from .slashing.missed_blocks
- Update .slashing.signing_infos to set jailed, tombstoned, and set jail end time far into the future
⚠️ Note: If you’re only slashing (not removing) a validator, you’ll still need to update all the same modules but adjust values accordingly.
Dealing With Rounding Errors
Working with large numbers can introduce rounding mismatches. When starting the chain, you might see an error like:
expected bonded tokens: 123456789, found: 123456788
If the difference is tiny (1-2 units), it’s usually fine to manually correct the number in the genesis. If it’s larger, revisit your calculations. It helped me to just log a lot of the numbers I calculated to sanity check them before actually editing the exported state.
Step 3: Adding a Post-Fork Upgrade (Optional)
If your genesis file is too large or you want to introduce more data later, you can plan a software upgrade.
Three options:
1. Genesis Proposal: Include a governance proposal that’s voted on in the forked chain’s first 24 hours.
2. Genesis Upgrade Plan: Trigger an upgrade automatically at the genesis height.
3. Planned Upgrade Later: Define an upgrade height for future migration.
For options 2 and 3, modify your InitChainer() function:
- Detect a genesis upgrade plan
- Set up a version map for the upgrade handler
- Apply the upgrade after InitGenesis
Make sure migrations are registered in the modules being upgraded so the upgrade executes cleanly when triggered. If you would like to see an example of an InitChainer that supports this take a look here.
Final Thoughts
Every social fork is different. What doesn’t change is the level of care and precision required. You are editing core state and rewriting chain history—do so responsibly.
When possible: – Automate state changes with scripts – Document all edits and share them with your community – Strive for transparency
I hope this guide helps you navigate your own social fork. If you’d like to see sample bash scripts that I used to edit the state look here. And if you read this and thought “I could use this to test upgrade migrations on exported state from our chain” then look here to see how I set up a docker environment to do just that. Feel free to reach out or contribute improvements if you’ve been through a fork yourself!