From 00cfade84dcde78f048ec18095da3c9e910efbfc Mon Sep 17 00:00:00 2001 From: BlockchainEngineer Date: Thu, 7 May 2026 11:38:24 +0000 Subject: [PATCH] security(genesis): add Terraform module for validated IBFT 2.0 genesis access control - Enforces Separation of Duties: validator/admin/pause addresses must be distinct - Validates chainId does not collide with mainnet/testnet IDs - Requires minimum 4 validators for IBFT 2.0 safety - Alloc map distinguishes roles with different balances - extraData placeholder to be injected by key-ceremony tooling Co-Authored-By: Paperclip --- infra/modules/besu-genesis/main.tf | 157 +++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 infra/modules/besu-genesis/main.tf diff --git a/infra/modules/besu-genesis/main.tf b/infra/modules/besu-genesis/main.tf new file mode 100644 index 0000000..67b5255 --- /dev/null +++ b/infra/modules/besu-genesis/main.tf @@ -0,0 +1,157 @@ +/** + * Terraform module: besu-genesis + * + * Generates and validates an IBFT 2.0 genesis.json with strict access-control rules. + * Enforces Separation of Duties: validator, admin, and pause keys must be distinct. + */ + +variable "chain_id" { + description = "Chain ID for the JCC blockchain. Must not collide with mainnet (1) or known testnets." + type = number + default = 13371337 + + validation { + condition = var.chain_id != 1 && var.chain_id != 5 && var.chain_id != 11155111 && var.chain_id != 137 && var.chain_id != 80001 + error_message = "chain_id must not collide with Ethereum mainnet, Goerli, Sepolia, Polygon mainnet, or Mumbai." + } +} + +variable "validator_addresses" { + description = "List of 20-byte Ethereum addresses for IBFT 2.0 validators. Minimum 4 for IBFT safety." + type = list(string) + + validation { + condition = length(var.validator_addresses) >= 4 + error_message = "IBFT 2.0 requires at least 4 validator addresses." + } +} + +variable "admin_addresses" { + description = "List of admin addresses with minimal genesis balance. Must be distinct from validators." + type = list(string) + default = [] +} + +variable "pause_key_address" { + description = "Emergency pause key address. Must be distinct from validators and admins." + type = string + default = "" +} + +variable "faucet_address" { + description = "Optional faucet / deployer address with larger balance for local devnet." + type = string + default = "" +} + +locals { + all_whitelisted = concat(var.validator_addresses, var.admin_addresses) + _pause = var.pause_key_address != "" ? [var.pause_key_address] : [] + _faucet = var.faucet_address != "" ? [var.faucet_address] : [] + all_addresses = concat(local.all_whitelisted, local._pause, local._faucet) + + # Normalise to lower-case for comparison + normalized = [for a in local.all_addresses : lower(a)] + + # Separation of duties: no address may hold more than one role + duplicates = [for a in local.normalized : a if length([for b in local.normalized : b if b == a]) > 1] +} + +# Validate uniqueness across roles +resource "null_resource" "validate_unique_roles" { + triggers = { + always_run = timestamp() + } + + provisioner "local-exec" { + command = <<-EOT + if [ "${length(local.duplicates)}" -ne 0 ]; then + echo "ERROR: Address overlap detected between validator/admin/pause roles: ${join(",", local.duplicates)}" + exit 1 + fi + echo "Role separation OK." + EOT + } +} + +# Build alloc map +locals { + validator_alloc = { + for addr in var.validator_addresses : lower(addr) => { + comment = "validator" + balance = "0xad78ebc5ac6200000" # 2000 ETH in wei (devnet default) + } + } + + admin_alloc = { + for addr in var.admin_addresses : lower(addr) => { + comment = "admin" + balance = "0x56bc75e2d63100000" # 100 ETH in wei + } + } + + pause_alloc = var.pause_key_address != "" ? { + lower(var.pause_key_address) = { + comment = "pause-key" + balance = "0x0" + } + } : {} + + faucet_alloc = var.faucet_address != "" ? { + lower(var.faucet_address) = { + comment = "faucet" + balance = "0x56bc75e2d63100000" + } + } : {} + + alloc = merge(local.validator_alloc, local.admin_alloc, local.pause_alloc, local.faucet_alloc) +} + +# Render genesis.json via template +locals { + genesis = { + config = { + chainId = var.chain_id + homesteadBlock = 0 + eip150Block = 0 + eip150Hash = "0x0000000000000000000000000000000000000000000000000000000000000000" + 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 = "0x0000000000000000000000000000000000000000" + } + } + nonce = "0x0" + timestamp = "0x${substr(sha256(join(",", var.validator_addresses)), 0, 8)}" + gasLimit = "0x1fffffffffffff" + difficulty = "0x1" + mixHash = "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365" + coinbase = "0x0000000000000000000000000000000000000000" + alloc = local.alloc + extraData = "REPLACE_WITH_EXTRA_DATA" # Computed externally by key-ceremony tooling + } +} + +# Output validated genesis JSON +output "genesis_json" { + description = "Validated genesis.json object. extraData must still be injected by key-ceremony script." + value = local.genesis + sensitive = false +} + +output "validated_addresses" { + description = "Deduplicated list of all whitelisted addresses." + value = distinct(local.normalized) +}