An independent, EVM-compatible Layer-1 blockchain with its own native currency (GIG), validator network, block explorer and faucet. A complete, working chain you can run on one machine in about forty seconds — and rename into your own.
npm run stack # or double-click run.bat on Windows| Consensus | QBFT (Byzantine fault tolerant, immediate finality, no reorgs) |
| Client | Hyperledger Besu 26.8.1 |
| Block time | 2 seconds under load, 300s idle |
| Native currency | GIG (18 decimals) |
| Total supply | 1,000,000,000 GIG |
| Devnet chain ID | 4043 |
| Licence | MIT |
This repository was "vibe coded". It was built by an AI agent (Claude Code) working from a written brief, across a single extended session. Every line of it — the consensus config, the indexer, the API, the websites, the tests and this sentence — was written that way. The brief that started it is in the git history.
That is a statement of fact, not a disclaimer, and it cuts both ways:
- What it means in practice. The chain genuinely runs. Failure modes were induced rather
than reasoned about: quorum loss was tested by killing validators under load, crash recovery
by
SIGKILL-ing the indexer mid-write, the faucet's accounting by draining it. A later adversarial security audit found and fixed 22 confirmed defects, several of them real (a genesis generator that would have funded a public network with keys from a tutorial; an indexer heartbeat that permanently skipped block 0). The commit messages record what was tried and what was wrong, because that is where the reasoning lives. - What it does not mean. It has not been reviewed by an independent engineer. It has never run anywhere but one desktop. No third party has audited the contracts or the consensus configuration. Treat it as a well-tested starting point, not as production infrastructure.
Nothing here is launched. The devnet is real and running. Mainnet (chain ID 4041) has never been started, GIG has no monetary value, none of it is for sale, and the faucet hands out valueless test tokens. Before any of that changes, read HANDOVER.md — three decisions are open and one of them is legal.
The chain's identity lives in one file: blockchain/config/chain.config.json. Name,
ticker, description, chain IDs, supply, block time. Everything else is generated from it, and
the websites read it at build time — nothing is hardcoded.
node scripts/rebrand.mjs --list --name "Aurora" --symbol AURnode scripts/rebrand.mjs --name "Aurora" --symbol AUR --description "One paragraph about your chain."Then bash scripts/create-genesis.sh --force for a fresh genesis and your own validator keys,
and npm run stack. The script refuses to run on a dirty working tree, so git diff shows
exactly what changed and git checkout . undoes it.
Pick your own chain IDs (--chain-id, --testnet-id, --devnet-id). They are meant to be
globally unique — check chainlist.org first. A collision means a
transaction signed for one chain is replayable on the other.
This project is complete and paused, not abandoned. Before touching anything:
| Read | For |
|---|---|
| RUN.md | Starting, pausing and stopping it — the day-to-day commands |
run.bat / stop.bat |
Windows: double-click to start or stop the whole stack |
| HANDOVER.md | What state this is in, what is unfinished, the decisions still open |
| DEPLOY.md | Running it — local, measured hardware specs, production, incidents |
| CLAUDE.md | Architecture, conventions, and the 31 gotchas that will otherwise cost you a day |
| giggora-dossier.html | All three in one offline, printable page (npm run docs rebuilds it) |
- Docker Desktop, running
- Node.js 24 or newer (the indexer, API and faucet run TypeScript directly)
- Bash (Git Bash on Windows)
make is optional — every target has a script equivalent.
npm install
bash scripts/create-genesis.sh
bash scripts/start-network.sh
node scripts/verify-network.mjsThat generates a genesis and four validator keys, starts four validators plus one RPC node, and runs the acceptance test against the live chain.
To tear everything down and start clean:
docker compose down -v && rm -rf blockchain/nodes blockchain/genesis/networkFiles| Field | Value |
|---|---|
| Network name | Giggora Devnet |
| RPC URL | http://localhost:8545 |
| Chain ID | 4043 |
| Currency symbol | GIG |
| Block explorer | http://localhost:3000 (see below) |
Devnet test accounts. These private keys are published by Foundry/Anvil and Besu. They are public knowledge and hold no real value. Never use them on testnet or mainnet.
| Account | Address | Private key | Balance |
|---|---|---|---|
| Anvil #0 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 |
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 |
50,000,000 GIG |
| Anvil #1 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 |
0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d |
25,000,000 GIG |
| Anvil #2 | 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC |
0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a |
25,000,000 GIG |
Treasury, ecosystem, and team allocations use the standard Besu dev accounts — see
blockchain/config/chain.config.json.
The chain alone needs nothing else. The explorer is three more processes — indexer, API, UI — and there are two ways to run them.
Containers (one command):
npm run stackThat is docker compose --profile explorer up -d --build: chain, indexer, API
and UI together, on http://localhost:3000 with the API on 4100. It builds the
same images deploy/explorer/ uses, so the devnet exercises what a real
deployment would ship.
They sit behind a compose profile rather than running by default: the two
images cost ~800 MB, and on Docker Desktop that lands in a virtual disk that
never shrinks. Someone who only wants a chain to point MetaMask at should not
pay for an explorer they did not ask for. Plain docker compose up -d stays
chain-only.
On the host (for editing the code):
node indexer/src/index.tsnode explorer-api/src/server.tsnpm run web:devRun one way or the other, never both — they bind the same ports.
If the explorer shows old numbers, the indexer is not running. The page says so itself: it carries a banner reading "This page may be out of date" whenever the indexer has not checked in for 30 seconds. It is reading a database, not the chain, and it will not pretend otherwise.
blockchain/config/chain.config.json is the single source of truth. Everything else is
generated from it:
chain.config.json
|
| scripts/gen-config.mjs
v
qbftConfigFile.json + .env
|
| besu operator generate-blockchain-config
v
genesis.json + validator keys
|
v
docker-compose.yml -> running network
Never hand-edit .env, qbftConfigFile.json, or genesis.json. Change chain.config.json and
re-run bash scripts/create-genesis.sh.
The generator verifies that allocations sum to the declared total supply and refuses to emit a genesis if they do not.
| Script | npm | make | Does |
|---|---|---|---|
node scripts/gen-config.mjs |
npm run config |
make config |
Regenerate .env + qbftConfigFile.json |
bash scripts/create-genesis.sh |
npm run genesis |
make genesis |
Full genesis + validator keys |
bash scripts/start-network.sh |
npm start |
make start |
Start the devnet |
| — | npm stop |
make stop |
Stop, keep chain data |
node scripts/verify-network.mjs |
npm run verify |
make test |
Acceptance test |
| — | npm run clean |
make clean |
Delete all chain data and keys |
bash scripts/setup-contracts.sh |
npm run setup:contracts |
make setup-contracts |
Install OpenZeppelin + forge-std |
bash scripts/forge.sh build |
npm run contracts:build |
make contracts |
Compile contracts |
bash scripts/forge.sh test |
npm run contracts:test |
make contracts-test |
Foundry test suite |
node scripts/deploy-contracts.mjs |
npm run contracts:deploy |
make deploy-contract |
Deploy + verify on chain |
| — | npm run stack |
make stack |
Everything: chain + indexer + API + explorer UI |
| — | npm run stack:down |
make stack-down |
Stop the whole stack |
| — | npm run stack:logs |
make stack-logs |
Follow explorer logs |
MetaMask / DApps Ports
| -----
v 8545 RPC HTTP (public)
giggora-rpc ......................8546 RPC WS (public)
| (no validator key)
| p2p
+----+----+----+----+
| | | | | 8551-8554 validator debug RPC
val-1 ... val-4 (bound to 127.0.0.1)
(hold signing keys, never public) 30303 p2p (internal only)
Validators are never publicly reachable. The RPC node holds no validator key, and the ADMIN,
DEBUG, and MINER RPC namespaces are disabled everywhere.
Every item here was hit and fixed during Phase 2. They are all load-bearing — if you "simplify" one away, the chain breaks in a way that is genuinely hard to diagnose.
1. Cancun works, but ONLY because the beacon-roots contract is pre-deployed.
Giggora runs Cancun. On a non-PoS chain that normally fails: Cancun system-calls the EIP-4788
beacon-roots contract every block, and if that address is empty Besu logs
Invalid system call address forever. The genesis therefore pre-deploys the contract at
0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02, which is the documented fix for private networks.
gen-config.mjs refuses to build a Cancun genesis without it.
Cancun is not a luxury here — OpenZeppelin 5.6 emits mcopy, a Cancun opcode, so a
Shanghai-only chain cannot compile current OpenZeppelin at all. Contracts must match the chain:
evm_version = "cancun"2. fixedBaseFee: true is mandatory, and it is subtle.
Without it the EIP-1559 base fee decays ~12.5% per empty block. On an idle chain it drops below
min-gas-price, and because eth_maxPriorityFeePerGas returns 0 here, wallets estimate a
maxFeePerGas under the node's floor. Transactions are then accepted into the pool and
silently never mined — MetaMask just spins forever. fixedBaseFee pins the base fee at
baseFeePerGas, so baseFee == minGasPrice and estimates always clear the floor.
zeroBaseFee would also "fix" it, but makes gas free and the chain trivially spammable.
gen-config.mjs refuses to build a genesis where the two values disagree.
3. Enode URLs need IP addresses, not hostnames.
enode://…@giggora-validator-1:30303 is rejected with Invalid ip address, and every node
using that bootnode crash-loops. Hence the static IPs in docker-compose.yml.
4. Set --p2p-host explicitly, with --nat-method=NONE.
Left alone, Besu's Docker NAT manager advertises enode://…@0.0.0.0:30303. Peers cannot dial
back, and the bootnode ends up with 0 peers while everything stalls.
5. Peer discovery alone does not form a mesh here — static-nodes.json does.
With discovery only, every node connects to the bootnode and to nobody else. QBFT validators
cannot exchange prepare/commit messages and the chain sticks at block 1.
create-genesis.sh writes a static-nodes.json into each node's data directory listing every
other node (never itself — self-references are rejected).
6. An idle chain looks broken but isn't.
emptyblockperiodseconds is 60, so with no transactions you get one block a minute. The 2-second
block period applies under load. Do not conclude the chain is halted from a 10-second sample —
send a transaction instead. Both verify-network.mjs checks are written this way.
7. Besu's generate-blockchain-config exits 1 on success.
It reports Output directory already exists against a genuinely absent directory while writing
correct output. create-genesis.sh therefore validates the artifacts (genesis chain ID, extraData,
key count) instead of trusting the exit code — and still fails hard on any other error.
8. Clique is dead — do not follow older tutorials.
Clique PoA has been deprecated in go-ethereum since v1.14 and was removed outright in Besu
26.4.0. puppeth no longer exists. Any guide using either is EOL. Giggora uses QBFT.
Sample ERC-20 / ERC-721 / ERC-1155 contracts live in contracts/, built with
Foundry (run via Docker — no local install needed) and OpenZeppelin 5.6.1.
bash scripts/setup-contracts.sh # OpenZeppelin + forge-std (pinned)
bash scripts/forge.sh test # 28 unit tests
node scripts/deploy-contracts.mjs # deploy to devnet + verify on-chain logs| Contract | Standard | Symbol | Notes |
|---|---|---|---|
GigToken |
ERC-20 | GTT | Burnable, owner-mintable, 1,000,000 initial supply |
GigNFT |
ERC-721 | GIGNFT | Enumerable + URI storage, so the explorer can list collections |
GigMultiToken |
ERC-1155 | GIGMT | Supply-tracking, plus non-standard name/symbol for token detection |
Deployed addresses are recorded in deployments/<network>.json.
The deploy script does more than deploy: it reads the emitted logs back off the chain and asserts
their exact shape — ERC-20 Transfer has 3 topics with the value in data, ERC-721 Transfer
has 4 topics with empty data. That distinction is exactly what the Phase 4 indexer will use to
tell the two standards apart.
The indexer reads blocks, transactions, receipts and logs from the RPC node into PostgreSQL, decoding ERC-20/721/1155 transfers as it goes.
docker compose up -d postgres # start the database
bash scripts/db-migrate.sh # apply schema
node indexer/src/index.ts --once # catch up to the chain head, then exit
node indexer/src/index.ts # follow the head continuously
node indexer/src/index.ts --status # how far behind are we?Written in TypeScript and run directly by Node 24's native type stripping — no build step.
What makes it crash-safe. A block's rows and the checkpoint advance inside a single
database transaction, so last_processed_block can never be ahead of the data it
describes. A kill -9 at any instant rolls back cleanly and the next start resumes
exactly where it stopped. Every insert is ON CONFLICT DO NOTHING, so re-indexing a
range is a no-op rather than a duplicate-key crash.
There is deliberately no reorg handling. QBFT has absolute finality, so a committed block can never be replaced. This removes what is normally the hardest part of writing an indexer. If Giggora ever moves to probabilistic finality this assumption breaks, and blocks would need a canonical/orphaned flag.
node scripts/verify-indexer.ts # compare the database against the RPC node
node scripts/test-indexer-recovery.ts # SIGKILL mid-write, verify clean recovery
node scripts/crosscheck-blockscout.ts # diff against an independent implementationThree layers, deliberately: the RPC node is canonical truth, the recovery test proves durability, and Blockscout catches the kind of error a single implementation would make consistently in both its writer and its reader.
Blockscout runs alongside as an independent cross-check, in its own database:
docker compose -f docker-compose.yml -f docker-compose.blockscout.yml up -d blockscoutIts API is then at http://localhost:4000/api/v2/blocks.
Two settings are non-obvious and were both required to make it index at all:
ETHEREUM_JSONRPC_WS_URLis required even withETHEREUM_JSONRPC_TRANSPORT: http. Without it the realtime fetcher never establishes a chain head, andblock_catchuplogsIndex already caught upwith a null range forever — it looks perfectly healthy while indexing nothing at all.- Besu needs the
TXPOOLnamespace enabled. Otherwise Blockscout's pending-transaction fetcher getsMethod not enabled, interprets it as the whole node being down, and flaps between fallback URLs.
For ERC-1155 batch transfers the two indexers disagree by design:
| Giggora | Blockscout | |
|---|---|---|
| Rows per batch log | one per token id | one per log |
| Ids / amounts | separate rows, keyed by batch_index |
token_ids / amounts arrays |
Ours is normalised so "every transfer of token id X" is an indexed lookup instead of an
array scan. Neither is wrong, so crosscheck-blockscout.ts compares at log granularity.
This surfaced as a real off-by-one before it was understood — the cross-check earning its
keep on its first run.
Read-only HTTP API over the indexed data. Fastify, run directly by Node 24 — no build step.
node explorer-api/src/server.ts # http://localhost:4100
node scripts/test-api.ts # 51 contract tests against the running APIAll §23 routes (/api/stats, /api/blocks, /api/transactions, /api/address/...,
/api/tokens, /api/contracts, /api/search) plus the §24 Etherscan-shaped public
surface under /api/v1/.
Keyset pagination, never OFFSET, and no total counts. OFFSET makes Postgres
materialise and discard every skipped row, so cost grows linearly with page depth and one
crawler can saturate the pool; it is also unstable, because a new block shifts every row
and consecutive pages then duplicate or skip entries. count(*) over transactions is an
unbounded sequential scan, so exact totals come from counters the indexer maintains inside
its own per-block transaction.
Sentinel defaults instead of IS NULL cursors. The obvious
($1::bigint IS NULL OR number <= $1) cannot be extracted as an index start condition, so
the uncursored first page — the most requested page of all — silently degrades to a full
index scan while cursored pages look fine. Substituting the maximum of the key domain keeps
the predicate number <= $1, with an identical plan on page 1 and page 100,000.
Address feeds are UNION ALL, never from = $1 OR to = $1. The OR makes Postgres
BitmapOr both indexes and then sort every transaction touching the address to return 25
rows — a disk-spilling sort reachable from a 42-character URL. Two independently bounded
index scans are unioned instead, and self-transfers are de-duplicated in JS over the tiny
result.
No response schemas. Fastify serialises with fast-json-stringify, which coerces to
the declared type. A wei field declared number would be silently rounded through a double,
downstream of pg returning it correctly and of every other safeguard. Omitting the schema
means plain JSON.stringify and no coercion — every uint256 leaves as a string, and a test
asserts it.
The schema holds no balance state, so token holdings and holder counts are not derivable.
Those fields return null with an explicit reason rather than a slow, wrong number
aggregated from transfer history. Native balance is read from the node.
npm --prefix explorer-web install
npm --prefix explorer-web run build
npm --prefix explorer-web run start # http://localhost:3000
node scripts/test-explorer-web.mjs # 26 tests against the running UINext.js App Router + Tailwind 4. All §14 routes: /, /blocks, /block/[number],
/transactions, /tx/[hash], /address/[address], /tokens, /token/[address],
/contracts, /validators, /charts, /search.
Point it at a different API with NEXT_PUBLIC_API_BASE (browser) and API_BASE (server).
The brief asks for holder counts, token holdings and validator uptime. None of those are derivable from the indexed data, so the UI says so in place of each one rather than showing a plausible number. Summing transfer history to guess a balance would be both unbounded and wrong — it misses the genesis allocation and gas spend entirely. An explorer that displays a confident wrong balance is worse than one that admits the gap.
Validator statistics are shown, because §27 explicitly permits deriving them from indexed blocks — but from a bounded recent window, never a full-table aggregate behind an anonymous page load.
Every wei value is formatted with BigInt arithmetic, never Number(). The whole stack
has been built to keep uint256 intact; parsing it into a double in the render layer would
throw that away at the final step. A test asserts the treasury balance renders exactly.
Verified at 375px: the document's scrollWidth stays exactly 375 while wide tables scroll
inside their own container — the page body never scrolls horizontally. Dark and light
themes are both complete palettes, applied before first paint so there is no flash.
Giggora is a standard EVM chain, so MetaMask and any other EIP-1193 wallet work without a plugin or a fork.
# network config, one-click add, devnet test keys
http://localhost:3000/connect-wallet
# sample DApp: connect, balance, send GIG, send ERC-20, wait for confirmation
node dapp/serve.mjs # http://localhost:3001
node scripts/test-wallet.mjs # 19 wallet compatibility testsThe DApp is deliberately dependency-free — raw EIP-1193, no web3 library, no bundler. What it demonstrates is the chain; a framework in the middle would obscure that.
They do not drive the MetaMask extension UI, and this repository does not claim to. A wallet is two things — a signer and a JSON-RPC client — so the tests cover both parts Giggora is responsible for:
- every RPC method a wallet calls during a send, including
eth_feeHistoryandeth_maxPriorityFeePerGas, without which a wallet cannot build an EIP-1559 fee; - real secp256k1-signed transactions, legacy and type-2, broadcast through
eth_sendRawTransaction— byte-for-byte the submission path MetaMask uses after the user clicks Confirm; - EIP-155 replay protection: a transaction signed for chain 4044 is rejected with
Wrong chainId, while the identical transaction signed for 4043 is accepted.
What remains unverified here is Consensys's extension UI, which is not our code.
viem's walletClient silently overrides the chainId you pass with its own
configured chain. The first version of the replay-protection test signed "for chain 4044"
through the client, got a valid 4043 transaction back, and reported that Giggora accepts
foreign-chain transactions — an alarming finding that was purely an artefact of the test.
Signing offline with account.signTransaction takes the chain id literally, and is the
only way to actually exercise this.
- Validator keys live in
blockchain/nodes/*/key, are gitignored, and are never baked into an image or exposed over RPC. .envis generated and contains no secrets.- The minimum gas price is deliberately non-zero — a zero-gas chain is trivially spammable.
- Devnet keys are public knowledge. Testnet and mainnet require a real key ceremony.
Full detail in docs/architecture.md §11.
- docs/architecture.md — foundation evaluation, consensus design, network architecture, phase plan, risks, and complexity estimate.
MIT. Copyright (c) 2026 Giggora contributors.
Fork it, rename it and run your own chain -- nothing here asks permission.
npm run rebrand renames the network everywhere in one pass.