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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user