6af3f606a2
- Fix docker-compose.yml p2p port mappings (30304:30303, 30305:30303) - Fix typo BESUS_GENESIS_FILE -> BESU_GENESIS_FILE - Add missing volumes/environment to validator2 and validator3 services - Remove outdated package-lock.json to let CI regenerate cleanly - Validate genesis determinism and extraData alignment Co-Authored-By: Paperclip <noreply@paperclip.ing>
161 lines
4.9 KiB
JavaScript
161 lines
4.9 KiB
JavaScript
const { createHash } = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
let secp256k1 = null;
|
|
let keccak256 = null;
|
|
let RLP = null;
|
|
|
|
try {
|
|
({ secp256k1 } = require('ethereum-cryptography/secp256k1'));
|
|
({ keccak256 } = require('ethereum-cryptography/keccak'));
|
|
RLP = require('@ethereumjs/rlp').RLP;
|
|
} catch (e) {
|
|
console.error('ERROR: key generation deps not available.');
|
|
console.error('Run: npm install ethereum-cryptography @ethereumjs/rlp');
|
|
process.exit(1);
|
|
}
|
|
|
|
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 seedToBuffer(s) {
|
|
if (typeof s === 'string') return Buffer.from(s.slice(2), 'hex');
|
|
return s;
|
|
}
|
|
|
|
function privateKeyToAddress(privKey) {
|
|
const pub = secp256k1.getPublicKey(seedToBuffer(privKey), false);
|
|
const pubNoHeader = pub.slice(1);
|
|
const hash = keccak256(pubNoHeader);
|
|
return '0x' + Buffer.from(hash).subarray(-20).toString('hex');
|
|
}
|
|
|
|
function padHexTo(s, n) {
|
|
s = s.startsWith('0x') ? s.slice(2) : s;
|
|
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');
|
|
|
|
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');
|
|
}
|
|
|
|
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 pub = secp256k1.getPublicKey(seedToBuffer(v.privateKey), false);
|
|
fs.writeFileSync(path.join(dir, 'key.pub'), Buffer.from(pub).toString('hex'), 'utf8');
|
|
}
|
|
|
|
// Build extraData via RLP
|
|
const encodedValidators = validators.map(v => hexToBuf(padHexTo(v.address, 20)));
|
|
const extraDataItems = [
|
|
Buffer.alloc(32, 0),
|
|
encodedValidators,
|
|
[],
|
|
Buffer.alloc(8, 0),
|
|
[]
|
|
];
|
|
const extraData = '0x' + Buffer.from(RLP.encode(extraDataItems)).toString('hex');
|
|
|
|
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: DETERMINISTIC_TIMESTAMP,
|
|
gasLimit: '0x1fffffffffffff',
|
|
difficulty: '0x1',
|
|
mixHash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365',
|
|
coinbase: '0x' + '00'.repeat(20),
|
|
alloc,
|
|
extraData
|
|
};
|
|
|
|
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 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));
|