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 <noreply@paperclip.ing>
This commit is contained in:
BlockchainEngineer
2026-05-07 11:38:24 +00:00
parent 6ff89f26ac
commit 00cfade84d
+157
View File
@@ -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)
}