PQ-Agile Chain is an account-based blockchain demonstrator for studying post-quantum key rotation at the ledger layer. It uses pqcrypto for signature generation and verification, then makes key migration part of chain validation rather than a wallet-side convention.
The implementation keeps account_id stable across key changes. The active algo_id, public_key, security_floor, nonce, and balance are state variables derived by replaying the chain from genesis.
0.4.0 adds a split explorer UI, a SQLite-backed read model rebuilt from the canonical ledger, atomic persistence for chain and wallet files, and explicit wallet inspection and rewrap commands.
- account-based state, not UTXO
- deterministic transaction serialization
- replay-based validation from genesis on every load
genesis_allocation,transfer, androtate_keytransactions- fixed-difficulty proof-of-work for block creation
- CLI commands for encrypted wallet generation, wallet inspection, wallet rewrap, transfers, mining, and key rotation
- atomic writes for
chain.jsonand wallet files - SQLite read index for blocks, transactions, accounts, and wallet metadata
- split FastAPI explorer UI and API behind
jrti.org/qc
The narrow question this repository answers is: how can a ledger accept post-quantum key replacement without treating the account itself as disposable?
The answer used here is straightforward:
- identity is a stable
account_id - signing material is replaceable state
- rotation is a first-class transaction type
- old and new keys both participate in authorization
- downgrade prevention is handled by a chain rule, not by operator discipline
This repository does not propose a new signature scheme or a new consensus protocol. It is a concrete reference implementation of one ledger-level rotation model.
Each account stores:
account_idlabelalgo_idpublic_keysecurity_floornoncebalance
genesis_allocation
- valid only in block
0 - creates an initial account state and balance
transfer
- debits one existing account and credits another
- carries the sender's current
algo_id,public_key,nonce, and signature
rotate_key
- keeps
account_idunchanged - replaces the active algorithm and public key for that account
- can optionally raise
security_floor - requires authorization from both the current key and the replacement key
Each block stores:
indexprevious_hashtimestampdifficultynoncetransactionsblock_hash
block_hash is recomputed from the block header payload during replay and must satisfy the configured proof-of-work prefix rule.
Chain validation is replay-based. Loading a chain replays every block from genesis and recomputes account state.
transfer is accepted only if all of the following hold:
- the sender account exists
- the recipient account exists
amount > 0nonce == sender.nonce + 1- the presented
algo_idmatches the sender's active on-chain algorithm - the presented
public_keymatches the sender's active on-chain key - the signature verifies over the canonical transfer payload
- the sender has sufficient balance
rotate_key is accepted only if all of the following hold:
- the account exists
nonce == account.nonce + 1- the old algorithm and public key match the active on-chain key
- the old key signs the rotation authorization payload
- the new key signs a separate possession proof
- the rotation changes key material
- the new backend satisfies the current
security_floor - the requested floor does not lower the existing floor
Once a rotation is mined, the previous key is no longer valid for future transfers.
More detail: docs/novelty.md
This repository does not implement its own post-quantum signature primitive. It currently wraps:
pqcrypto.sign.ml_dsa_65pqcrypto.sign.ml_dsa_87pqcrypto.sign.falcon_512pqcrypto.sign.falcon_1024pqcrypto.sign.sphincs_shake_256s_simple
The chain refers to those backends through the local ids:
ml-dsa-65ml-dsa-87falcon-512falcon-1024sphincs-shake-256s-simple
security_floor is a repository-local policy value assigned in src/pq_agile_chain/crypto_backends.py. In this codebase it is used to block configured downgrade paths. It should be read as an application rule for this demonstrator, not as a stand-alone cryptographic claim beyond the backend mapping configured here.
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'
export PQ_AGILE_CHAIN_WALLET_PASSWORD='change-me-if-you-want-encrypted-wallets'
.venv/bin/pq-agile-chain demo --workdir demo-output
.venv/bin/pq-agile-chain-webThe built-in demo performs the following sequence:
- create two
ml-dsa-65wallets - initialize a genesis block
- submit and mine a transfer
- rotate one account to
sphincs-shake-256s-simple - mine the rotation
- verify that the old key is rejected
- verify that a downgrade below
security_flooris rejected
Running the web app against that workspace creates demo-output/read-model.sqlite3, a rebuildable query index used by the explorer.
Create wallets:
export PQ_AGILE_CHAIN_WALLET_PASSWORD='change-me-if-you-want-encrypted-wallets'
.venv/bin/pq-agile-chain create-wallet --output wallets/alice.json --label alice --algo ml-dsa-65 --security-floor 3 --password-env PQ_AGILE_CHAIN_WALLET_PASSWORD
.venv/bin/pq-agile-chain create-wallet --output wallets/bob.json --label bob --algo ml-dsa-65 --security-floor 3 --password-env PQ_AGILE_CHAIN_WALLET_PASSWORDInspect or rewrite wallet storage:
.venv/bin/pq-agile-chain wallet-info --wallet wallets/alice.json
.venv/bin/pq-agile-chain rewrap-wallet --wallet wallets/alice.json --new-password-env PQ_AGILE_CHAIN_WALLET_PASSWORDInitialize genesis:
.venv/bin/pq-agile-chain init --chain chain.json --difficulty 2 --allocation wallets/alice.json=120 --allocation wallets/bob.json=25Queue a transfer and mine it:
.venv/bin/pq-agile-chain transfer --chain chain.json --wallet wallets/alice.json --wallet-password-env PQ_AGILE_CHAIN_WALLET_PASSWORD --to-wallet wallets/bob.json --amount 15
.venv/bin/pq-agile-chain mine --chain chain.jsonRotate Alice to a different backend and mine the rotation:
.venv/bin/pq-agile-chain rotate-key --chain chain.json --wallet wallets/alice.json --wallet-password-env PQ_AGILE_CHAIN_WALLET_PASSWORD --new-wallet-out wallets/alice-rotated.json --new-wallet-password-env PQ_AGILE_CHAIN_WALLET_PASSWORD --new-algo sphincs-shake-256s-simple --new-security-floor 5
.venv/bin/pq-agile-chain mine --chain chain.jsonReplay and validate the chain:
.venv/bin/pq-agile-chain validate --chain chain.jsonRun the web app locally:
.venv/bin/pq-agile-chain-webBy default the service listens on 127.0.0.1:8401.
The browser explorer in 0.4.0 is split into packaged assets under src/pq_agile_chain/templates/ and src/pq_agile_chain/static/. The service keeps chain.json authoritative and rebuilds read-model.sqlite3 from replay whenever chain or wallet sources change.
API surface:
GET /api/health: liveness checkGET /api/state: current chain summary, wallet list, account list, and mempool snapshotGET /api/blocks: paginated block summariesGET /api/blocks/{index}: block payload and transaction detail for one blockGET /api/transactions: paginated transaction summaries, withstatusandkindfiltersPOST /api/demo/bootstrap: reset the local workspace and create a fresh demo chainPOST /api/transfer: enqueue a signed transferPOST /api/rotate: enqueue a key rotation and create the replacement wallet filePOST /api/mine: mine the current mempool into the next block
If PQ_AGILE_CHAIN_ADMIN_TOKEN is set, the write endpoints require X-Admin-Token or Authorization: Bearer .... The bundled browser explorer includes a session-local token field and forwards that value on write requests.
If PQ_AGILE_CHAIN_WALLET_PASSWORD is set, demo and workspace wallets are stored encrypted at rest and unlocked inside the service for signing operations.
The deployed explorer runs at jrti.org/qc. In the current deployment that path may also be protected at the nginx layer, while the application independently enforces write-token checks.
Run tests:
.venv/bin/pytest -qThe test suite currently covers:
- valid transfer replay
- forged signature rejection
- nonce reuse rejection
- stale-key rejection after rotation
security_floordowngrade rejection- repeated web bootstrap of the same workspace
- encrypted wallet round-trip and signing
- expanded PQ backend smoke coverage
- write-endpoint token enforcement
- block-detail and filtered transaction API responses
- SQLite index rebuild after manual ledger changes
- recovery from a corrupt read-index file
- CLI wallet inspection and rewrap flows
- invalid chain JSON reported as a structured CLI error
The repository includes an Ubuntu plus systemd plus nginx deployment scaffold for jrti.org/qc:
deploy/systemd/pq-agile-chain.servicedeploy/nginx/jrti.org-qc.confdeploy/README.md
The intended shape is:
uvicornbinds to127.0.0.1:8401- the existing
jrti.orgnginx vhost includes the/qcsnippet - nginx proxies
/qc/to the local FastAPI process - nginx can restrict network access, and the application can separately require
PQ_AGILE_CHAIN_ADMIN_TOKENfor write operations systemdreads operator settings from an env file such as/etc/pq-agile-chain/pq-agile-chain.env
src/pq_agile_chain/crypto_backends.py: backend registry and signing adapterssrc/pq_agile_chain/models.py: wallet, transaction, block, and state dataclassessrc/pq_agile_chain/chain.py: replay engine and validation rulessrc/pq_agile_chain/mining.py: proof-of-work loop and block hashingsrc/pq_agile_chain/cli.py: command-line interfacesrc/pq_agile_chain/service.py: filesystem-backed workspace used by the web APIsrc/pq_agile_chain/read_index.py: rebuildable SQLite query model for explorer readssrc/pq_agile_chain/web_app.py: FastAPI app factory and routessrc/pq_agile_chain/web.py: thin ASGI and CLI entrypointsrc/pq_agile_chain/templates/index.html: packaged explorer shellsrc/pq_agile_chain/static/app.cssandsrc/pq_agile_chain/static/app.js: packaged explorer assetstests/test_chain.py: chain-level regression teststests/test_web.py: API explorer and workspace teststests/test_index.py: SQLite read-index rebuild and recovery teststests/test_cli.py: wallet-inspection and migration CLI testsdocs/novelty.md: technical note on the rotation model
- This is a single-node local state machine. It does not implement peer discovery, block propagation, fork choice between competing nodes, or Byzantine consensus.
- Proof-of-work is intentionally minimal: fixed difficulty, no retargeting, no timestamp discipline, no miner rewards, and no economic security model.
- Wallet files can now be encrypted at rest with
scryptplus AES-GCM and can be inspected or rewrapped from the CLI, but there is still no mnemonic format, HSM integration, or hardware wallet path. security_flooris a local policy integer attached to configured backends in this repository. It is useful for downgrade prevention inside this demo, but it is not a substitute for a formal external security evaluation.- The backend registry is still small and policy-driven. Adding more PQ schemes still requires code changes and explicit decisions about how local
security_floorvalues are assigned. chain.jsonremains the canonical ledger, whileread-model.sqlite3is only a rebuildable local read model. There is still no distributed database, no pruning strategy for long histories, and no replication layer.- The write API mutates local filesystem state. Application-level token gating is supported now, but a public deployment should still pair it with network-level controls such as the current nginx restrictions.
- The explorer is an operational view over one workspace, not a multi-tenant block explorer for arbitrary chains.
MIT