diff --git a/devnet/README.md b/devnet/README.md index 588cbf8..31af3bd 100644 --- a/devnet/README.md +++ b/devnet/README.md @@ -7,15 +7,15 @@ - Docker Engine ≥ 24.0 - Docker Compose ≥ 2.20 - `jq` (for scripts) -- Ports `8545` and `8546` free on localhost +- Node.js ≥ 18 (for key-generation scripts) ## Start / Stop / Reset ```bash -cd scripts -./devnet-up.sh # creates network, volumes, and starts 3 validators -./devnet-down.sh # stops containers but preserves data -./devnet-reset.sh # stops containers and wipes chain data +# From repo root +./scripts/devnet-up.sh # brings up 3 validators, waits for block ≥1 +./scripts/devnet-down.sh # stops containers +./scripts/devnet-reset.sh # stops containers and wipes chain data + volumes ``` ## Validator Endpoints @@ -28,8 +28,17 @@ cd scripts ## Genesis & Keys -- `besu-config/genesis.json` — reproducible genesis block. -- `validator-keys/` — **devnet-only** private node keys and IBFT extra-data. +- `besu-config/genesis.json` — reproducible genesis block (chainId 1337, blockPeriodSeconds 2). +- `besu-config/static-nodes.json` — fixed enodes so validators discover each other immediately. +- `validator-keys/` — **devnet-only** private node keys, addresses, and public keys. + +All of the above are generated deterministically from the seed `jcc-devnet-v1` by running: + +```bash +cd scripts +npm install +node generate-static-keys-run.js ../devnet +``` ## Block Production Verification @@ -43,18 +52,27 @@ curl -s -X POST http://localhost:8545 \ You should see a hex block number ≥ `0x1` within ~30 seconds of startup. +## CI / Image Build + +The `.github/workflows/ci.yml` builds and tags the `jcc/besu` image without pushing: + +```bash +docker build -f devnet/Dockerfile.besu -t jcc/besu:latest . +``` + ## Troubleshooting -- **Port conflicts:** edit `scripts/devnet-up.sh` port mappings or stop conflicting services. +- **Port conflicts:** edit port mappings in `docker-compose.yml` (root) and `devnet/docker-compose.yml`. - **Permission denied:** ensure `scripts/*.sh` are executable (`chmod +x scripts/*.sh`). -- **Peers not connecting:** check `docker network inspect jcc-devnet` and validator logs. +- **Peers not connecting:** check `docker network inspect jcc-devnet` and validator logs; verify `static-nodes.json` enodes match container hostnames (`validator1`, `validator2`, `validator3`). ## Files -- `docker-compose.yml` — service definitions +- `docker-compose.yml` (repo root) — canonical devnet + monitoring compose file +- `devnet/docker-compose.yml` — standalone devnet compose (also includes Prometheus/Grafana) - `besu-config/genesis.json` — genesis block -- `validator-keys/` — node keys (`key`, `key.pub`, `address`) and `ibftExtraData` -- `scripts/` — operational helpers +- `validator-keys/` — node keys (`key`, `key.pub`, `address`) +- `scripts/` — operational helpers and key generation ## Notes diff --git a/devnet/besu-config/genesis.json b/devnet/besu-config/genesis.json index fc1aefb..a22c8bc 100644 --- a/devnet/besu-config/genesis.json +++ b/devnet/besu-config/genesis.json @@ -23,7 +23,7 @@ } }, "nonce": "0x0", - "timestamp": "0x69fc12d9", + "timestamp": "0xb33210b9", "gasLimit": "0x1fffffffffffff", "difficulty": "0x1", "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", diff --git a/devnet/docker-compose.yml b/devnet/docker-compose.yml index 70ec9e6..13e2596 100644 --- a/devnet/docker-compose.yml +++ b/devnet/docker-compose.yml @@ -20,7 +20,7 @@ services: - besu-data-1:/opt/besu/data environment: - BESU_DATA_PATH=/opt/besu/data - - BESUS_GENESIS_FILE=/opt/besu/genesis.json + - BESU_GENESIS_FILE=/opt/besu/genesis.json command: - --genesis-file=/opt/besu/genesis.json - --data-path=/opt/besu/data @@ -63,7 +63,7 @@ services: - "8547:8547" - "8548:8548" - "9546:9546" - - "30304:30304" + - "30304:30303" # P2P volumes: - ./validator-keys/validator2:/opt/besu/keys - ./besu-config/genesis.json:/opt/besu/genesis.json:ro @@ -71,7 +71,7 @@ services: - besu-data-2:/opt/besu/data environment: - BESU_DATA_PATH=/opt/besu/data - - BESUS_GENESIS_FILE=/opt/besu/genesis.json + - BESU_GENESIS_FILE=/opt/besu/genesis.json command: - --genesis-file=/opt/besu/genesis.json - --data-path=/opt/besu/data @@ -94,7 +94,7 @@ services: - --sync-mode=FULL - --discovery-enabled=false - --p2p-enabled=true - - --p2p-port=30304 + - --p2p-port=30303 - --logging=INFO healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8547 2>/dev/null || exit 1"] @@ -113,7 +113,7 @@ services: - "8549:8549" - "8550:8550" - "9547:9547" - - "30305:30305" + - "30305:30303" # P2P volumes: - ./validator-keys/validator3:/opt/besu/keys - ./besu-config/genesis.json:/opt/besu/genesis.json:ro @@ -121,7 +121,7 @@ services: - besu-data-3:/opt/besu/data environment: - BESU_DATA_PATH=/opt/besu/data - - BESUS_GENESIS_FILE=/opt/besu/genesis.json + - BESU_GENESIS_FILE=/opt/besu/genesis.json command: - --genesis-file=/opt/besu/genesis.json - --data-path=/opt/besu/data @@ -144,7 +144,7 @@ services: - --sync-mode=FULL - --discovery-enabled=false - --p2p-enabled=true - - --p2p-port=30305 + - --p2p-port=30303 - --logging=INFO healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8549 2>/dev/null || exit 1"] diff --git a/docker-compose.yml b/docker-compose.yml index 0229b05..5267030 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,55 +1,82 @@ services: - # ── Besu validators (IBFT 2.0, 4 nodes for BFT safety with f=1) ── + # ── Besu validators (IBFT 2.0, 3 nodes) ── besu-validator-1: - image: hyperledger/besu:24.12 + image: hyperledger/besu:24.6.0 container_name: besu-validator-1 + hostname: validator1 environment: - BESU_LOGGING=INFO volumes: - - ./besu-config:/besu/config:ro - - ./validator-keys/validator-1:/besu/keys:ro + - ./devnet/besu-config:/besu/config:ro + - ./devnet/validator-keys/validator1:/besu/keys:ro - besu-data-1:/besu/data ports: - - "8545:8545" # RPC (only node1 exposes host-facing RPC) - - "8551:8551" # Engine (if needed later) + - "8545:8545" # RPC + - "8546:8546" # WS + - "8551:8551" # Engine - "9545:9545" # Metrics + - "30303:30303" # P2P command: > - --config-file=/besu/config/besu-ibft.conf + --genesis-file=/besu/config/genesis.json --data-path=/besu/data + --node-private-key-file=/besu/keys/key + --static-nodes-file=/besu/config/static-nodes.json --p2p-host=0.0.0.0 --p2p-port=30303 --rpc-http-enabled --rpc-http-host=0.0.0.0 --rpc-http-port=8545 - --rpc-http-api=ETH,NET,WEB3,IBFT,TXPOOL,TRACE + --rpc-http-api=ETH,NET,WEB3,IBFT --rpc-http-cors-origins="*" --host-allowlist="*" + --rpc-ws-enabled --rpc-ws-host=0.0.0.0 --rpc-ws-port=8546 + --rpc-ws-api=ETH,NET,WEB3,IBFT --metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545 - --bootnodes=enode://a1@besu-validator-1:30303,enode://a2@besu-validator-2:30303 + --min-gas-price=0 + --sync-mode=FULL + --discovery-enabled=false + --p2p-enabled=true + --logging=INFO networks: - jcc-devnet healthcheck: - test: ["CMD-SHELL", "curl -sf http://localhost:8545 -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"eth_syncing\",\"params\":[],\"id\":1}' || exit 1"] + test: ["CMD-SHELL", "curl -sf http://localhost:8545 -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1}' || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 30s besu-validator-2: - image: hyperledger/besu:24.12 + image: hyperledger/besu:24.6.0 container_name: besu-validator-2 + hostname: validator2 volumes: - - ./besu-config:/besu/config:ro - - ./validator-keys/validator-2:/besu/keys:ro + - ./devnet/besu-config:/besu/config:ro + - ./devnet/validator-keys/validator2:/besu/keys:ro - besu-data-2:/besu/data ports: - - "9546:9545" + - "8547:8545" # RPC + - "8548:8546" # WS + - "9546:9545" # Metrics + - "30304:30303" # P2P command: > - --config-file=/besu/config/besu-ibft.conf + --genesis-file=/besu/config/genesis.json --data-path=/besu/data + --node-private-key-file=/besu/keys/key + --static-nodes-file=/besu/config/static-nodes.json --p2p-host=0.0.0.0 --p2p-port=30303 + --rpc-http-enabled --rpc-http-host=0.0.0.0 --rpc-http-port=8545 + --rpc-http-api=ETH,NET,WEB3,IBFT + --rpc-http-cors-origins="*" + --host-allowlist="*" + --rpc-ws-enabled --rpc-ws-host=0.0.0.0 --rpc-ws-port=8546 + --rpc-ws-api=ETH,NET,WEB3,IBFT --metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545 - --bootnodes=enode://a1@besu-validator-1:30303 + --min-gas-price=0 + --sync-mode=FULL + --discovery-enabled=false + --p2p-enabled=true + --logging=INFO networks: - jcc-devnet depends_on: @@ -57,43 +84,37 @@ services: condition: service_healthy besu-validator-3: - image: hyperledger/besu:24.12 + image: hyperledger/besu:24.6.0 container_name: besu-validator-3 + hostname: validator3 volumes: - - ./besu-config:/besu/config:ro - - ./validator-keys/validator-3:/besu/keys:ro + - ./devnet/besu-config:/besu/config:ro + - ./devnet/validator-keys/validator3:/besu/keys:ro - besu-data-3:/besu/data ports: - - "9547:9545" + - "8549:8545" # RPC + - "8550:8546" # WS + - "9547:9545" # Metrics + - "30305:30303" # P2P command: > - --config-file=/besu/config/besu-ibft.conf + --genesis-file=/besu/config/genesis.json --data-path=/besu/data + --node-private-key-file=/besu/keys/key + --static-nodes-file=/besu/config/static-nodes.json --p2p-host=0.0.0.0 --p2p-port=30303 + --rpc-http-enabled --rpc-http-host=0.0.0.0 --rpc-http-port=8545 + --rpc-http-api=ETH,NET,WEB3,IBFT + --rpc-http-cors-origins="*" + --host-allowlist="*" + --rpc-ws-enabled --rpc-ws-host=0.0.0.0 --rpc-ws-port=8546 + --rpc-ws-api=ETH,NET,WEB3,IBFT --metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545 - --bootnodes=enode://a1@besu-validator-1:30303 - networks: - - jcc-devnet - depends_on: - besu-validator-1: - condition: service_healthy - - besu-validator-4: - image: hyperledger/besu:24.12 - container_name: besu-validator-4 - volumes: - - ./besu-config:/besu/config:ro - - ./validator-keys/validator-4:/besu/keys:ro - - besu-data-4:/besu/data - ports: - - "9548:9545" - command: > - --config-file=/besu/config/besu-ibft.conf - --data-path=/besu/data - --p2p-host=0.0.0.0 - --p2p-port=30303 - --metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545 - --bootnodes=enode://a1@besu-validator-1:30303 + --min-gas-price=0 + --sync-mode=FULL + --discovery-enabled=false + --p2p-enabled=true + --logging=INFO networks: - jcc-devnet depends_on: @@ -164,7 +185,6 @@ volumes: besu-data-1: besu-data-2: besu-data-3: - besu-data-4: prometheus-data: grafana-data: loki-data: diff --git a/scripts/devnet-down.sh b/scripts/devnet-down.sh old mode 100644 new mode 100755 diff --git a/scripts/devnet-reset.sh b/scripts/devnet-reset.sh old mode 100644 new mode 100755 diff --git a/scripts/devnet-up.sh b/scripts/devnet-up.sh old mode 100644 new mode 100755 diff --git a/scripts/generate-devnet-config.js b/scripts/generate-devnet-config.js old mode 100644 new mode 100755 index 47a74ca..3da9202 --- a/scripts/generate-devnet-config.js +++ b/scripts/generate-devnet-config.js @@ -1,248 +1,23 @@ -const { createHash } = require('crypto'); -const fs = require('fs'); +#!/usr/bin/env node +/** + * generate-devnet-config.js — deterministic devnet config generator + * + * Thin wrapper that delegates to generate-static-keys-run.js so there is one + * source of truth for key derivation, genesis construction, and extraData RLP + * encoding. + * + * Usage: + * node scripts/generate-devnet-config.js [outDir] + */ +const { spawnSync } = require('child_process'); const path = require('path'); -// Lightweight pure-Node key generation using built-in crypto instead of npm deps. -// We use createECDH('secp256k1') for key operations and keccak via tiny-js implementation below. - -const keccakPermutation = () => { - // Minimal keccak-256 implementation for address derivation. - // Full reference implementation adapted for brevity. - const KECCAK_ROUNDS = 24; - const keccakf = (s) => { - const KeccakF_RoundConstants = [ - 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n, - 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n, - 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an, - 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n, - 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an, - 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n, - ]; - const KeccakF_RotationConstants = [ - [0, 1, 62, 28, 27], [36, 44, 6, 55, 20], [3, 10, 43, 25, 39], - [41, 45, 15, 21, 8], [18, 2, 61, 56, 14], - ]; - const c = new Array(5).fill(0n); - const d = new Array(5).fill(0n); - const b = Array.from({ length: 5 }, () => new Array(5).fill(0n)); - - for (let round = 0; round < KECCAK_ROUNDS; round++) { - // θ step - for (let i = 0; i < 5; i++) { - c[i] = s[i][0] ^ s[i][1] ^ s[i][2] ^ s[i][3] ^ s[i][4]; - } - for (let i = 0; i < 5; i++) { - d[i] = c[(i + 4) % 5] ^ rotl(c[(i + 1) % 5], 1); - } - for (let i = 0; i < 5; i++) for (let j = 0; j < 5; j++) s[i][j] ^= d[i]; - - // ρ + π steps - for (let i = 0; i < 5; i++) for (let j = 0; j < 5; j++) { - b[j][(2 * i + 3 * j) % 5] = rotl(s[i][j], KeccakF_RotationConstants[j][i]); - } - // χ step - for (let i = 0; i < 5; i++) for (let j = 0; j < 5; j++) { - s[i][j] = b[i][j] ^ (~b[(i + 1) % 5][j] & b[(i + 2) % 5][j]); - } - // ι step - s[0][0] ^= KeccakF_RoundConstants[round]; - } - }; - const rotl = (x, n) => (x << BigInt(n)) | (x >> BigInt(64 - n)); - - const bitsToBytes = (bits) => bits >>> 3; - const byteToBits = (len) => len << 3; - - return function keccak256(input) { - const rate = 1088; - const capacity = 512; - const outputBits = 256; - - // Initialize state as 5x5 64-bit lanes - const state = Array.from({ length: 5 }, () => new Array(5).fill(0n)); - - const inputBytes = typeof input === 'string' ? Buffer.from(input, 'hex') : input; - const blockSize = bitsToBytes(rate); - - let offset = 0; - for (let i = 0; i < inputBytes.length; i++) { - const laneI = (offset >>> 3) % 5; - const laneJ = ((offset >>> 3) / 5 | 0) % 5; - const shift = BigInt((offset % 8) * 8); - state[laneI][laneJ] ^= BigInt(inputBytes[i]) << shift; - offset++; - if (offset === blockSize) { - keccakf(state); - offset = 0; - } - } - - // Padding: append 0x01 then 0x00... then 0x80 - const paddedByte = (offset >>> 3) % 5; - const paddedLane = ((offset >>> 3) / 5 | 0) % 5; - const shift = BigInt((offset % 8) * 8); - state[paddedByte][paddedLane] ^= 0x01n << shift; - state[((blockSize - 1) % 5)][Math.floor((blockSize - 1) / 5) % 5] ^= 0x80n << (BigInt(((blockSize - 1) % 8) * 8)); - keccakf(state); - - // Squeeze output - let out = Buffer.alloc(bitsToBytes(outputBits)); - for (let i = 0; i < bitsToBytes(outputBits); i++) { - const laneI = (i % 5); - const laneJ = Math.floor(i / 5) % 5; - const shift = BigInt((i % 8) * 8); - out[i] = Number((state[laneI][laneJ] >> shift) & 0xffn); - } - return out; - }; -}; - -const keccak256 = keccakPermutation(); - -function derivePrivateKey(seed, label) { - let buf = Buffer.from(`${seed}-${label}`, 'utf8'); - for (let i = 0; i < 1000; i++) buf = createHash('sha256').update(buf).digest(); - return '0x' + buf.toString('hex'); -} - -function privateKeyToAddress(privKey) { - const ecdh = require('crypto').createECDH('secp256k1'); - ecdh.setPrivateKey(Buffer.from(privKey.slice(2), 'hex')); - const pub = ecdh.getPublicKey(null, 'uncompressed'); // 65 bytes - const pubNoHeader = pub.slice(1); // 64 bytes - const hash = keccak256(pubNoHeader); - return '0x' + hash.subarray(-20).toString('hex'); -} - -function enodeFromPrivateKey(privKey, ip, port = 30303) { - const ecdh = require('crypto').createECDH('secp256k1'); - ecdh.setPrivateKey(Buffer.from(privKey.slice(2), 'hex')); - const pub = ecdh.getPublicKey(null, 'uncompressed'); - const pubNoHeader = pub.slice(1); - const nodeId = keccak256(pubNoHeader).toString('hex'); - return `enode://${nodeId}@${ip}:${port}`; -} - -const seed = process.env.DEVNET_SEED || 'jcc-devnet-v1'; const outDir = process.argv[2] || path.join(__dirname, '..', 'devnet'); -const validators = []; -for (let i = 1; i <= 3; i++) { - const priv = derivePrivateKey(seed, `validator-${i}`); - const addr = privateKeyToAddress(priv); - validators.push({ name: `validator${i}`, privateKey: priv, address: addr }); -} +const script = path.join(__dirname, 'generate-static-keys-run.js'); +const result = spawnSync('node', [script, outDir], { + stdio: 'inherit', + env: process.env +}); -// Write key files -for (const v of validators) { - const dir = path.join(outDir, 'validator-keys', v.name); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(path.join(dir, 'key'), v.privateKey.slice(2), 'utf8'); - fs.writeFileSync(path.join(dir, 'address'), v.address, 'utf8'); - const ecdh = require('crypto').createECDH('secp256k1'); - ecdh.setPrivateKey(Buffer.from(v.privateKey.slice(2), 'hex')); - fs.writeFileSync(path.join(dir, 'key.pub'), ecdh.getPublicKey(null, 'hex'), 'utf8'); -} - -// extraData: RLP([vanity, validators, vote, round, seals]) -// Besu requires validators as list of 20-byte addresses in RLP list format. -// We'll build RLP manually because Besu expects the extraData properly encoded. -// RLP helper: -function rlpEncode(item) { - if (Buffer.isBuffer(item)) { - if (item.length === 1 && item[0] < 0x80) return item; - if (item.length <= 55) { - return Buffer.concat([Buffer.from([0x80 + item.length]), item]); - } - const lenBytes = Buffer.from(item.length.toString(16), 'hex').length === 0 ? Buffer.from([0x00]) : Buffer.from(item.length.toString(16), 'hex'); - const lenPrefix = Buffer.from([0xb7 + lenBytes.length]); - return Buffer.concat([lenPrefix, lenBytes, item]); - } else if (Array.isArray(item)) { - let payload = Buffer.alloc(0); - for (const child of item) payload = Buffer.concat([payload, rlpEncode(child)]); - if (payload.length <= 55) { - return Buffer.concat([Buffer.from([0xc0 + payload.length]), payload]); - } - const lenHex = payload.length.toString(16).padStart(payload.length > 0xff ? 4 : 2, '0'); - const lenBytes = Buffer.from(lenHex, 'hex'); - return Buffer.concat([Buffer.from([0xf7 + lenBytes.length]), lenBytes, payload]); - } else { - throw new Error('Unsupported RLP item'); - } -} - -const encodedValidators = validators.map(v => Buffer.from(v.address.slice(2), 'hex')); -const extraData = '0x' + rlpEncode([ - Buffer.alloc(32, 0), // vanity - encodedValidators, // list of validator addresses - [], // vote - Buffer.alloc(8, 0), // round - [] // seals -]).toString('hex'); - -// Build allocations -const alloc = {}; -for (const v of validators) { - alloc[v.address.slice(2)] = { comment: v.name, balance: '0xad78ebc5ac6200000' }; -} -alloc['f39Fd6e51aad88F6F4ce6aB8827279cffFb92266'] = { comment: 'Anvil default #0', balance: '0x56bc75e2d63100000' }; - -const genesis = { - config: { - chainId: 1337, - homesteadBlock: 0, - eip150Block: 0, - eip150Hash: '0x' + '00'.repeat(32), - eip155Block: 0, - eip158Block: 0, - byzantiumBlock: 0, - constantinopleBlock: 0, - petersburgBlock: 0, - istanbulBlock: 0, - muirGlacierBlock: 0, - berlinBlock: 0, - londonBlock: 0, - zeroBaseFee: true, - ibft2: { - blockperiodseconds: 2, - epochlength: 30000, - requesttimeoutseconds: 4, - blockreward: '0x0', - miningbeneficiary: '0x' + '00'.repeat(20) - } - }, - nonce: '0x0', - timestamp: '0x' + Math.floor(Date.now() / 1000).toString(16), - gasLimit: '0x1fffffffffffff', - difficulty: '0x1', - mixHash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365', - coinbase: '0x' + '00'.repeat(20), - alloc, - extraData -}; - -fs.writeFileSync(path.join(outDir, 'besu-config', 'genesis.json'), JSON.stringify(genesis, null, 2), 'utf8'); - -const staticNodes = validators.map(v => enodeFromPrivateKey(v.privateKey, v.name, 30303)); -fs.writeFileSync(path.join(outDir, 'besu-config', 'static-nodes.json'), JSON.stringify(staticNodes, null, 2), 'utf8'); - -const summary = { - seed, - validators: validators.map(v => ({ - name: v.name, - address: v.address, - nodeId: (() => { - const ecdh = require('crypto').createECDH('secp256k1'); - ecdh.setPrivateKey(Buffer.from(v.privateKey.slice(2), 'hex')); - return keccak256(ecdh.getPublicKey(null, 'uncompressed').slice(1)).toString('hex'); - })(), - enode: enodeFromPrivateKey(v.privateKey, v.name, 30303) - })), - extraData, - genesisFile: 'besu-config/genesis.json', - staticNodesFile: 'besu-config/static-nodes.json' -}; -fs.writeFileSync(path.join(outDir, 'besu-config', 'nodes-summary.json'), JSON.stringify(summary, null, 2), 'utf8'); - -console.log('Devnet config generated successfully.'); -console.log(JSON.stringify(summary, null, 2)); +process.exit(result.status || 0); diff --git a/scripts/generate-static-keys-run.js b/scripts/generate-static-keys-run.js index 8df1ee7..ca9fee3 100644 --- a/scripts/generate-static-keys-run.js +++ b/scripts/generate-static-keys-run.js @@ -29,7 +29,7 @@ function seedToBuffer(s) { function privateKeyToAddress(privKey) { const pub = secp256k1.getPublicKey(seedToBuffer(privKey), false); - const pubNoHeader = pub.slice(1); // 64 bytes + const pubNoHeader = pub.slice(1); const hash = keccak256(pubNoHeader); return '0x' + Buffer.from(hash).subarray(-20).toString('hex'); } @@ -39,10 +39,29 @@ function padHexTo(s, n) { return s.padStart(n * 2, '0'); } +function privateKeyToNodeId(privKey) { + const pub = secp256k1.getPublicKey(seedToBuffer(privKey), false); + const pubNoHeader = pub.slice(1); + const hash = keccak256(pubNoHeader); + return Buffer.from(hash).toString('hex'); +} + +function enodeFromPrivateKey(privKey, host, port = 30303) { + return `enode://${privateKeyToNodeId(privKey)}@${host}:${port}`; +} + const seed = process.env.DEVNET_SEED || 'jcc-devnet-v1'; const outDir = process.argv[2] || path.join(__dirname, '..', 'devnet'); -// Node.js util for hex bytes to Buffer +function seedToTimestamp(s) { + let buf = Buffer.from(s, 'utf8'); + for (let i = 0; i < 50; i++) buf = createHash('sha256').update(buf).digest(); + const ts = buf.readUInt32BE(0); + return '0x' + ts.toString(16); +} + +const DETERMINISTIC_TIMESTAMP = seedToTimestamp(seed); + function hexToBuf(hex) { return Buffer.from(hex.startsWith('0x') ? hex.slice(2) : hex, 'hex'); } @@ -67,15 +86,14 @@ for (const v of validators) { // Build extraData via RLP const encodedValidators = validators.map(v => hexToBuf(padHexTo(v.address, 20))); const extraDataItems = [ - Buffer.alloc(32, 0), // vanity + Buffer.alloc(32, 0), encodedValidators, - [], // vote - Buffer.alloc(8, 0), // round - [] // seals + [], + Buffer.alloc(8, 0), + [] ]; const extraData = '0x' + Buffer.from(RLP.encode(extraDataItems)).toString('hex'); -// Build allocations const alloc = {}; for (const v of validators) { alloc[v.address.slice(2)] = { comment: v.name, balance: '0xad78ebc5ac6200000' }; @@ -107,7 +125,7 @@ const genesis = { } }, nonce: '0x0', - timestamp: '0x' + Math.floor(Date.now() / 1000).toString(16), + timestamp: DETERMINISTIC_TIMESTAMP, gasLimit: '0x1fffffffffffff', difficulty: '0x1', mixHash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365', @@ -116,10 +134,27 @@ const genesis = { extraData }; -fs.writeFileSync(path.join(outDir, 'besu-config', 'genesis.json'), JSON.stringify(genesis, null, 2), 'utf8'); +const configDir = path.join(outDir, 'besu-config'); +fs.mkdirSync(configDir, { recursive: true }); +fs.writeFileSync(path.join(configDir, 'genesis.json'), JSON.stringify(genesis, null, 2), 'utf8'); -const summary = { seed, validators, extraData, genesisFile: 'besu-config/genesis.json' }; -fs.writeFileSync(path.join(outDir, 'besu-config', 'nodes-summary.json'), JSON.stringify(summary, null, 2), 'utf8'); +const staticNodes = validators.map(v => enodeFromPrivateKey(v.privateKey, v.name, 30303)); +fs.writeFileSync(path.join(configDir, 'static-nodes.json'), JSON.stringify(staticNodes, null, 2), 'utf8'); + +const summary = { + seed, + validators: validators.map(v => ({ + name: v.name, + privateKey: v.privateKey, + address: v.address, + nodeId: privateKeyToNodeId(v.privateKey), + enode: enodeFromPrivateKey(v.privateKey, v.name, 30303) + })), + extraData, + genesisFile: 'besu-config/genesis.json', + staticNodesFile: 'besu-config/static-nodes.json' +}; +fs.writeFileSync(path.join(configDir, 'nodes-summary.json'), JSON.stringify(summary, null, 2), 'utf8'); console.log('Devnet config generated successfully.'); console.log(JSON.stringify(summary, null, 2)); diff --git a/scripts/package-lock.json b/scripts/package-lock.json deleted file mode 100644 index d7c78e2..0000000 --- a/scripts/package-lock.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "name": "jcc-devnet-scripts", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "jcc-devnet-scripts", - "version": "0.0.1", - "dependencies": { - "@ethereumjs/rlp": "^5.0.0", - "ethereum-cryptography": "^2.2.0" - } - }, - "node_modules/@ethereumjs/rlp": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.2.tgz", - "integrity": "sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==", - "license": "MPL-2.0", - "bin": { - "rlp": "bin/rlp.cjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", - "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.4.2", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0" - } - } - } -} diff --git a/scripts/package.json b/scripts/package.json index 1f21dee..1840909 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,9 +1,15 @@ { "name": "jcc-devnet-scripts", - "version": "0.0.1", - "private": true, + "version": "1.0.0", + "description": "Deterministic devnet key and genesis generation scripts for JCC Blockchain", + "scripts": { + "generate": "node generate-static-keys-run.js ../devnet" + }, "dependencies": { - "ethereum-cryptography": "^2.2.0", - "@ethereumjs/rlp": "^5.0.0" + "ethereum-cryptography": "^2.2.1", + "@ethereumjs/rlp": "^5.0.2" + }, + "engines": { + "node": ">=18.0.0" } -} +} \ No newline at end of file