2025-12-12 14:57:48 -08:00
#!/usr/bin/env python3
"""
Validation and Decision Logic Tree for Besu Network Configuration
Prevents erroneous configurations through comprehensive validation
"""
from typing import Dict , Any , List , Optional , Tuple
from pathlib import Path
import re
class ValidationError ( Exception ) :
""" Custom exception for validation errors """
pass
class ConfigurationValidator :
""" Validates configuration and applies decision logic """
def __init__ ( self , config : Dict [ str , Any ] , project_root : Path ) :
self . config = config
self . project_root = project_root
self . errors : List [ str ] = [ ]
self . warnings : List [ str ] = [ ]
self . decision_tree_applied : List [ str ] = [ ]
def validate_all ( self ) - > Tuple [ bool , List [ str ] , List [ str ] ] :
""" Validate entire configuration """
self . errors = [ ]
self . warnings = [ ]
self . decision_tree_applied = [ ]
# Run validation in order
self . validate_genesis ( )
self . validate_network ( )
self . validate_besu_config ( )
self . validate_deployment ( )
self . validate_dependencies ( )
self . apply_decision_tree ( )
self . validate_resources ( )
self . validate_security ( )
return len ( self . errors ) == 0 , self . errors , self . warnings
def validate_genesis ( self ) :
""" Validate genesis configuration """
genesis = self . config . get ( ' genesis ' , { } )
# Chain ID validation
chain_id = genesis . get ( ' chainId ' , 0 )
if chain_id < 1 or chain_id > 2147483647 :
self . errors . append ( f " Chain ID must be between 1 and 2147483647, got { chain_id } " )
elif chain_id in [ 1 , 3 , 4 , 5 , 42 ] : # Mainnet, Ropsten, Rinkeby, Goerli, Kovan
self . warnings . append ( f " Chain ID { chain_id } is reserved for Ethereum networks. Consider using a different chain ID. " )
# Block period validation
block_period = genesis . get ( ' blockPeriodSeconds ' , 0 )
if block_period < 1 or block_period > 60 :
self . errors . append ( f " Block period must be between 1 and 60 seconds, got { block_period } " )
elif block_period < 2 :
self . warnings . append ( f " Block period of { block_period } second(s) may cause network instability. Recommended minimum: 2 seconds " )
# Epoch length validation
epoch_length = genesis . get ( ' epochLength ' , 0 )
if epoch_length < 1000 or epoch_length > 1000000 :
self . errors . append ( f " Epoch length must be between 1000 and 1000000, got { epoch_length } " )
elif epoch_length < 10000 :
self . warnings . append ( f " Epoch length of { epoch_length } may cause frequent validator set changes. Recommended minimum: 10000 " )
# Request timeout validation
request_timeout = genesis . get ( ' requestTimeoutSeconds ' , 0 )
if request_timeout < 1 or request_timeout > 60 :
self . errors . append ( f " Request timeout must be between 1 and 60 seconds, got { request_timeout } " )
elif request_timeout > = block_period :
self . warnings . append ( f " Request timeout ( { request_timeout } s) should be less than block period ( { block_period } s) " )
# Gas limit validation
gas_limit = genesis . get ( ' gasLimit ' , ' 0x0 ' )
try :
gas_limit_int = int ( gas_limit , 16 )
if gas_limit_int < 5000 :
self . errors . append ( f " Gas limit too low: { gas_limit } . Minimum: 0x1388 (5000) " )
elif gas_limit_int > 0x7fffffffffffffff :
self . errors . append ( f " Gas limit too high: { gas_limit } . Maximum: 0x7fffffffffffffff " )
except ValueError :
self . errors . append ( f " Invalid gas limit format: { gas_limit } " )
# Validator count validation
validators = self . config . get ( ' validators ' , [ ] )
if len ( validators ) < 1 :
self . errors . append ( " At least one validator is required for IBFT2 consensus " )
elif len ( validators ) < 4 :
self . warnings . append ( f " Only { len ( validators ) } validator(s) configured. For production, recommend at least 4 validators for fault tolerance " )
elif len ( validators ) % 2 == 0 :
# Even number of validators can cause consensus issues
self . warnings . append ( f " Even number of validators ( { len ( validators ) } ) can cause consensus deadlocks. Consider using an odd number " )
# Validator address validation
for i , validator in enumerate ( validators ) :
if not re . match ( r ' ^0x[a-fA-F0-9] {40} $ ' , validator ) :
self . errors . append ( f " Invalid validator address { i + 1 } : { validator } . Must be a valid Ethereum address (0x followed by 40 hex characters) " )
def validate_network ( self ) :
""" Validate network configuration """
network = self . config . get ( ' network ' , { } )
nodes = self . config . get ( ' nodes ' , { } )
# Cluster name validation
cluster_name = network . get ( ' clusterName ' , ' ' )
if not cluster_name :
self . errors . append ( " Cluster name is required " )
elif not re . match ( r ' ^[a-z0-9-]+$ ' , cluster_name ) :
self . errors . append ( f " Invalid cluster name: { cluster_name } . Must contain only lowercase letters, numbers, and hyphens " )
elif len ( cluster_name ) > 63 :
self . errors . append ( f " Cluster name too long: { cluster_name } . Maximum length: 63 characters " )
# Resource group validation
resource_group = network . get ( ' resourceGroup ' , ' ' )
if not resource_group :
self . errors . append ( " Resource group name is required " )
elif not re . match ( r ' ^[a-zA-Z0-9._()-]+$ ' , resource_group ) :
self . errors . append ( f " Invalid resource group name: { resource_group } . Must contain only alphanumeric characters, periods, underscores, parentheses, and hyphens " )
elif len ( resource_group ) > 90 :
self . errors . append ( f " Resource group name too long: { resource_group } . Maximum length: 90 characters " )
# VNet address space validation
vnet_address = network . get ( ' vnetAddressSpace ' , ' ' )
if not self . _validate_cidr ( vnet_address ) :
self . errors . append ( f " Invalid VNet address space: { vnet_address } . Must be valid CIDR notation " )
# Subnet validation
subnets = network . get ( ' subnets ' , { } )
vnet_cidr = self . _parse_cidr ( vnet_address )
if vnet_cidr :
vnet_network , vnet_mask = vnet_cidr
for subnet_name , subnet_cidr in subnets . items ( ) :
if not self . _validate_cidr ( subnet_cidr ) :
self . errors . append ( f " Invalid { subnet_name } subnet: { subnet_cidr } . Must be valid CIDR notation " )
else :
2026-06-07 21:51:32 -07:00
parsed_subnet = self . _parse_cidr ( subnet_cidr )
if parsed_subnet is None :
continue
subnet_network , subnet_mask = parsed_subnet
2025-12-12 14:57:48 -08:00
if subnet_network and not self . _is_subnet_of ( subnet_network , subnet_mask , vnet_network , vnet_mask ) :
self . errors . append ( f " { subnet_name } subnet { subnet_cidr } is not within VNet { vnet_address } " )
if subnet_mask < vnet_mask :
self . errors . append ( f " { subnet_name } subnet { subnet_cidr } has larger mask than VNet { vnet_address } " )
# Node count validation
validator_count = nodes . get ( ' validatorCount ' , 0 )
sentry_count = nodes . get ( ' sentryCount ' , 0 )
rpc_count = nodes . get ( ' rpcCount ' , 0 )
if validator_count < 1 :
self . errors . append ( " At least one validator is required " )
if sentry_count < 1 :
self . warnings . append ( " No sentries configured. Validators will be directly exposed to the network " )
if rpc_count < 1 :
self . warnings . append ( " No RPC nodes configured. No public RPC access will be available " )
# VM size validation
vm_sizes = self . config . get ( ' vmSizes ' , { } )
for vm_type , vm_size in vm_sizes . items ( ) :
if not vm_size :
self . errors . append ( f " VM size for { vm_type } is required " )
elif not re . match ( r ' ^Standard_[A-Z][0-9]+[a-z]*_v[0-9]+$ ' , vm_size ) :
self . warnings . append ( f " VM size { vm_size } may not be valid. Standard format: Standard_[Series][Size]_v[Version] " )
def validate_besu_config ( self ) :
""" Validate Besu configuration """
besu = self . config . get ( ' besu ' , { } )
ports = self . config . get ( ' ports ' , { } )
# Port validation
used_ports = set ( )
port_configs = [
( ' p2p ' , ports . get ( ' p2p ' , 30303 ) ) ,
( ' rpcHttp ' , ports . get ( ' rpcHttp ' , 8545 ) ) ,
( ' rpcWs ' , ports . get ( ' rpcWs ' , 8546 ) ) ,
( ' metrics ' , ports . get ( ' metrics ' , 9545 ) ) ,
]
for port_name , port_value in port_configs :
if port_value < 1024 and port_value != 0 :
self . warnings . append ( f " Port { port_name } ( { port_value } ) is in privileged range (0-1023). May require root access " )
elif port_value in used_ports :
self . errors . append ( f " Port conflict: { port_name } port { port_value } is already in use " )
else :
used_ports . add ( port_value )
# RPC configuration validation
for node_type in [ ' validators ' , ' sentries ' , ' rpc ' ] :
node_config = besu . get ( node_type , { } )
# Validators should not have RPC enabled
if node_type == ' validators ' and node_config . get ( ' rpcHttpEnabled ' , False ) :
self . warnings . append ( " Validators have RPC enabled. For security, validators should have RPC disabled " )
# RPC nodes should have RPC enabled
if node_type == ' rpc ' and not node_config . get ( ' rpcHttpEnabled ' , False ) :
self . errors . append ( " RPC nodes must have RPC HTTP enabled " )
# RPC nodes should not have P2P enabled
if node_type == ' rpc ' and node_config . get ( ' p2pEnabled ' , True ) :
self . warnings . append ( " RPC nodes have P2P enabled. For security, RPC nodes should have P2P disabled " )
# CORS validation
rpc_config = besu . get ( ' rpc ' , { } )
cors_origins = rpc_config . get ( ' corsOrigins ' , [ ] )
if cors_origins :
for origin in cors_origins :
if origin == ' * ' :
self . warnings . append ( " CORS origin ' * ' allows all origins. Consider restricting to specific domains for security " )
elif not ( origin . startswith ( ' http:// ' ) or origin . startswith ( ' https:// ' ) ) :
self . warnings . append ( f " CORS origin ' { origin } ' should include protocol (http:// or https://) " )
def validate_deployment ( self ) :
""" Validate deployment configuration """
deployment = self . config . get ( ' deployment ' , { } )
# Deployment type validation
deployment_type = deployment . get ( ' type ' , ' ' )
if deployment_type not in [ ' aks ' , ' vm ' , ' both ' ] :
self . errors . append ( f " Invalid deployment type: { deployment_type } . Must be ' aks ' , ' vm ' , or ' both ' " )
# VM deployment validation
if deployment . get ( ' vmEnabled ' , False ) :
vm_config = deployment . get ( ' vm ' , { } )
# SSH key validation
ssh_key_path = vm_config . get ( ' sshPublicKey ' , ' ' )
if ssh_key_path :
expanded_path = Path ( ssh_key_path ) . expanduser ( )
if not expanded_path . exists ( ) :
self . errors . append ( f " SSH public key not found: { ssh_key_path } " )
else :
# Validate SSH key format
try :
with open ( expanded_path , ' r ' ) as f :
key_content = f . read ( ) . strip ( )
if not key_content . startswith ( ' ssh-rsa ' ) and not key_content . startswith ( ' ssh-ed25519 ' ) :
self . warnings . append ( f " SSH key may not be in correct format: { ssh_key_path } " )
except Exception as e :
self . errors . append ( f " Error reading SSH key: { e } " )
# Regions validation
regions = vm_config . get ( ' regions ' , [ ] )
if not regions :
self . errors . append ( " At least one region is required for VM deployment " )
elif len ( regions ) > 10 :
self . warnings . append ( f " Deploying to { len ( regions ) } regions may be costly. Consider reducing number of regions " )
def validate_dependencies ( self ) :
""" Validate dependencies between configuration sections """
# Check if validator count matches genesis validators
genesis_validators = len ( self . config . get ( ' validators ' , [ ] ) )
node_validators = self . config . get ( ' nodes ' , { } ) . get ( ' validatorCount ' , 0 )
if genesis_validators > 0 and node_validators > 0 and genesis_validators != node_validators :
self . warnings . append ( f " Validator count mismatch: { genesis_validators } validators in genesis, { node_validators } validators in node configuration " )
# Check if monitoring is enabled but no monitoring components selected
monitoring = self . config . get ( ' monitoring ' , { } )
if monitoring . get ( ' enabled ' , False ) :
has_monitoring = (
monitoring . get ( ' prometheusEnabled ' , False ) or
monitoring . get ( ' grafanaEnabled ' , False ) or
monitoring . get ( ' lokiEnabled ' , False )
)
if not has_monitoring :
self . warnings . append ( " Monitoring is enabled but no monitoring components are selected " )
# Check if Blockscout is enabled but RPC is disabled
blockscout = self . config . get ( ' blockscout ' , { } )
if blockscout . get ( ' enabled ' , False ) :
rpc_config = self . config . get ( ' besu ' , { } ) . get ( ' rpc ' , { } )
if not rpc_config . get ( ' rpcHttpEnabled ' , False ) :
self . errors . append ( " Blockscout requires RPC to be enabled. Enable RPC HTTP for RPC nodes " )
def apply_decision_tree ( self ) :
""" Apply decision tree logic to fix or warn about configurations """
# Decision Tree 1: Validator Count and Consensus
validator_count = len ( self . config . get ( ' validators ' , [ ] ) )
if validator_count == 1 :
self . decision_tree_applied . append ( " Single validator detected - network will be centralized " )
self . warnings . append ( " Single validator configuration: Network will be centralized. Not suitable for production " )
elif validator_count == 2 :
self . decision_tree_applied . append ( " Two validators detected - risk of deadlock " )
self . warnings . append ( " Two validators: Risk of consensus deadlock if one validator goes offline " )
elif validator_count == 3 :
self . decision_tree_applied . append ( " Three validators detected - can tolerate 1 failure " )
self . warnings . append ( " Three validators: Can tolerate 1 failure. For production, recommend at least 4 validators " )
# Decision Tree 2: Network Architecture
sentry_count = self . config . get ( ' nodes ' , { } ) . get ( ' sentryCount ' , 0 )
validator_count = self . config . get ( ' nodes ' , { } ) . get ( ' validatorCount ' , 0 )
if sentry_count == 0 and validator_count > 0 :
self . decision_tree_applied . append ( " No sentries configured - validators exposed directly " )
self . warnings . append ( " No sentries: Validators will be directly exposed to the network. Consider adding sentries for security " )
elif sentry_count > 0 and sentry_count < validator_count :
self . decision_tree_applied . append ( " Fewer sentries than validators - may cause connectivity issues " )
self . warnings . append ( f " Fewer sentries ( { sentry_count } ) than validators ( { validator_count } ). Recommend at least { validator_count } sentries " )
# Decision Tree 3: RPC Configuration
rpc_count = self . config . get ( ' nodes ' , { } ) . get ( ' rpcCount ' , 0 )
rpc_config = self . config . get ( ' besu ' , { } ) . get ( ' rpc ' , { } )
if rpc_count > 0 :
if not rpc_config . get ( ' rpcHttpEnabled ' , False ) :
self . errors . append ( " RPC nodes configured but RPC HTTP is disabled " )
if rpc_config . get ( ' p2pEnabled ' , True ) :
self . decision_tree_applied . append ( " RPC nodes have P2P enabled - security risk " )
self . warnings . append ( " RPC nodes have P2P enabled. For security, disable P2P on RPC nodes " )
# Decision Tree 4: Deployment Type and Resources
deployment = self . config . get ( ' deployment ' , { } )
nodes = self . config . get ( ' nodes ' , { } )
vm_sizes = self . config . get ( ' vmSizes ' , { } )
if deployment . get ( ' vmEnabled ' , False ) :
total_nodes = nodes . get ( ' validatorCount ' , 0 ) + nodes . get ( ' sentryCount ' , 0 ) + nodes . get ( ' rpcCount ' , 0 )
if total_nodes > 50 :
self . decision_tree_applied . append ( " Large VM deployment detected - cost consideration " )
self . warnings . append ( f " Large VM deployment: { total_nodes } nodes. Consider using VM Scale Sets for cost optimization " )
# Check VM sizes are appropriate
for vm_type , vm_size in vm_sizes . items ( ) :
if ' D2 ' in vm_size and vm_type == ' rpc ' :
self . warnings . append ( f " RPC nodes using { vm_size } may have insufficient resources. Recommend D4s_v3 or larger " )
if ' D2 ' in vm_size and vm_type == ' validator ' :
self . warnings . append ( f " Validators using { vm_size } may have insufficient resources. Recommend D4s_v3 or larger " )
# Decision Tree 5: Security Configuration
rpc_config = self . config . get ( ' besu ' , { } ) . get ( ' rpc ' , { } )
cors_origins = rpc_config . get ( ' corsOrigins ' , [ ] )
host_allowlist = rpc_config . get ( ' hostAllowlist ' , [ ] )
if rpc_config . get ( ' rpcHttpEnabled ' , False ) :
if not cors_origins and not host_allowlist :
self . decision_tree_applied . append ( " RPC enabled without CORS or host restrictions - security risk " )
self . warnings . append ( " RPC enabled without CORS or host restrictions. Consider adding security restrictions " )
if ' 0.0.0.0 ' in host_allowlist or ' * ' in host_allowlist :
self . decision_tree_applied . append ( " RPC host allowlist allows all hosts - security risk " )
self . warnings . append ( " RPC host allowlist allows all hosts. Consider restricting to specific hosts " )
# Decision Tree 6: Resource Allocation
nodes = self . config . get ( ' nodes ' , { } )
vm_sizes = self . config . get ( ' vmSizes ' , { } )
besu_config = self . config . get ( ' besu ' , { } )
validator_count = nodes . get ( ' validatorCount ' , 0 )
if validator_count > 10 :
self . decision_tree_applied . append ( " Large validator count - resource consideration " )
self . warnings . append ( f " Large validator count ( { validator_count } ). Ensure sufficient network bandwidth and resources " )
# Check JVM options match VM sizes
for node_type in [ ' validators ' , ' sentries ' , ' rpc ' ] :
jvm_options = besu_config . get ( node_type , { } ) . get ( ' jvmOptions ' , ' ' )
vm_size = vm_sizes . get ( node_type . replace ( ' s ' , ' ' ) , ' ' )
if ' Xmx ' in jvm_options :
# Extract memory from JVM options
memory_match = re . search ( r ' -Xmx( \ d+)([gGmM]) ' , jvm_options )
if memory_match :
memory_value = int ( memory_match . group ( 1 ) )
memory_unit = memory_match . group ( 2 ) . upper ( )
memory_gb = memory_value if memory_unit == ' G ' else memory_value / 1024
# Check if memory is appropriate for VM size
if ' D2 ' in vm_size and memory_gb > 8 :
self . warnings . append ( f " { node_type } JVM memory ( { memory_gb } GB) may exceed VM size { vm_size } capacity " )
elif ' D4 ' in vm_size and memory_gb > 16 :
self . warnings . append ( f " { node_type } JVM memory ( { memory_gb } GB) may exceed VM size { vm_size } capacity " )
def validate_resources ( self ) :
""" Validate resource allocation """
nodes = self . config . get ( ' nodes ' , { } )
vm_sizes = self . config . get ( ' vmSizes ' , { } )
# Check if node counts are reasonable
total_nodes = nodes . get ( ' validatorCount ' , 0 ) + nodes . get ( ' sentryCount ' , 0 ) + nodes . get ( ' rpcCount ' , 0 )
if total_nodes > 100 :
self . warnings . append ( f " Total node count ( { total_nodes } ) is very high. Consider if this is necessary " )
# Check VM size consistency
validator_size = vm_sizes . get ( ' validator ' , ' ' )
sentry_size = vm_sizes . get ( ' sentry ' , ' ' )
rpc_size = vm_sizes . get ( ' rpc ' , ' ' )
# RPC nodes typically need more resources
if rpc_size and validator_size :
if self . _compare_vm_sizes ( rpc_size , validator_size ) < 0 :
self . warnings . append ( " RPC nodes have smaller VM size than validators. RPC nodes typically need more resources " )
def validate_security ( self ) :
""" Validate security configuration """
# Check if validators have RPC enabled (security risk)
besu_config = self . config . get ( ' besu ' , { } )
validator_config = besu_config . get ( ' validators ' , { } )
if validator_config . get ( ' rpcHttpEnabled ' , False ) :
self . warnings . append ( " Security: Validators have RPC enabled. Validators should not expose RPC endpoints " )
# Check CORS configuration
rpc_config = besu_config . get ( ' rpc ' , { } )
cors_origins = rpc_config . get ( ' corsOrigins ' , [ ] )
if ' * ' in cors_origins :
self . warnings . append ( " Security: CORS allows all origins ( ' * ' ). This is a security risk in production " )
# Check host allowlist
host_allowlist = rpc_config . get ( ' hostAllowlist ' , [ ] )
if ' 0.0.0.0 ' in host_allowlist or ' * ' in host_allowlist :
self . warnings . append ( " Security: Host allowlist allows all hosts. This is a security risk in production " )
def _validate_cidr ( self , cidr : str ) - > bool :
""" Validate CIDR notation """
try :
parts = cidr . split ( ' / ' )
if len ( parts ) != 2 :
return False
ip = parts [ 0 ]
mask = int ( parts [ 1 ] )
if mask < 0 or mask > 32 :
return False
ip_parts = ip . split ( ' . ' )
if len ( ip_parts ) != 4 :
return False
return all ( 0 < = int ( part ) < = 255 for part in ip_parts )
except ( ValueError , AttributeError ) :
return False
def _parse_cidr ( self , cidr : str ) - > Optional [ Tuple [ str , int ] ] :
""" Parse CIDR notation """
try :
parts = cidr . split ( ' / ' )
if len ( parts ) != 2 :
return None
ip = parts [ 0 ]
mask = int ( parts [ 1 ] )
return ( ip , mask )
except ( ValueError , AttributeError ) :
return None
def _is_subnet_of ( self , subnet_ip : str , subnet_mask : int , network_ip : str , network_mask : int ) - > bool :
""" Check if subnet is within network """
if subnet_mask < network_mask :
return False
# Simple check: if masks are equal, IPs must be equal
# For production, should do proper bitwise comparison
return True
def _compare_vm_sizes ( self , size1 : str , size2 : str ) - > int :
""" Compare VM sizes. Returns -1 if size1 < size2, 0 if equal, 1 if size1 > size2 """
# Extract size number from VM size string (e.g., D4s_v3 -> 4)
def extract_size ( size : str ) - > int :
match = re . search ( r ' D( \ d+) ' , size )
return int ( match . group ( 1 ) ) if match else 0
size1_num = extract_size ( size1 )
size2_num = extract_size ( size2 )
if size1_num < size2_num :
return - 1
elif size1_num > size2_num :
return 1
else :
return 0