QikChain is a Polygon Edge–based EVM chain with a custom Go CLI for deterministic genesis generation, PoA/PoS switching, and devnet orchestration.
Design principle: switching consensus is a configuration change, not a rewrite.
- Overview
- Native Token
- Architecture
- Requirements
- Build
- Releases
- Genesis Pipeline
- Devnet: IBFT 4-node
- Metrics
- Network Status UI
- TX Lab (Dev-only Transaction Testing UI)
- Wallet + Faucet (Devnet)
- CI / Health Checks
- Troubleshooting
- Repo Layout
- Roadmap
- License
QikChain provides:
- Deterministic genesis generation (stable outputs for identical inputs)
- Clean PoA ↔ PoS switching via config/flags
- Environment-scoped allocations (devnet/staging/mainnet)
- Devnet scripts: start / stop / status
- Operator-friendly status output (human + JSON, plus tail logs)
- Fixed-supply native token policy (QIK)
| Property | Value |
|---|---|
| Name | QIK |
| Symbol | QIK |
| Decimals | 18 |
| Supply posture | Fixed supply |
| Phase 1 PoS rewards | 0 (disabled) |
Token metadata lives in: config/token.json
This repo’s Polygon Edge build expects --chain to point to a combined genesis document where .genesis is an embedded object.
Default output:
build/genesis.json— combined chain config + embedded Ethereum genesis (used by devnet scripts)
Optional split outputs are still supported for alternate tooling/builds:
build/chain.json— chain config withgenesisas a string pathbuild/genesis-eth.json— Ethereum-style genesis file
combined genesis.json (example)
{
"name": "qikchain",
"bootnodes": [],
"params": {
"chainID": 100,
"minGasPrice": "0",
"engine": {
"ibft": {
"type": "PoA",
"validatorType": "ecdsa",
"blockTime": "2s"
}
}
},
"genesis": {
"alloc": {
"0x1000000000000000000000000000000000000001": { "balance": "1000000000000000000000000" }
},
"gasLimit": "0x1c9c380",
"difficulty": "0x1",
"extraData": "0x",
"baseFeeEnabled": false
}
}- Go (matching repo toolchain)
./bin/polygon-edgeavailable (repo-managed or built separately)curlrecommended (RPC/metrics checks)jqrecommended (status JSON mode & debugging)
Build the CLI:
go build -o ./bin/qikchain ./cmd/qikchainVerify binaries:
./bin/qikchain --help
./bin/polygon-edge version || trueArtifacts are published automatically to GitHub Releases when you push a semantic version tag (vX.Y.Z).
Cut a release:
git tag vX.Y.Z
git push --tagsThe release workflow builds deterministic-leaning archives for:
linux/amd64linux/arm64
Each archive is uploaded as qikchain_<os>_<arch>.tar.gz and a SHA256SUMS file is attached to the release.
Artifacts are created under dist/ in CI and for local builds. To produce the same output locally:
make release-localVerify checksums after downloading release assets:
sha256sum -c SHA256SUMSReproducible build settings used by release builds:
-trimpath-buildvcs=false-ldflagswithmain.version,main.commit, andmain.date
main.date is derived from the tagged commit timestamp (SOURCE_DATE_EPOCH in CI) for stable metadata across reruns of the same commit.
Use qikchain edge caps to detect Polygon Edge CLI capabilities for scripts and genesis builders.
Human-readable report:
./bin/qikchain edge capsMachine-readable JSON:
./bin/qikchain edge caps --json
./bin/qikchain edge caps --json --prettyOptional flags:
--edge-binpath to the edge binary (default./bin/polygon-edge)--timeoutcommand timeout for external edge calls (default3s)
Environment allocations live under:
config/allocations/devnet.jsonconfig/allocations/staging.jsonconfig/allocations/mainnet.json
Commands:
./bin/qikchain allocations verify --file config/allocations/devnet.json
./bin/qikchain allocations report --file config/allocations/devnet.json
./bin/qikchain allocations render --file config/allocations/devnet.jsonPoA devnet example:
./bin/qikchain genesis build --consensus poa --env devnet --chain-id 100 --allocations config/allocations/devnet.json --token config/token.json --out-combined build/genesis.json --out-chain build/chain.json --out-genesis build/genesis-eth.jsonValidate:
./bin/qikchain genesis validate --chain build/genesis.jsonFor Phase 1 PoS:
- Staking enabled
- Validator set contract used
- Rewards disabled (
rewardPerBlock = 0)
PoS build example:
./bin/qikchain genesis build --consensus pos --env devnet --chain-id 100 --pos-deployments build/deployments/pos.local.json --out-combined build/genesis.json --out-chain build/chain.json --out-genesis build/genesis-eth.jsonUse Make targets as a state-aware devnet manager:
make up
make down
make reset
make status
make logsNotes:
make upis idempotent: it reuses existingbuild/genesis.jsonand skips regeneration when present.make up RESET=1behaves like reset and wipes prior chain state + genesis before starting.make resetstops devnet, removesbuild/genesis.jsonand.data/, then starts fresh.make statusreturns success only when PID is alive and RPC health checks (eth_blockNumber) succeed.make logstails.logs/devnet.log(usemake logs-followto stream).
Scripts:
scripts/devnet-ibft4.sh— start (PoA or PoS)scripts/devnet-ibft4-stop.sh— stopscripts/devnet-ibft4-status.sh— status (human/JSON/logs)
make upNormal start:
make upIf build/genesis.json already exists, startup now reuses it and prints:
Genesis already exists — skipping generation.
Reset chain:
make up RESET=1When RESET=1, startup wipes prior chain state (.data/) and removes build/genesis.json before regenerating genesis artifacts.
make up is idempotent when build/genesis.json is unchanged. If existing node data was initialized with a different genesis, startup fails fast with:
Genesis changed since this data dir was initialized.
Run: make up RESET=1
Reset and rebuild devnet state when genesis inputs change:
make up RESET=1Use the containerized devnet when you want a reproducible 4-node network with persistent named volumes:
docker compose up --buildRPC endpoints from host:
- node1:
http://localhost:8545 - node2:
http://localhost:8546 - node3:
http://localhost:8547 - node4:
http://localhost:8548
Quick check peer connectivity:
curl -s -X POST http://localhost:8545 \
-H "content-type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"net_peerCount\",\"params\":[]}"Reset the docker devnet (removes named volumes and chain state):
docker compose down -vYou can also use Make targets:
make docker-devnet-up
make docker-devnet-logs
make docker-devnet-down # stop containers
make docker-devnet-down RESET=1 # stop + remove volumesmake up RESET=1 CONSENSUS=pos| Variable | Default | Meaning |
|---|---|---|
CONSENSUS |
poa |
poa or pos |
RESET |
0 |
1 wipes .data/ibft4 |
INSECURE_SECRETS |
1 |
dev-only local key storage |
CHAIN_ID |
100 |
chain ID |
BLOCK_GAS_LIMIT |
15000000 |
target gas limit |
MIN_GAS_PRICE |
0 |
min gas price |
BASE_FEE_ENABLED |
false |
EIP-1559 style base fee toggle |
GENESIS_OUT |
build/genesis.json |
combined chain config + embedded genesis (used by devnet) |
CHAIN_OUT |
build/chain.json |
split chain config output (optional) |
GENESIS_ETH_OUT |
build/genesis-eth.json |
split ethereum genesis output (optional) |
make downOptions:
make downHuman mode:
make statusTail logs:
make logs
make logs-followJSON mode (CI-friendly):
JSON=1 ./scripts/devnet-ibft4-status.sh | jq .
JSON=1 LOGS=1 LOG_LINES=40 ./scripts/devnet-ibft4-status.sh | jq .The repository includes a tiny status dashboard backed by the existing qikchain CLI.
Start the UI (background/nohup):
make status-uimake status-ui now:
- Installs dependencies (
npm ciwhenpackage-lock.jsonexists, otherwisenpm install) - Runs the server in the background with
nohup - Writes process state to
.data/status-ui/status-ui.pid - Writes logs to
.data/status-ui/status-ui.log - Defaults to
STATUS_UI_HOST=0.0.0.0,STATUS_UI_PORT=8788, andRPC_URLS=http://127.0.0.1:8545,http://127.0.0.1:8546,http://127.0.0.1:8547,http://127.0.0.1:8548 - Calls
http://127.0.0.1:<STATUS_UI_PORT>/api/statusfor readiness checks - Is idempotent (if already running, it reports the existing process and exits)
Examples:
# custom RPC endpoints
RPC_URLS="http://127.0.0.1:8545,http://127.0.0.1:8546" make status-ui
# bind loopback only
STATUS_UI_HOST=127.0.0.1 STATUS_UI_PORT=8788 make status-ui
# hardened mode
READONLY_PROD=1 \
AUTH_USER=admin \
AUTH_PASS=strongpassword \
CACHE_MS=2000 \
STATUS_UI_HOST=127.0.0.1 \
STATUS_UI_PORT=8788 \
make status-uiStop and logs:
make stop-ui
make status-ui-logs
make status-ui-statusEnvironment overrides:
RPC_URLS(comma-separated list)RPC_URL(single-endpoint fallback whenRPC_URLSis unset)CACHE_MS(default:1000)READONLY_PROD(1enables hardening mode)AUTH_USERandAUTH_PASS(enable basic auth only when both are set)STATUS_UI_HOST(default:0.0.0.0)STATUS_UI_PORT(default:8788)RPC_TIMEOUT_MS(default:2000, per-RPC request timeout)
Security note:
- Binding to
0.0.0.0exposes the UI on all network interfaces. On shared/public hosts, protect access withAUTH_USER+AUTH_PASS, firewall/VPN rules, and considerREADONLY_PROD=1.
Status API highlights:
GET /api/statusreturns cached status with summary fields includingminBlockHead,maxBlockHead, andheadDivergence.DIVERGENCE_WARN(default:3) marks status degraded when at least 2 nodes are up and divergence exceeds the threshold.GET /healthzreuses the same cache and returns200when healthy, otherwise503.
Security and auth:
AUTH_USER+AUTH_PASSenables Basic Auth globally for/,/api/status,/healthz, and tx endpoints.READONLY_PROD=1hardens output and defaults host to127.0.0.1.
Transaction features (disabled by default):
- Tx API routes are available under
/api/tx/*and are always token-gated. - Server-side gate behavior:
READONLY=1→ tx routes return403 {"error":"readonly"}.- Missing
TX_TOKEN→ tx routes return503 {"error":"tx_disabled"}. - Wrong
X-TX-TOKENheader → tx routes return401 {"error":"unauthorized"}.
- UI config endpoint:
GET /api/configreturns{ readonly: boolean, txEnabled: boolean }.
Enable tx actions for dev/testing:
export TX_TOKEN=replace-with-strong-shared-secret
export TX_FROM_PRIVATE_KEY=0x<dev-only-private-key>
export RPC_URL=http://127.0.0.1:8545
# optional
export BURN_ADDRESS=0x000000000000000000000000000000000000dEaD
export CHAIN_ID=1101Run in safe readonly prod mode:
export READONLY=1
unset TX_TOKEN
unset TX_FROM_PRIVATE_KEYTX_FROM_PRIVATE_KEY is for dev-only flows. Never use funded mainnet/private production keys.
Safety limits:
- JSON request body is limited to 16kb.
TX_RATE_LIMIT_PER_MIN(default:10) per IP across tx endpoints.RAW_TX_MAX_BYTES(default:8192) for/api/tx/send-raw.DEPLOY_GAS_CAP(default:2000000) for test deploy.
Write endpoints:
POST /api/tx/send-weisends 1 wei to burn address.POST /api/tx/deploy-test-contractdeploys an embedded minimal test contract bytecode.POST /api/tx/send-rawforwards a pre-signed raw transaction.
Examples:
Send 1 wei:
curl -u user:pass -H "X-TX-TOKEN: $TX_TOKEN" -H "content-type: application/json" -d '{"rpcUrl":"http://127.0.0.1:8545"}' http://127.0.0.1:8788/api/tx/send-weiDeploy test contract:
curl -u user:pass -H "X-TX-TOKEN: $TX_TOKEN" -H "content-type: application/json" -d '{"rpcUrl":"http://127.0.0.1:8545"}' http://127.0.0.1:8788/api/tx/deploy-test-contractSubmit raw transaction:
curl -u user:pass -H "X-TX-TOKEN: $TX_TOKEN" -H "content-type: application/json" -d '{"rawTxHex":"0x...","rpcUrl":"http://127.0.0.1:8545"}' http://127.0.0.1:8788/api/tx/send-raw
⚠️ Dev/test environments only. Disabled by default.
TX Lab adds a guarded transaction testing backend + UI at / (status UI) with tabs for Accounts, Scenarios, Runner, Live Monitor, and Results.
TX_LAB_ENABLE=1is required to enable any TX Lab API/UX.- Raw private-key loading is blocked unless
TX_LAB_INSECURE_KEYS=1. - All mutating endpoints require
X-TX-LAB-TOKEN. - Default bind is loopback (
TX_LAB_HOST=127.0.0.1). - Private keys are never returned by API responses.
TX_LAB_ENABLE=0
TX_LAB_INSECURE_KEYS=0
TX_LAB_HOST=127.0.0.1
TX_LAB_PORT=8799
TX_LAB_TOKEN=
TX_LAB_RPC_URL=http://127.0.0.1:8545
TX_LAB_DB_PATH=.data/txlab/txlab-runs.jsonl
TX_LAB_ACCOUNTS_FILE=.data/txlab/accounts.json
TX_LAB_MAX_CONCURRENCY=100
TX_LAB_MAX_TX_PER_RUN=10000{
"chainId": 100,
"accounts": [
{ "label": "sender-01", "privateKey": "0xabc...", "address": "0x..." },
{ "label": "receiver-01", "privateKey": "0xdef..." }
]
}If address is omitted, TX Lab derives it from the private key and validates key/address pairing.
{
"name": "burst-native-transfers",
"mode": "native-transfer",
"txType": "legacy",
"senderSelection": ["sender-01", "sender-02"],
"receiverSelection": ["receiver-01", "receiver-02", "receiver-03"],
"valueWei": "1000000000000000",
"txCount": 500,
"concurrency": 25,
"rateLimitTps": 50,
"waitMode": "wait-receipt",
"timeoutSeconds": 30,
"randomizeReceivers": true
}Supported modes (MVP):
native-transferraw-tx(rawTxHex)contract-deploy(bytecode)contract-call(contractAddress+data, orabi+method+args)
# starts status-ui server with TX Lab enabled, on TX_LAB_PORT
make tx-lab-up
make tx-lab-status
make tx-lab-logs
make tx-lab-stopRead-only:
GET /api/txlab/healthGET /api/txlab/configGET /api/txlab/accountsPOST /api/txlab/accounts/refreshGET /api/txlab/scenariosGET /api/txlab/runsGET /api/txlab/runs/:idGET /api/txlab/runs/:id/results
Mutating (X-TX-LAB-TOKEN required):
POST /api/txlab/accounts/loadPOST /api/txlab/accounts/groupPOST /api/txlab/scenariosPOST /api/txlab/runs/startPOST /api/txlab/runs/:id/stop
export TX_LAB_TOKEN=dev-only-strong-token
# load accounts from local file
curl -H "X-TX-LAB-TOKEN: $TX_LAB_TOKEN" -H "content-type: application/json" \
-d '{"path":".data/txlab/accounts.json"}' \
http://127.0.0.1:8799/api/txlab/accounts/load
# list account summaries
curl http://127.0.0.1:8799/api/txlab/accounts
# save scenario
curl -H "X-TX-LAB-TOKEN: $TX_LAB_TOKEN" -H "content-type: application/json" \
-d @scenario.json \
http://127.0.0.1:8799/api/txlab/scenarios
# start run
curl -H "X-TX-LAB-TOKEN: $TX_LAB_TOKEN" -H "content-type: application/json" \
-d '{"scenarioName":"burst-native-transfers"}' \
http://127.0.0.1:8799/api/txlab/runs/start
# stop run
curl -H "X-TX-LAB-TOKEN: $TX_LAB_TOKEN" -H "content-type: application/json" \
-d '{}' \
http://127.0.0.1:8799/api/txlab/runs/<run-id>/stop
# fetch results
curl http://127.0.0.1:8799/api/txlab/runs/<run-id>/results- Nonces are reserved per sender during parallel runs to reduce collision risk.
- Runs support fixed-count bursts and optional TPS limiting.
- Run summaries and per-tx outcomes persist in
.data/txlab/runs/*.jsonand summary JSONL (TX_LAB_DB_PATH). - Error classes include nonce, underpriced replacement, insufficient funds, intrinsic gas, execution reverted, invalid sender/raw tx, timeout, rpc/network, unknown.
tx_lab_disabled: setTX_LAB_ENABLE=1.unauthorized: setTX_LAB_TOKENand sendX-TX-LAB-TOKEN.insecure_keys_disabled: setTX_LAB_INSECURE_KEYS=1for local-only key loading.- account file missing: verify
TX_LAB_ACCOUNTS_FILE/ request path. - invalid private key mismatch: fix address/private key pairing.
insufficient_funds: fund senders.nonce_too_low: refresh account nonces / avoid stale concurrent runs.- RPC errors: verify
TX_LAB_RPC_URLand node availability.
This Polygon Edge build exposes metrics via:
--prometheus <addr:port>
Not --metrics.
Devnet ports:
- node1: http://127.0.0.1:9090/metrics
- node2: http://127.0.0.1:9091/metrics
- node3: http://127.0.0.1:9092/metrics
- node4: http://127.0.0.1:9093/metrics
Use the standalone faucet and wallet CLI helpers to speed up devnet testing.
The faucet listens on its own port (
8787by default), separate from the status UI (8788default).
Defaults used by the faucet workflow:
FAUCET_HOST=0.0.0.0FAUCET_PORT=8787FAUCET_RPC_URL=http://127.0.0.1:8545FAUCET_AMOUNT_WEI=100000000000000000(0.1 ETH)FAUCET_TOKEN=devtoken-change-me
For safety, override FAUCET_TOKEN instead of using the default value.
Start chain (existing flow):
make upFirst-time faucet setup:
make faucet-init
# edit .env.faucet and set FAUCET_PRIVATE_KEY + FAUCET_TOKEN
make faucet-upOpen the faucet UI:
make faucet-uiThen open one of the printed URLs in your browser (for example http://127.0.0.1:8787/).
UI notes:
- Put
FAUCET_TOKENin.env.faucet, start faucet, then paste the token into the UI. - The token is required on every request (
X-FAUCET-TOKEN) and is stored in browserlocalStorageasqik_faucet_token. - Click Connect MetaMask to auto-fill the destination address.
- You can create additional recipient addresses inside MetaMask and paste/select them in the UI.
Common faucet commands:
make faucet-status
make faucet-url
make faucet-logs
make faucet-stop
make faucet-restartCreate wallet:
make wallet-newFund wallet:
make faucet-send TO=0x...Check balance:
make wallet-balance ADDRESS=0x...Send transaction:
make wallet-send FROM_PK=... TO=0x... VALUE_WEI=1Prefunding note:
If faucet tx fails with insufficient funds, prefund the faucet address in genesis or transfer funds to it after chain start.
Additional helper:
make wallet-new OUT=.secrets/another-wallet.json
Run the dedicated CI healthcheck locally:
bash scripts/ci/healthcheck-devnet.shWhat it validates:
- Starts a fresh PoA IBFT devnet (
INSECURE_SECRETS=1 RESET=1 CONSENSUS=poa) - Waits for node1 RPC readiness
- Polls
JSON=1 ./scripts/devnet-ibft4-status.shuntil:.ok == true.sealing == truenode1.rpc.peerCountHex >= 0x1when available
- Prints diagnostics on failure (status JSON, node1 log tail, listener ports)
- Always stops devnet on exit (success or failure)
Use make test-tx to run end-to-end transaction-path checks against devnet.
The tx suite (scripts/tests/tx_integration.sh) validates:
- send 1 wei to burn address,
- deploy embedded test contract bytecode,
- sign and submit a raw transaction.
Run locally:
make test-txOptional environment variables:
RPC_URL(defaulthttp://127.0.0.1:8545)SENDER_PRIVATE_KEY(preferred key source)FAUCET_PRIVATE_KEY(fallback key source)CI_TX=1(include tx suite insidemake test/ CI entrypoint)TX_HTTP_URL(optional tx HTTP endpoint, e.g.http://127.0.0.1:8788/api/tx/send-wei)TX_TOKEN(required whenTX_HTTP_URLis set)GAS_MULTIPLIER(default1.2)
Skip/fail behavior:
- If no sender key can be resolved (
SENDER_PRIVATE_KEY,FAUCET_PRIVATE_KEY, or dev-only key discovery under.data), tx tests printtx tests skipped: no keyand exit0. - If
TX_HTTP_URLis explicitly set butTX_TOKENis missing, tx tests fail with a clear error. - If tx HTTP mode is not enabled/available, the suite falls back to raw JSON-RPC.
CI notes:
scripts/ci/run.shruns tx tests only whenCI_TX=1.- GitHub Actions sets
CI_TX=1and passes optional secrets:SENDER_PRIVATE_KEYorFAUCET_PRIVATE_KEYTX_HTTP_URLandTX_TOKEN(only needed for UI tx endpoint mode)
Your Edge version uses --prometheus. Ensure your startup script passes --prometheus instead.
Your Edge build expects an embedded genesis object in the file passed to --chain.
Use the combined output:
./bin/qikchain genesis build --out-combined build/genesis.json
./bin/polygon-edge server --chain build/genesis.json ...Use split outputs (build/chain.json + build/genesis-eth.json) only for alternate tooling/builds that require a string genesis path.
This usually means you’re running a stale ./bin/qikchain binary with legacy validation rules.
Rebuild:
go build -o ./bin/qikchain ./cmd/qikchain- Check node logs for validator/IBFT messages:
LOGS=1 ./scripts/devnet-ibft4-status.sh
- Confirm block number advances on node1:
curl -s -X POST http://127.0.0.1:8545 -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' | jq .
- Ensure validators are configured correctly for PoA.
Stop the devnet:
make downIf something is still holding ports:
CLEAN_PORTS=1 FORCE=1 ./scripts/devnet-ibft4-stop.shbin/
qikchain
polygon-edge
build/
genesis.json
chain.json
genesis-eth.json
deployments/ # PoS deployments (devnet)
config/
token.json
allocations/
devnet.json
staging.json
mainnet.json
consensus/
poa.json
pos.json
genesis.template.json
scripts/
devnet-ibft4.sh
devnet-ibft4-stop.sh
devnet-ibft4-status.sh
.data/
ibft4/
node1/
node2/
node3/
node4/
See ROADMAP.md:
- Phase 0: IBFT PoA devnets (current)
- Phase 1: IBFT PoS devnet (same scripts; different overlay + staking management)
- Phase 2: Operator UX (validator onboarding, key management, metrics)
- Phase 3: Production hardening (key backend, snapshots/backups, monitoring/upgrades)
TBD
Milestone 1 adds PoS staking state/contracts/tooling for devnet workflows. It does not change runtime consensus behavior yet.
Deploy contracts:
make pos-deploy RPC_URL=http://127.0.0.1:8545 PRIVATE_KEY=0x...Mint devnet QIK:
make pos-mint TO=0x... AMOUNT=100000000000000000000000 PRIVATE_KEY=0x...Register validator metadata:
make pos-register OPERATOR_PK=0x... MONIKER=validator-1 ENDPOINT=http://127.0.0.1:30303 NODE_ID_HEX=0x1234 BLS_PUBKEY_HEX=0xabcdStake tokens:
make pos-stake OPERATOR_PK=0x... AMOUNT=1000000000000000000000Snapshot active set:
make pos-snapshot EPOCH=1 OPERATORS=0xabc...,0xdef... OWNER_PK=0x...Query deployment + staking info:
make pos-info- Snapshot submission is owner-driven in this milestone (placeholder for future deterministic derivation from stake state).
- Keep private keys in env vars or secret files; avoid shell history leaks.
- Use
docs/pos/erc20-pos-skeleton.mdfor the current on-chain interface/state spec.