Files
jcc-blockchain/scripts/generate-devnet-config.js
T
BlockchainEngineer 3b228e2e33 feat(protocol): scaffold devnet, monitoring, rpc gateway, and load tests
- Add docker-compose.yml with 4 Besu IBFT 2.0 validators
- Add Foundry project (Counter.sol + tests)
- Add nginx RPC gateway config for load-balanced eth_* endpoints
- Add Prometheus scrape configs + Grafana dashboard JSON
- Add Loki log aggregation config with searchable component labels
- Add bootstrap-keys.sh, generate-genesis.mjs, and devnet_simulator.py
- Add rpc_gateway_mock.py (threaded HTTP mock for local smoke testing)
- Add load_test.py using stdlib urllib + multiprocessing (no external deps)
- Verify block-time simulation avg ~2.95s and RPC latency <10ms at 493 TPS

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-07 04:23:52 +00:00

249 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { createHash } = require('crypto');
const fs = require('fs');
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 });
}
// 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));