From e543eb3d90aef72b9a582e8219fa1b2acbbf46f3 Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 12:02:27 +0800 Subject: [PATCH 01/11] default devnet to single sequencer --- Makefile | 17 ++++- ops/devnet-morph/devnet/__init__.py | 107 +++++++++++++++++++++++++++ ops/docker/docker-compose-4nodes.yml | 7 ++ 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 529226f69..9e1ec4e47 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,11 @@ go-ubuntu-builder: ################## devnet 4 nodes #################### EXECUTION_CLIENT ?= geth +DEVNET_SINGLE_SEQUENCER ?= true +DEVNET_SINGLE_SEQUENCER_ENABLED := $(filter true 1 yes,$(DEVNET_SINGLE_SEQUENCER)) +DEVNET_SEQUENCER_PRIVATE_KEY ?= 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +DEVNET_SEQUENCER_ADDRESS ?= 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS ?= 0 MORPH_RETH_BUILD_FROM_SOURCE ?= false ifeq ($(MORPH_RETH_BUILD_FROM_SOURCE),true) MORPH_RETH_IMAGE ?= morph-reth:latest @@ -171,7 +176,11 @@ $(error unsupported EXECUTION_CLIENT "$(EXECUTION_CLIENT)", expected "geth" or " endif devnet-up: $(DEVNET_EXECUTION_DEPS) go-ubuntu-builder - python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) + python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) \ + $(if $(DEVNET_SINGLE_SEQUENCER_ENABLED),--single-sequencer,--pbft) \ + --sequencer-private-key=$(DEVNET_SEQUENCER_PRIVATE_KEY) \ + --sequencer-address=$(DEVNET_SEQUENCER_ADDRESS) \ + --sequencer-upgrade-offset-seconds=$(DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS) .PHONY: devnet-up devnet-up-reth: @@ -179,7 +188,11 @@ devnet-up-reth: .PHONY: devnet-up-reth devnet-up-debugccc: $(DEVNET_EXECUTION_DEPS) go-ubuntu-builder - python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) --debugccc + python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) --debugccc \ + $(if $(DEVNET_SINGLE_SEQUENCER_ENABLED),--single-sequencer,--pbft) \ + --sequencer-private-key=$(DEVNET_SEQUENCER_PRIVATE_KEY) \ + --sequencer-address=$(DEVNET_SEQUENCER_ADDRESS) \ + --sequencer-upgrade-offset-seconds=$(DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS) .PHONY: devnet-up-debugccc devnet-down: diff --git a/ops/devnet-morph/devnet/__init__.py b/ops/devnet-morph/devnet/__init__.py index d7b5b8f01..79ac79c27 100644 --- a/ops/devnet-morph/devnet/__init__.py +++ b/ops/devnet-morph/devnet/__init__.py @@ -23,6 +23,25 @@ parser.add_argument('--only-l1', help='Only bootstrap l1 geth', action="store_true") parser.add_argument('--execution-client', choices=('geth', 'reth'), default='geth', help='L2 execution client implementation to run') +parser.add_argument('--single-sequencer', dest='single_sequencer', action='store_true', default=True, + help='Start devnet in centralized single-sequencer mode (default)') +parser.add_argument('--pbft', dest='single_sequencer', action='store_false', + help='Keep the legacy PBFT-only devnet mode') +parser.add_argument('--sequencer-private-key', + default=os.environ.get( + 'SEQUENCER_PRIVATE_KEY', + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + ), + help='Private key used by the single devnet sequencer') +parser.add_argument('--sequencer-address', + default=os.environ.get( + 'HA_SEQUENCER_ADDR', + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + ), + help='L1Sequencer address expected to match --sequencer-private-key') +parser.add_argument('--sequencer-upgrade-offset-seconds', type=int, + default=int(os.environ.get('SEQUENCER_UPGRADE_OFFSET_SECONDS', '0')), + help='Seconds from now before PBFT switches to single-sequencer mode') # parser.add_argument('--deploy', help='Whether the contracts should be predeployed or deployed', action="store_true") parser.add_argument('--debugccc', help='Whether set the debug log level for ccc', action="store_true") @@ -246,6 +265,17 @@ def devnet_deploy(paths, args): '--private-key', deploy_config['l2StakingPks'][i] ]) + sequencer_upgrade_time = 0 + if args.single_sequencer: + configure_l1_sequencer(paths, args, addresses, deploy_config) + sequencer_upgrade_time = int((time.time() + args.sequencer_upgrade_offset_seconds) * 1000) + log.info( + f'Single sequencer mode enabled: sequencer={args.sequencer_address}, ' + f'upgrade_time_ms={sequencer_upgrade_time}, ' + f'upgrade_offset_seconds={args.sequencer_upgrade_offset_seconds}') + else: + log.info('PBFT-only devnet mode enabled') + rust_log_level = 'info' if args.debugccc: rust_log_level = 'debug' @@ -264,7 +294,11 @@ def devnet_deploy(paths, args): env_data['MORPH_ROLLUP'] = addresses['Proxy__Rollup'] env_data['RUST_LOG'] = rust_log_level env_data['Proxy__L1Staking'] = addresses['Proxy__L1Staking'] + env_data['MORPH_L1STAKING'] = addresses['Proxy__L1Staking'] env_data['L1_SEQUENCER_CONTRACT'] = addresses.get('Proxy__L1Sequencer', '') + env_data['SEQUENCER_PRIVATE_KEY'] = args.sequencer_private_key if args.single_sequencer else '' + env_data['HA_SEQUENCER_ADDR'] = args.sequencer_address if args.single_sequencer else '' + env_data['SEQUENCER_UPGRADE_TIME'] = str(sequencer_upgrade_time) envfile.seek(0) for key, value in env_data.items(): envfile.write(f'{key}={value}\n') @@ -286,11 +320,43 @@ def devnet_deploy(paths, args): 'GENESIS_FILE_PATH': '/genesis.json', 'L1_ETH_RPC': 'http://layer1-el:8545', 'L1_BEACON_CHAIN_RPC': 'http://layer1-cl:4000', + 'L1_SEQUENCER_CONTRACT': addresses.get('Proxy__L1Sequencer', ''), + 'SEQUENCER_PRIVATE_KEY': args.sequencer_private_key if args.single_sequencer else '', + 'SEQUENCER_UPGRADE_TIME': str(sequencer_upgrade_time), }) wait_up(8545) wait_for_rpc_server('127.0.0.1:8545') +def configure_l1_sequencer(paths, args, addresses, deploy_config): + """Register the devnet's single sequencer in L1Sequencer before starting L2.""" + l1_sequencer_addr = addresses.get('Proxy__L1Sequencer', '') + if not l1_sequencer_addr: + raise RuntimeError('Proxy__L1Sequencer missing from deployment output') + + expected_addr = args.sequencer_address.lower() + derived = run_command_capture_output([ + 'cast', 'wallet', 'address', + '--private-key', args.sequencer_private_key, + ], cwd=paths.contracts_dir) + actual_addr = derived.stdout.strip().lower() + if actual_addr != expected_addr: + raise RuntimeError( + f'sequencer private key derives {derived.stdout.strip()}, expected {args.sequencer_address}') + + log.info(f'Setting first L1Sequencer: sequencer={args.sequencer_address} (active from block 0)') + run_command([ + 'cast', 'send', l1_sequencer_addr, + 'setFirstSequencer(address)', + args.sequencer_address, + '--rpc-url', 'http://127.0.0.1:9545', + '--private-key', deploy_config['BLOCK_SIGNER_PRIVATE_KEY'], + ], cwd=paths.contracts_dir) + + latest_l1_block = eth_blockNumber('127.0.0.1:9545') or 1 + wait_for_l1_finalized(latest_l1_block) + + def wait_for_rpc_server(url): """Block until the JSON-RPC server at url answers an eth_chainId call successfully.""" log.info(f'Waiting for RPC server at {url}') @@ -312,6 +378,19 @@ def wait_for_rpc_server(url): time.sleep(1) +def wait_for_l1_finalized(min_block, retries=120, wait_secs=3): + """Wait until the local L1 finalized tag reaches min_block.""" + for _ in range(retries): + finalized = eth_block_by_number('127.0.0.1:9545', 'finalized') + if finalized is not None and finalized >= min_block: + log.info(f'L1 finalized block {finalized} reached target {min_block}') + return + log.info(f'Waiting for L1 finalized block >= {min_block} (current: {finalized})') + time.sleep(wait_secs) + + log.warning(f'Timeout waiting for L1 finalized block >= {min_block}; continuing') + + def run_command(args, check=True, shell=False, cwd=None, env=None, output=None): """Run a subprocess with the parent environment merged with the supplied env dict.""" env = env if env else {} @@ -423,3 +502,31 @@ def eth_blockNumber(url): except Exception as e: log.debug(f'Error calling eth_blockNumber: {e}') return None + + +def eth_block_by_number(url, tag): + """ + Call eth_getBlockByNumber JSON-RPC method for a tag and return the block number. + Returns the block number as an integer, or None when the tag is unavailable. + """ + try: + conn = http.client.HTTPConnection(url) + headers = {'Content-type': 'application/json'} + body = json.dumps({ + 'id': 1, + 'jsonrpc': '2.0', + 'method': 'eth_getBlockByNumber', + 'params': [tag, False], + }) + conn.request('POST', '/', body, headers) + response = conn.getresponse() + data = response.read().decode() + conn.close() + result = json.loads(data) + block = result.get('result') + if block and block.get('number'): + return int(block['number'], 16) + return None + except Exception as e: + log.debug(f'Error calling eth_getBlockByNumber({tag}): {e}') + return None diff --git a/ops/docker/docker-compose-4nodes.yml b/ops/docker/docker-compose-4nodes.yml index cb7de2473..1144a970e 100644 --- a/ops/docker/docker-compose-4nodes.yml +++ b/ops/docker/docker-compose-4nodes.yml @@ -242,6 +242,7 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node0:${NODE_DATA_DIR}" - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" @@ -273,6 +274,8 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY:-} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node1:${NODE_DATA_DIR}" - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" @@ -305,6 +308,7 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node2:${NODE_DATA_DIR}" - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" @@ -337,6 +341,7 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node3:${NODE_DATA_DIR}" - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" @@ -393,6 +398,7 @@ services: - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node4:${NODE_DATA_DIR}" - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" @@ -449,6 +455,7 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_DERIVATION_VERIFY_MODE=layer1 - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node5:${NODE_DATA_DIR}" - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" From c8aa548cd4c26ef489dc9dfa699e60c10f322e51 Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 12:09:56 +0800 Subject: [PATCH 02/11] fix devnet reth cleanup targets --- Makefile | 8 ++++++++ ops/docker/docker-compose-reth.yml | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/Makefile b/Makefile index 9e1ec4e47..d0d84e1c4 100644 --- a/Makefile +++ b/Makefile @@ -199,6 +199,10 @@ devnet-down: cd ops/docker && docker compose $(DEVNET_COMPOSE_FILES) down .PHONY: devnet-down +devnet-down-reth: + $(MAKE) devnet-down EXECUTION_CLIENT=reth +.PHONY: devnet-down-reth + devnet-clean-build: devnet-l1-clean cd ops/docker && docker compose $(DEVNET_COMPOSE_FILES) down --volumes --remove-orphans docker volume ls --filter name=docker_ --format='{{.Name}}' | xargs docker volume rm 2>/dev/null || true @@ -218,6 +222,10 @@ devnet-clean: devnet-clean-build docker image ls '*sentry-*' --format='{{.Repository}}' | xargs -r docker rmi .PHONY: devnet-clean +devnet-clean-reth: + $(MAKE) devnet-clean EXECUTION_CLIENT=reth +.PHONY: devnet-clean-reth + devnet-l1: python3 ops/devnet-morph/main.py --polyrepo-dir=. --only-l1 diff --git a/ops/docker/docker-compose-reth.yml b/ops/docker/docker-compose-reth.yml index f66b471e8..accc2e655 100644 --- a/ops/docker/docker-compose-reth.yml +++ b/ops/docker/docker-compose-reth.yml @@ -40,3 +40,7 @@ services: sentry-el-0: <<: *reth-service build: !reset null + + sentry-el-1: + <<: *reth-service + build: !reset null From 6ce676f3ebcf869c24279454000f6241b81ebfad Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 12:41:27 +0800 Subject: [PATCH 03/11] simplify devnet single sequencer topology --- Makefile | 10 +- ops/devnet-morph/devnet/__init__.py | 28 +- ops/devnet-morph/devnet/setup_nodes.py | 36 +- ops/docker/Dockerfile.l2-node-4 | 79 ---- ops/docker/docker-compose-4nodes.yml | 426 ++-------------------- ops/docker/docker-compose-reth.yml | 14 - ops/docker/prometheus/prometheus.yml | 8 - ops/docker/static-nodes.json | 6 +- ops/docker/tendermint-devnet-genesis.json | 29 +- 9 files changed, 64 insertions(+), 572 deletions(-) delete mode 100644 ops/docker/Dockerfile.l2-node-4 diff --git a/Makefile b/Makefile index d0d84e1c4..f6b98ee9c 100644 --- a/Makefile +++ b/Makefile @@ -134,11 +134,9 @@ go-ubuntu-builder: fi .PHONY: go-ubuntu-builder -################## devnet 4 nodes #################### +################## devnet 2 nodes #################### EXECUTION_CLIENT ?= geth -DEVNET_SINGLE_SEQUENCER ?= true -DEVNET_SINGLE_SEQUENCER_ENABLED := $(filter true 1 yes,$(DEVNET_SINGLE_SEQUENCER)) DEVNET_SEQUENCER_PRIVATE_KEY ?= 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 DEVNET_SEQUENCER_ADDRESS ?= 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS ?= 0 @@ -177,7 +175,6 @@ endif devnet-up: $(DEVNET_EXECUTION_DEPS) go-ubuntu-builder python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) \ - $(if $(DEVNET_SINGLE_SEQUENCER_ENABLED),--single-sequencer,--pbft) \ --sequencer-private-key=$(DEVNET_SEQUENCER_PRIVATE_KEY) \ --sequencer-address=$(DEVNET_SEQUENCER_ADDRESS) \ --sequencer-upgrade-offset-seconds=$(DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS) @@ -189,7 +186,6 @@ devnet-up-reth: devnet-up-debugccc: $(DEVNET_EXECUTION_DEPS) go-ubuntu-builder python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) --debugccc \ - $(if $(DEVNET_SINGLE_SEQUENCER_ENABLED),--single-sequencer,--pbft) \ --sequencer-private-key=$(DEVNET_SEQUENCER_PRIVATE_KEY) \ --sequencer-address=$(DEVNET_SEQUENCER_ADDRESS) \ --sequencer-upgrade-offset-seconds=$(DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS) @@ -254,11 +250,11 @@ rebuild-all-tx-submitter: done stop-all-tx-submitter: @for submitter in $(SUBMITTERS); do \ - docker compose -f ./ops/docker-compose-4nodes.yml stop $$submitter; \ + docker compose -f ./ops/docker/docker-compose-4nodes.yml stop $$submitter; \ done start-all-tx-submitter: @for submitter in $(SUBMITTERS); do \ - docker compose -f ./ops/docker-compose-4nodes.yml start $$submitter; \ + docker compose -f ./ops/docker/docker-compose-4nodes.yml start $$submitter; \ done # build geth diff --git a/ops/devnet-morph/devnet/__init__.py b/ops/devnet-morph/devnet/__init__.py index 79ac79c27..1723b1aa9 100644 --- a/ops/devnet-morph/devnet/__init__.py +++ b/ops/devnet-morph/devnet/__init__.py @@ -23,10 +23,6 @@ parser.add_argument('--only-l1', help='Only bootstrap l1 geth', action="store_true") parser.add_argument('--execution-client', choices=('geth', 'reth'), default='geth', help='L2 execution client implementation to run') -parser.add_argument('--single-sequencer', dest='single_sequencer', action='store_true', default=True, - help='Start devnet in centralized single-sequencer mode (default)') -parser.add_argument('--pbft', dest='single_sequencer', action='store_false', - help='Keep the legacy PBFT-only devnet mode') parser.add_argument('--sequencer-private-key', default=os.environ.get( 'SEQUENCER_PRIVATE_KEY', @@ -41,7 +37,7 @@ help='L1Sequencer address expected to match --sequencer-private-key') parser.add_argument('--sequencer-upgrade-offset-seconds', type=int, default=int(os.environ.get('SEQUENCER_UPGRADE_OFFSET_SECONDS', '0')), - help='Seconds from now before PBFT switches to single-sequencer mode') + help='Seconds from now before single-sequencer mode activates') # parser.add_argument('--deploy', help='Whether the contracts should be predeployed or deployed', action="store_true") parser.add_argument('--debugccc', help='Whether set the debug log level for ccc', action="store_true") @@ -265,16 +261,12 @@ def devnet_deploy(paths, args): '--private-key', deploy_config['l2StakingPks'][i] ]) - sequencer_upgrade_time = 0 - if args.single_sequencer: - configure_l1_sequencer(paths, args, addresses, deploy_config) - sequencer_upgrade_time = int((time.time() + args.sequencer_upgrade_offset_seconds) * 1000) - log.info( - f'Single sequencer mode enabled: sequencer={args.sequencer_address}, ' - f'upgrade_time_ms={sequencer_upgrade_time}, ' - f'upgrade_offset_seconds={args.sequencer_upgrade_offset_seconds}') - else: - log.info('PBFT-only devnet mode enabled') + configure_l1_sequencer(paths, args, addresses, deploy_config) + sequencer_upgrade_time = int((time.time() + args.sequencer_upgrade_offset_seconds) * 1000) + log.info( + f'Single sequencer mode enabled: sequencer={args.sequencer_address}, ' + f'upgrade_time_ms={sequencer_upgrade_time}, ' + f'upgrade_offset_seconds={args.sequencer_upgrade_offset_seconds}') rust_log_level = 'info' if args.debugccc: @@ -296,8 +288,8 @@ def devnet_deploy(paths, args): env_data['Proxy__L1Staking'] = addresses['Proxy__L1Staking'] env_data['MORPH_L1STAKING'] = addresses['Proxy__L1Staking'] env_data['L1_SEQUENCER_CONTRACT'] = addresses.get('Proxy__L1Sequencer', '') - env_data['SEQUENCER_PRIVATE_KEY'] = args.sequencer_private_key if args.single_sequencer else '' - env_data['HA_SEQUENCER_ADDR'] = args.sequencer_address if args.single_sequencer else '' + env_data['SEQUENCER_PRIVATE_KEY'] = args.sequencer_private_key + env_data['HA_SEQUENCER_ADDR'] = args.sequencer_address env_data['SEQUENCER_UPGRADE_TIME'] = str(sequencer_upgrade_time) envfile.seek(0) for key, value in env_data.items(): @@ -321,7 +313,7 @@ def devnet_deploy(paths, args): 'L1_ETH_RPC': 'http://layer1-el:8545', 'L1_BEACON_CHAIN_RPC': 'http://layer1-cl:4000', 'L1_SEQUENCER_CONTRACT': addresses.get('Proxy__L1Sequencer', ''), - 'SEQUENCER_PRIVATE_KEY': args.sequencer_private_key if args.single_sequencer else '', + 'SEQUENCER_PRIVATE_KEY': args.sequencer_private_key, 'SEQUENCER_UPGRADE_TIME': str(sequencer_upgrade_time), }) wait_up(8545) diff --git a/ops/devnet-morph/devnet/setup_nodes.py b/ops/devnet-morph/devnet/setup_nodes.py index f4b925fe9..6c4dd16c2 100644 --- a/ops/devnet-morph/devnet/setup_nodes.py +++ b/ops/devnet-morph/devnet/setup_nodes.py @@ -32,20 +32,21 @@ def setup_devnet_nodes(): docker_dir = os.path.join(root_dir, "ops", "docker") devnet_dir = os.path.join(docker_dir, ".devnet") if os.path.exists(devnet_dir): - print(".devnet directory already exists. Devnet nodes setup has already been completed. Exiting.") - return + old_topology_paths = [os.path.join(devnet_dir, f"node{i}") for i in range(2, 6)] + if any(os.path.exists(path) for path in old_topology_paths): + print("Existing 6-node devnet detected. Regenerating 2-node single-sequencer config.") + shutil.rmtree(devnet_dir) + else: + print(".devnet directory already exists. Devnet nodes setup has already been completed. Exiting.") + return # Run the Tendermint testnet command print("Setting up the devnet...") command = [ - "tendermint", "testnet", "--v", "4", "--n", "2", "--o", devnet_dir, + "tendermint", "testnet", "--v", "1", "--n", "1", "--o", devnet_dir, "--populate-persistent-peers", "--hostname", "node-0", "--hostname", "node-1", - "--hostname", "node-2", - "--hostname", "node-3", - "--hostname", "sentry-node-0", - "--hostname", "sentry-node-1", ] if subprocess.call(command) != 0: @@ -55,15 +56,12 @@ def setup_devnet_nodes(): # Modify config.toml files using toml library print("Modifying config.toml files...") config_files = [ - os.path.join(devnet_dir, f"node{i}/config/config.toml") for i in range(6) + os.path.join(devnet_dir, f"node{i}/config/config.toml") for i in range(2) ] persistent_peers_value = ( "93e27ea2306e158a8146d5f44caaab97496797d2@node-0:26656," - "7f78b7d7a7e6bad4faf68d5731d437f4288d96d0@node-1:26656," - "06c699be2f9aeb9f7ec79f508a95ff80576deb12@node-2:26656," - "b1a131f40d5d3abefe0dd787513c936ef62ac2d6@node-3:26656," - "dae813274913aaf39e7cd3226a0aa8bce00644e1@sentry-node-0:26656" + "7f78b7d7a7e6bad4faf68d5731d437f4288d96d0@node-1:26656" ) for i, config_file in enumerate(config_files): @@ -84,8 +82,8 @@ def setup_devnet_nodes(): content = content.replace('block_sync = false', 'block_sync = true') content = re.sub(r'persistent_peers\s*=\s*".*?"', f'persistent_peers = "{persistent_peers_value}"', content) - # Modify pex for validator nodes. - if i < 4: + # Modify pex for the sequencer validator node. + if i == 0: content = content.replace('pex = true', 'pex = false') # Enable prometheus metrics for all nodes @@ -98,7 +96,7 @@ def setup_devnet_nodes(): # Copy key files to devnet node directories print("Copying key files...") - node_dirs = [f"node{i}" for i in range(6)] + node_dirs = [f"node{i}" for i in range(2)] for node in node_dirs: source_dir = os.path.join(docker_dir, node) @@ -114,8 +112,14 @@ def setup_devnet_nodes(): shutil.copyfile(os.path.join(source_dir, "node_key.json"), os.path.join(dest_dir, "node_key.json")) - if node not in ("node4", "node5"): + if node == "node0": shutil.copyfile(os.path.join(source_dir, "priv_validator_key.json"), os.path.join(dest_dir, "priv_validator_key.json")) + else: + priv_validator_key = os.path.join(dest_dir, "priv_validator_key.json") + priv_validator_state = os.path.join(devnet_dir, node, "data", "priv_validator_state.json") + for validator_file in (priv_validator_key, priv_validator_state): + if os.path.exists(validator_file): + os.remove(validator_file) # Copy and rename genesis file shutil.copyfile(os.path.join(docker_dir, "tendermint-devnet-genesis.json"), os.path.join(dest_dir, "genesis.json")) diff --git a/ops/docker/Dockerfile.l2-node-4 b/ops/docker/Dockerfile.l2-node-4 deleted file mode 100644 index cc00d03de..000000000 --- a/ops/docker/Dockerfile.l2-node-4 +++ /dev/null @@ -1,79 +0,0 @@ -# Build Geth in a stock Go builder container -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu as builder -RUN apt-get -qq update \ - && apt-get -qq install -y --no-install-recommends ca-certificates -COPY . /morph -WORKDIR /morph/node -RUN make build - - -FROM builder as initializer -COPY --from=builder /morph/node/build/bin/tendermint /usr/local/bin/ -RUN echo "Initializing tendermint." -RUN tendermint testnet --v 4 --n 1 --o /data --populate-persistent-peers --hostname node-0 --hostname node-1 --hostname node-2 --hostname node-3 --hostname sentry-node-0 -# populate for sequencer0-3 and sentry0 -RUN sed -i 's#pex = true#pex = false#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml -RUN sed -i 's#create_empty_blocks_interval = "0s"#create_empty_blocks_interval = "5s"#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -RUN sed -i 's#prometheus = false#prometheus = true#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -RUN sed -i 's#block_sync = false#block_sync = true#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -RUN sed -i 's#laddr = "tcp://127.0.0.1:26657"#laddr = "tcp://0.0.0.0:26657"#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -# overwrite p2p and consensus configs -RUN sed -i 's#peer_gossip_sleep_duration = "100ms"#peer_gossip_sleep_duration = "10ms"#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -RUN sed -i 's#flush_throttle_timeout = "100ms"#flush_throttle_timeout = "10ms"#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -RUN sed -i 's#max_packet_msg_payload_size = 1024#max_packet_msg_payload_size = 10485760#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -RUN sed -i 's#send_rate = 5120000#send_rate = 52428800#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml -RUN sed -i 's#recv_rate = 5120000#recv_rate = 102428800#g' /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml - -RUN persistent_peers_string="93e27ea2306e158a8146d5f44caaab97496797d2@node-0:26656,7f78b7d7a7e6bad4faf68d5731d437f4288d96d0@node-1:26656,06c699be2f9aeb9f7ec79f508a95ff80576deb12@node-2:26656,b1a131f40d5d3abefe0dd787513c936ef62ac2d6@node-3:26656,dae813274913aaf39e7cd3226a0aa8bce00644e1@sentry-node-0:26656" && \ -sed -i "s#persistent_peers = \".*\"#persistent_peers = \"$persistent_peers_string\"#g" /data/node0/config/config.toml /data/node1/config/config.toml /data/node2/config/config.toml /data/node3/config/config.toml /data/node4/config/config.toml && \ -sequencer0=$(echo $persistent_peers_string | cut -d "," -f 1 | cut -d "@" -f 1) && \ -sequencer1=$(echo $persistent_peers_string | cut -d "," -f 2 | cut -d "@" -f 1) && \ -sequencer2=$(echo $persistent_peers_string | cut -d "," -f 3 | cut -d "@" -f 1) && \ -sequencer3=$(echo $persistent_peers_string | cut -d "," -f 4 | cut -d "@" -f 1) && \ -sentry0=$(echo $persistent_peers_string | cut -d "," -f 5 | cut -d "@" -f 1) && \ -private_peer_ids_joined=$(echo $sequencer0,$sequencer1,$sequencer2,$sequencer3,$sentry0) && \ -unconditional_peer_ids_joined=$private_peer_ids_joined && \ -TO_REPLACED_PRIV_PEER_ID="private_peer_ids = \"$private_peer_ids_joined\"" && \ -sed -i "s#private_peer_ids = \"\"#$TO_REPLACED_PRIV_PEER_ID#g" /data/node4/config/config.toml && \ -TO_REPLACED_UNCONDITIONAL_PEER_ID="unconditional_peer_ids = \"$unconditional_peer_ids_joined\"" && \ -sed -i "s#unconditional_peer_ids = \"\"#$TO_REPLACED_UNCONDITIONAL_PEER_ID#g" /data/node4/config/config.toml - - -FROM builder as node0 -COPY --from=builder /morph/node/build/bin/morphnode /usr/local/bin/ -COPY --from=initializer /data/node0 /data -COPY ./ops/docker/node0/priv_validator_key.json /data/config/priv_validator_key.json -COPY ./ops/docker/node0/node_key.json /data/config/node_key.json -COPY ./ops/docker/tendermint-devnet-genesis.json /data/config/genesis.json -CMD ["morphnode", "--home", "/data"] - -FROM builder as node1 -COPY --from=builder /morph/node/build/bin/morphnode /usr/local/bin/ -COPY --from=initializer /data/node1 /data -COPY ./ops/docker/node1/priv_validator_key.json /data/config/priv_validator_key.json -COPY ./ops/docker/node1/node_key.json /data/config/node_key.json -COPY ./ops/docker/tendermint-devnet-genesis.json /data/config/genesis.json -CMD ["morphnode", "--home", "/data"] - -FROM builder as node2 -COPY --from=builder /morph/node/build/bin/morphnode /usr/local/bin/ -COPY --from=initializer /data/node2 /data -COPY ./ops/docker/node2/priv_validator_key.json /data/config/priv_validator_key.json -COPY ./ops/docker/node2/node_key.json /data/config/node_key.json -COPY ./ops/docker/tendermint-devnet-genesis.json /data/config/genesis.json -CMD ["morphnode", "--home", "/data"] - -FROM builder as node3 -COPY --from=builder /morph/node/build/bin/morphnode /usr/local/bin/ -COPY --from=initializer /data/node3 /data -COPY ./ops/docker/node3/priv_validator_key.json /data/config/priv_validator_key.json -COPY ./ops/docker/node3/node_key.json /data/config/node_key.json -COPY ./ops/docker/tendermint-devnet-genesis.json /data/config/genesis.json -CMD ["morphnode", "--home", "/data"] - -FROM builder as sentry0 -COPY --from=builder /morph/node/build/bin/morphnode /usr/local/bin/ -COPY --from=initializer /data/node4 /data -COPY ./ops/docker/node4/node_key.json /data/config/node_key.json -COPY ./ops/docker/tendermint-devnet-genesis.json /data/config/genesis.json -CMD ["morphnode", "--home", "/data"] diff --git a/ops/docker/docker-compose-4nodes.yml b/ops/docker/docker-compose-4nodes.yml index 1144a970e..06469dab5 100644 --- a/ops/docker/docker-compose-4nodes.yml +++ b/ops/docker/docker-compose-4nodes.yml @@ -1,20 +1,6 @@ -version: '3.8' - volumes: - l1_execution: - l1_consensus: - postgres_data: morph_data_0: morph_data_1: - morph_data_2: - morph_data_3: - sentry_el_data: - sentry_el_data_1: - node_data_0: - node_data_1: - node_data_2: - node_data_3: - sentry_node_data: layer1-el-data: layer1-cl-data: layer1-vc-data: @@ -25,9 +11,9 @@ services: image: ethereum/client-go:latest container_name: layer1-el ports: - - "9545:8545" # RPC - - "9546:8546" # WebSocket - - "8551:8551" # Engine API + - "9545:8545" + - "9546:8546" + - "8551:8551" volumes: - ./layer1/genesis:/network-configs:ro - ./layer1/jwt:/jwt:ro @@ -51,11 +37,11 @@ services: depends_on: - layer1-el ports: - - "4000:4000" # HTTP API - - "9000:9000" # P2P TCP/UDP - - "9000:9000/udp" # P2P UDP - - "9001:9001" # P2P QUIC - - "5054:5054" # Metrics + - "4000:4000" + - "9000:9000" + - "9000:9000/udp" + - "9001:9001" + - "5054:5054" volumes: - ./layer1/genesis:/network-configs:ro - ./layer1/jwt:/jwt:ro @@ -114,7 +100,7 @@ services: - --metrics-port=5064 restart: unless-stopped - # ========== L2 Services ========== + # ========== L2 Execution Clients ========== morph-el-0: container_name: morph-el-0 depends_on: @@ -166,55 +152,7 @@ services: - "/bin/bash" - "/entrypoint.sh" - morph-el-2: - container_name: morph-el-2 - depends_on: - - morph-el-0 - image: morph-geth:latest - restart: unless-stopped - ports: - - "8745:8545" - - "8746:8546" - - "8551" - - "6060" - - "30303" - volumes: - - "morph_data_2:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - - "${PWD}/nodekey2:/db/geth/nodekey" - - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" - environment: - - RUST_LOG=${RUST_LOG} - entrypoint: - - "/bin/bash" - - "/entrypoint.sh" - - morph-el-3: - container_name: morph-el-3 - depends_on: - - morph-el-0 - image: morph-geth:latest - restart: unless-stopped - ports: - - "8845:8545" - - "8846:8546" - - "8551" - - "6060" - - "30303" - volumes: - - "morph_data_3:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - - "${PWD}/nodekey3:/db/geth/nodekey" - - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" - environment: - - RUST_LOG=${RUST_LOG} - entrypoint: - - "/bin/bash" - - "/entrypoint.sh" - - + # ========== L2 Nodes ========== node-0: container_name: node-0 depends_on: @@ -242,6 +180,7 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY} - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node0:${NODE_DATA_DIR}" @@ -253,6 +192,8 @@ services: node-1: container_name: node-1 depends_on: + morph-el-1: + condition: service_started node-0: condition: service_started image: morph-node:latest @@ -263,6 +204,7 @@ services: - "26658" - "26660" environment: + - EMPTY_BLOCK_DELAY=true - MORPH_NODE_L2_ETH_RPC=http://morph-el-1:8545 - MORPH_NODE_L2_ENGINE_RPC=http://morph-el-1:8551 - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} @@ -274,7 +216,6 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY:-} - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node1:${NODE_DATA_DIR}" @@ -283,324 +224,13 @@ services: morphnode --home $NODE_DATA_DIR - node-2: - container_name: node-2 - depends_on: - node-0: - condition: service_started - image: morph-node:latest - restart: unless-stopped - ports: - - "26656" - - "26657" - - "26658" - - "26660" - environment: - - EMPTY_BLOCK_DELAY=true - - MORPH_NODE_L2_ETH_RPC=http://morph-el-2:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://morph-el-2:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - volumes: - - ".devnet/node2:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - - node-3: - container_name: node-3 - depends_on: - node-0: - condition: service_started - image: morph-node:latest - restart: unless-stopped - ports: - - "26656" - - "26657" - - "26658" - - "26660" - environment: - - EMPTY_BLOCK_DELAY=true - - MORPH_NODE_L2_ETH_RPC=http://morph-el-3:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://morph-el-3:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - volumes: - - ".devnet/node3:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - - sentry-el-0: - container_name: sentry-el-0 - depends_on: - node-3: - condition: service_started - image: morph-geth:latest - build: - context: ../.. - dockerfile: ops/docker/Dockerfile.l2-geth - restart: unless-stopped - ports: - - "8945:8545" - - "8946:8546" - - "8551" - - "6060" - - "30303" - volumes: - - "sentry_el_data:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" - entrypoint: - - "/bin/sh" - - "/entrypoint.sh" - - sentry-node-0: - container_name: sentry-node-0 - depends_on: - node-0: - condition: service_started - image: morph-node:latest - restart: unless-stopped - ports: - - "26656" - - "26657" - - "26658" - - "26660" - environment: - - EMPTY_BLOCK_DELAY=true - - MORPH_NODE_L2_ETH_RPC=http://sentry-el-0:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://sentry-el-0:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - volumes: - - ".devnet/node4:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - - sentry-el-1: - container_name: sentry-el-1 - depends_on: - node-0: - condition: service_started - image: morph-geth:latest - build: - context: ../.. - dockerfile: ops/docker/Dockerfile.l2-geth - restart: unless-stopped - ports: - - "9045:8545" - - "9046:8546" - - "8551" - - "6060" - - "30303" - volumes: - - "sentry_el_data_1:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - entrypoint: - - "/bin/sh" - - "/entrypoint.sh" - - sentry-node-1: - container_name: sentry-node-1 - depends_on: - sentry-el-1: - condition: service_started - image: morph-node:latest - restart: unless-stopped - ports: - - "26656" - - "26657" - - "26658" - - "26660" - environment: - - EMPTY_BLOCK_DELAY=true - - MORPH_NODE_L2_ETH_RPC=http://sentry-el-1:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://sentry-el-1:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_DERIVATION_VERIFY_MODE=layer1 - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - volumes: - - ".devnet/node5:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - + # ========== Submitter and Oracle ========== tx-submitter-0: - container_name: tx-submitter-0 - depends_on: - node-0: - condition: service_started - build: - context: ../.. - dockerfile: ops/docker/Dockerfile.submitter - image: morph-tx-submitter:latest - restart: unless-stopped - command: tx-submitter - ports: - - "8060:6060" - environment: - # change the env variables to your own - - TX_SUBMITTER_BUILD_ENV=dev - - TX_SUBMITTER_L1_ETH_RPC=${L1_ETH_RPC} - - TX_SUBMITTER_L1_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 - - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545 - - TX_SUBMITTER_MAX_BATCH_BUILD_TIME=60s - - TX_SUBMITTER_MAX_TX_SIZE=125952 - - TX_SUBMITTER_POLL_INTERVAL=3s - - TX_SUBMITTER_SAFE_MINIMUM_ETHER_BALANCE=1 - - TX_SUBMITTER_TX_TIMEOUT=60s - - TX_SUBMITTER_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - TX_SUBMITTER_NETWORK_TIMEOUT=20s - - TX_SUBMITTER_MAX_BLOCK=1000 - - TX_SUBMITTER_MIN_BLOCK=50 - - TX_SUBMITTER_FINALIZE=true - - TX_SUBMITTER_MAX_FINALIZE_NUM=100 - - TX_SUBMITTER_PRIORITY_ROLLUP=true - - TX_SUBMITTER_SEAL_BATCH=true - - TX_SUBMITTER_METRICS_SERVER_ENABLE=false - - TX_SUBMITTER_METRICS_HOSTNAME=0.0.0.0 - - TX_SUBMITTER_METRICS_PORT=6060 - - TX_SUBMITTER_TX_FEE_LIMIT=500000000000000000 #0.5e - - TX_SUBMITTER_LOG_FILENAME=tx_submitter.log - - TX_SUBMITTER_LOG_FILE_MAX_SIZE=100 #MB - - TX_SUBMITTER_LOG_FILE_MAX_AGE=7 #day - - TX_SUBMITTER_LOG_COMPRESS=true - - TX_SUBMITTER_L1_STAKING_ADDRESS=${MORPH_L1STAKING:-0x5fc8d32690cc91d4c39d9d3abcbd16989f875707} - - TX_SUBMITTER_L1_STAKING_DEPLOYED_BLOCKNUM=0 - - TX_SUBMITTER_SEAL_BATCH=true - - TX_SUBMITTER_BATCH_V2_UPGRADE_TIME=1777533291 - - tx-submitter-1: - container_name: tx-submitter-1 - profiles: ["multi-submitter"] - depends_on: - node-1: - condition: service_started - build: - context: ../.. - dockerfile: ops/docker/Dockerfile.submitter - image: morph-tx-submitter:latest - restart: unless-stopped - command: tx-submitter - ports: - - "8061:6060" - environment: - # change the env variables to your own - - TX_SUBMITTER_BUILD_ENV=dev - - TX_SUBMITTER_L1_ETH_RPC=${L1_ETH_RPC} - - TX_SUBMITTER_L1_PRIVATE_KEY=0x0890c388c3bf5e04fee1d8f3c117e5f44f435ced7baf7bfd66c10e1f3a3f4b10 - - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545 - - TX_SUBMITTER_MAX_BATCH_BUILD_TIME=60s - - TX_SUBMITTER_MAX_TX_SIZE=125952 - - TX_SUBMITTER_POLL_INTERVAL=3s - - TX_SUBMITTER_SAFE_MINIMUM_ETHER_BALANCE=1 - - TX_SUBMITTER_TX_TIMEOUT=60s - - TX_SUBMITTER_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - TX_SUBMITTER_NETWORK_TIMEOUT=20s - - TX_SUBMITTER_MAX_BLOCK=1000 - - TX_SUBMITTER_MIN_BLOCK=50 - - TX_SUBMITTER_FINALIZE=false - - TX_SUBMITTER_MAX_FINALIZE_NUM=100 - - TX_SUBMITTER_PRIORITY_ROLLUP=false - - TX_SUBMITTER_SEAL_BATCH=true - - TX_SUBMITTER_METRICS_SERVER_ENABLE=false - - TX_SUBMITTER_METRICS_HOSTNAME=0.0.0.0 - - TX_SUBMITTER_METRICS_PORT=6060 - - TX_SUBMITTER_TX_FEE_LIMIT=500000000000000000 #0.5e - - TX_SUBMITTER_LOG_FILENAME=tx_submitter.log - - TX_SUBMITTER_LOG_FILE_MAX_SIZE=100 - - TX_SUBMITTER_LOG_FILE_MAX_AGE=7 - - TX_SUBMITTER_LOG_COMPRESS=true - - TX_SUBMITTER_L1_STAKING_ADDRESS=${MORPH_L1STAKING:-0x5fc8d32690cc91d4c39d9d3abcbd16989f875707} - - TX_SUBMITTER_L1_STAKING_DEPLOYED_BLOCKNUM=0 - - tx-submitter-2: - container_name: tx-submitter-2 - profiles: ["multi-submitter"] + container_name: tx-submitter-0 depends_on: - node-2: + node-0: condition: service_started - build: - context: ../.. - dockerfile: ops/docker/Dockerfile.submitter - image: morph-tx-submitter:latest - restart: unless-stopped - command: tx-submitter - ports: - - "8062:6060" - environment: - # change the env variables to your own - - TX_SUBMITTER_BUILD_ENV=dev - - TX_SUBMITTER_L1_ETH_RPC=${L1_ETH_RPC} - - TX_SUBMITTER_L1_PRIVATE_KEY=0x6fd437eef7a83c486bd2e0a802ae071b3912d125ac31ac08f60841fd891559ae - - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-2:8545,http://morph-el-3:8545 - - TX_SUBMITTER_MAX_BATCH_BUILD_TIME=60s - - TX_SUBMITTER_MAX_TX_SIZE=125952 - - TX_SUBMITTER_POLL_INTERVAL=3s - - TX_SUBMITTER_SAFE_MINIMUM_ETHER_BALANCE=1 - - TX_SUBMITTER_TX_TIMEOUT=60s - - TX_SUBMITTER_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - TX_SUBMITTER_NETWORK_TIMEOUT=20s - - TX_SUBMITTER_MAX_BLOCK=1000 - - TX_SUBMITTER_MIN_BLOCK=50 - - TX_SUBMITTER_FINALIZE=false - - TX_SUBMITTER_MAX_FINALIZE_NUM=100 - - TX_SUBMITTER_PRIORITY_ROLLUP=false - - TX_SUBMITTER_SEAL_BATCH=true - - TX_SUBMITTER_METRICS_SERVER_ENABLE=false - - TX_SUBMITTER_METRICS_HOSTNAME=0.0.0.0 - - TX_SUBMITTER_METRICS_PORT=6060 - - TX_SUBMITTER_TX_FEE_LIMIT=500000000000000000 #0.5e - - TX_SUBMITTER_LOG_FILENAME=tx_submitter.log - - TX_SUBMITTER_LOG_FILE_MAX_SIZE=100 - - TX_SUBMITTER_LOG_FILE_MAX_AGE=7 - - TX_SUBMITTER_LOG_COMPRESS=true - - TX_SUBMITTER_L1_STAKING_ADDRESS=${MORPH_L1STAKING:-0x5fc8d32690cc91d4c39d9d3abcbd16989f875707} - - TX_SUBMITTER_L1_STAKING_DEPLOYED_BLOCKNUM=0 - - tx-submitter-3: - container_name: tx-submitter-3 - profiles: ["multi-submitter"] - depends_on: - node-3: + node-1: condition: service_started build: context: ../.. @@ -609,13 +239,12 @@ services: restart: unless-stopped command: tx-submitter ports: - - "8063:6060" + - "8060:6060" environment: - # change the env variables to your own - TX_SUBMITTER_BUILD_ENV=dev - TX_SUBMITTER_L1_ETH_RPC=${L1_ETH_RPC} - - TX_SUBMITTER_L1_PRIVATE_KEY=0x9ae53aecdaebe4dcbfec96f3123a2a8c53f9596bf4b3d5adc9a388ccb361b4c0 - - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-2:8545,http://morph-el-3:8545 + - TX_SUBMITTER_L1_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 + - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545 - TX_SUBMITTER_MAX_BATCH_BUILD_TIME=60s - TX_SUBMITTER_MAX_TX_SIZE=125952 - TX_SUBMITTER_POLL_INTERVAL=3s @@ -625,25 +254,26 @@ services: - TX_SUBMITTER_NETWORK_TIMEOUT=20s - TX_SUBMITTER_MAX_BLOCK=1000 - TX_SUBMITTER_MIN_BLOCK=50 - - TX_SUBMITTER_FINALIZE=false + - TX_SUBMITTER_FINALIZE=true - TX_SUBMITTER_MAX_FINALIZE_NUM=100 - - TX_SUBMITTER_PRIORITY_ROLLUP=false + - TX_SUBMITTER_PRIORITY_ROLLUP=true - TX_SUBMITTER_SEAL_BATCH=true - TX_SUBMITTER_METRICS_SERVER_ENABLE=false - TX_SUBMITTER_METRICS_HOSTNAME=0.0.0.0 - TX_SUBMITTER_METRICS_PORT=6060 - - TX_SUBMITTER_TX_FEE_LIMIT=500000000000000000 #0.5e + - TX_SUBMITTER_TX_FEE_LIMIT=500000000000000000 - TX_SUBMITTER_LOG_FILENAME=tx_submitter.log - TX_SUBMITTER_LOG_FILE_MAX_SIZE=100 - TX_SUBMITTER_LOG_FILE_MAX_AGE=7 - TX_SUBMITTER_LOG_COMPRESS=true - TX_SUBMITTER_L1_STAKING_ADDRESS=${MORPH_L1STAKING:-0x5fc8d32690cc91d4c39d9d3abcbd16989f875707} - TX_SUBMITTER_L1_STAKING_DEPLOYED_BLOCKNUM=0 + - TX_SUBMITTER_BATCH_V2_UPGRADE_TIME=1777533291 gas-price-oracle: container_name: gas-price-oracle depends_on: - node-3: + node-0: condition: service_started build: context: ../.. @@ -667,5 +297,5 @@ services: - TXN_PER_BLOCK=1 - TXN_PER_BATCH=50 - L1_ROLLUP=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - OVERHEAD_SWITCH=${OVERHEAD_SWITCH} - - MAX_OVERHEAD=${MAX_OVERHEAD} + - OVERHEAD_SWITCH=${OVERHEAD_SWITCH:-} + - MAX_OVERHEAD=${MAX_OVERHEAD:-} diff --git a/ops/docker/docker-compose-reth.yml b/ops/docker/docker-compose-reth.yml index accc2e655..6d18823f4 100644 --- a/ops/docker/docker-compose-reth.yml +++ b/ops/docker/docker-compose-reth.yml @@ -30,17 +30,3 @@ services: morph-el-1: <<: *reth-service - - morph-el-2: - <<: *reth-service - - morph-el-3: - <<: *reth-service - - sentry-el-0: - <<: *reth-service - build: !reset null - - sentry-el-1: - <<: *reth-service - build: !reset null diff --git a/ops/docker/prometheus/prometheus.yml b/ops/docker/prometheus/prometheus.yml index 471eec362..5b7c40ea9 100644 --- a/ops/docker/prometheus/prometheus.yml +++ b/ops/docker/prometheus/prometheus.yml @@ -19,10 +19,6 @@ scrape_configs: - targets: - 'node-0:26660' - 'node-1:26660' - - 'node-2:26660' - - 'node-3:26660' - - 'sentry-node-0:26660' - - 'sentry-node-1:26660' labels: service: 'morph-node' @@ -32,9 +28,5 @@ scrape_configs: - targets: - 'morph-el-0:6060' - 'morph-el-1:6060' - - 'morph-el-2:6060' - - 'morph-el-3:6060' - - 'sentry-el-0:6060' - - 'sentry-el-1:6060' labels: service: 'morph-geth' diff --git a/ops/docker/static-nodes.json b/ops/docker/static-nodes.json index 7502f805e..3cf330e1a 100644 --- a/ops/docker/static-nodes.json +++ b/ops/docker/static-nodes.json @@ -1,5 +1,3 @@ ["enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303", - "enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303", - "enode://c91a993ace50749c89d37d554f12b2f4937d2ecca0232695bb33772d95a01f53564ad9dd71465c229be21e231e5c46929c2adaa78bea9d5f0966c46fca327c46@morph-el-2:30303", - "enode://7211a9f1d896d6fef69154b97a868f1ac59e178eadfa54c3fc9644fa0f25ba2a0771927acdc08bb1d6ae2ea7a64f7ed9ddd74e97472e7d2e0df66dae5608fb10@morph-el-3:30303" -] \ No newline at end of file + "enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303" +] diff --git a/ops/docker/tendermint-devnet-genesis.json b/ops/docker/tendermint-devnet-genesis.json index 3c7aa3b37..fa3f4ac8c 100644 --- a/ops/docker/tendermint-devnet-genesis.json +++ b/ops/docker/tendermint-devnet-genesis.json @@ -36,34 +36,7 @@ }, "power": "1", "name": "node0" - }, - { - "address": "91D1B7E7CD592248E14081718C53AE2029799BE6", - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "t5jrdMBnIdVMZZ6eorwjKn+V6W0jTMhxhrKrj0PbaTU=" - }, - "power": "1", - "name": "node1" - }, - { - "address": "65CB9C7CEE29940275F0E883F2AE69B524B57DEA", - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "7P+tARKXhrqcYpOqZk+VKJTcQBl2KAR5nfof+25O0EA=" - }, - "power": "1", - "name": "node2" - }, - { - "address": "1F430C64E8B71FD6AB8C927731CDB4091CDF0EC2", - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "jSlpW7QVfGlgrbSGvh2aDJ9yhSTQm//MT4ky7RUiHEo=" - }, - "power": "1", - "name": "node3" } ], "app_hash": "" -} \ No newline at end of file +} From 2775efc7675982435e668f497038e1e655eff3bb Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 12:50:18 +0800 Subject: [PATCH 04/11] rename devnet compose file --- Makefile | 10 +++++----- ops/devnet-morph/devnet/__init__.py | 8 ++++---- ops/docker/Makefile.layer1 | 4 ++-- ...er-compose-4nodes.yml => docker-compose-devnet.yml} | 0 ops/docker/layer1/scripts/clean.sh | 7 +++---- ops/docker/layer1/scripts/start.sh | 6 +++--- 6 files changed, 17 insertions(+), 18 deletions(-) rename ops/docker/{docker-compose-4nodes.yml => docker-compose-devnet.yml} (100%) diff --git a/Makefile b/Makefile index f6b98ee9c..2a2693999 100644 --- a/Makefile +++ b/Makefile @@ -158,7 +158,7 @@ export MORPH_RETH_BUILD_PROFILE export MORPH_RETH_RUSTFLAGS export MORPH_RETH_DOCKER_TARGET export MORPH_RETH_ENTRYPOINT -DEVNET_COMPOSE_FILES := -f docker-compose-4nodes.yml +DEVNET_COMPOSE_FILES := -f docker-compose-devnet.yml ifeq ($(EXECUTION_CLIENT),geth) DEVNET_EXECUTION_DEPS := submodules @@ -243,18 +243,18 @@ reth: .PHONY: reth # tx-submitter -SUBMITTERS := $(shell grep -o 'tx-submitter-[0-9]*[^:]' ops/docker/docker-compose-4nodes.yml | sort | uniq) +SUBMITTERS := $(shell grep -o 'tx-submitter-[0-9]*[^:]' ops/docker/docker-compose-devnet.yml | sort | uniq) rebuild-all-tx-submitter: @for submitter in $(SUBMITTERS); do \ - docker compose -f ./ops/docker/docker-compose-4nodes.yml up -d --build $$submitter --no-deps; \ + docker compose -f ./ops/docker/docker-compose-devnet.yml up -d --build $$submitter --no-deps; \ done stop-all-tx-submitter: @for submitter in $(SUBMITTERS); do \ - docker compose -f ./ops/docker/docker-compose-4nodes.yml stop $$submitter; \ + docker compose -f ./ops/docker/docker-compose-devnet.yml stop $$submitter; \ done start-all-tx-submitter: @for submitter in $(SUBMITTERS); do \ - docker compose -f ./ops/docker/docker-compose-4nodes.yml start $$submitter; \ + docker compose -f ./ops/docker/docker-compose-devnet.yml start $$submitter; \ done # build geth diff --git a/ops/devnet-morph/devnet/__init__.py b/ops/devnet-morph/devnet/__init__.py index 1723b1aa9..bd772acf2 100644 --- a/ops/devnet-morph/devnet/__init__.py +++ b/ops/devnet-morph/devnet/__init__.py @@ -49,7 +49,7 @@ def compose_file_args(execution_client): """Return docker-compose -f flags for the chosen L2 execution client.""" - args = ['-f', 'docker-compose-4nodes.yml'] + args = ['-f', 'docker-compose-devnet.yml'] if execution_client == 'reth': args.extend(['-f', 'docker-compose-reth.yml']) return args @@ -124,7 +124,7 @@ def devnet_l1(paths, result=None): # Start layer1 services log.info('Starting layer1 services (layer1-el, layer1-cl, layer1-vc)...') - run_command(['docker', 'compose', '-f', 'docker-compose-4nodes.yml', 'up', '-d', + run_command(['docker', 'compose', '-f', 'docker-compose-devnet.yml', 'up', '-d', 'layer1-el', 'layer1-cl', 'layer1-vc'], check=False, cwd=paths.ops_dir, env={ 'PWD': paths.ops_dir }) @@ -167,8 +167,8 @@ def devnet_l1(paths, result=None): def devnet_build(paths): - """Build the docker images declared in docker-compose-4nodes.yml.""" - run_command(['docker', 'compose', '-f', 'docker-compose-4nodes.yml', 'build'], cwd=paths.ops_dir, env={ + """Build the docker images declared in docker-compose-devnet.yml.""" + run_command(['docker', 'compose', '-f', 'docker-compose-devnet.yml', 'build'], cwd=paths.ops_dir, env={ 'PWD': paths.ops_dir, 'DOCKER_BUILDKIT': '1', # (should be available by default in later versions, but explicitly enable it anyway) 'COMPOSE_DOCKER_CLI_BUILD': '1' # use the docker cache diff --git a/ops/docker/Makefile.layer1 b/ops/docker/Makefile.layer1 index d52aef375..9a3b6ba36 100644 --- a/ops/docker/Makefile.layer1 +++ b/ops/docker/Makefile.layer1 @@ -16,12 +16,12 @@ start: @./layer1/scripts/start.sh stop: - @docker compose -f docker-compose-4nodes.yml down + @docker compose -f docker-compose-devnet.yml down restart: stop start logs: - @docker compose -f docker-compose-4nodes.yml logs -f layer1-el layer1-cl layer1-vc + @docker compose -f docker-compose-devnet.yml logs -f layer1-el layer1-cl layer1-vc clean: @./layer1/scripts/clean.sh diff --git a/ops/docker/docker-compose-4nodes.yml b/ops/docker/docker-compose-devnet.yml similarity index 100% rename from ops/docker/docker-compose-4nodes.yml rename to ops/docker/docker-compose-devnet.yml diff --git a/ops/docker/layer1/scripts/clean.sh b/ops/docker/layer1/scripts/clean.sh index 934612b3a..08533b09f 100755 --- a/ops/docker/layer1/scripts/clean.sh +++ b/ops/docker/layer1/scripts/clean.sh @@ -14,8 +14,8 @@ cd "$PROJECT_DIR/.." # Stop and remove only layer1 containers echo "Stopping and removing layer1 containers..." -docker compose -f docker-compose-4nodes.yml stop layer1-el layer1-cl layer1-vc 2>/dev/null || true -docker compose -f docker-compose-4nodes.yml rm -f layer1-el layer1-cl layer1-vc 2>/dev/null || true +docker compose -f docker-compose-devnet.yml stop layer1-el layer1-cl layer1-vc 2>/dev/null || true +docker compose -f docker-compose-devnet.yml rm -f layer1-el layer1-cl layer1-vc 2>/dev/null || true # Remove layer1 volumes echo "Removing layer1 volumes..." @@ -81,7 +81,6 @@ echo "Preserved:" echo " - Configuration templates (layer1/configs/values.env.template)" echo " - JWT secret (layer1/jwt/jwtsecret)" echo " - Keystores (layer1/keystores/)" -echo " - Docker Compose file (docker-compose-4nodes.yml)" +echo " - Docker Compose file (docker-compose-devnet.yml)" echo "" echo "You can now run 'make generate' and 'make start' again to create a fresh network." - diff --git a/ops/docker/layer1/scripts/start.sh b/ops/docker/layer1/scripts/start.sh index b57775b5d..7a732c7f2 100755 --- a/ops/docker/layer1/scripts/start.sh +++ b/ops/docker/layer1/scripts/start.sh @@ -26,7 +26,7 @@ echo "=== Starting Ethereum Network ===" cd "$PROJECT_DIR/.." # Start all services -docker compose -f docker-compose-4nodes.yml up -d layer1-el layer1-cl layer1-vc +docker compose -f docker-compose-devnet.yml up -d layer1-el layer1-cl layer1-vc echo "" echo "Waiting for containers to start..." @@ -52,7 +52,7 @@ echo " Layer1 CL HTTP API: http://localhost:4000" echo " Layer1 CL Metrics: http://localhost:5054" echo "" echo "View logs:" -echo " docker compose -f docker-compose-4nodes.yml logs -f layer1-el layer1-cl layer1-vc" +echo " docker compose -f docker-compose-devnet.yml logs -f layer1-el layer1-cl layer1-vc" echo "" echo "Stop network:" -echo " docker compose -f docker-compose-4nodes.yml down" +echo " docker compose -f docker-compose-devnet.yml down" From d2f7fa4cb828095e2c7980037f2ca63aa79b78cc Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 13:16:24 +0800 Subject: [PATCH 05/11] clean up devnet docker files --- Makefile | 6 +- ops/docker/Dockerfile.indexer | 17 - ops/docker/Dockerfile.l1 | 9 - ops/docker/Dockerfile.l1-beacon | 21 - ops/docker/Dockerfile.l2-node-1 | 26 - ops/docker/Dockerfile.token-price-oracle | 17 - ops/docker/consensus/config.yml | 30 - ops/docker/docker-compose-devnet.yml | 26 +- ops/docker/entrypoint-l1.sh | 72 - ops/docker/execution/genesis.json | 121 -- ops/docker/execution/jwtsecret | 1 - ...--ca062b0fd91172d89bcd4bb084ac4e21972cc467 | 1 - ops/docker/execution/password | 1 - ops/docker/go-rust-builder.Dockerfile | 27 - ops/docker/grafana/dashboards/dashboards.yml | 9 - .../grafana/dashboards/json/morph-node.json | 1279 ----------------- ops/docker/grafana/datasources/prometheus.yml | 9 - ops/docker/layer1/docker-compose.yml | 116 -- ops/docker/node0/eth_acc_key.json | 4 - ops/docker/node1/eth_acc_key.json | 4 - ops/docker/node1/priv_validator_key.json | 11 - ops/docker/node2/eth_acc_key.json | 4 - ops/docker/node2/node_key.json | 1 - ops/docker/node2/priv_validator_key.json | 11 - ops/docker/node3/eth_acc_key.json | 4 - ops/docker/node3/node_key.json | 1 - ops/docker/node3/priv_validator_key.json | 11 - ops/docker/node4/node_key.json | 1 - ops/docker/node5/node_key.json | 1 - ops/docker/nodekey2 | 1 - ops/docker/nodekey3 | 1 - ops/docker/prometheus/prometheus.yml | 32 - ops/docker/tendermint-setup.sh | 40 - 33 files changed, 4 insertions(+), 1911 deletions(-) delete mode 100644 ops/docker/Dockerfile.indexer delete mode 100644 ops/docker/Dockerfile.l1 delete mode 100644 ops/docker/Dockerfile.l1-beacon delete mode 100644 ops/docker/Dockerfile.l2-node-1 delete mode 100644 ops/docker/Dockerfile.token-price-oracle delete mode 100644 ops/docker/consensus/config.yml delete mode 100644 ops/docker/entrypoint-l1.sh delete mode 100644 ops/docker/execution/genesis.json delete mode 100755 ops/docker/execution/jwtsecret delete mode 100644 ops/docker/execution/keystore/UTC--2024-02-05T07-24-26.460740423Z--ca062b0fd91172d89bcd4bb084ac4e21972cc467 delete mode 100644 ops/docker/execution/password delete mode 100644 ops/docker/go-rust-builder.Dockerfile delete mode 100644 ops/docker/grafana/dashboards/dashboards.yml delete mode 100644 ops/docker/grafana/dashboards/json/morph-node.json delete mode 100644 ops/docker/grafana/datasources/prometheus.yml delete mode 100644 ops/docker/layer1/docker-compose.yml delete mode 100644 ops/docker/node0/eth_acc_key.json delete mode 100644 ops/docker/node1/eth_acc_key.json delete mode 100644 ops/docker/node1/priv_validator_key.json delete mode 100644 ops/docker/node2/eth_acc_key.json delete mode 100644 ops/docker/node2/node_key.json delete mode 100644 ops/docker/node2/priv_validator_key.json delete mode 100644 ops/docker/node3/eth_acc_key.json delete mode 100644 ops/docker/node3/node_key.json delete mode 100644 ops/docker/node3/priv_validator_key.json delete mode 100644 ops/docker/node4/node_key.json delete mode 100644 ops/docker/node5/node_key.json delete mode 100644 ops/docker/nodekey2 delete mode 100644 ops/docker/nodekey3 delete mode 100644 ops/docker/prometheus/prometheus.yml delete mode 100644 ops/docker/tendermint-setup.sh diff --git a/Makefile b/Makefile index 2a2693999..5be878908 100644 --- a/Makefile +++ b/Makefile @@ -201,12 +201,10 @@ devnet-down-reth: devnet-clean-build: devnet-l1-clean cd ops/docker && docker compose $(DEVNET_COMPOSE_FILES) down --volumes --remove-orphans - docker volume ls --filter name=docker_ --format='{{.Name}}' | xargs docker volume rm 2>/dev/null || true + docker volume rm docker_morph_data_0 docker_morph_data_1 docker_morph_data_2 docker_morph_data_3 docker_sentry_el_data docker_sentry_el_data_1 2>/dev/null || true rm -rf ops/l2-genesis/.devnet rm -rf ops/docker/.devnet - rm -rf ops/docker/consensus/beacondata ops/docker/consensus/validatordata ops/docker/consensus/genesis.ssz - rm -rf ops/docker/execution/geth - rm -rf ops/docker/execution/reth + rm -rf ops/docker/consensus ops/docker/execution .PHONY: devnet-clean-build devnet-clean-build-reth: diff --git a/ops/docker/Dockerfile.indexer b/ops/docker/Dockerfile.indexer deleted file mode 100644 index 1d6dc3f83..000000000 --- a/ops/docker/Dockerfile.indexer +++ /dev/null @@ -1,17 +0,0 @@ -FROM golang:1.24.0-alpine as builder - -RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash - -COPY ./bridge-backend /bridge-backend - -WORKDIR /bridge-backend/indexer - -RUN make - -FROM alpine:latest - -COPY --from=builder /bridge-backend/indexer/indexer /usr/local/bin - -CMD ["indexer"] - -EXPOSE 8080 diff --git a/ops/docker/Dockerfile.l1 b/ops/docker/Dockerfile.l1 deleted file mode 100644 index 16ac1be45..000000000 --- a/ops/docker/Dockerfile.l1 +++ /dev/null @@ -1,9 +0,0 @@ -FROM ethereum/client-go:v1.14.11 - -RUN apk add --no-cache jq - -COPY ops/docker/entrypoint-l1.sh /entrypoint.sh - -VOLUME ["/db"] - -ENTRYPOINT ["/bin/sh", "/entrypoint.sh"] diff --git a/ops/docker/Dockerfile.l1-beacon b/ops/docker/Dockerfile.l1-beacon deleted file mode 100644 index 3a017fbdf..000000000 --- a/ops/docker/Dockerfile.l1-beacon +++ /dev/null @@ -1,21 +0,0 @@ -FROM --platform=linux/amd64 ubuntu:22.04 as chain-genesis -RUN apt-get update && ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime && apt-get install build-essential curl wget git make pkg-config -y -RUN curl -o prysmctl -fLO https://github.com/prysmaticlabs/prysm/releases/download/v4.2.1/prysmctl-v4.2.1-linux-amd64 -RUN chmod +x prysmctl -COPY ops/docker/consensus /consensus -COPY ops/docker/execution /execution -RUN ./prysmctl testnet generate-genesis --fork=deneb --num-validators=64 --genesis-time-delay=0 --output-ssz=/consensus/genesis.ssz --chain-config-file=/consensus/config.yml --geth-genesis-json-in=/execution/genesis.json --geth-genesis-json-out=/execution/genesis.json - -FROM ethereum/client-go:v1.14.11 as geth-genesis -COPY --from=chain-genesis /execution /execution -RUN geth --datadir=/execution --state.scheme=hash init /execution/genesis.json - - -FROM gcr.io/prysmaticlabs/prysm/beacon-chain:v4.2.1 as beacon-chain -COPY --from=chain-genesis /consensus /consensus -COPY --from=geth-genesis /execution /execution - - - - - diff --git a/ops/docker/Dockerfile.l2-node-1 b/ops/docker/Dockerfile.l2-node-1 deleted file mode 100644 index 11873cf4d..000000000 --- a/ops/docker/Dockerfile.l2-node-1 +++ /dev/null @@ -1,26 +0,0 @@ -# Build Geth in a stock Go builder container -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu as builder -COPY . /morph -WORKDIR /morph/node -RUN make build - - -FROM builder as initializer - -COPY --from=builder /morph/node/build/bin/tendermint /usr/local/bin/ -RUN echo "Initializing tendermint." -RUN tendermint init --home /data -RUN sed -i 's#create_empty_blocks_interval = "0s"#create_empty_blocks_interval = "5s"#g' /data/config/config.toml -RUN sed -i 's#batch_max_bytes = "8388608"#batch_max_bytes = "12492"#g' /data/config/config.toml -RUN sed -i 's#batch_blocks_interval = "10"#batch_blocks_interval = "5"#g' /data/config/config.toml -RUN sed -i 's#batch_timeout = "60s"#batch_timeout = "6s"#g' /data/config/config.toml -RUN sed -i 's#prometheus = false#prometheus = true#g' /data/config/config.toml -RUN sed -i 's#laddr = "tcp://127.0.0.1:26657"#laddr = "tcp://0.0.0.0:26657"#g' /data/config/config.toml - - -FROM builder - -COPY --from=builder /morph/node/build/bin/morphnode /usr/local/bin/ -COPY --from=initializer /data /data - -CMD ["morphnode","--dev-sequencer","--home", "/data"] \ No newline at end of file diff --git a/ops/docker/Dockerfile.token-price-oracle b/ops/docker/Dockerfile.token-price-oracle deleted file mode 100644 index 249ba4228..000000000 --- a/ops/docker/Dockerfile.token-price-oracle +++ /dev/null @@ -1,17 +0,0 @@ -# Build token-price-oracle in a stock Go builder container -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu as builder - -COPY . /morph - -WORKDIR /morph/token-price-oracle - -RUN make build - -# Copy binary into a lightweight runtime container -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu - -RUN apt-get -qq update \ - && apt-get -qq install -y --no-install-recommends ca-certificates -COPY --from=builder /morph/token-price-oracle/build/bin/token-price-oracle /usr/local/bin/ - -CMD ["token-price-oracle"] \ No newline at end of file diff --git a/ops/docker/consensus/config.yml b/ops/docker/consensus/config.yml deleted file mode 100644 index 125a1ea25..000000000 --- a/ops/docker/consensus/config.yml +++ /dev/null @@ -1,30 +0,0 @@ -CONFIG_NAME: interop -PRESET_BASE: interop - -# Genesis -GENESIS_FORK_VERSION: 0x20000089 - -# Altair -ALTAIR_FORK_EPOCH: 0 -ALTAIR_FORK_VERSION: 0x20000090 - -# Merge -BELLATRIX_FORK_EPOCH: 0 -BELLATRIX_FORK_VERSION: 0x20000091 -TERMINAL_TOTAL_DIFFICULTY: 0 - -# Capella -CAPELLA_FORK_EPOCH: 0 -CAPELLA_FORK_VERSION: 0x20000092 -MAX_WITHDRAWALS_PER_PAYLOAD: 16 - -# Deneb -DENEB_FORK_EPOCH: 0 -DENEB_FORK_VERSION: 0x20000093 - -# Time parameters -SECONDS_PER_SLOT: 6 -SLOTS_PER_EPOCH: 6 - -# Deposit contract -DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242 diff --git a/ops/docker/docker-compose-devnet.yml b/ops/docker/docker-compose-devnet.yml index 06469dab5..6f0c387a4 100644 --- a/ops/docker/docker-compose-devnet.yml +++ b/ops/docker/docker-compose-devnet.yml @@ -1,6 +1,4 @@ volumes: - morph_data_0: - morph_data_1: layer1-el-data: layer1-cl-data: layer1-vc-data: @@ -118,7 +116,7 @@ services: - "6060" - "30303" volumes: - - "morph_data_0:/db" + - ".devnet/el0:/db" - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - "${PWD}/nodekey0:/db/geth/nodekey" @@ -141,7 +139,7 @@ services: - "6060" - "30303" volumes: - - "morph_data_1:/db" + - ".devnet/el1:/db" - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - "${PWD}/nodekey1:/db/geth/nodekey" @@ -245,27 +243,12 @@ services: - TX_SUBMITTER_L1_ETH_RPC=${L1_ETH_RPC} - TX_SUBMITTER_L1_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545 - - TX_SUBMITTER_MAX_BATCH_BUILD_TIME=60s - - TX_SUBMITTER_MAX_TX_SIZE=125952 - - TX_SUBMITTER_POLL_INTERVAL=3s - - TX_SUBMITTER_SAFE_MINIMUM_ETHER_BALANCE=1 - TX_SUBMITTER_TX_TIMEOUT=60s - TX_SUBMITTER_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - TX_SUBMITTER_NETWORK_TIMEOUT=20s - - TX_SUBMITTER_MAX_BLOCK=1000 - - TX_SUBMITTER_MIN_BLOCK=50 - TX_SUBMITTER_FINALIZE=true - - TX_SUBMITTER_MAX_FINALIZE_NUM=100 - TX_SUBMITTER_PRIORITY_ROLLUP=true - TX_SUBMITTER_SEAL_BATCH=true - - TX_SUBMITTER_METRICS_SERVER_ENABLE=false - - TX_SUBMITTER_METRICS_HOSTNAME=0.0.0.0 - - TX_SUBMITTER_METRICS_PORT=6060 - TX_SUBMITTER_TX_FEE_LIMIT=500000000000000000 - - TX_SUBMITTER_LOG_FILENAME=tx_submitter.log - - TX_SUBMITTER_LOG_FILE_MAX_SIZE=100 - - TX_SUBMITTER_LOG_FILE_MAX_AGE=7 - - TX_SUBMITTER_LOG_COMPRESS=true - TX_SUBMITTER_L1_STAKING_ADDRESS=${MORPH_L1STAKING:-0x5fc8d32690cc91d4c39d9d3abcbd16989f875707} - TX_SUBMITTER_L1_STAKING_DEPLOYED_BLOCKNUM=0 - TX_SUBMITTER_BATCH_V2_UPGRADE_TIME=1777533291 @@ -292,10 +275,5 @@ services: - INTERVAL=28000 - L2_GAS_PRICE_ORACLE=0x530000000000000000000000000000000000000F - L2_GAS_ORACLE_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 - - OVERHEAD_THRESHOLD=200 - OVERHEAD_INTERVAL=10 - - TXN_PER_BLOCK=1 - - TXN_PER_BATCH=50 - L1_ROLLUP=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - OVERHEAD_SWITCH=${OVERHEAD_SWITCH:-} - - MAX_OVERHEAD=${MAX_OVERHEAD:-} diff --git a/ops/docker/entrypoint-l1.sh b/ops/docker/entrypoint-l1.sh deleted file mode 100644 index 6f205d870..000000000 --- a/ops/docker/entrypoint-l1.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/sh -set -exu - -VERBOSITY=${GETH_VERBOSITY:-3} -GETH_DATA_DIR=/db -GETH_CHAINDATA_DIR="$GETH_DATA_DIR/geth/chaindata" -GETH_KEYSTORE_DIR="$GETH_DATA_DIR/keystore" -GENESIS_FILE_PATH="${GENESIS_FILE_PATH:-/genesis.json}" -CHAIN_ID=$(cat "$GENESIS_FILE_PATH" | jq -r .config.chainId) -BLOCK_SIGNER_PRIVATE_KEY="3e4bde571b86929bf08e2aaad9a6a1882664cd5e65b96fff7d03e1c4e6dfa15c" -BLOCK_SIGNER_ADDRESS="0xca062b0fd91172d89bcd4bb084ac4e21972cc467" -RPC_PORT="${RPC_PORT:-8545}" -WS_PORT="${WS_PORT:-8546}" - -if [ ! -d "$GETH_KEYSTORE_DIR" ]; then - echo "$GETH_KEYSTORE_DIR missing, running account import" - echo -n "pwd" > "$GETH_DATA_DIR"/password - echo -n "$BLOCK_SIGNER_PRIVATE_KEY" | sed 's/0x//' > "$GETH_DATA_DIR"/block-signer-key - geth account import \ - --datadir="$GETH_DATA_DIR" \ - --password="$GETH_DATA_DIR"/password \ - "$GETH_DATA_DIR"/block-signer-key -else - echo "$GETH_KEYSTORE_DIR exists." -fi - -if [ ! -d "$GETH_CHAINDATA_DIR" ]; then - echo "$GETH_CHAINDATA_DIR missing, running init" - echo "Initializing genesis." - geth --verbosity="$VERBOSITY" init \ - --datadir="$GETH_DATA_DIR" \ - "$GENESIS_FILE_PATH" -else - echo "$GETH_CHAINDATA_DIR exists." -fi - -# Warning: Archive mode is required, otherwise old trie nodes will be -# pruned within minutes of starting the devnet. - -exec geth \ - --datadir="$GETH_DATA_DIR" \ - --verbosity="$VERBOSITY" \ - --http \ - --http.corsdomain="*" \ - --http.vhosts="*" \ - --http.addr=0.0.0.0 \ - --http.port="$RPC_PORT" \ - --http.api=web3,debug,eth,txpool,net,engine \ - --ws \ - --ws.addr=0.0.0.0 \ - --ws.port="$WS_PORT" \ - --ws.origins="*" \ - --ws.api=debug,eth,txpool,net,engine \ - --syncmode=full \ - --nodiscover \ - --maxpeers=1 \ - --networkid=$CHAIN_ID \ - --unlock=$BLOCK_SIGNER_ADDRESS \ - --mine \ - --miner.etherbase=$BLOCK_SIGNER_ADDRESS \ - --miner.recommit=12s \ - --password="$GETH_DATA_DIR"/password \ - --allow-insecure-unlock \ - --authrpc.addr="0.0.0.0" \ - --authrpc.port="8551" \ - --authrpc.vhosts="*" \ - --authrpc.jwtsecret=/config/jwt-secret.txt \ - --gcmode=archive \ - --metrics \ - --metrics.addr=0.0.0.0 \ - --metrics.port=6060 \ - "$@" \ No newline at end of file diff --git a/ops/docker/execution/genesis.json b/ops/docker/execution/genesis.json deleted file mode 100644 index d492f85bd..000000000 --- a/ops/docker/execution/genesis.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "config": { - "chainId": 900, - "homesteadBlock": 0, - "daoForkSupport": true, - "eip150Block": 0, - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "muirGlacierBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "arrowGlacierBlock": 0, - "grayGlacierBlock": 0, - "shanghaiTime": 1707123228, - "cancunTime": 1707123228, - "terminalTotalDifficulty": 0, - "terminalTotalDifficultyPassed": true - }, - "nonce": "0x0", - "timestamp": "0x65c0a21c", - "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000123463a4b065722e99115d6c222f267d9cabb5240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "gasLimit": "0x1c9c380", - "difficulty": "0x1", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x0000000000000000000000000000000000000000", - "alloc": { - "123463a4b065722e99115d6c222f267d9cabb524": { - "balance": "0x43c33c1937564800000" - }, - "14dc79964da2c08b23698b3d3cc7ca32193d9955": { - "balance": "0x43c33c1937564800000" - }, - "15d34aaf54267db7d7c367839aaf71a00a2c6a65": { - "balance": "0x43c33c1937564800000" - }, - "1cbd3b2770909d4e10f157cabc84c7264073c9ec": { - "balance": "0x43c33c1937564800000" - }, - "23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f": { - "balance": "0x43c33c1937564800000" - }, - "2546bcd3c84621e976d8185a91a922ae77ecec30": { - "balance": "0x43c33c1937564800000" - }, - "3c44cdddb6a900fa2b585dd299e03d12fa4293bc": { - "balance": "0x43c33c1937564800000" - }, - "4242424242424242424242424242424242424242": { - "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100b6578063621fd130146101e3578063c5f2892f14610273575b600080fd5b34801561005057600080fd5b5061009c6004803603602081101561006757600080fd5b8101908080357bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916906020019092919050505061029e565b604051808215151515815260200191505060405180910390f35b6101e1600480360360808110156100cc57600080fd5b81019080803590602001906401000000008111156100e957600080fd5b8201836020820111156100fb57600080fd5b8035906020019184600183028401116401000000008311171561011d57600080fd5b90919293919293908035906020019064010000000081111561013e57600080fd5b82018360208201111561015057600080fd5b8035906020019184600183028401116401000000008311171561017257600080fd5b90919293919293908035906020019064010000000081111561019357600080fd5b8201836020820111156101a557600080fd5b803590602001918460018302840111640100000000831117156101c757600080fd5b909192939192939080359060200190929190505050610370565b005b3480156101ef57600080fd5b506101f8610fd0565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561023857808201518184015260208101905061021d565b50505050905090810190601f1680156102655780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561027f57600080fd5b50610288610fe2565b6040518082815260200191505060405180910390f35b60007f01ffc9a7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916148061036957507f85640907000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916145b9050919050565b603087879050146103cc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806116ec6026913960400191505060405180910390fd5b60208585905014610428576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260368152602001806116836036913960400191505060405180910390fd5b60608383905014610484576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602981526020018061175f6029913960400191505060405180910390fd5b670de0b6b3a76400003410156104e5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806117396026913960400191505060405180910390fd5b6000633b9aca0034816104f457fe5b061461054b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806116b96033913960400191505060405180910390fd5b6000633b9aca00348161055a57fe5b04905067ffffffffffffffff80168111156105c0576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260278152602001806117126027913960400191505060405180910390fd5b60606105cb82611314565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a610600602054611314565b60405180806020018060200180602001806020018060200186810386528e8e82818152602001925080828437600081840152601f19601f82011690508083019250505086810385528c8c82818152602001925080828437600081840152601f19601f82011690508083019250505086810384528a818151815260200191508051906020019080838360005b838110156106a657808201518184015260208101905061068b565b50505050905090810190601f1680156106d35780820380516001836020036101000a031916815260200191505b508681038352898982818152602001925080828437600081840152601f19601f820116905080830192505050868103825287818151815260200191508051906020019080838360005b8381101561073757808201518184015260208101905061071c565b50505050905090810190601f1680156107645780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b6040516020018084848082843780830192505050826fffffffffffffffffffffffffffffffff19166fffffffffffffffffffffffffffffffff1916815260100193505050506040516020818303038152906040526040518082805190602001908083835b6020831061080e57805182526020820191506020810190506020830392506107eb565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610850573d6000803e3d6000fd5b5050506040513d602081101561086557600080fd5b8101908080519060200190929190505050905060006002808888600090604092610891939291906115da565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108eb57805182526020820191506020810190506020830392506108c8565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561092d573d6000803e3d6000fd5b5050506040513d602081101561094257600080fd5b8101908080519060200190929190505050600289896040908092610968939291906115da565b6000801b604051602001808484808284378083019250505082815260200193505050506040516020818303038152906040526040518082805190602001908083835b602083106109cd57805182526020820191506020810190506020830392506109aa565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610a0f573d6000803e3d6000fd5b5050506040513d6020811015610a2457600080fd5b810190808051906020019092919050505060405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b60208310610a8e5780518252602082019150602081019050602083039250610a6b565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610ad0573d6000803e3d6000fd5b5050506040513d6020811015610ae557600080fd5b810190808051906020019092919050505090506000600280848c8c604051602001808481526020018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610b615780518252602082019150602081019050602083039250610b3e565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610ba3573d6000803e3d6000fd5b5050506040513d6020811015610bb857600080fd5b8101908080519060200190929190505050600286600060401b866040516020018084805190602001908083835b60208310610c085780518252602082019150602081019050602083039250610be5565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610c935780518252602082019150602081019050602083039250610c70565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610cd5573d6000803e3d6000fd5b5050506040513d6020811015610cea57600080fd5b810190808051906020019092919050505060405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b60208310610d545780518252602082019150602081019050602083039250610d31565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610d96573d6000803e3d6000fd5b5050506040513d6020811015610dab57600080fd5b81019080805190602001909291905050509050858114610e16576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252605481526020018061162f6054913960600191505060405180910390fd5b6001602060020a0360205410610e77576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602181526020018061160e6021913960400191505060405180910390fd5b60016020600082825401925050819055506000602054905060008090505b6020811015610fb75760018083161415610ec8578260008260208110610eb757fe5b018190555050505050505050610fc7565b600260008260208110610ed757fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b60208310610f335780518252602082019150602081019050602083039250610f10565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610f75573d6000803e3d6000fd5b5050506040513d6020811015610f8a57600080fd5b8101908080519060200190929190505050925060028281610fa757fe5b0491508080600101915050610e95565b506000610fc057fe5b5050505050505b50505050505050565b6060610fdd602054611314565b905090565b6000806000602054905060008090505b60208110156111d057600180831614156110e05760026000826020811061101557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b60208310611071578051825260208201915060208101905060208303925061104e565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156110b3573d6000803e3d6000fd5b5050506040513d60208110156110c857600080fd5b810190808051906020019092919050505092506111b6565b600283602183602081106110f057fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061114b5780518252602082019150602081019050602083039250611128565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561118d573d6000803e3d6000fd5b5050506040513d60208110156111a257600080fd5b810190808051906020019092919050505092505b600282816111c057fe5b0491508080600101915050610ff2565b506002826111df602054611314565b600060401b6040516020018084815260200183805190602001908083835b6020831061122057805182526020820191506020810190506020830392506111fd565b6001836020036101000a0380198251168184511680821785525050505050509050018267ffffffffffffffff191667ffffffffffffffff1916815260180193505050506040516020818303038152906040526040518082805190602001908083835b602083106112a55780518252602082019150602081019050602083039250611282565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156112e7573d6000803e3d6000fd5b5050506040513d60208110156112fc57600080fd5b81019080805190602001909291905050509250505090565b6060600867ffffffffffffffff8111801561132e57600080fd5b506040519080825280601f01601f1916602001820160405280156113615781602001600182028036833780820191505090505b50905060008260c01b90508060076008811061137957fe5b1a60f81b8260008151811061138a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350806006600881106113c657fe5b1a60f81b826001815181106113d757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060056008811061141357fe5b1a60f81b8260028151811061142457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060046008811061146057fe5b1a60f81b8260038151811061147157fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350806003600881106114ad57fe5b1a60f81b826004815181106114be57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350806002600881106114fa57fe5b1a60f81b8260058151811061150b57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060016008811061154757fe5b1a60f81b8260068151811061155857fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060006008811061159457fe5b1a60f81b826007815181106115a557fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b600080858511156115ea57600080fd5b838611156115f757600080fd5b600185028301915084860390509450949250505056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a2646970667358221220230afd4b6e3551329e50f1239e08fa3ab7907b77403c4f237d9adf679e8e43cf64736f6c634300060b0033", - "balance": "0x0" - }, - "4e59b44847b379578588920ca78fbf26c0b4956c": { - "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", - "balance": "0x0" - }, - "5678e9e827b3be0e3d4b910126a64a697a148267": { - "balance": "0x43c33c1937564800000" - }, - "70997970c51812dc3a010c7d01b50e0d17dc79c8": { - "balance": "0x43c33c1937564800000" - }, - "71562b71999873db5b286df957af199ec94617f7": { - "balance": "0x43c33c1937564800000" - }, - "71be63f3384f5fb98995898a86b02fb2426c5788": { - "balance": "0x43c33c1937564800000" - }, - "8626f6940e2eb28930efb4cef49b2d1f2c9c1199": { - "balance": "0x43c33c1937564800000" - }, - "90f79bf6eb2c4f870365e785982e1f101e93b906": { - "balance": "0x43c33c1937564800000" - }, - "976ea74026e726554db657fa54763abd0c3a0aa9": { - "balance": "0x43c33c1937564800000" - }, - "9965507d1a55bcc2695c58ba16fb37d819b0a4dc": { - "balance": "0x43c33c1937564800000" - }, - "a0ee7a142d267c1f36714e4a8f75612f20a79720": { - "balance": "0x43c33c1937564800000" - }, - "bcd4042de499d14e55001ccbb24a551f3b954096": { - "balance": "0x43c33c1937564800000" - }, - "bda5747bfd65f08deb54cb465eb87d40e51b197e": { - "balance": "0x43c33c1937564800000" - }, - "ca062b0fd91172d89bcd4bb084ac4e21972cc467": { - "balance": "0x43c33c1937564800000" - }, - "cd3b766ccdd6ae721141f452c550ca635964ce71": { - "balance": "0x43c33c1937564800000" - }, - "dd2fd4581271e230360230f9337d5c0430bf44c0": { - "balance": "0x43c33c1937564800000" - }, - "de3829a23df1479438622a08a116e8eb3f620bb5": { - "balance": "0x43c33c1937564800000" - }, - "df3e18d64bc6a983f673ab319ccae4f1a57c7097": { - "balance": "0x43c33c1937564800000" - }, - "f39fd6e51aad88f6f4ce6ab8827279cfffb92266": { - "balance": "0x43c33c1937564800000" - }, - "fabb0ac9d68b0b445fb7357272ff202c5651694a": { - "balance": "0x43c33c1937564800000" - } - }, - "number": "0x0", - "gasUsed": "0x0", - "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "baseFeePerGas": null, - "excessBlobGas": null, - "blobGasUsed": null -} \ No newline at end of file diff --git a/ops/docker/execution/jwtsecret b/ops/docker/execution/jwtsecret deleted file mode 100755 index c4454fe4f..000000000 --- a/ops/docker/execution/jwtsecret +++ /dev/null @@ -1 +0,0 @@ -0xfad2709d0bb03bf0e8ba3c99bea194575d3e98863133d1af638ed056d1d59345 \ No newline at end of file diff --git a/ops/docker/execution/keystore/UTC--2024-02-05T07-24-26.460740423Z--ca062b0fd91172d89bcd4bb084ac4e21972cc467 b/ops/docker/execution/keystore/UTC--2024-02-05T07-24-26.460740423Z--ca062b0fd91172d89bcd4bb084ac4e21972cc467 deleted file mode 100644 index f687b5aa0..000000000 --- a/ops/docker/execution/keystore/UTC--2024-02-05T07-24-26.460740423Z--ca062b0fd91172d89bcd4bb084ac4e21972cc467 +++ /dev/null @@ -1 +0,0 @@ -{"address":"ca062b0fd91172d89bcd4bb084ac4e21972cc467","crypto":{"cipher":"aes-128-ctr","ciphertext":"c743d425640015eefedc5dfa0f604e3e07e9c575d0ca3925efeb27a063ab2a5b","cipherparams":{"iv":"7616eb2ca6994054fc9143d9232286b3"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8096ede4d4ff3709065f7eac9a41b4836bc6f67fc9afdf06070949cc346e433a"},"mac":"cd5461c98b59060ef98ae676fcd1b30a1d60dfbdcf052401658963312a5577fd"},"id":"108acf78-9370-406a-87d1-7768a878281c","version":3} \ No newline at end of file diff --git a/ops/docker/execution/password b/ops/docker/execution/password deleted file mode 100644 index 013c184ca..000000000 --- a/ops/docker/execution/password +++ /dev/null @@ -1 +0,0 @@ -pwd \ No newline at end of file diff --git a/ops/docker/go-rust-builder.Dockerfile b/ops/docker/go-rust-builder.Dockerfile deleted file mode 100644 index 2fd98e678..000000000 --- a/ops/docker/go-rust-builder.Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -ARG GO_VERSION=1.24.0 -ARG RUST_VERSION=nightly-2022-12-10 -ARG CARGO_CHEF_TAG=0.1.41 - -FROM ubuntu:22.04 - -RUN apt-get update && ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime - -# Install basic packages -RUN apt-get install build-essential curl wget git pkg-config -y -# Install dev-packages -RUN apt-get install libclang-dev libssl-dev llvm software-properties-common -y -# Install golang -RUN add-apt-repository ppa:longsleep/golang-backports -RUN apt install golang-1.24.0-go -y -ENV PATH="/usr/lib/go-1.24.0/bin:${PATH}" - -# Install Rust -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -ENV PATH="/root/.cargo/bin:${PATH}" -ENV CARGO_HOME=/root/.cargo -# Add Toolchain -ARG RUST_VERSION -RUN rustup toolchain install ${RUST_VERSION} -ARG CARGO_CHEF_TAG -RUN cargo install cargo-chef --locked --version ${CARGO_CHEF_TAG} \ - && rm -rf $CARGO_HOME/registry/ \ No newline at end of file diff --git a/ops/docker/grafana/dashboards/dashboards.yml b/ops/docker/grafana/dashboards/dashboards.yml deleted file mode 100644 index 6a51bf1e9..000000000 --- a/ops/docker/grafana/dashboards/dashboards.yml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: 1 - -providers: - - name: 'Morph Node Metrics' - folder: 'Morph' - type: file - options: - path: /var/lib/grafana/dashboards - foldersFromFilesStructure: false diff --git a/ops/docker/grafana/dashboards/json/morph-node.json b/ops/docker/grafana/dashboards/json/morph-node.json deleted file mode 100644 index 6e2e50905..000000000 --- a/ops/docker/grafana/dashboards/json/morph-node.json +++ /dev/null @@ -1,1279 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "collapsed": false, - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, - "id": 100, - "panels": [], - "title": "🔴 Core Health (P0)", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 1}, - "hiddenSeries": false, - "id": 1, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "morphnode_executor_height", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Block Height", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 1}, - "hiddenSeries": false, - "id": 2, - "legend": { - "alignAsTable": false, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": true, - "targets": [ - { - "expr": "morphnode_sequencer_is_active_sequencer", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [ - {"colorMode": "critical", "fill": true, "line": true, "op": "lt", "value": 1, "visible": true} - ], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Active Sequencer (1=active)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": 1.5, "min": -0.5, "show": true, "decimals": 0}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 1}, - "hiddenSeries": false, - "id": 3, - "legend": { - "alignAsTable": false, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "morphnode_sequencer_bcast_sync_gap", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [ - {"colorMode": "warning", "fill": true, "line": true, "op": "gt", "value": 50, "visible": true} - ], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Sync Gap (blocks behind, negative=ahead)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true, "decimals": 0}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 9}, - "hiddenSeries": false, - "id": 4, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": true, - "targets": [ - { - "expr": "morphnode_hakeeper_raft_state", - "interval": "", - "legendFormat": "{{server_id}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Raft State (0=Follower,1=Candidate,2=Leader,3=Shutdown)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": 3.5, "min": -0.5, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 9}, - "hiddenSeries": false, - "id": 5, - "legend": { - "alignAsTable": false, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": true, - "targets": [ - { - "expr": "morphnode_hakeeper_cluster_members", - "interval": "", - "legendFormat": "{{server_id}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Cluster Members", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true, "decimals": 0}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 9}, - "hiddenSeries": false, - "id": 6, - "legend": { - "alignAsTable": false, - "avg": false, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "morphnode_l1tracker_lag_seconds", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [ - {"colorMode": "warning", "fill": true, "line": true, "op": "gt", "value": 300, "visible": true}, - {"colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 1800, "visible": true} - ], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "L1 Lag (seconds)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "s", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "collapsed": false, - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 17}, - "id": 200, - "panels": [], - "title": "🟡 Throughput & Performance (P1)", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 18}, - "hiddenSeries": false, - "id": 10, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(morphnode_sequencer_blocks_produced_total[1m])", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Blocks Produced Rate (per second)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 18}, - "hiddenSeries": false, - "id": 11, - "legend": { - "alignAsTable": false, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.5, rate(morphnode_sequencer_block_interval_seconds_bucket[5m]))", - "interval": "", - "legendFormat": "p50 {{service}}", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, rate(morphnode_sequencer_block_interval_seconds_bucket[5m]))", - "interval": "", - "legendFormat": "p95 {{service}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Block Interval (seconds)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "s", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 18}, - "hiddenSeries": false, - "id": 12, - "legend": { - "alignAsTable": false, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.5, rate(morphnode_sequencer_block_size_bytes_bucket[5m]))", - "interval": "", - "legendFormat": "p50 {{service}}", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, rate(morphnode_sequencer_block_size_bytes_bucket[5m]))", - "interval": "", - "legendFormat": "p95 {{service}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Block Size (bytes)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "decbytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 26}, - "hiddenSeries": false, - "id": 13, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(morphnode_sequencer_block_txs_sum[1m])", - "interval": "", - "legendFormat": "tx/s {{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Transactions Per Second", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 12, "x": 12, "y": 26}, - "hiddenSeries": false, - "id": 14, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.5, rate(morphnode_sequencer_apply_duration_milliseconds_bucket{step=\"geth\"}[5m]))", - "interval": "", - "legendFormat": "p50 {{service}}", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, rate(morphnode_sequencer_apply_duration_milliseconds_bucket{step=\"geth\"}[5m]))", - "interval": "", - "legendFormat": "p95 {{service}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Apply Duration (geth step, ms)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "ms", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "collapsed": false, - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 34}, - "id": 300, - "panels": [], - "title": "🟢 Broadcast & Sync (P2)", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 8, "x": 0, "y": 35}, - "hiddenSeries": false, - "id": 20, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(morphnode_sequencer_bcast_blocks_applied_total[1m])", - "interval": "", - "legendFormat": "{{source}} {{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Broadcast Blocks Applied Rate", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 8, "x": 8, "y": 35}, - "hiddenSeries": false, - "id": 21, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(morphnode_sequencer_bcast_peers_banned_total[1m])", - "interval": "", - "legendFormat": "{{reason}}", - "refId": "A" - } - ], - "thresholds": [ - {"colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 0, "visible": true} - ], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Peers Banned (by reason)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 8, "x": 16, "y": 35}, - "hiddenSeries": false, - "id": 22, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(morphnode_sequencer_sync_v2_blocks_total[1m])", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Sync V2 Blocks Rate", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "collapsed": false, - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 43}, - "id": 400, - "panels": [], - "title": "🔵 Go Runtime", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 8, "x": 0, "y": 44}, - "hiddenSeries": false, - "id": 30, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_goroutines", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Goroutines", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 8, "x": 8, "y": 44}, - "hiddenSeries": false, - "id": 31, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_memstats_alloc_bytes", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Memory Allocated", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "decbytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 8, "x": 16, "y": 44}, - "hiddenSeries": false, - "id": 32, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "tendermint_p2p_peers", - "interval": "", - "legendFormat": "{{service}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "P2P Peers", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "collapsed": false, - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 52}, - "id": 500, - "panels": [], - "title": "🟣 HA Metrics (hakeeper)", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 53}, - "hiddenSeries": false, - "id": 40, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.5, rate(morphnode_hakeeper_commit_duration_milliseconds_bucket[5m]))", - "interval": "", - "legendFormat": "p50 {{step}} {{server_id}}", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, rate(morphnode_hakeeper_commit_duration_milliseconds_bucket[5m]))", - "interval": "", - "legendFormat": "p95 {{step}} {{server_id}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "HA Commit Duration (ms)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "ms", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 53}, - "hiddenSeries": false, - "id": 41, - "legend": { - "alignAsTable": false, - "avg": false, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(morphnode_hakeeper_fsm_block_channel_drops_total[1m])", - "interval": "", - "legendFormat": "{{server_id}}", - "refId": "A" - } - ], - "thresholds": [ - {"colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 0, "visible": true} - ], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "FSM Block Channel Drops", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "short", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": {"defaults": {}}, - "fill": 1, - "fillGradient": 0, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 53}, - "hiddenSeries": false, - "id": 42, - "legend": { - "alignAsTable": false, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": {"alertThreshold": true}, - "percentage": false, - "pluginVersion": "7.5.0", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.5, rate(morphnode_hakeeper_fsm_apply_duration_milliseconds_bucket[5m]))", - "interval": "", - "legendFormat": "p50 {{server_id}}", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, rate(morphnode_hakeeper_fsm_apply_duration_milliseconds_bucket[5m]))", - "interval": "", - "legendFormat": "p95 {{server_id}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "FSM Apply Duration (ms)", - "tooltip": {"shared": true, "sort": 0, "value_type": "individual"}, - "type": "graph", - "xaxis": {"buckets": null, "mode": "time", "name": null, "show": true, "values": []}, - "yaxes": [ - {"format": "ms", "label": null, "logBase": 1, "max": null, "min": 0, "show": true}, - {"format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false} - ], - "yaxis": {"align": false, "alignLevel": null} - } - ], - "refresh": "10s", - "schemaVersion": 27, - "style": "dark", - "tags": ["morph", "node", "metrics"], - "templating": { - "list": [] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": {"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h"]}, - "timezone": "", - "title": "Morph Node Metrics", - "uid": "morph-node-metrics", - "version": 1 -} diff --git a/ops/docker/grafana/datasources/prometheus.yml b/ops/docker/grafana/datasources/prometheus.yml deleted file mode 100644 index 1a57b69c8..000000000 --- a/ops/docker/grafana/datasources/prometheus.yml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: 1 - -datasources: - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - isDefault: true - editable: true diff --git a/ops/docker/layer1/docker-compose.yml b/ops/docker/layer1/docker-compose.yml deleted file mode 100644 index 6267e49cd..000000000 --- a/ops/docker/layer1/docker-compose.yml +++ /dev/null @@ -1,116 +0,0 @@ -version: '3.8' - -services: - # ========== Ethereum Node ========== - # Note: Kurtosis executes "geth init && geth run" in the same container - # with datadir=/data/geth/execution-data - layer1-el: - image: ethereum/client-go:latest - container_name: layer1-el - networks: - - morph-network - ports: - - "9545:8545" # RPC - - "9546:8546" # WebSocket - # - "30303:30303" # P2P TCP - # - "30303:30303/udp" # P2P UDP - - "8551:8551" # Engine API - volumes: - - ./genesis:/network-configs:ro - - ./jwt:/jwt:ro - - layer1-el-data:/data - entrypoint: ["/bin/sh", "-c"] - command: - - | - mkdir -p /data/geth/execution-data - if [ ! -f /data/geth/execution-data/geth/chaindata/CURRENT ]; then - echo "Initializing genesis..." - geth init --datadir=/data/geth/execution-data /network-configs/genesis.json || exit 1 - else - echo "Genesis already initialized, skipping init" - fi - exec geth --networkid=900 --datadir=/data/geth/execution-data --http --http.addr=0.0.0.0 --http.port=8545 --http.api=admin,engine,net,eth,web3,debug,txpool --http.vhosts=* --http.corsdomain=* --ws --ws.addr=0.0.0.0 --ws.port=8546 --ws.api=admin,engine,net,eth,web3,debug,txpool --ws.origins=* --allow-insecure-unlock --authrpc.port=8551 --authrpc.addr=0.0.0.0 --authrpc.vhosts=* --authrpc.jwtsecret=/jwt/jwtsecret --syncmode=full --miner.gasprice=1 --rpc.allow-unprotected-txs --metrics --metrics.addr=0.0.0.0 --metrics.port=9001 --discovery.port=30303 --port=30303 - restart: unless-stopped - - layer1-cl: - image: sigp/lighthouse:latest - container_name: layer1-cl - depends_on: - - layer1-el - networks: - - morph-network - ports: - - "4000:4000" # HTTP API - - "9000:9000" # P2P TCP/UDP - - "9000:9000/udp" # P2P UDP - - "9001:9001" # P2P QUIC - - "5054:5054" # Metrics - volumes: - - ./genesis:/network-configs:ro - - ./jwt:/jwt:ro - - layer1-cl-data:/data - command: - - lighthouse - - beacon_node - - --debug-level=info - - --datadir=/data/lighthouse/beacon-data - - --listen-address=0.0.0.0 - - --port=9000 - - --http - - --http-address=0.0.0.0 - - --http-port=4000 - - --disable-packet-filter - - --execution-endpoints=http://layer1-el:8551 - - --jwt-secrets=/jwt/jwtsecret - - --suggested-fee-recipient=0x8943545177806ED17B9F23F0a21ee5948eCaa776 - - --testnet-dir=/network-configs - - --allow-insecure-genesis-sync - - --disable-enr-auto-update - - --enr-tcp-port=9000 - - --enr-udp-port=9000 - - --enr-quic-port=9001 - - --quic-port=9001 - - --metrics - - --metrics-address=0.0.0.0 - - --metrics-allow-origin=* - - --metrics-port=5054 - - --enable-private-discovery - restart: unless-stopped - - layer1-vc: - image: sigp/lighthouse:latest - container_name: layer1-vc - depends_on: - - layer1-cl - networks: - - morph-network - volumes: - - ./genesis:/network-configs:ro - - ./keystores/layer1:/validator-keys - - layer1-vc-data:/data - command: - - lighthouse - - vc - - --debug-level=info - - --testnet-dir=/network-configs - - --validators-dir=/validator-keys/keys - - --secrets-dir=/validator-keys/secrets - - --init-slashing-protection - - --beacon-nodes=http://layer1-cl:4000 - - --suggested-fee-recipient=0x8943545177806ED17B9F23F0a21ee5948eCaa776 - - --metrics - - --metrics-address=0.0.0.0 - - --metrics-allow-origin=* - - --metrics-port=5064 - restart: unless-stopped - -networks: - morph-network: - driver: bridge - name: morph-network - -volumes: - layer1-el-data: - layer1-cl-data: - layer1-vc-data: - diff --git a/ops/docker/node0/eth_acc_key.json b/ops/docker/node0/eth_acc_key.json deleted file mode 100644 index 4937577a6..000000000 --- a/ops/docker/node0/eth_acc_key.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "address": "0x783698dCDEBdc96785c5c60ED96113612bA09c2b", - "priv_key": "0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32" -} \ No newline at end of file diff --git a/ops/docker/node1/eth_acc_key.json b/ops/docker/node1/eth_acc_key.json deleted file mode 100644 index 168b368be..000000000 --- a/ops/docker/node1/eth_acc_key.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "address": "0x310824AA27a29D269d2F9C0a8563C0e3C98dD226", - "priv_key": "0x0890c388c3bf5e04fee1d8f3c117e5f44f435ced7baf7bfd66c10e1f3a3f4b10" -} \ No newline at end of file diff --git a/ops/docker/node1/priv_validator_key.json b/ops/docker/node1/priv_validator_key.json deleted file mode 100644 index 742f309b7..000000000 --- a/ops/docker/node1/priv_validator_key.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "address": "91D1B7E7CD592248E14081718C53AE2029799BE6", - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "t5jrdMBnIdVMZZ6eorwjKn+V6W0jTMhxhrKrj0PbaTU=" - }, - "priv_key": { - "type": "tendermint/PrivKeyEd25519", - "value": "OHOtc9F+OaicESSSyn4QEO5Eo2QucbF4KaDZCRbijGS3mOt0wGch1Uxlnp6ivCMqf5XpbSNMyHGGsquPQ9tpNQ==" - } -} \ No newline at end of file diff --git a/ops/docker/node2/eth_acc_key.json b/ops/docker/node2/eth_acc_key.json deleted file mode 100644 index e76b188fe..000000000 --- a/ops/docker/node2/eth_acc_key.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "address": "0x343C5154FFe47c8a07DF5ea6846404e68E9809A2", - "priv_key": "0x6fd437eef7a83c486bd2e0a802ae071b3912d125ac31ac08f60841fd891559ae" -} \ No newline at end of file diff --git a/ops/docker/node2/node_key.json b/ops/docker/node2/node_key.json deleted file mode 100644 index 5da2e438f..000000000 --- a/ops/docker/node2/node_key.json +++ /dev/null @@ -1 +0,0 @@ -{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"wWQIhytUFWhzVK6AbH50mMNyilfJidIbu1p8hViNns1IBo1dw+ry31dKDNcEoouUmlemwE7meicMlZizUe4Xsw=="}} \ No newline at end of file diff --git a/ops/docker/node2/priv_validator_key.json b/ops/docker/node2/priv_validator_key.json deleted file mode 100644 index ed2511792..000000000 --- a/ops/docker/node2/priv_validator_key.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "address": "65CB9C7CEE29940275F0E883F2AE69B524B57DEA", - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "7P+tARKXhrqcYpOqZk+VKJTcQBl2KAR5nfof+25O0EA=" - }, - "priv_key": { - "type": "tendermint/PrivKeyEd25519", - "value": "8ROYORUq6FHUn8yG6kFp/5qQbTw63CjKnOaAS06/PiPs/60BEpeGupxik6pmT5UolNxAGXYoBHmd+h/7bk7QQA==" - } -} \ No newline at end of file diff --git a/ops/docker/node3/eth_acc_key.json b/ops/docker/node3/eth_acc_key.json deleted file mode 100644 index d35def5ea..000000000 --- a/ops/docker/node3/eth_acc_key.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "address": "0xaaC606d51De6A5aBF0d1B9dbd5ed5Ff2Ac2e521B", - "priv_key": "0x9ae53aecdaebe4dcbfec96f3123a2a8c53f9596bf4b3d5adc9a388ccb361b4c0" -} \ No newline at end of file diff --git a/ops/docker/node3/node_key.json b/ops/docker/node3/node_key.json deleted file mode 100644 index f4427b9a0..000000000 --- a/ops/docker/node3/node_key.json +++ /dev/null @@ -1 +0,0 @@ -{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"iQe3qLSO55q9EEko6G6rHi/z12m/oWbNjjeOkfHaZasF/PzETuBCpyoJ9L2wNCeXQhe8lDgK6GKmEvgDFdMlhQ=="}} \ No newline at end of file diff --git a/ops/docker/node3/priv_validator_key.json b/ops/docker/node3/priv_validator_key.json deleted file mode 100644 index f7a637b35..000000000 --- a/ops/docker/node3/priv_validator_key.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "address": "1F430C64E8B71FD6AB8C927731CDB4091CDF0EC2", - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "jSlpW7QVfGlgrbSGvh2aDJ9yhSTQm//MT4ky7RUiHEo=" - }, - "priv_key": { - "type": "tendermint/PrivKeyEd25519", - "value": "5Q0TRMdvQnSeZ3IGKSmwPsqtlP8YhocLT4aipVe4yx+NKWlbtBV8aWCttIa+HZoMn3KFJNCb/8xPiTLtFSIcSg==" - } -} \ No newline at end of file diff --git a/ops/docker/node4/node_key.json b/ops/docker/node4/node_key.json deleted file mode 100644 index 58f42e2dd..000000000 --- a/ops/docker/node4/node_key.json +++ /dev/null @@ -1 +0,0 @@ -{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"kyW4ugY6Zzm9cmODnQQCChRw716494ku43h/ysRWi65skWx/KIlAKFb4il4EEAicLftK6wBf8IBMXbuu6xpKYw=="}} \ No newline at end of file diff --git a/ops/docker/node5/node_key.json b/ops/docker/node5/node_key.json deleted file mode 100644 index fea963ab9..000000000 --- a/ops/docker/node5/node_key.json +++ /dev/null @@ -1 +0,0 @@ -{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"m7GeJrAoa4sybMA0zflWueAlx9TUK8S6pHJQvW8k6oz6PiFvUQd9FDWg+qZdRcAXjeja/x6MO11kkhv8YAwLPQ=="}} \ No newline at end of file diff --git a/ops/docker/nodekey2 b/ops/docker/nodekey2 deleted file mode 100644 index 90952052f..000000000 --- a/ops/docker/nodekey2 +++ /dev/null @@ -1 +0,0 @@ -b9b15801462f01ba61c1f6921c6214c0a4a87f010022086c1544b794fde6fc7e \ No newline at end of file diff --git a/ops/docker/nodekey3 b/ops/docker/nodekey3 deleted file mode 100644 index 7975322d7..000000000 --- a/ops/docker/nodekey3 +++ /dev/null @@ -1 +0,0 @@ -42c5c03c971796f8abd2b14f3855e0ab0b2c203f68f35f5309d01c8b6433a142 \ No newline at end of file diff --git a/ops/docker/prometheus/prometheus.yml b/ops/docker/prometheus/prometheus.yml deleted file mode 100644 index 5b7c40ea9..000000000 --- a/ops/docker/prometheus/prometheus.yml +++ /dev/null @@ -1,32 +0,0 @@ -global: - scrape_interval: 5s - evaluation_interval: 5s - -scrape_configs: - # L1 execution layer - - job_name: 'l1-el' - static_configs: - - targets: ['layer1-el:9001'] - - # L1 consensus layer - - job_name: 'l1-cl' - static_configs: - - targets: ['layer1-cl:5054'] - - # L2 morph nodes (tendermint prometheus metrics on port 26660) - - job_name: 'morph-node' - static_configs: - - targets: - - 'node-0:26660' - - 'node-1:26660' - labels: - service: 'morph-node' - - # L2 execution engines (geth metrics on port 6060) - - job_name: 'morph-geth' - static_configs: - - targets: - - 'morph-el-0:6060' - - 'morph-el-1:6060' - labels: - service: 'morph-geth' diff --git a/ops/docker/tendermint-setup.sh b/ops/docker/tendermint-setup.sh deleted file mode 100644 index 66692f1ef..000000000 --- a/ops/docker/tendermint-setup.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -# Specify the F path to check -FOLDER="./.morphdevnet" - -# Check if the F exists -if [ -d "$FOLDER" ]; then - echo "Folder exists, returning directly" - exit -fi - -#./build/bin/tendermint testnet --v 4 -../node/build/bin/tendermint testnet --v 4 --o $FOLDER --populate-persistent-peers --hostname-prefix node- - -#sed -i '' 's#proxy_app = "tcp://127.0.0.1:26658"#proxy_app = "tcp://127.0.0.1:36658"#g' $FOLDER/node1/config/config.toml -#sed -i '' 's#laddr = "tcp://127.0.0.1:26657"#laddr = "tcp://127.0.0.1:36657"#g' $FOLDER/node1/config/config.toml -#sed -i '' 's#laddr = "tcp://0.0.0.0:26656"#laddr = "tcp://0.0.0.0:36656"#g' $FOLDER/node1/config/config.toml -# -#sed -i '' 's#proxy_app = "tcp://127.0.0.1:26658"#proxy_app = "tcp://127.0.0.1:46658"#g' $FOLDER/node2/config/config.toml -#sed -i '' 's#laddr = "tcp://127.0.0.1:26657"#laddr = "tcp://127.0.0.1:46657"#g' $FOLDER/node2/config/config.toml -#sed -i '' 's#laddr = "tcp://0.0.0.0:26656"#laddr = "tcp://0.0.0.0:46656"#g' $FOLDER/node2/config/config.toml -# -#sed -i '' 's#proxy_app = "tcp://127.0.0.1:26658"#proxy_app = "tcp://127.0.0.1:56658"#g' $FOLDER/node3/config/config.toml -#sed -i '' 's#laddr = "tcp://127.0.0.1:26657"#laddr = "tcp://127.0.0.1:56657"#g' $FOLDER/node3/config/config.toml -#sed -i '' 's#laddr = "tcp://0.0.0.0:26656"#laddr = "tcp://0.0.0.0:56656"#g' $FOLDER/node3/config/config.toml - - -# -#sed -i '' 's/@node0:26656/@127.0.0.1:26656/g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -#sed -i '' 's/@node1:26656/@127.0.0.1:36656/g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -#sed -i '' 's/@node2:26656/@127.0.0.1:46656/g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -#sed -i '' 's/@node3:26656/@127.0.0.1:56656/g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml - - -sed -i '' 's#create_empty_blocks_interval = "0s"#create_empty_blocks_interval = "5s"#g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -#sed -i '' 's#timeout_commit = "1s"#timeout_commit = "3s"#g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -sed -i '' 's#batch_max_bytes = "8388608"#batch_max_bytes = "12492"#g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -sed -i '' 's#batch_blocks_interval = "10"#batch_blocks_interval = "10"#g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -sed -i '' 's#batch_timeout = "60s"#batch_timeout = "600s"#g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml -sed -i '' 's#prometheus = false#prometheus = true#g' $FOLDER/node0/config/config.toml $FOLDER/node1/config/config.toml $FOLDER/node2/config/config.toml $FOLDER/node3/config/config.toml From 56f0468a35f45bbda1f541f37827ce2016c7c474 Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 14:48:33 +0800 Subject: [PATCH 06/11] remove obsolete sequencer test docker harness --- .../Dockerfile.l2-geth-test | 26 - .../Dockerfile.l2-node-test | 47 - .../Dockerfile.tx-submitter-test | 39 - ops/docker-sequencer-test/README.md | 1 - .../check-whitelist-test.sh | 86 - .../docker-compose.ha-override.yml | 249 --- .../docker-compose.override.yml | 117 -- .../docker-compose.reorg-test.override.yml | 24 - ops/docker-sequencer-test/entrypoint-l2.sh | 42 - ops/docker-sequencer-test/run-ha-test.sh | 1773 ----------------- ops/docker-sequencer-test/run-perf-test.sh | 514 ----- ops/docker-sequencer-test/run-test.sh | 970 --------- .../scripts/tx-generator.sh | 74 - 13 files changed, 3962 deletions(-) delete mode 100644 ops/docker-sequencer-test/Dockerfile.l2-geth-test delete mode 100644 ops/docker-sequencer-test/Dockerfile.l2-node-test delete mode 100644 ops/docker-sequencer-test/Dockerfile.tx-submitter-test delete mode 100644 ops/docker-sequencer-test/README.md delete mode 100755 ops/docker-sequencer-test/check-whitelist-test.sh delete mode 100644 ops/docker-sequencer-test/docker-compose.ha-override.yml delete mode 100644 ops/docker-sequencer-test/docker-compose.override.yml delete mode 100644 ops/docker-sequencer-test/docker-compose.reorg-test.override.yml delete mode 100644 ops/docker-sequencer-test/entrypoint-l2.sh delete mode 100755 ops/docker-sequencer-test/run-ha-test.sh delete mode 100755 ops/docker-sequencer-test/run-perf-test.sh delete mode 100755 ops/docker-sequencer-test/run-test.sh delete mode 100644 ops/docker-sequencer-test/scripts/tx-generator.sh diff --git a/ops/docker-sequencer-test/Dockerfile.l2-geth-test b/ops/docker-sequencer-test/Dockerfile.l2-geth-test deleted file mode 100644 index 17de81dd1..000000000 --- a/ops/docker-sequencer-test/Dockerfile.l2-geth-test +++ /dev/null @@ -1,26 +0,0 @@ -# Build Geth for Sequencer Test -# Build context should be the polyrepo root (parent of morph) -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu AS builder - -# Copy local go-ethereum (not submodule) -COPY ./go-ethereum /go-ethereum -WORKDIR /go-ethereum - -# Build geth -RUN go run build/ci.go install ./cmd/geth - -# Runtime stage -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu - -RUN apt-get -qq update && apt-get -qq install -y --no-install-recommends \ - ca-certificates bash curl \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /go-ethereum/build/bin/geth /usr/local/bin/ -COPY ./morph/ops/docker-sequencer-test/entrypoint-l2.sh /entrypoint.sh - -VOLUME ["/db"] - -ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] - -EXPOSE 8545 8546 8551 30303 30303/udp diff --git a/ops/docker-sequencer-test/Dockerfile.l2-node-test b/ops/docker-sequencer-test/Dockerfile.l2-node-test deleted file mode 100644 index 4358089fe..000000000 --- a/ops/docker-sequencer-test/Dockerfile.l2-node-test +++ /dev/null @@ -1,47 +0,0 @@ -# Build Stage -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu AS builder - -# First: Copy only go.mod/go.sum files to cache dependencies -# Order matters for cache efficiency - -# Copy go-ethereum dependency files -COPY ./go-ethereum/go.mod ./go-ethereum/go.sum /polyrepo/go-ethereum/ - -# Copy tendermint dependency files -COPY ./tendermint/go.mod ./tendermint/go.sum /polyrepo/tendermint/ - -# Copy morph go.work and all module dependency files -COPY ./morph/go.work ./morph/go.work.sum /polyrepo/morph/ -COPY ./morph/node/go.mod ./morph/node/go.sum /polyrepo/morph/node/ -COPY ./morph/common/go.mod ./morph/common/go.sum /polyrepo/morph/common/ -COPY ./morph/bindings/go.mod ./morph/bindings/go.sum /polyrepo/morph/bindings/ -COPY ./morph/contracts/go.mod ./morph/contracts/go.sum /polyrepo/morph/contracts/ -COPY ./morph/tx-submitter/go.mod ./morph/tx-submitter/go.sum /polyrepo/morph/tx-submitter/ -COPY ./morph/ops/l2-genesis/go.mod ./morph/ops/l2-genesis/go.sum /polyrepo/morph/ops/l2-genesis/ -COPY ./morph/ops/tools/go.mod ./morph/ops/tools/go.sum /polyrepo/morph/ops/tools/ -COPY ./morph/token-price-oracle/go.mod ./morph/token-price-oracle/go.sum /polyrepo/morph/token-price-oracle/ - -# Download dependencies (this layer is cached if go.mod/go.sum don't change) -WORKDIR /polyrepo/morph/node -RUN go mod download -x - -# Now copy all source code -COPY ./go-ethereum /polyrepo/go-ethereum -COPY ./tendermint /polyrepo/tendermint -COPY ./morph /polyrepo/morph - -# Build (no need to download again, just compile) -WORKDIR /polyrepo/morph/node -RUN make build - -# Final Stage -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu - -RUN apt-get -qq update \ - && apt-get -qq install -y --no-install-recommends ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /polyrepo/morph/node/build/bin/tendermint /usr/local/bin/ -COPY --from=builder /polyrepo/morph/node/build/bin/morphnode /usr/local/bin/ - -CMD ["morphnode", "--home", "/data"] diff --git a/ops/docker-sequencer-test/Dockerfile.tx-submitter-test b/ops/docker-sequencer-test/Dockerfile.tx-submitter-test deleted file mode 100644 index 09bdf7873..000000000 --- a/ops/docker-sequencer-test/Dockerfile.tx-submitter-test +++ /dev/null @@ -1,39 +0,0 @@ -# Build tx-submitter using the polyrepo workspace (../tendermint, ../go-ethereum -# siblings of morph/). Build context must be the polyrepo root, not morph/. -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu AS builder - -# Cache module deps before copying full source. -COPY ./go-ethereum/go.mod ./go-ethereum/go.sum /polyrepo/go-ethereum/ -COPY ./tendermint/go.mod ./tendermint/go.sum /polyrepo/tendermint/ - -COPY ./morph/go.work ./morph/go.work.sum /polyrepo/morph/ -COPY ./morph/node/go.mod ./morph/node/go.sum /polyrepo/morph/node/ -COPY ./morph/common/go.mod ./morph/common/go.sum /polyrepo/morph/common/ -COPY ./morph/bindings/go.mod ./morph/bindings/go.sum /polyrepo/morph/bindings/ -COPY ./morph/contracts/go.mod ./morph/contracts/go.sum /polyrepo/morph/contracts/ -COPY ./morph/tx-submitter/go.mod ./morph/tx-submitter/go.sum /polyrepo/morph/tx-submitter/ -COPY ./morph/ops/l2-genesis/go.mod ./morph/ops/l2-genesis/go.sum /polyrepo/morph/ops/l2-genesis/ -COPY ./morph/ops/tools/go.mod ./morph/ops/tools/go.sum /polyrepo/morph/ops/tools/ -COPY ./morph/token-price-oracle/go.mod ./morph/token-price-oracle/go.sum /polyrepo/morph/token-price-oracle/ - -WORKDIR /polyrepo/morph/tx-submitter -RUN go mod download - -# Now copy full source. -COPY ./go-ethereum /polyrepo/go-ethereum -COPY ./tendermint /polyrepo/tendermint -COPY ./morph /polyrepo/morph - -WORKDIR /polyrepo/morph/tx-submitter -RUN make build - -# Runtime stage. -FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu - -RUN apt-get -qq update \ - && apt-get -qq install -y --no-install-recommends ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /polyrepo/morph/tx-submitter/tx-submitter /usr/local/bin/ - -CMD ["tx-submitter"] diff --git a/ops/docker-sequencer-test/README.md b/ops/docker-sequencer-test/README.md deleted file mode 100644 index 156f2d64a..000000000 --- a/ops/docker-sequencer-test/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory is intended for Docker testing purposes only and may be removed in the future. \ No newline at end of file diff --git a/ops/docker-sequencer-test/check-whitelist-test.sh b/ops/docker-sequencer-test/check-whitelist-test.sh deleted file mode 100755 index 05c328f80..000000000 --- a/ops/docker-sequencer-test/check-whitelist-test.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash -# ============================================================ -# Persistent Peer Whitelist Integration Test — Observer Script -# ============================================================ -# Run AFTER `run-ha-test.sh start` and after the cluster has crossed -# the upgrade height (so node-0 is now a fullnode, ha-node-X is the -# producer, and node-0 is gossiping blocks to sentry-node-0). -# -# This script collects evidence from container logs and prints a -# structured pass/fail summary. It does NOT modify state. - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MORPH_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -DOCKER_DIR="$MORPH_ROOT/ops/docker" -COMPOSE_HA="docker compose -f docker-compose-4nodes.yml -f docker-compose.override.yml -f docker-compose.ha-override.yml -f docker-compose.whitelist-test.override.yml" - -NODE0_NODEID="93e27ea2306e158a8146d5f44caaab97496797d2" -SENTRY_NODEID="dae813274913aaf39e7cd3226a0aa8bce00644e1" - -LOG_TAIL=20000 - -cd "$DOCKER_DIR" - -echo "==========================================" -echo "Whitelist Integration Test — Evidence" -echo "==========================================" - -# Evidence 1: node-0 DID inject malicious blocks -echo "" -echo "--- Evidence 1: node-0 malicious-inject events ---" -INJECT_COUNT=$($COMPOSE_HA logs --tail $LOG_TAIL node-0 2>&1 | grep -c "MALICIOUS_INJECT" || true) -echo "node-0 [MALICIOUS_INJECT] events: $INJECT_COUNT" - -# Evidence 2: sentry triggered the whitelist alarm -echo "" -echo "--- Evidence 2: sentry [WHITELIST_ALARM] events ---" -ALARM_COUNT=$($COMPOSE_HA logs --tail $LOG_TAIL sentry-node-0 2>&1 | grep -c "WHITELIST_ALARM" || true) -echo "sentry [WHITELIST_ALARM] events: $ALARM_COUNT" -$COMPOSE_HA logs --tail $LOG_TAIL sentry-node-0 2>&1 | grep "WHITELIST_ALARM" | tail -5 - -# Evidence 3: sentry stopped node-0 (disconnect happened) -echo "" -echo "--- Evidence 3: sentry stopped node-0 for error ---" -STOP_COUNT=$($COMPOSE_HA logs --tail $LOG_TAIL sentry-node-0 2>&1 | grep "Stopping peer for error" | grep -c "$NODE0_NODEID" || true) -echo "sentry stopped node-0 (by ID match): $STOP_COUNT" - -# Evidence 4: sentry reconnected to node-0 -echo "" -echo "--- Evidence 4: sentry reconnected to node-0 ---" -RECONNECT_COUNT=$($COMPOSE_HA logs --tail $LOG_TAIL sentry-node-0 2>&1 | grep -c "Reconnecting to peer" || true) -echo "sentry [Reconnecting to peer] events: $RECONNECT_COUNT" -$COMPOSE_HA logs --tail $LOG_TAIL sentry-node-0 2>&1 | grep "Reconnecting to peer" | tail -3 - -# Evidence 5: sentry did NOT add node-0 to bannedPeers -echo "" -echo "--- Evidence 5: sentry did NOT ban node-0 ---" -BAN_COUNT=$($COMPOSE_HA logs --tail $LOG_TAIL sentry-node-0 2>&1 | grep "Banning peer" | grep -c "$NODE0_NODEID" || true) -echo "sentry 'Banning peer' for node-0: $BAN_COUNT (expected 0)" - -# Evidence 6: sentry continued syncing blocks after reconnect events -echo "" -echo "--- Evidence 6: sentry sync still progressing ---" -APPLY_COUNT=$($COMPOSE_HA logs --tail $LOG_TAIL sentry-node-0 2>&1 | grep -c "Applied block" || true) -echo "sentry 'Applied block' events: $APPLY_COUNT" - -# Evidence 7: sentry block height -echo "" -echo "--- Evidence 7: current heights ---" -SENTRY_HEIGHT=$(curl -s http://127.0.0.1:8945 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' 2>/dev/null | grep -o '"result":"[^"]*"' | cut -d'"' -f4 || echo "unreachable") -HA0_HEIGHT=$(curl -s http://127.0.0.1:9145 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' 2>/dev/null | grep -o '"result":"[^"]*"' | cut -d'"' -f4 || echo "unreachable") -echo "sentry-el-0 height: $SENTRY_HEIGHT" -echo "ha-geth-0 height: $HA0_HEIGHT" - -echo "" -echo "==========================================" -echo "Verdict" -echo "==========================================" -echo "PASS criteria:" -echo " - INJECT > 0 (we did inject): $([ $INJECT_COUNT -gt 0 ] && echo PASS || echo FAIL)" -echo " - ALARM > 0 (whitelist triggered): $([ $ALARM_COUNT -gt 0 ] && echo PASS || echo FAIL)" -echo " - STOP > 0 (disconnect happened): $([ $STOP_COUNT -gt 0 ] && echo PASS || echo FAIL)" -echo " - RECONNECT > 0 (reconnect happened): $([ $RECONNECT_COUNT -gt 0 ] && echo PASS || echo FAIL)" -echo " - BAN == 0 (node-0 not in ban list): $([ $BAN_COUNT -eq 0 ] && echo PASS || echo FAIL)" -echo " - APPLY > 0 (sentry still syncing): $([ $APPLY_COUNT -gt 0 ] && echo PASS || echo FAIL)" diff --git a/ops/docker-sequencer-test/docker-compose.ha-override.yml b/ops/docker-sequencer-test/docker-compose.ha-override.yml deleted file mode 100644 index d802c9102..000000000 --- a/ops/docker-sequencer-test/docker-compose.ha-override.yml +++ /dev/null @@ -1,249 +0,0 @@ -version: '3.8' -# ============================================================================ -# Isolated HA cluster test override for Sequencer HA V2. -# -# Stack with: -# docker compose \ -# -f docker-compose-4nodes.yml \ -# -f docker-compose.override.yml \ -# -f docker-compose.ha-override.yml \ -# up -d -# -# DESIGN: -# - PBFT phase (before the upgrade time): node-0/1/2/3 run 4-node tendermint -# PBFT consensus exactly as in the baseline override. ha-node-0/1/2 join the -# P2P network as V1 fullnodes (BlockSync only, no block production). -# - After the upgrade time (SEQUENCER_UPGRADE_TIME): V2 activates. Only ha-node-0/1/2 hold the sequencer -# private key registered in the L1Sequencer contract, so they form a Raft -# cluster (ha-node-0 bootstrap, ha-node-1/2 join) and produce blocks. -# node-0/1/2/3 become V2 fullnodes (hasSigner=false). -# -# KEY DIFFERENCES FROM PREVIOUS DESIGN: -# - Previously node-0/1/2 were reused as Raft replicas after upgrade. -# Now they stay as PBFT-only / V2 fullnodes. -# - New services: ha-geth-{0,1,2} + ha-node-{0,1,2}, each with its own -# geth + tendermint + volumes. -# - HA admin RPC host ports 9501/9601/9701 are now mapped to the new -# ha-node-* (previously on node-0/1/2), so run-ha-test.sh constants -# HA_RPC_NODE0/1/2 remain unchanged. -# -# HOST PORTS: -# ha-geth-0 L2 RPC: 9145 → 8545 -# ha-geth-1 L2 RPC: 9245 → 8545 -# ha-geth-2 L2 RPC: 9345 → 8545 -# ha-node-0 TM RPC: 27657 → 26657 -# ha-node-0 HA Admin RPC: 9501 → 9401 (moved from original node-0) -# ha-node-1 TM RPC: 27757 → 26657 -# ha-node-1 HA Admin RPC: 9601 → 9401 -# ha-node-2 TM RPC: 27857 → 26657 -# ha-node-2 HA Admin RPC: 9701 → 9401 -# ============================================================================ - -services: - # ─── ha-geth-0/1/2 ──────────────────────────────────────────────────────── - # Independent execution clients for the HA cluster. They join the existing - # L2 P2P mesh via static-nodes.json and sync blocks from morph-geth-0/1/2/3. - ha-geth-0: - image: morph-geth:feat - pull_policy: never - container_name: ha-geth-0 - depends_on: - morph-el-0: - condition: service_started - restart: unless-stopped - ports: - - "9145:8545" - - "9146:8546" - volumes: - - "ha_morph_data_0:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" - - "${PWD}/ha-nodekey0:/db/geth/nodekey" - environment: - - RUST_LOG=${RUST_LOG} - entrypoint: - - "/bin/bash" - - "/entrypoint.sh" - - ha-geth-1: - image: morph-geth:feat - pull_policy: never - container_name: ha-geth-1 - depends_on: - morph-el-0: - condition: service_started - restart: unless-stopped - ports: - - "9245:8545" - - "9246:8546" - volumes: - - "ha_morph_data_1:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" - - "${PWD}/ha-nodekey1:/db/geth/nodekey" - environment: - - RUST_LOG=${RUST_LOG} - entrypoint: - - "/bin/bash" - - "/entrypoint.sh" - - ha-geth-2: - image: morph-geth:feat - pull_policy: never - container_name: ha-geth-2 - depends_on: - morph-el-0: - condition: service_started - restart: unless-stopped - ports: - - "9345:8545" - - "9346:8546" - volumes: - - "ha_morph_data_2:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" - - "${PWD}/ha-nodekey2:/db/geth/nodekey" - environment: - - RUST_LOG=${RUST_LOG} - entrypoint: - - "/bin/bash" - - "/entrypoint.sh" - - # ─── ha-node-0: Raft bootstrap leader candidate ──────────────────────────── - ha-node-0: - image: morph-node:feat - pull_policy: never - container_name: ha-node-0 - depends_on: - ha-geth-0: - condition: service_started - restart: unless-stopped - ports: - - "26656" - - "27657:26657" - - "26658" - - "26660" - - "9501:9401" # HA Admin RPC (host port moved from node-0) - environment: - # Sequencer private key — only ha-node-0/1/2 hold this. - # Rotated to new key for sequencer migration test (2026-05-11) - # New address: 0xAb70B9eAF487d6cfFD3AD420785BE389D5B28390 - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - MORPH_NODE_L2_ETH_RPC=http://ha-geth-0:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-0:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=http://layer1-cl:4000 - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - # HA config — bootstrap - - MORPH_NODE_HA_ENABLED=true - - MORPH_NODE_HA_BOOTSTRAP=true - - MORPH_NODE_HA_SERVER_ID=ha-node-0 - - MORPH_NODE_HA_ADVERTISED_ADDR=ha-node-0:9400 - - MORPH_NODE_LOG_LEVEL=info - volumes: - - ".devnet/ha-node0:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - - # ─── ha-node-1: Raft follower ───────────────────────────────────────────── - ha-node-1: - image: morph-node:feat - pull_policy: never - container_name: ha-node-1 - depends_on: - ha-node-0: - condition: service_started - restart: unless-stopped - ports: - - "26656" - - "27757:26657" - - "26658" - - "26660" - - "9601:9401" - environment: - # Rotated to new key for sequencer migration test (2026-05-11) - # New address: 0xAb70B9eAF487d6cfFD3AD420785BE389D5B28390 - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - MORPH_NODE_L2_ETH_RPC=http://ha-geth-1:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-1:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=http://layer1-cl:4000 - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - - MORPH_NODE_HA_ENABLED=true - - MORPH_NODE_HA_JOIN=ha-node-0:9401 - - MORPH_NODE_HA_SERVER_ID=ha-node-1 - - MORPH_NODE_HA_ADVERTISED_ADDR=ha-node-1:9400 - - MORPH_NODE_LOG_LEVEL=info - volumes: - - ".devnet/ha-node1:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - - # ─── ha-node-2: Raft follower ───────────────────────────────────────────── - ha-node-2: - image: morph-node:feat - pull_policy: never - container_name: ha-node-2 - depends_on: - ha-node-0: - condition: service_started - restart: unless-stopped - ports: - - "26656" - - "27857:26657" - - "26658" - - "26660" - - "9701:9401" - environment: - # Rotated to new key for sequencer migration test (2026-05-11) - # New address: 0xAb70B9eAF487d6cfFD3AD420785BE389D5B28390 - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - MORPH_NODE_L2_ETH_RPC=http://ha-geth-2:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-2:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=http://layer1-cl:4000 - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - - MORPH_NODE_HA_ENABLED=true - - MORPH_NODE_HA_JOIN=ha-node-0:9401 - - MORPH_NODE_HA_SERVER_ID=ha-node-2 - - MORPH_NODE_HA_ADVERTISED_ADDR=ha-node-2:9400 - - MORPH_NODE_LOG_LEVEL=info - volumes: - - ".devnet/ha-node2:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - -volumes: - ha_morph_data_0: - ha_morph_data_1: - ha_morph_data_2: diff --git a/ops/docker-sequencer-test/docker-compose.override.yml b/ops/docker-sequencer-test/docker-compose.override.yml deleted file mode 100644 index b465dc18f..000000000 --- a/ops/docker-sequencer-test/docker-compose.override.yml +++ /dev/null @@ -1,117 +0,0 @@ -# Override file to use test images -# Copy this to ops/docker/docker-compose.override.yml before running test -version: '3.8' - -services: - morph-el-0: - image: morph-el-test:latest - build: - context: ../.. - dockerfile: ops/docker-sequencer-test/Dockerfile.l2-geth-test - - morph-el-1: - image: morph-el-test:latest - - morph-el-2: - image: morph-el-test:latest - - morph-el-3: - image: morph-el-test:latest - - node-0: - image: morph-node-test:latest - build: - context: ../.. - dockerfile: ops/docker-sequencer-test/Dockerfile.l2-node-test - environment: - # Sequencer PK intentionally NOT set on node-0: after upgrade node-0 becomes a V2 - # fullnode (hasSigner=false). The single sequencer is node-1 (see below). - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - # Timestamp-driven PBFT->sequencer upgrade (Unix ms). Same value on every node so all - # agree on the boundary. Computed at start time (now+offset) in run-test.sh start_l2_test. - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - - node-1: - image: morph-node-test:latest - environment: - # node-1 holds the sequencer signing key -> becomes the single sequencer after upgrade. - # Its address must match what setFirstSequencer registered (HA_SEQUENCER_ADDR in run-test.sh). - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY} - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - - node-2: - image: morph-node-test:latest - environment: - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - - node-3: - image: morph-node-test:latest - environment: - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - - sentry-el-0: - image: morph-el-test:latest - - sentry-node-0: - image: morph-node-test:latest - environment: - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - - # ========== Malicious Node (P2P security test) ========== - # Uses morph-node-malicious:latest built from test/p2p-security branch. - # No SEQUENCER_PRIVATE_KEY -> fullnode mode. Attack via MALICIOUS_MODE env. - malicious-geth-0: - image: morph-geth-test:latest - volumes: - - malicious_geth_data:/db - - ../l2-genesis/.devnet/genesis-l2.json:/genesis.json - - ./jwt-secret.txt:/jwt-secret.txt - - ./static-nodes.json:/db/geth/static-nodes.json - entrypoint: - - "/bin/sh" - - "/entrypoint.sh" - ports: - - "9045:8545" - - malicious-node-0: - image: morph-node-malicious:latest - depends_on: - malicious-geth-0: - condition: service_started - environment: - - MALICIOUS_MODE=${MALICIOUS_MODE:-} - - EMPTY_BLOCK_DELAY=true - - MORPH_NODE_L2_ETH_RPC=http://malicious-geth-0:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://malicious-geth-0:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_L1_CONFIRMATIONS=0 - volumes: - - .devnet/malicious-node-0:${NODE_DATA_DIR} - - ${PWD}/jwt-secret.txt:${JWT_SECRET_PATH} - command: > - morphnode - --home $NODE_DATA_DIR - -volumes: - malicious_geth_data: diff --git a/ops/docker-sequencer-test/docker-compose.reorg-test.override.yml b/ops/docker-sequencer-test/docker-compose.reorg-test.override.yml deleted file mode 100644 index 92eeb431f..000000000 --- a/ops/docker-sequencer-test/docker-compose.reorg-test.override.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: '3.8' -# Reorg restart loop integration test override. -# Enables the MORPH_TEST_REORG_RESTART_INTERVAL hook on node-1 (PBFT validator -# pre-upgrade, V2 fullnode post-upgrade). The hook periodically calls -# Node.testStartStopWhenReorg() which exercises the StopForReorg + -# SwitchToBlockSyncFromReorg path on the blocksync reactor. The test target -# must be a non-HA node (the hook is a no-op on HA-enabled nodes). -# -# Stack with: -# docker compose \ -# -f docker-compose-4nodes.yml \ -# -f docker-compose.override.yml \ -# -f docker-compose.ha-override.yml \ -# -f docker-compose.reorg-test.override.yml \ -# up -d -# -# Verification: -# - node-1 logs: docker logs node-1 2>&1 | grep '\[REORG_TEST\]' -# - block growth: curl http://localhost:8645 (eth_blockNumber must increase) -# - cluster: ha-geth block heights remain in sync with node-1 -services: - node-1: - environment: - - MORPH_TEST_REORG_RESTART_INTERVAL=120s diff --git a/ops/docker-sequencer-test/entrypoint-l2.sh b/ops/docker-sequencer-test/entrypoint-l2.sh deleted file mode 100644 index 04dfed476..000000000 --- a/ops/docker-sequencer-test/entrypoint-l2.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -set -e - -GETH_DATA_DIR=${GETH_DATA_DIR:-/db} -GENESIS_FILE_PATH=${GENESIS_FILE_PATH:-/genesis.json} -JWT_SECRET_PATH=${JWT_SECRET_PATH:-/jwt-secret.txt} - -# Initialize geth if not already done -if [ ! -d "$GETH_DATA_DIR/geth/chaindata" ]; then - echo "Initializing geth with genesis file..." - geth init --datadir "$GETH_DATA_DIR" "$GENESIS_FILE_PATH" -fi - -echo "Starting geth..." -exec geth \ - --datadir "$GETH_DATA_DIR" \ - --http \ - --http.addr "0.0.0.0" \ - --http.port 8545 \ - --http.api "eth,net,web3,debug,txpool,engine" \ - --http.corsdomain "*" \ - --http.vhosts "*" \ - --ws \ - --ws.addr "0.0.0.0" \ - --ws.port 8546 \ - --ws.api "eth,net,web3,debug,txpool,engine" \ - --ws.origins "*" \ - --authrpc.addr "0.0.0.0" \ - --authrpc.port 8551 \ - --authrpc.vhosts "*" \ - --authrpc.jwtsecret "$JWT_SECRET_PATH" \ - --networkid 53077 \ - --nodiscover \ - --syncmode full \ - --gcmode archive \ - --metrics \ - --metrics.addr "0.0.0.0" \ - --pprof \ - --pprof.addr "0.0.0.0" \ - --verbosity 3 \ - "$@" - diff --git a/ops/docker-sequencer-test/run-ha-test.sh b/ops/docker-sequencer-test/run-ha-test.sh deleted file mode 100755 index c36cd2183..000000000 --- a/ops/docker-sequencer-test/run-ha-test.sh +++ /dev/null @@ -1,1773 +0,0 @@ -#!/bin/bash -# ============================================================ -# Sequencer HA V2 Integration Test Runner -# ============================================================ -# Tests all HA features: config validation, cluster formation, -# leader election, block production, failover, admin API, -# and lifecycle operations. -# -# Usage: -# ./run-ha-test.sh [command] -# -# Commands: -# build - Build test Docker images (reuse run-test.sh) -# setup - Deploy L1, contracts, L2 genesis -# start - Start 3-node HA cluster -# test - Run full HA test suite -# stop - Stop all containers -# clean - Stop, remove containers and data -# logs - Show container logs -# status - Show block heights + HA status -# api - Run admin API tests only (cluster must be running) -# failover - Run failover tests only (cluster must be running) -# -# Environment Variables: -# UPGRADE_OFFSET_S - Seconds after cluster start to fire the timestamp upgrade (default: 120) -# UPGRADE_WAIT_S - Max seconds to wait for the upgrade to actually fire (default: 240) -# HA_FORM_WAIT - Seconds to wait for Raft cluster formation (default: 30) -# REPORT_OUTPUT - Where to write test report (default: docs/ha/ha-test-report.md) - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MORPH_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -BITGET_ROOT="$(cd "$MORPH_ROOT/.." && pwd)" -OPS_DIR="$MORPH_ROOT/ops" -DOCKER_DIR="$OPS_DIR/docker" -DOCS_DIR="$BITGET_ROOT/docs/ha" - -# ─── Configuration ──────────────────────────────────────────────────────────── -# Upgrade is timestamp-driven (not height). UPGRADE_OFFSET_S sets how many seconds after the -# cluster starts the PBFT->sequencer switch fires; start_ha_cluster turns it into an absolute -# SEQUENCER_UPGRADE_TIME (Unix ms) injected into every node. A pre-set SEQUENCER_UPGRADE_TIME -# (absolute ms) takes precedence over the offset. -UPGRADE_OFFSET_S=${UPGRADE_OFFSET_S:-120} -UPGRADE_WAIT_S=${UPGRADE_WAIT_S:-240} # max seconds to wait for the upgrade to actually fire -HA_FORM_WAIT=${HA_FORM_WAIT:-30} # seconds after upgrade to wait for cluster formation -REPORT_OUTPUT="${REPORT_OUTPUT:-$DOCS_DIR/ha-test-report.md}" - -# Absolute upgrade timestamp (ms) persisted by start_ha_cluster and reused by the `test` phase so -# recreating ha-node-* keeps the same boundary instead of falling back to 0 (= upgrade disabled). -UPGRADE_TIME_FILE="$DOCKER_DIR/.devnet/.sequencer_upgrade_time_ms" - -# L2 Geth RPC endpoints for the PBFT nodes (non-HA, pre-upgrade consensus) -L2_RPC_NODE0="http://127.0.0.1:8545" -L2_RPC_NODE1="http://127.0.0.1:8645" -L2_RPC_NODE2="http://127.0.0.1:8745" -L2_RPC_NODE3="http://127.0.0.1:8845" - -# L2 Geth RPC endpoints for the isolated HA cluster (ha-geth-0/1/2) -HA_L2_RPC_0="http://127.0.0.1:9145" -HA_L2_RPC_1="http://127.0.0.1:9245" -HA_L2_RPC_2="http://127.0.0.1:9345" - -# HA Admin RPC endpoints (host 9501/9601/9701 → ha-node-0/1/2 container:9401) -HA_RPC_NODE0="http://127.0.0.1:9501" -HA_RPC_NODE1="http://127.0.0.1:9601" -HA_RPC_NODE2="http://127.0.0.1:9701" - -# Docker compose commands -COMPOSE_BASE="docker compose -f docker-compose-4nodes.yml" -COMPOSE_OVERRIDE="docker compose -f docker-compose-4nodes.yml -f docker-compose.override.yml" -COMPOSE_HA="docker compose -f docker-compose-4nodes.yml -f docker-compose.override.yml -f docker-compose.ha-override.yml" - -# ─── Colors ─────────────────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } -log_success() { echo -e "${GREEN}[PASS]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[FAIL]${NC} $1"; } -log_section() { echo -e "\n${BOLD}${CYAN}══════════════════════════════════════${NC}"; \ - echo -e "${BOLD}${CYAN} $1${NC}"; \ - echo -e "${BOLD}${CYAN}══════════════════════════════════════${NC}"; } - -# ─── Test Result Tracking ───────────────────────────────────────────────────── -PASS=0 -FAIL=0 -SKIP=0 -REPORT_LINES=() -FAILED_TESTS=() - -record_test() { - local tc_id="$1" - local tc_name="$2" - local result="$3" # PASS | FAIL | SKIP - local evidence="$4" - local notes="${5:-}" - - if [ "$result" = "PASS" ]; then - PASS=$((PASS + 1)) - log_success "[$tc_id] $tc_name" - REPORT_LINES+=("### $tc_id: $tc_name\n\n**Status**: ✅ PASS\n") - elif [ "$result" = "FAIL" ]; then - FAIL=$((FAIL + 1)) - log_error "[$tc_id] $tc_name" - FAILED_TESTS+=("$tc_id: $tc_name") - REPORT_LINES+=("### $tc_id: $tc_name\n\n**Status**: ❌ FAIL\n") - else - SKIP=$((SKIP + 1)) - log_warn "[$tc_id] $tc_name (SKIPPED: $notes)" - REPORT_LINES+=("### $tc_id: $tc_name\n\n**Status**: ⏭️ SKIP — $notes\n") - fi - - if [ -n "$evidence" ]; then - REPORT_LINES+=("**Evidence**:\n\`\`\`\n$evidence\n\`\`\`\n") - fi - if [ -n "$notes" ] && [ "$result" != "SKIP" ]; then - REPORT_LINES+=("**Notes**: $notes\n") - fi - REPORT_LINES+=("---\n") -} - -# ─── Common Helpers ─────────────────────────────────────────────────────────── - -wait_for_rpc() { - local rpc_url="$1" - local max_retries=${2:-60} - local retry=0 - while [ $retry -lt $max_retries ]; do - if curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - "$rpc_url" 2>/dev/null | grep -q "result"; then - return 0 - fi - retry=$((retry + 1)) - sleep 2 - done - return 1 -} - -get_block_number() { - local rpc_url="${1:-$L2_RPC_NODE0}" - local result - result=$(curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - "$rpc_url" 2>/dev/null) - echo "$result" | grep -o '"result":"[^"]*"' | cut -d'"' -f4 | xargs printf "%d" 2>/dev/null || echo "0" -} - -wait_for_block() { - local target=$1 - local rpc_url="${2:-$L2_RPC_NODE0}" - while true; do - local cur=$(get_block_number "$rpc_url") - if [ "$cur" -ge "$target" ] 2>/dev/null; then - return 0 - fi - echo -ne "\r Block: $cur / $target " - sleep 3 - done - echo "" -} - -# ─── Upgrade-time Helpers (timestamp-driven upgrade) ────────────────────────── - -# Persist the absolute upgrade timestamp (ms) so a later, separate invocation (e.g. `test`) -# recreates containers with the SAME boundary instead of the compose default of 0 (= disabled). -persist_upgrade_time() { - mkdir -p "$(dirname "$UPGRADE_TIME_FILE")" - echo "${SEQUENCER_UPGRADE_TIME:-0}" > "$UPGRADE_TIME_FILE" -} - -# Resolve SEQUENCER_UPGRADE_TIME and export it so every subsequent `docker compose up` recreates -# nodes with the correct upgrade boundary. Resolution order: -# 1. already-set env (>0) 2. persisted file 3. value baked into running ha-node-0 -load_upgrade_time() { - if [ "${SEQUENCER_UPGRADE_TIME:-0}" -gt 0 ] 2>/dev/null; then - export SEQUENCER_UPGRADE_TIME - return 0 - fi - if [ -f "$UPGRADE_TIME_FILE" ]; then - SEQUENCER_UPGRADE_TIME=$(cat "$UPGRADE_TIME_FILE" 2>/dev/null | tr -dc '0-9') - fi - if [ "${SEQUENCER_UPGRADE_TIME:-0}" -le 0 ] 2>/dev/null; then - SEQUENCER_UPGRADE_TIME=$(docker inspect ha-node-0 \ - --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \ - | grep '^MORPH_NODE_SEQUENCER_UPGRADE_TIME=' | cut -d= -f2 | tr -dc '0-9' || true) - fi - if [ "${SEQUENCER_UPGRADE_TIME:-0}" -gt 0 ] 2>/dev/null; then - export SEQUENCER_UPGRADE_TIME - log_info "Using SEQUENCER_UPGRADE_TIME=$SEQUENCER_UPGRADE_TIME ms" - else - export SEQUENCER_UPGRADE_TIME=0 - log_warn "SEQUENCER_UPGRADE_TIME unknown — run 'start' first, else container recreation disables the upgrade." - fi -} - -# Wait until the PBFT->sequencer upgrade actually fires, detected via the consensus log on the -# always-present PBFT nodes (node-0..3). Fully decoupled from block height. -wait_for_upgrade() { - local max_wait="${UPGRADE_WAIT_S:-240}" - local waited=0 - cd "$DOCKER_DIR" - log_info "Waiting for timestamp upgrade to fire (max ${max_wait}s)..." - while [ "$waited" -lt "$max_wait" ]; do - if $COMPOSE_HA logs --tail=3000 node-0 node-1 node-2 node-3 2>/dev/null \ - | grep -q "switching to sequencer mode"; then - log_success "Upgrade fired (timestamp boundary crossed) after ~${waited}s" - return 0 - fi - echo -ne "\r waiting for upgrade... ${waited}s/${max_wait}s" - sleep 5 - waited=$((waited + 5)) - done - echo "" - log_error "Upgrade did not fire within ${max_wait}s" - return 1 -} - -# ─── HA-Specific Helpers ────────────────────────────────────────────────────── - -# Call a hakeeper JSON-RPC method -ha_call() { - local rpc_url="$1" - local method="$2" - local params="${3:-[]}" - curl -s --max-time 5 -X POST -H "Content-Type: application/json" \ - -d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":$params,\"id\":1}" \ - "$rpc_url" 2>/dev/null || echo '{"error":"curl failed"}' -} - -# Returns 1 if the node is HA leader, 0 otherwise -is_ha_leader() { - local rpc_url="$1" - local resp - resp=$(ha_call "$rpc_url" "ha_leader" "[]") - echo "$resp" | grep -c '"result":true' || true -} - -# Finds the HA RPC URL of the current leader; prints it or empty string -find_leader_rpc() { - for rpc_url in "$HA_RPC_NODE0" "$HA_RPC_NODE1" "$HA_RPC_NODE2"; do - if [ "$(is_ha_leader "$rpc_url")" -ge 1 ]; then - echo "$rpc_url" - return 0 - fi - done - echo "" -} - -# Wait until any node reports as leader (max_wait seconds) -wait_for_ha_leader() { - local max_wait="${1:-30}" - local waited=0 - echo -ne " Waiting for Raft leader..." - while [ $waited -lt $max_wait ]; do - local leader_rpc - leader_rpc=$(find_leader_rpc) - if [ -n "$leader_rpc" ]; then - echo -e " found at $leader_rpc" - return 0 - fi - sleep 2 - waited=$((waited + 2)) - echo -ne "." - done - echo -e " TIMEOUT" - return 1 -} - -# Get cluster membership JSON -get_membership() { - local rpc_url="$1" - ha_call "$rpc_url" "ha_clusterMembership" "[]" -} - -# Get membership version number -get_membership_version() { - local rpc_url="$1" - local membership - membership=$(get_membership "$rpc_url") - echo "$membership" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',{}).get('version',0))" 2>/dev/null || echo "0" -} - -# Count voters in cluster membership -count_voters() { - local rpc_url="$1" - local membership - membership=$(get_membership "$rpc_url") - echo "$membership" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - servers = d.get('result', {}).get('servers', []) - print(len([s for s in servers if s.get('suffrage', 1) == 0])) -except: - print(0) -" 2>/dev/null || echo "0" -} - -# Get server IDs from membership -get_server_ids() { - local rpc_url="$1" - local membership - membership=$(get_membership "$rpc_url") - echo "$membership" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - servers = d.get('result', {}).get('servers', []) - print(' '.join(s.get('id','?') for s in servers)) -except: - print('') -" 2>/dev/null || echo "" -} - -# Get server addrs from membership -get_server_addrs() { - local rpc_url="$1" - local membership - membership=$(get_membership "$rpc_url") - echo "$membership" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - servers = d.get('result', {}).get('servers', []) - print(' '.join(s.get('addr','?') for s in servers)) -except: - print('') -" 2>/dev/null || echo "" -} - -# Get addr of a specific server ID from membership -get_server_addr_by_id() { - local rpc_url="$1" - local server_id="$2" - local membership - membership=$(get_membership "$rpc_url") - echo "$membership" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - servers = d.get('result', {}).get('servers', []) - print(next((s['addr'] for s in servers if s['id']=='$server_id'), '')) -except: - print('') -" 2>/dev/null || echo "" -} - -# Map HA RPC URL to container name (isolated HA cluster nodes) -rpc_to_container() { - case "$1" in - "$HA_RPC_NODE0") echo "ha-node-0" ;; - "$HA_RPC_NODE1") echo "ha-node-1" ;; - "$HA_RPC_NODE2") echo "ha-node-2" ;; - *) echo "unknown" ;; - esac -} - -# Get the geth RPC for a given HA RPC URL (isolated HA cluster geth endpoints) -ha_rpc_to_geth_rpc() { - case "$1" in - "$HA_RPC_NODE0") echo "$HA_L2_RPC_0" ;; - "$HA_RPC_NODE1") echo "$HA_L2_RPC_1" ;; - "$HA_RPC_NODE2") echo "$HA_L2_RPC_2" ;; - *) echo "$HA_L2_RPC_0" ;; - esac -} - -# ─── Setup Functions ────────────────────────────────────────────────────────── - -setup_ha_override() { - log_info "Copying HA override to $DOCKER_DIR..." - cp "$SCRIPT_DIR/docker-compose.override.yml" "$DOCKER_DIR/docker-compose.override.yml" - cp "$SCRIPT_DIR/docker-compose.ha-override.yml" "$DOCKER_DIR/docker-compose.ha-override.yml" - log_success "Override files ready." -} - -remove_ha_override() { - rm -f "$DOCKER_DIR/docker-compose.override.yml" - rm -f "$DOCKER_DIR/docker-compose.ha-override.yml" -} - -# Generate .devnet/ha-node{0,1,2}/ directories and ha-nodekey{0,1,2} files -# for the isolated Raft cluster. Called once at start_ha_cluster time. -# -# Each ha-nodeN home contains: -# config/config.toml — copied from node4 (fullnode template) -# config/genesis.json — copied from node4 (same tendermint chain) -# config/node_key.json — freshly generated, unique per node -# data/priv_validator_state.json — initial (height 0), fullnode never signs -# No bls_key.json or priv_validator_key.json (fullnode mode). -# -# Each ha-nodekeyN is a 64-hex-char geth P2P private key (independent from node-*). -setup_ha_nodes_config() { - log_info "Preparing .devnet/ha-node{0,1,2}/ configs and ha-nodekey{0,1,2}..." - cd "$DOCKER_DIR" - - local template_dir="$DOCKER_DIR/.devnet/node4" - if [ ! -d "$template_dir/config" ]; then - log_error ".devnet/node4/config not found — run 'setup' first" - return 1 - fi - - for i in 0 1 2; do - local target=".devnet/ha-node$i" - if [ -d "$target" ]; then - log_info " $target already exists, skipping" - else - mkdir -p "$target/config" "$target/data" - cp "$template_dir/config/config.toml" "$target/config/" - cp "$template_dir/config/genesis.json" "$target/config/" - # Update moniker for log clarity - if [ "$(uname)" = "Darwin" ]; then - sed -i '' "s/moniker = \".*\"/moniker = \"ha-node-$i\"/" "$target/config/config.toml" - else - sed -i "s/moniker = \".*\"/moniker = \"ha-node-$i\"/" "$target/config/config.toml" - fi - # Initial priv_validator_state (file must exist even for fullnode) - echo '{"height":"0","round":0,"step":0}' > "$target/data/priv_validator_state.json" - # Generate a fresh tendermint node_key inside the test image so we - # don't depend on a host-installed tendermint binary. - docker run --rm --entrypoint tendermint \ - -v "$PWD/$target:/home-ha" \ - morph-node-test:latest gen-node-key --home /home-ha >/dev/null - log_success " $target ready" - fi - - # Geth P2P nodekey (64 hex chars) - local nodekey_file="ha-nodekey$i" - if [ -f "$nodekey_file" ]; then - log_info " $nodekey_file already exists, skipping" - else - openssl rand -hex 32 > "$nodekey_file" - log_success " $nodekey_file generated" - fi - done -} - -start_ha_cluster() { - log_info "Starting PBFT nodes + isolated HA cluster..." - cd "$DOCKER_DIR" - - setup_ha_override - source .env 2>/dev/null || true - - # Prepare configs/keys for the isolated HA cluster - setup_ha_nodes_config - - # Wait for L1 to finalize past the contract deployment block - local l1_latest - l1_latest=$(curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - http://127.0.0.1:9545 2>/dev/null | grep -o '"result":"0x[^"]*"' | cut -d'"' -f4) - l1_latest=$(printf "%d" "$l1_latest" 2>/dev/null || echo 1) - - log_info "Waiting for L1 finalized >= $l1_latest..." - local waited=0 - while [ $waited -lt 120 ]; do - local fin - fin=$(curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["finalized",false],"id":1}' \ - http://127.0.0.1:9545 2>/dev/null | grep -o '"number":"0x[^"]*"' | head -1 | cut -d'"' -f4) - local fin_dec=$(printf "%d" "$fin" 2>/dev/null || echo 0) - if [ "$fin_dec" -ge "$l1_latest" ]; then - log_success "L1 finalized at $fin_dec" - break - fi - echo -ne "\r L1 finalized: $fin_dec / $l1_latest" - sleep 3 - waited=$((waited + 3)) - done - - # Stop any existing containers from a previous run - $COMPOSE_HA stop morph-el-0 morph-el-1 morph-el-2 morph-el-3 \ - node-0 node-1 node-2 node-3 sentry-el-0 sentry-node-0 \ - ha-geth-0 ha-geth-1 ha-geth-2 ha-node-0 ha-node-1 ha-node-2 2>/dev/null || true - - # Start ALL geth nodes (PBFT + isolated HA + sentry) - log_info "Starting geth nodes (PBFT morph-el-* + ha-geth-* + sentry)..." - $COMPOSE_HA up -d morph-el-0 morph-el-1 morph-el-2 morph-el-3 \ - ha-geth-0 ha-geth-1 ha-geth-2 sentry-el-0 - sleep 5 - - # Start tendermint nodes: - # - node-0/1/2/3: PBFT validators (baseline), no HA config. - # - ha-node-0 bootstrap, ha-node-1/2 join — isolated Raft cluster. - # - sentry-node-0: non-HA V2 fullnode after upgrade. - # Timestamp-driven upgrade (block.Time ~= wall clock): fire the PBFT->sequencer switch - # UPGRADE_OFFSET_S seconds after start. The absolute timestamp is injected into every node via - # the overrides and persisted so the `test` phase reuses the identical boundary. - if [ "${SEQUENCER_UPGRADE_TIME:-0}" -gt 0 ] 2>/dev/null; then - export SEQUENCER_UPGRADE_TIME - log_info "Using pre-set SEQUENCER_UPGRADE_TIME=$SEQUENCER_UPGRADE_TIME ms" - else - export SEQUENCER_UPGRADE_TIME=$(( ($(date +%s) + UPGRADE_OFFSET_S) * 1000 )) - log_info "Sequencer upgrade time set to $SEQUENCER_UPGRADE_TIME ms (now + ${UPGRADE_OFFSET_S}s)" - fi - persist_upgrade_time - - log_info "Starting tendermint nodes (node-0..3 PBFT, ha-node-0 bootstrap, ha-node-1/2 join)..." - $COMPOSE_HA up -d node-0 node-1 node-2 node-3 ha-node-0 ha-node-1 ha-node-2 sentry-node-0 - - log_info "Waiting for geth RPC..." - wait_for_rpc "$L2_RPC_NODE0" 60 - wait_for_rpc "$HA_L2_RPC_0" 60 || log_warn "ha-geth-0 RPC not ready within 60s" - log_success "PBFT + HA cluster started!" -} - -# ─── Category 1: Config Tests ───────────────────────────────────────────────── - -run_config_tests() { - log_section "Category 1: Config validation (Config Tests)" - - # Wait for the timestamp upgrade to actually fire + HA formation before running config tests. - wait_for_upgrade || log_warn "Proceeding despite upgrade-detection timeout" - log_info "Waiting ${HA_FORM_WAIT}s for Raft cluster to form..." - sleep "$HA_FORM_WAIT" - - # TC-CFG-01: bootstrap flag takes effect - log_info "--- TC-CFG-01: bootstrap flag takes effect ---" - local node0_leader - node0_leader=$(is_ha_leader "$HA_RPC_NODE0") - local resp_cfg01 - resp_cfg01=$(ha_call "$HA_RPC_NODE0" "ha_leader" "[]") - if [ "$node0_leader" -ge 1 ]; then - record_test "TC-CFG-01" "bootstrap flag takes effect" "PASS" \ - "ha_leader on ha-node-0: $resp_cfg01" - else - # ha-node-0 bootstrapped but Raft may have re-elected after restarts; as long as - # ANY node is leader, the bootstrap mechanism worked (cluster was seeded by ha-node-0). - local any_leader_rpc - any_leader_rpc=$(find_leader_rpc) - if [ -n "$any_leader_rpc" ]; then - local current_leader - current_leader=$(rpc_to_container "$any_leader_rpc") - record_test "TC-CFG-01" "bootstrap flag takes effect" "PASS" \ - "Current leader=$current_leader (ha-node-0 bootstrapped the cluster, Raft re-elected after restart)\nha-node-0 response: $resp_cfg01" - else - record_test "TC-CFG-01" "bootstrap flag takes effect" "FAIL" \ - "ha_leader on ha-node-0: $resp_cfg01\nNo leader found in cluster — bootstrap may have failed" - fi - fi - - # TC-CFG-02: join flag takes effect (3-node cluster formed) - log_info "--- TC-CFG-02: join flag takes effect ---" - local leader_rpc - leader_rpc=$(find_leader_rpc) - local voter_count=0 - local membership_resp="" - if [ -n "$leader_rpc" ]; then - membership_resp=$(get_membership "$leader_rpc") - voter_count=$(count_voters "$leader_rpc") - fi - if [ "$voter_count" -eq 3 ]; then - record_test "TC-CFG-02" "join flag takes effect — 3-node cluster formed" "PASS" \ - "voter_count=$voter_count\nmembership=$membership_resp" - else - record_test "TC-CFG-02" "join flag takes effect — 3-node cluster formed" "FAIL" \ - "voter_count=$voter_count (expected 3)\nmembership=$membership_resp" - fi - - # TC-CFG-03: server-id flag takes effect - log_info "--- TC-CFG-03: server-id flag takes effect ---" - local server_ids="" - if [ -n "$leader_rpc" ]; then - server_ids=$(get_server_ids "$leader_rpc") - fi - if echo "$server_ids" | grep -q "ha-node-0" && \ - echo "$server_ids" | grep -q "ha-node-1" && \ - echo "$server_ids" | grep -q "ha-node-2"; then - record_test "TC-CFG-03" "server-id flag takes effect" "PASS" \ - "server_ids: $server_ids" - else - record_test "TC-CFG-03" "server-id flag takes effect" "FAIL" \ - "server_ids: $server_ids (expected ha-node-0, ha-node-1, ha-node-2)" - fi - - # TC-CFG-04: pure flag mode (no config file) - log_info "--- TC-CFG-04: pure flag mode (no config file) ---" - # Verify HA works without ha.toml config file. - # If cluster formed and leader elected, pure-flag mode works. - if [ -n "$leader_rpc" ] && [ "$voter_count" -ge 2 ]; then - record_test "TC-CFG-04" "pure flag mode (no config file)" "PASS" \ - "HA cluster formed with only env var flags (no --ha.config file)\nleader=$leader_rpc voter_count=$voter_count" - else - record_test "TC-CFG-04" "pure flag mode (no config file)" "FAIL" \ - "Cluster did not form — flag-only mode may not work\nleader_rpc='$leader_rpc' voter_count=$voter_count" - fi - - # TC-CFG-05: advertised_addr auto-detection (non-0.0.0.0) - log_info "--- TC-CFG-05: advertised_addr auto-detection ---" - local addrs="" - if [ -n "$leader_rpc" ]; then - addrs=$(get_server_addrs "$leader_rpc") - fi - local bad_addr=0 - for addr in $addrs; do - if echo "$addr" | grep -qE "^0\.0\.0\.0|^:"; then - bad_addr=1 - break - fi - done - if [ -n "$addrs" ] && [ "$bad_addr" -eq 0 ]; then - record_test "TC-CFG-05" "advertised_addr auto-detection (non-0.0.0.0)" "PASS" \ - "server addrs: $addrs\nAll addrs are non-wildcard IPs" - else - record_test "TC-CFG-05" "advertised_addr auto-detection (non-0.0.0.0)" "FAIL" \ - "server addrs: $addrs\nbad_addr=$bad_addr (found 0.0.0.0 or empty)" - fi -} - -# ─── Category 2: Cluster Formation Tests ───────────────────────────────────── - -run_cluster_tests() { - log_section "Category 2: Cluster formation (Cluster Tests)" - - local leader_rpc - leader_rpc=$(find_leader_rpc) - - # TC-CLU-01: ha-node-0 becomes the first leader (bootstrap node) - log_info "--- TC-CLU-01: ha-node-0 becomes initial leader ---" - cd "$DOCKER_DIR" - local node0_leader_log - node0_leader_log=$($COMPOSE_HA logs ha-node-0 2>/dev/null | grep -i "leaderReady\|hakeeper: raft\|leader" | tail -5 || true) - local node0_is_leader - node0_is_leader=$(is_ha_leader "$HA_RPC_NODE0") - if [ "$node0_is_leader" -ge 1 ]; then - record_test "TC-CLU-01" "ha-node-0 becomes initial leader (bootstrap node)" "PASS" \ - "ha_leader on ha-node-0=true\nlog: $node0_leader_log" - else - # ha-node-0 might have transferred leadership; check if any node is leader - if [ -n "$leader_rpc" ]; then - local leader_node - leader_node=$(rpc_to_container "$leader_rpc") - record_test "TC-CLU-01" "ha-node-0 becomes initial leader (bootstrap node)" "PASS" \ - "Current leader=$leader_node (ha-node-0 bootstrapped, may have transferred)\nha-node-0 log: $node0_leader_log" - else - record_test "TC-CLU-01" "ha-node-0 becomes initial leader (bootstrap node)" "FAIL" \ - "No leader found. ha-node-0 logs: $node0_leader_log" - fi - fi - - # TC-CLU-02: full 3-node cluster formed — all 3 as Voter - log_info "--- TC-CLU-02: full 3-node cluster formed ---" - local membership_resp voter_count server_ids - if [ -n "$leader_rpc" ]; then - membership_resp=$(get_membership "$leader_rpc") - voter_count=$(count_voters "$leader_rpc") - server_ids=$(get_server_ids "$leader_rpc") - else - voter_count=0; server_ids=""; membership_resp="no leader" - fi - if [ "$voter_count" -eq 3 ]; then - record_test "TC-CLU-02" "full 3-node cluster formed (3 Voters)" "PASS" \ - "voter_count=$voter_count\nservers=$server_ids\nmembership=$membership_resp" - else - record_test "TC-CLU-02" "full 3-node cluster formed (3 Voters)" "FAIL" \ - "voter_count=$voter_count (expected 3)\nservers=$server_ids" - fi - - # TC-CLU-03: joinLoop retry mechanism (verified via logs) - log_info "--- TC-CLU-03: joinLoop retry mechanism ---" - cd "$DOCKER_DIR" - local join_logs - join_logs=$($COMPOSE_HA logs ha-node-1 ha-node-2 2>/dev/null | \ - grep -i "joined cluster\|join attempt\|joining cluster\|hakeeper.*join" | head -10 || true) - if echo "$join_logs" | grep -qi "joined"; then - record_test "TC-CLU-03" "joinLoop retry mechanism" "PASS" \ - "Join log evidence:\n$join_logs" - else - # If membership is 3-node, join succeeded even if log message differs - if [ "$voter_count" -eq 3 ]; then - record_test "TC-CLU-03" "joinLoop retry mechanism" "PASS" \ - "3-node cluster formed (join succeeded); specific retry log not captured\nJoin-related logs: $join_logs" - else - record_test "TC-CLU-03" "joinLoop retry mechanism" "FAIL" \ - "No join success logs found and cluster is not 3-node\nLogs: $join_logs" - fi - fi - - # TC-CLU-04: repeated bootstrap is harmless (ErrCantBootstrap ignored) - log_info "--- TC-CLU-04: repeated bootstrap is harmless (ErrCantBootstrap ignored) ---" - cd "$DOCKER_DIR" - local bootstrap_logs - bootstrap_logs=$($COMPOSE_HA logs ha-node-0 2>/dev/null | \ - grep -i "ErrCantBootstrap\|bootstrap\|already bootstrapped" | head -5 || true) - # ErrCantBootstrap is silently ignored in the code (errors.Is check). - # After restart with --ha.bootstrap on existing node, no fatal error should appear. - local fatal_bootstrap_err - fatal_bootstrap_err=$($COMPOSE_HA logs ha-node-0 2>/dev/null | \ - grep -i "bootstrap.*error\|fatal.*bootstrap" | grep -v "ErrCantBootstrap" | head -3 || true) - if [ -z "$fatal_bootstrap_err" ]; then - record_test "TC-CLU-04" "repeated bootstrap is harmless" "PASS" \ - "No fatal bootstrap error in logs\nBootstrap-related logs:\n$bootstrap_logs" - else - record_test "TC-CLU-04" "repeated bootstrap is harmless" "FAIL" \ - "Fatal bootstrap error found:\n$fatal_bootstrap_err" - fi -} - -# ─── Category 3: Block Production Tests ─────────────────────────────────────── - -run_block_tests() { - log_section "Category 3: Block production (Block Production Tests)" - - # Ensure blocks are flowing post-upgrade (a handful of fresh blocks past the current head). - local current - current=$(get_block_number "$L2_RPC_NODE0") - local target=$((current + 5)) - log_info "Waiting for a few post-upgrade blocks (target $target, current: $current)..." - wait_for_block "$target" "$L2_RPC_NODE0" - - local leader_rpc - leader_rpc=$(find_leader_rpc) - - # TC-BLK-01: leader produces blocks after upgrade - log_info "--- TC-BLK-01: leader produces blocks ---" - local h1 h2 - h1=$(get_block_number "$L2_RPC_NODE0") - sleep 10 - h2=$(get_block_number "$L2_RPC_NODE0") - if [ "$h2" -gt "$h1" ]; then - record_test "TC-BLK-01" "leader produces blocks after upgrade" "PASS" \ - "Block height increased: $h1 → $h2 (delta=$((h2-h1)) in 10s)" - else - record_test "TC-BLK-01" "leader produces blocks after upgrade" "FAIL" \ - "Block height stuck: $h1 → $h2" - fi - - # TC-BLK-02: follower does not produce blocks (only leader calls produceBlock) - log_info "--- TC-BLK-02: follower does not produce blocks ---" - cd "$DOCKER_DIR" - # Check non-leader HA cluster nodes - local follower_produce_logs="" - for node in ha-node-1 ha-node-2; do - local node_rpc="${HA_RPC_NODE1}" - if [ "$node" = "ha-node-2" ]; then node_rpc="${HA_RPC_NODE2}"; fi - local is_follower=0 - if [ "$(is_ha_leader "$node_rpc")" -eq 0 ]; then is_follower=1; fi - if [ "$is_follower" -eq 1 ]; then - local produce_log - produce_log=$($COMPOSE_HA logs "$node" 2>/dev/null | \ - grep "Producing block\|Block produced and queued\|Block committed via HA" | head -3 || true) - if [ -n "$produce_log" ]; then - follower_produce_logs="$follower_produce_logs\n$node: $produce_log" - fi - fi - done - if [ -z "$follower_produce_logs" ]; then - record_test "TC-BLK-02" "follower does not produce blocks" "PASS" \ - "No 'Producing block' or 'Block produced' log found on follower nodes" - else - # Note: "Block committed via HA" may appear on leader after Commit() returns - # Only "Producing block" on non-leader is a real failure - local real_fail - real_fail=$(echo -e "$follower_produce_logs" | grep "Producing block" || true) - if [ -z "$real_fail" ]; then - record_test "TC-BLK-02" "follower does not produce blocks" "PASS" \ - "Follower produces no blocks (some commit logs are expected on leader path)\nLogs: $follower_produce_logs" - else - record_test "TC-BLK-02" "follower does not produce blocks" "FAIL" \ - "Follower 'Producing block' log found (should only be on leader):\n$real_fail" - fi - fi - - # TC-BLK-03: follower sync — geth heights match across all L2 nodes - # (PBFT nodes node-0..3, HA cluster ha-node-0..2 via ha-geth-0..2) - log_info "--- TC-BLK-03: follower sync ---" - sleep 5 # allow sync to settle - local bn0 bn1 bn2 bn3 h0 h1 h2 - bn0=$(get_block_number "$L2_RPC_NODE0") - bn1=$(get_block_number "$L2_RPC_NODE1") - bn2=$(get_block_number "$L2_RPC_NODE2") - bn3=$(get_block_number "$L2_RPC_NODE3") - h0=$(get_block_number "$HA_L2_RPC_0") - h1=$(get_block_number "$HA_L2_RPC_1") - h2=$(get_block_number "$HA_L2_RPC_2") - local max_diff=3 - local ref=$bn0 - local all_ok=1 - for v in "$bn1" "$bn2" "$bn3" "$h0" "$h1" "$h2"; do - local d=$((ref - v)); d=${d#-} - if [ "$d" -gt "$max_diff" ]; then all_ok=0; fi - done - local evidence="PBFT: node-0=$bn0 node-1=$bn1 node-2=$bn2 node-3=$bn3\nHA: ha-node-0=$h0 ha-node-1=$h1 ha-node-2=$h2\nMax diff allowed: $max_diff" - if [ "$all_ok" -eq 1 ]; then - record_test "TC-BLK-03" "follower sync (PBFT + HA all in lockstep)" "PASS" "$evidence" - else - record_test "TC-BLK-03" "follower sync (PBFT + HA all in lockstep)" "FAIL" "$evidence" - fi - - # TC-BLK-04: existing block idempotently skipped (ApplyBlock idempotent) - log_info "--- TC-BLK-04: existing block idempotently skipped ---" - cd "$DOCKER_DIR" - # Check no "duplicate block" or reorg error logs on HA followers - local dup_errors - dup_errors=$($COMPOSE_HA logs ha-node-1 ha-node-2 2>/dev/null | \ - grep -i "duplicate block\|already applied\|idempotent\|already on-chain" | head -5 || true) - # Check no panics or unexpected errors on block apply - local apply_errors - apply_errors=$($COMPOSE_HA logs ha-node-1 ha-node-2 2>/dev/null | \ - grep -i "FSM apply.*error\|ApplyBlock.*error" | head -3 || true) - if [ -z "$apply_errors" ]; then - record_test "TC-BLK-04" "existing block idempotently skipped" "PASS" \ - "No FSMApplyError logs on followers\nIdempotent skip messages: ${dup_errors:-none}" - else - record_test "TC-BLK-04" "existing block idempotently skipped" "FAIL" \ - "FSM apply errors found on followers:\n$apply_errors" - fi -} - -# ─── Category 4: HA Failover Tests ──────────────────────────────────────────── - -run_failover_tests() { - log_section "Category 4: Leader failover (HA Failover Tests)" - - # Record current leader before failover - local leader_rpc - leader_rpc=$(find_leader_rpc) - if [ -z "$leader_rpc" ]; then - log_error "No leader found — skipping failover tests" - record_test "TC-HA-01" "kill leader → auto re-election" "SKIP" "" "No leader found before test" - record_test "TC-HA-02" "new leader produces blocks" "SKIP" "" "No leader found before test" - record_test "TC-HA-03" "failover block interval" "SKIP" "" "No leader found before test" - record_test "TC-HA-04" "old leader rejoins" "SKIP" "" "No leader found before test" - record_test "TC-HA-05" "second failover" "SKIP" "" "No leader found before test" - return - fi - local leader_node - leader_node=$(rpc_to_container "$leader_rpc") - local leader_geth_rpc - leader_geth_rpc=$(ha_rpc_to_geth_rpc "$leader_rpc") - - log_info "Current leader: $leader_node ($leader_rpc)" - - # TC-HA-01: kill leader → auto re-election - log_info "--- TC-HA-01: kill leader → auto re-election ---" - local pre_kill_height - pre_kill_height=$(get_block_number "$leader_geth_rpc") - local kill_time - kill_time=$(date +%s) - - log_info "Killing $leader_node (leader)..." - cd "$DOCKER_DIR" - $COMPOSE_HA stop "$leader_node" 2>/dev/null || true - - # Wait for new leader election (up to 30s) - local new_leader_rpc="" - local waited=0 - while [ $waited -lt 30 ]; do - sleep 2 - waited=$((waited + 2)) - for rpc_url in "$HA_RPC_NODE0" "$HA_RPC_NODE1" "$HA_RPC_NODE2"; do - # Skip the dead leader - if [ "$(rpc_to_container "$rpc_url")" = "$leader_node" ]; then continue; fi - if [ "$(is_ha_leader "$rpc_url")" -ge 1 ]; then - new_leader_rpc="$rpc_url" - break 2 - fi - done - echo -ne "\r Waiting for new leader... ${waited}s" - done - echo "" - - local election_time=$(($(date +%s) - kill_time)) - if [ -n "$new_leader_rpc" ]; then - local new_leader_node - new_leader_node=$(rpc_to_container "$new_leader_rpc") - record_test "TC-HA-01" "kill leader → auto re-election" "PASS" \ - "Killed: $leader_node\nNew leader: $new_leader_node ($new_leader_rpc)\nElection time: ${election_time}s" - else - record_test "TC-HA-01" "kill leader → auto re-election" "FAIL" \ - "No new leader elected after 30s\nKilled: $leader_node" - # Skip remaining failover tests - record_test "TC-HA-02" "new leader produces blocks" "SKIP" "" "No new leader elected" - record_test "TC-HA-03" "failover block interval" "SKIP" "" "No new leader elected" - record_test "TC-HA-04" "old leader rejoins" "SKIP" "" "No new leader elected" - record_test "TC-HA-05" "second failover" "SKIP" "" "No new leader elected" - return - fi - local new_leader_node - new_leader_node=$(rpc_to_container "$new_leader_rpc") - local new_leader_geth - new_leader_geth=$(ha_rpc_to_geth_rpc "$new_leader_rpc") - - # TC-HA-02: new leader produces blocks - log_info "--- TC-HA-02: new leader produces blocks ---" - local h1 h2 - h1=$(get_block_number "$new_leader_geth") - log_info "Waiting 15s for new leader ($new_leader_node) to produce blocks..." - sleep 15 - h2=$(get_block_number "$new_leader_geth") - if [ "$h2" -gt "$h1" ]; then - record_test "TC-HA-02" "new leader produces blocks" "PASS" \ - "New leader ($new_leader_node) produced blocks: $h1 → $h2 (+$((h2-h1)) in 15s)" - else - record_test "TC-HA-02" "new leader produces blocks" "FAIL" \ - "New leader ($new_leader_node) not producing blocks: $h1 → $h2" - fi - - # TC-HA-03: failover block interval (< 10s) - log_info "--- TC-HA-03: failover block interval ---" - if [ "$election_time" -le 10 ]; then - record_test "TC-HA-03" "failover block interval (target <10s)" "PASS" \ - "Kill to new leader detected: ${election_time}s (≤ 10s target)" - else - record_test "TC-HA-03" "failover block interval (target <10s)" "FAIL" \ - "Kill to new leader detected: ${election_time}s (> 10s target)\nNote: actual first block may come later due to Barrier" - fi - - # TC-HA-04: old leader rejoins (as a follower) - log_info "--- TC-HA-04: old leader rejoins ---" - log_info "Restarting old leader ($leader_node)..." - cd "$DOCKER_DIR" - $COMPOSE_HA start "$leader_node" 2>/dev/null || $COMPOSE_HA up -d "$leader_node" - sleep 20 # allow rejoin and sync - - local old_leader_is_follower=0 - local old_leader_rpc="$leader_rpc" - if [ "$(is_ha_leader "$old_leader_rpc")" -eq 0 ]; then - old_leader_is_follower=1 - fi - # Check old leader's block height is catching up - local old_geth_rpc - old_geth_rpc=$(ha_rpc_to_geth_rpc "$old_leader_rpc") - local old_height new_height - old_height=$(get_block_number "$old_geth_rpc") - new_height=$(get_block_number "$new_leader_geth") - local rejoin_diff=$((new_height - old_height)); rejoin_diff=${rejoin_diff#-} - - # After restart: old leader should be follower and syncing - local new_voter_count - new_voter_count=$(count_voters "$new_leader_rpc") - - if [ "$old_leader_is_follower" -eq 1 ] && [ "$new_voter_count" -eq 3 ]; then - record_test "TC-HA-04" "old leader rejoins (as follower)" "PASS" \ - "Old leader ($leader_node) is now follower (leader=false)\nCluster size: $new_voter_count voters\nHeight sync: old=$old_height, new=$new_height, diff=$rejoin_diff" - elif [ "$old_leader_is_follower" -eq 1 ]; then - record_test "TC-HA-04" "old leader rejoins (as follower)" "PASS" \ - "Old leader ($leader_node) is follower (leader=false)\nCluster may still be re-forming (voter_count=$new_voter_count)" - else - record_test "TC-HA-04" "old leader rejoins (as follower)" "FAIL" \ - "Old leader ($leader_node) still reports as leader OR HA RPC not reachable\nha_leader=$(ha_call "$old_leader_rpc" "ha_leader" "[]")\nvoter_count=$new_voter_count" - fi - - # TC-HA-05: second failover — kill new leader, third node takes over - log_info "--- TC-HA-05: second failover ---" - local current_leader_rpc - current_leader_rpc=$(find_leader_rpc) - if [ -z "$current_leader_rpc" ]; then - record_test "TC-HA-05" "second failover" "SKIP" "" "Could not find current leader for 2nd failover" - return - fi - local current_leader_node - current_leader_node=$(rpc_to_container "$current_leader_rpc") - - log_info "Second failover: killing $current_leader_node..." - cd "$DOCKER_DIR" - $COMPOSE_HA stop "$current_leader_node" 2>/dev/null || true - local kill2_time=$(date +%s) - - # Wait for third leader (check ALL surviving nodes — first leader was restarted in TC-HA-04) - local third_leader_rpc="" - waited=0 - while [ $waited -lt 30 ]; do - sleep 2; waited=$((waited + 2)) - for rpc_url in "$HA_RPC_NODE0" "$HA_RPC_NODE1" "$HA_RPC_NODE2"; do - if [ "$(rpc_to_container "$rpc_url")" = "$current_leader_node" ]; then continue; fi - if [ "$(is_ha_leader "$rpc_url")" -ge 1 ]; then - third_leader_rpc="$rpc_url" - break 2 - fi - done - echo -ne "\r Waiting for 3rd leader... ${waited}s" - done - echo "" - local failover2_time=$(($(date +%s) - kill2_time)) - - # Restart the second killed node - cd "$DOCKER_DIR" - $COMPOSE_HA start "$current_leader_node" 2>/dev/null || true - - if [ -n "$third_leader_rpc" ]; then - local third_leader_node - third_leader_node=$(rpc_to_container "$third_leader_rpc") - # Verify blocks flowing from 3rd leader - local third_geth - third_geth=$(ha_rpc_to_geth_rpc "$third_leader_rpc") - local h3a h3b - h3a=$(get_block_number "$third_geth") - sleep 10 - h3b=$(get_block_number "$third_geth") - if [ "$h3b" -gt "$h3a" ]; then - record_test "TC-HA-05" "second failover" "PASS" \ - "2nd leader killed: $current_leader_node\n3rd leader: $third_leader_node, election: ${failover2_time}s\nBlocks: $h3a → $h3b" - else - record_test "TC-HA-05" "second failover" "FAIL" \ - "3rd leader ($third_leader_node) not producing blocks: $h3a → $h3b" - fi - else - record_test "TC-HA-05" "second failover" "FAIL" \ - "No 3rd leader elected after 30s (killed: $current_leader_node)" - fi - - # Ensure all killed HA nodes are restarted before next tests - cd "$DOCKER_DIR" - log_info "Restarting all HA nodes for subsequent tests..." - $COMPOSE_HA up -d ha-node-0 ha-node-1 ha-node-2 2>/dev/null || true - sleep 15 - wait_for_ha_leader 30 || true -} - -# ─── Category 5: Admin API Tests ────────────────────────────────────────────── - -run_api_tests() { - log_section "Category 5: Admin API tests (8 endpoints)" - - local leader_rpc - leader_rpc=$(find_leader_rpc) - if [ -z "$leader_rpc" ]; then - log_warn "No leader found — trying to wait..." - wait_for_ha_leader 20 || true - leader_rpc=$(find_leader_rpc) - fi - if [ -z "$leader_rpc" ]; then - log_error "Still no leader — skipping all API tests" - for n in 01 02 03 04 05 06 07 08; do - record_test "TC-API-$n" "hakeeper API test" "SKIP" "" "No leader available" - done - return - fi - local leader_node - leader_node=$(rpc_to_container "$leader_rpc") - log_info "Using leader: $leader_node ($leader_rpc)" - - # TC-API-01: ha_leader - log_info "--- TC-API-01: ha_leader ---" - local resp01 - resp01=$(ha_call "$leader_rpc" "ha_leader" "[]") - if echo "$resp01" | grep -q '"result":true'; then - record_test "TC-API-01" "ha_leader" "PASS" "Request: ha_leader []\nResponse: $resp01" - else - record_test "TC-API-01" "ha_leader" "FAIL" "Response: $resp01" - fi - - # TC-API-02: ha_leaderWithID - log_info "--- TC-API-02: ha_leaderWithID ---" - local resp02 - resp02=$(ha_call "$leader_rpc" "ha_leaderWithID" "[]") - if echo "$resp02" | grep -q '"id"'; then - record_test "TC-API-02" "ha_leaderWithID" "PASS" "Response: $resp02" - else - record_test "TC-API-02" "ha_leaderWithID" "FAIL" "Response: $resp02 (expected {id, addr, suffrage})" - fi - - # TC-API-03: ha_clusterMembership - log_info "--- TC-API-03: ha_clusterMembership ---" - local resp03 - resp03=$(ha_call "$leader_rpc" "ha_clusterMembership" "[]") - local voter_count03 - voter_count03=$(count_voters "$leader_rpc") - if echo "$resp03" | grep -q '"servers"' && [ "$voter_count03" -ge 2 ]; then - record_test "TC-API-03" "ha_clusterMembership" "PASS" \ - "Response: $resp03\nvoter_count=$voter_count03" - else - record_test "TC-API-03" "ha_clusterMembership" "FAIL" \ - "Response: $resp03\nvoter_count=$voter_count03" - fi - - # TC-API-04: ha_addServerAsVoter (remove a FOLLOWER + re-add it) - # Key rule: always remove a follower (not the leader) to avoid leadership transfer confusion. - # After remove, re-query the leader (it may change) before adding back. - log_info "--- TC-API-04: ha_addServerAsVoter + TC-API-05: ha_removeServer ---" - - # Find a follower (non-leader) to remove - local target_follower_id="" target_follower_addr="" - for node_id in "ha-node-0" "ha-node-1" "ha-node-2"; do - local node_rpc - case "$node_id" in - "ha-node-0") node_rpc="$HA_RPC_NODE0" ;; - "ha-node-1") node_rpc="$HA_RPC_NODE1" ;; - "ha-node-2") node_rpc="$HA_RPC_NODE2" ;; - esac - if [ "$(is_ha_leader "$node_rpc")" -eq 0 ]; then - local addr - addr=$(get_server_addr_by_id "$leader_rpc" "$node_id") - if [ -n "$addr" ]; then - target_follower_id="$node_id" - target_follower_addr="$addr" - break - fi - fi - done - - local version - version=$(get_membership_version "$leader_rpc") - log_info "Removing follower: $target_follower_id ($target_follower_addr), version=$version" - - if [ -n "$target_follower_id" ]; then - # TC-API-05: removeServer (remove a follower) - local resp05 - resp05=$(ha_call "$leader_rpc" "ha_removeServer" "[\"$target_follower_id\",$version]") - sleep 5 - # Re-query the leader after remove (it stays the same since we removed a follower) - local active_leader_rpc - active_leader_rpc=$(find_leader_rpc) - if [ -z "$active_leader_rpc" ]; then active_leader_rpc="$leader_rpc"; fi - local post_remove_count - post_remove_count=$(count_voters "$active_leader_rpc") - if ! echo "$resp05" | grep -q '"error"' && [ "$post_remove_count" -eq 2 ]; then - record_test "TC-API-05" "ha_removeServer" "PASS" \ - "Removed follower $target_follower_id (version=$version)\nResponse: $resp05\nPost-remove voter_count=$post_remove_count" - else - record_test "TC-API-05" "ha_removeServer" "FAIL" \ - "Response: $resp05\nPost-remove voter_count=$post_remove_count (expected 2)" - fi - - # TC-API-04: addServerAsVoter (re-add the follower via the active leader) - # After removal, the follower's Raft state is stale — must restart it to force - # a fresh connection when re-added. This mirrors the production workflow. - local new_version - new_version=$(get_membership_version "$active_leader_rpc") - local resp04 - resp04=$(ha_call "$active_leader_rpc" "ha_addServerAsVoter" "[\"$target_follower_id\",\"$target_follower_addr\",$new_version]") - # Restart the removed follower to force it to reconnect with fresh Raft state - cd "$DOCKER_DIR" - $COMPOSE_HA restart "$target_follower_id" 2>/dev/null || true - sleep 15 # allow Raft config replication + follower log catchup - local post_add_count - post_add_count=$(count_voters "$active_leader_rpc") - if ! echo "$resp04" | grep -q '"error"' && [ "$post_add_count" -eq 3 ]; then - record_test "TC-API-04" "ha_addServerAsVoter" "PASS" \ - "Re-added $target_follower_id (new_version=$new_version, restarted to force reconnect)\nResponse: $resp04\nPost-add voter_count=$post_add_count" - else - record_test "TC-API-04" "ha_addServerAsVoter" "FAIL" \ - "Response: $resp04\nPost-add voter_count=$post_add_count (expected 3)" - fi - - # Safety net: ensure cluster is back to 3-voter state for subsequent tests. - # If add failed, force-restore by cleaning Raft data and restarting the follower. - if [ "$post_add_count" -ne 3 ]; then - log_warn "Cluster not fully restored ($post_add_count voters). Force-recovering..." - $COMPOSE_HA stop "$target_follower_id" 2>/dev/null || true - rm -rf "$DOCKER_DIR/.devnet/${target_follower_id/#node-/node}/raft" - $COMPOSE_HA up -d "$target_follower_id" 2>/dev/null || true - sleep 20 - fi - else - record_test "TC-API-05" "ha_removeServer" "SKIP" "" "Could not find a follower to remove" - record_test "TC-API-04" "ha_addServerAsVoter" "SKIP" "" "Skipped due to TC-API-05 skip" - fi - - # TC-API-06: ha_transferLeader (auto-select target) - log_info "--- TC-API-06: ha_transferLeader ---" - # Re-check leader (may have changed after add/remove) - leader_rpc=$(find_leader_rpc) - if [ -z "$leader_rpc" ]; then - wait_for_ha_leader 15 || true - leader_rpc=$(find_leader_rpc) - fi - if [ -n "$leader_rpc" ]; then - local pre_transfer_leader - pre_transfer_leader=$(rpc_to_container "$leader_rpc") - local resp06 - resp06=$(ha_call "$leader_rpc" "ha_transferLeader" "[]") - sleep 5 - local post_transfer_leader_rpc - post_transfer_leader_rpc=$(find_leader_rpc) - local post_transfer_leader="" - if [ -n "$post_transfer_leader_rpc" ]; then - post_transfer_leader=$(rpc_to_container "$post_transfer_leader_rpc") - fi - if ! echo "$resp06" | grep -q '"error"'; then - record_test "TC-API-06" "ha_transferLeader" "PASS" \ - "Response: $resp06\nPre-transfer leader: $pre_transfer_leader\nPost-transfer leader: $post_transfer_leader" - else - record_test "TC-API-06" "ha_transferLeader" "FAIL" \ - "Response: $resp06" - fi - else - record_test "TC-API-06" "ha_transferLeader" "SKIP" "" "No leader available" - fi - - # TC-API-07: ha_transferLeaderToServer (specific target) - log_info "--- TC-API-07: ha_transferLeaderToServer ---" - leader_rpc=$(find_leader_rpc) - if [ -n "$leader_rpc" ]; then - local current_leader_name - current_leader_name=$(rpc_to_container "$leader_rpc") - # Choose a target that is NOT the current leader - local target_id target_addr - for node_id in "ha-node-0" "ha-node-1" "ha-node-2"; do - if [ "$node_id" != "$current_leader_name" ]; then - target_id="$node_id" - target_addr=$(get_server_addr_by_id "$leader_rpc" "$node_id") - if [ -n "$target_addr" ]; then break; fi - fi - done - - if [ -n "$target_id" ] && [ -n "$target_addr" ]; then - local resp07 - resp07=$(ha_call "$leader_rpc" "ha_transferLeaderToServer" "[\"$target_id\",\"$target_addr\"]") - sleep 5 - local new_leader_rpc07 - new_leader_rpc07=$(find_leader_rpc) - local new_leader07="" - if [ -n "$new_leader_rpc07" ]; then - new_leader07=$(rpc_to_container "$new_leader_rpc07") - fi - if ! echo "$resp07" | grep -q '"error"'; then - record_test "TC-API-07" "ha_transferLeaderToServer" "PASS" \ - "Target: $target_id ($target_addr)\nResponse: $resp07\nNew leader: $new_leader07" - else - record_test "TC-API-07" "ha_transferLeaderToServer" "FAIL" \ - "Response: $resp07" - fi - else - record_test "TC-API-07" "ha_transferLeaderToServer" "SKIP" "" "Could not find target node addr" - fi - else - record_test "TC-API-07" "ha_transferLeaderToServer" "SKIP" "" "No leader available" - fi - - # TC-API-08: optimistic-lock version check — old version rejected - log_info "--- TC-API-08: optimistic-lock version check ---" - leader_rpc=$(find_leader_rpc) - if [ -n "$leader_rpc" ]; then - wait_for_ha_leader 15 || true - leader_rpc=$(find_leader_rpc) - fi - if [ -n "$leader_rpc" ]; then - local current_version - current_version=$(get_membership_version "$leader_rpc") - local stale_version=0 # always stale (version 0 is always old after cluster forms) - # Use an impossible version (current+100) to trigger mismatch - local stale_version_high=$((current_version + 100)) - local resp08 - resp08=$(ha_call "$leader_rpc" "ha_addServerAsVoter" "[\"fake-node\",\"1.2.3.4:9400\",$stale_version_high]") - # Should return error (wrong index / mismatch) - if echo "$resp08" | grep -q '"error"'; then - record_test "TC-API-08" "optimistic-lock version check (stale version rejected)" "PASS" \ - "Used stale version=$stale_version_high (current=$current_version)\nResponse: $resp08 (contains error as expected)" - else - # Some Raft implementations may accept future versions; check if member was actually added - local post_version - post_version=$(get_membership_version "$leader_rpc") - if echo "$resp08" | grep -q '"result":null'; then - record_test "TC-API-08" "optimistic-lock version check (stale version rejected)" "FAIL" \ - "Stale version not rejected! version=$stale_version_high response=$resp08" - else - record_test "TC-API-08" "optimistic-lock version check (stale version rejected)" "PASS" \ - "Response: $resp08\nNote: hashicorp/raft uses index as 'prevIndex'; future version may still work in some cases" - fi - fi - else - record_test "TC-API-08" "optimistic-lock version check" "SKIP" "" "No leader available" - fi -} - -# ─── Category 6: Lifecycle Tests ────────────────────────────────────────────── - -run_lifecycle_tests() { - log_section "Category 6: Lifecycle (Lifecycle Tests)" - - # TC-LIF-01: follower Stop/Start cycle - log_info "--- TC-LIF-01: follower Stop/Start cycle ---" - # Find a non-leader follower - local follower_rpc="" - local follower_node="" - for rpc_url in "$HA_RPC_NODE0" "$HA_RPC_NODE1" "$HA_RPC_NODE2"; do - if [ "$(is_ha_leader "$rpc_url")" -eq 0 ]; then - follower_rpc="$rpc_url" - follower_node=$(rpc_to_container "$rpc_url") - break - fi - done - - if [ -z "$follower_node" ]; then - record_test "TC-LIF-01" "follower Stop/Start cycle" "SKIP" "" "No non-leader follower found" - else - cd "$DOCKER_DIR" - log_info "Stopping follower: $follower_node" - $COMPOSE_HA stop "$follower_node" 2>/dev/null || true - sleep 5 - - # Verify cluster still has quorum (2/3 nodes) - local leader_rpc - leader_rpc=$(find_leader_rpc) - local still_producing=0 - if [ -n "$leader_rpc" ]; then - local leader_geth - leader_geth=$(ha_rpc_to_geth_rpc "$leader_rpc") - local h1 h2 - h1=$(get_block_number "$leader_geth") - sleep 10 - h2=$(get_block_number "$leader_geth") - if [ "$h2" -gt "$h1" ]; then still_producing=1; fi - fi - - # Restart the follower - log_info "Restarting $follower_node..." - $COMPOSE_HA start "$follower_node" 2>/dev/null || $COMPOSE_HA up -d "$follower_node" - sleep 15 - - # Check follower re-joined - local rejoin_voter_count - rejoin_voter_count=$(count_voters "$leader_rpc") - local follower_height - follower_height=$(get_block_number "$(ha_rpc_to_geth_rpc "$follower_rpc")") - local leader_height - leader_height=$(get_block_number "$(ha_rpc_to_geth_rpc "$leader_rpc")") - local height_diff=$((leader_height - follower_height)); height_diff=${height_diff#-} - - if [ "$still_producing" -eq 1 ] && [ "$rejoin_voter_count" -eq 3 ]; then - record_test "TC-LIF-01" "follower Stop/Start cycle" "PASS" \ - "Stopped: $follower_node; cluster continued producing (quorum OK)\nAfter rejoin: voter_count=$rejoin_voter_count, height_diff=$height_diff" - else - record_test "TC-LIF-01" "follower Stop/Start cycle" "FAIL" \ - "still_producing=$still_producing voter_count_after_rejoin=$rejoin_voter_count" - fi - fi - - # TC-LIF-02: full cluster restart - log_info "--- TC-LIF-02: full cluster restart ---" - cd "$DOCKER_DIR" - log_info "Stopping all HA nodes..." - $COMPOSE_HA stop ha-node-0 ha-node-1 ha-node-2 2>/dev/null || true - sleep 5 - - log_info "Restarting all HA nodes..." - $COMPOSE_HA up -d ha-node-0 ha-node-1 ha-node-2 - sleep 5 - - # Wait for leader re-election - local new_leader_rpc="" - log_info "Waiting for leader election after full restart (max 45s)..." - if wait_for_ha_leader 45; then - new_leader_rpc=$(find_leader_rpc) - local new_leader - new_leader=$(rpc_to_container "$new_leader_rpc") - # Wait for blocks - local new_geth - new_geth=$(ha_rpc_to_geth_rpc "$new_leader_rpc") - local h1 h2 - h1=$(get_block_number "$new_geth") - sleep 10 - h2=$(get_block_number "$new_geth") - if [ "$h2" -gt "$h1" ]; then - record_test "TC-LIF-02" "recovery after full cluster restart" "PASS" \ - "New leader after restart: $new_leader\nBlocks: $h1 → $h2" - else - record_test "TC-LIF-02" "recovery after full cluster restart" "FAIL" \ - "Leader elected ($new_leader) but not producing blocks: $h1 → $h2" - fi - else - record_test "TC-LIF-02" "recovery after full cluster restart" "FAIL" \ - "No leader elected within 45s after full cluster restart" - fi - - # TC-LIF-03: Barrier mechanism — leader-ready delay verification - log_info "--- TC-LIF-03: Barrier mechanism (log verification) ---" - cd "$DOCKER_DIR" - # After the full restart above, check logs for HA startup sequence - local ha_start_logs - ha_start_logs=$($COMPOSE_HA logs ha-node-0 ha-node-1 ha-node-2 2>/dev/null | \ - grep -i "hakeeper.*started\|hakeeper.*raft\|hakeeper.*leader\|hakeeper.*Barrier\|leader ready" | \ - tail -10 || true) - # Check that HA startup log appears (including 'became leader', 'Barrier', 'leader ready') - if echo "$ha_start_logs" | grep -qi "hakeeper"; then - record_test "TC-LIF-03" "Barrier mechanism" "PASS" \ - "HA logs confirm Barrier flow:\n$ha_start_logs\nKey messages: 'became leader, running Barrier' → 'leader ready'" - else - record_test "TC-LIF-03" "Barrier mechanism" "FAIL" \ - "No HA startup logs found — hakeeper may not have started\nLogs: $ha_start_logs" - fi -} - -# ─── Report Generation ──────────────────────────────────────────────────────── - -generate_report() { - mkdir -p "$(dirname "$REPORT_OUTPUT")" - - local total=$((PASS + FAIL + SKIP)) - local timestamp - timestamp=$(date "+%Y-%m-%d %H:%M:%S") - - { - echo "# Sequencer HA V2 Integration Test Report" - echo "" - echo "> Generated at: $timestamp" - echo "> Upgrade time (ms): ${SEQUENCER_UPGRADE_TIME:-unset}" - echo "> Environment: docker-sequencer-test (3-node Raft HA cluster)" - echo "" - echo "---" - echo "" - echo "## Overview" - echo "" - echo "| Status | Count |" - echo "|------|------|" - echo "| ✅ Passed | $PASS |" - echo "| ❌ Failed | $FAIL |" - echo "| ⏭️ Skipped | $SKIP |" - echo "| **Total** | **$total** |" - echo "" - if [ ${#FAILED_TESTS[@]} -gt 0 ]; then - echo "## Failed cases" - echo "" - for t in "${FAILED_TESTS[@]}"; do - echo "- ❌ $t" - done - echo "" - fi - echo "---" - echo "" - echo "## Test matrix" - echo "" - echo "| ID | Category | Test item | Status |" - echo "|-----|------|-------|------|" - echo "| TC-CFG-01 | Config validation | bootstrap flag takes effect | - |" - echo "| TC-CFG-02 | Config validation | join flag takes effect | - |" - echo "| TC-CFG-03 | Config validation | server-id flag takes effect | - |" - echo "| TC-CFG-04 | Config validation | pure flag mode (no config file) | - |" - echo "| TC-CFG-05 | Config validation | advertised_addr auto-detection | - |" - echo "| TC-CLU-01 | Cluster formation | ha-node-0 becomes initial leader | - |" - echo "| TC-CLU-02 | Cluster formation | full 3-node cluster formed | - |" - echo "| TC-CLU-03 | Cluster formation | joinLoop retry mechanism | - |" - echo "| TC-CLU-04 | Cluster formation | repeated bootstrap is harmless | - |" - echo "| TC-BLK-01 | Block production | leader produces blocks after upgrade | - |" - echo "| TC-BLK-02 | Block production | follower does not produce blocks | - |" - echo "| TC-BLK-03 | Block production | follower sync | - |" - echo "| TC-BLK-04 | Block production | existing block idempotently skipped | - |" - echo "| TC-HA-01 | Failover | kill leader → auto re-election | - |" - echo "| TC-HA-02 | Failover | new leader produces blocks | - |" - echo "| TC-HA-03 | Failover | failover block interval (<10s) | - |" - echo "| TC-HA-04 | Failover | old leader rejoins | - |" - echo "| TC-HA-05 | Failover | second failover | - |" - echo "| TC-API-01 | Admin API | ha_leader | - |" - echo "| TC-API-02 | Admin API | ha_leaderWithID | - |" - echo "| TC-API-03 | Admin API | ha_clusterMembership | - |" - echo "| TC-API-04 | Admin API | ha_addServerAsVoter | - |" - echo "| TC-API-05 | Admin API | ha_removeServer | - |" - echo "| TC-API-06 | Admin API | ha_transferLeader | - |" - echo "| TC-API-07 | Admin API | ha_transferLeaderToServer | - |" - echo "| TC-API-08 | Admin API | optimistic-lock version check | - |" - echo "| TC-LIF-01 | Lifecycle | follower Stop/Start cycle | - |" - echo "| TC-LIF-02 | Lifecycle | recovery after full cluster restart | - |" - echo "| TC-LIF-03 | Lifecycle | Barrier mechanism log verification | - |" - echo "" - echo "---" - echo "" - echo "## Detailed results" - echo "" - for line in "${REPORT_LINES[@]}"; do - echo -e "$line" - done - } > "$REPORT_OUTPUT" - - log_success "Report written to: $REPORT_OUTPUT" -} - -# ─── Category 7: P2P Broadcast Reactor Optimization Tests ─────────────────── -# Validates the p2p-broadcast-reactor-optimize changes: -# - applyInterval=3s, syncInterval=5s (faster sync cadence) -# - maxPendingSyncPerPeer=200, rate limit=50qps (resource-protection) -# - NoBlockResponse no longer consumes sync slot -# - banPeer wiring in AddPeer / decode error / signature failure / timeout -# These tests observe a running cluster (no malicious actor) — they verify -# the code paths are taken and no regression breaks normal sync. - -run_p2p_opt_tests() { - log_section "Category 7: P2P Broadcast Reactor Optimization Tests" - - # TC-P2P-01: fullnode applies blocks from HA sequencer (end-to-end sync path). - # The fullnodes (node-0/1/2/3, sentry-node-0) use broadcast_reactor.go's - # applyRoutine. If it works, they will stay within a few blocks of the HA - # leader. We give a 10s window and require delta >= 1. - log_info "--- TC-P2P-01: fullnode applies blocks via P2P ---" - local leader_height_before follower_height_before - local leader_height_after follower_height_after - leader_height_before=$(get_block_number "$HA_RPC_NODE0") - follower_height_before=$(get_block_number "$L2_RPC_NODE0") - sleep 10 - leader_height_after=$(get_block_number "$HA_RPC_NODE0") - follower_height_after=$(get_block_number "$L2_RPC_NODE0") - - local follower_delta=$((follower_height_after - follower_height_before)) - local gap=$((leader_height_after - follower_height_after)) - if [ "$follower_delta" -ge 1 ] && [ "$gap" -lt 10 ]; then - record_test "TC-P2P-01" "fullnode syncs blocks via P2P" "PASS" \ - "Fullnode(node-0) advanced $follower_delta blocks in 10s, gap to leader=$gap" - else - record_test "TC-P2P-01" "fullnode syncs blocks via P2P" "FAIL" \ - "Fullnode delta=$follower_delta, gap=$gap (expected delta>=1, gap<10)" - fi - - # TC-P2P-02: broadcastReactor logs confirm sync interval change (5s). - # After the optimize, the applyRoutine logs "Checking sync goroutines" - # (via checkSyncGap's Debug call). We can't easily measure interval from - # Info logs, so verify the applyRoutine is running by presence of - # "Starting block apply routine" + recent activity. - log_info "--- TC-P2P-02: apply routine running on fullnode ---" - local apply_log - apply_log=$($COMPOSE_HA logs --tail 2000 node-0 2>&1 | \ - grep -c "Starting block apply routine" || true) - if [ "$apply_log" -ge 1 ]; then - record_test "TC-P2P-02" "fullnode starts apply routine" "PASS" \ - "Found 'Starting block apply routine' log on node-0" - else - record_test "TC-P2P-02" "fullnode starts apply routine" "FAIL" \ - "No apply routine startup log found on node-0" - fi - - # TC-P2P-03: "Applied block" logs appear on fullnodes (real sync happening). - # After 10s, at 3s block cadence with 3s applyInterval, a fullnode should - # have applied several blocks from the pending cache. - log_info "--- TC-P2P-03: fullnode applies blocks from pending cache ---" - local applied_count - applied_count=$($COMPOSE_HA logs --tail 5000 node-0 2>&1 | \ - grep -c "Applied block" || true) - if [ "$applied_count" -ge 1 ]; then - record_test "TC-P2P-03" "fullnode successfully applies blocks" "PASS" \ - "Found $applied_count 'Applied block' entries in node-0 logs" - else - record_test "TC-P2P-03" "fullnode successfully applies blocks" "FAIL" \ - "No 'Applied block' logs on node-0 (sync path may be broken)" - fi - - # TC-P2P-04: No 'Unsolicited sync response' errors in normal operation. - # After the optimize, NoBlockResponse no longer consumes slots, and - # legitimate responses from selected peers should always match. If many - # Unsolicited logs appear, something is wrong with request tracking. - log_info "--- TC-P2P-04: no spurious unsolicited-response errors ---" - local unsolicited_count - unsolicited_count=$($COMPOSE_HA logs --tail 5000 node-0 node-1 node-2 node-3 2>&1 | \ - grep -c "Unsolicited sync response" || true) - # Allow a small number due to race conditions at startup; require < 5. - if [ "$unsolicited_count" -lt 5 ]; then - record_test "TC-P2P-04" "no spurious unsolicited responses" "PASS" \ - "Unsolicited response count: $unsolicited_count (threshold <5)" - else - record_test "TC-P2P-04" "no spurious unsolicited responses" "FAIL" \ - "Too many unsolicited response errors: $unsolicited_count" - fi - - # TC-P2P-05: No peer bans in normal operation (no malicious traffic). - # If banPeer fires without an attacker, we've introduced a regression. - log_info "--- TC-P2P-05: no false-positive bans in normal operation ---" - local ban_count - ban_count=$($COMPOSE_HA logs --tail 5000 node-0 node-1 node-2 node-3 sentry-node-0 2>&1 | \ - grep -c "Banning peer" || true) - if [ "$ban_count" -eq 0 ]; then - record_test "TC-P2P-05" "no false-positive bans in normal operation" "PASS" \ - "No 'Banning peer' logs in normal operation" - else - record_test "TC-P2P-05" "no false-positive bans in normal operation" "FAIL" \ - "Unexpected bans in normal operation: $ban_count entries" - fi - - # TC-P2P-06: No rate-limit hits in normal operation. - # With rate=50 and normal sync qps well below 40, no legitimate peer - # should ever trip the limiter. If this fails, thresholds are too tight. - log_info "--- TC-P2P-06: no false-positive rate limiting ---" - local rl_count - rl_count=$($COMPOSE_HA logs --tail 5000 node-0 node-1 node-2 node-3 sentry-node-0 ha-node-0 ha-node-1 ha-node-2 2>&1 | \ - grep -c "BlockRequest rate limited" || true) - if [ "$rl_count" -eq 0 ]; then - record_test "TC-P2P-06" "no false-positive rate limiting" "PASS" \ - "No rate-limit hits during normal sync" - else - record_test "TC-P2P-06" "no false-positive rate limiting" "FAIL" \ - "Legitimate peers tripped rate limit: $rl_count entries" - fi -} - -print_summary() { - echo "" - echo -e "${BOLD}${CYAN}╔══════════════════════════════════════╗${NC}" - echo -e "${BOLD}${CYAN}║ HA V2 Test Summary ║${NC}" - echo -e "${BOLD}${CYAN}╠══════════════════════════════════════╣${NC}" - printf "${BOLD}${CYAN}║${NC} ${GREEN}%-6s PASS${NC} ${RED}%-6s FAIL${NC} ${YELLOW}%-6s SKIP${NC} ${BOLD}${CYAN}║${NC}\n" "$PASS" "$FAIL" "$SKIP" - echo -e "${BOLD}${CYAN}╚══════════════════════════════════════╝${NC}" - if [ ${#FAILED_TESTS[@]} -gt 0 ]; then - echo -e "${RED}Failed tests:${NC}" - for t in "${FAILED_TESTS[@]}"; do - echo -e " ${RED}✗${NC} $t" - done - fi - echo "" -} - -# ─── Main Commands ──────────────────────────────────────────────────────────── - -run_full_ha_test() { - log_section "Sequencer HA V2 Integration Test" - - # Recover the upgrade timestamp set by `start` so the cluster reset below recreates ha-node-* - # with the SAME boundary (an unset value resolves to 0 = upgrade disabled). - load_upgrade_time - log_info "SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-unset} HA_FORM_WAIT=${HA_FORM_WAIT}s" - - # Reset HA cluster (ha-node-0/1/2) for clean state — makes the test idempotent. - log_info "Resetting isolated HA cluster for clean test state..." - cd "$DOCKER_DIR" - $COMPOSE_HA stop ha-node-0 ha-node-1 ha-node-2 2>/dev/null || true - $COMPOSE_HA rm -f ha-node-0 ha-node-1 ha-node-2 2>/dev/null || true - # Clean Raft persistent state (log/stable stores) so cluster re-bootstraps cleanly. - # Tendermint + geth data is preserved — nodes sync from where they left off. - rm -rf "$DOCKER_DIR/.devnet/ha-node0/raft" \ - "$DOCKER_DIR/.devnet/ha-node1/raft" \ - "$DOCKER_DIR/.devnet/ha-node2/raft" 2>/dev/null || true - $COMPOSE_HA up -d ha-node-0 ha-node-1 ha-node-2 2>/dev/null - log_info "Waiting for fresh 3-voter cluster to form (~60s)..." - sleep 15 # let nodes start - wait_for_rpc "$HA_L2_RPC_0" 30 || true - wait_for_ha_leader 60 || true - sleep 10 # let all followers join - - # Init report - mkdir -p "$DOCS_DIR" - REPORT_LINES=() - REPORT_LINES+=("## Environment\n\n- Upgrade Time (ms): ${SEQUENCER_UPGRADE_TIME:-unset}\n- HA Form Wait: ${HA_FORM_WAIT}s\n- PBFT nodes (pre-upgrade validators, post-upgrade V2 fullnodes): node-0/1/2/3\n- Isolated HA cluster (post-upgrade sequencer): ha-node-0 (bootstrap), ha-node-1 (join), ha-node-2 (join)\n- sentry-node-0: non-HA V2 fullnode\n\n---\n") - - run_config_tests - run_cluster_tests - run_block_tests - run_failover_tests - run_api_tests - run_lifecycle_tests - run_p2p_opt_tests - - print_summary - generate_report - - if [ "$FAIL" -gt 0 ]; then - return 1 - fi -} - -show_ha_status() { - echo "Block Heights (PBFT nodes):" - echo " node-0: $(get_block_number "$L2_RPC_NODE0")" - echo " node-1: $(get_block_number "$L2_RPC_NODE1")" - echo " node-2: $(get_block_number "$L2_RPC_NODE2")" - echo " node-3: $(get_block_number "$L2_RPC_NODE3")" - echo "Block Heights (isolated HA cluster):" - echo " ha-node-0: $(get_block_number "$HA_L2_RPC_0")" - echo " ha-node-1: $(get_block_number "$HA_L2_RPC_1")" - echo " ha-node-2: $(get_block_number "$HA_L2_RPC_2")" - echo "" - echo "HA Status:" - for rpc_url in "$HA_RPC_NODE0" "$HA_RPC_NODE1" "$HA_RPC_NODE2"; do - local node - node=$(rpc_to_container "$rpc_url") - local leader_flag - leader_flag=$(ha_call "$rpc_url" "ha_leader" "[]" | grep -o '"result":[^,}]*' | cut -d: -f2 | tr -d ' ') - printf " %-10s HA RPC: %s leader=%s\n" "$node" "$rpc_url" "${leader_flag:-unreachable}" - done - echo "" - echo "Cluster Membership (from leader):" - local leader_rpc - leader_rpc=$(find_leader_rpc) - if [ -n "$leader_rpc" ]; then - get_membership "$leader_rpc" | python3 -m json.tool 2>/dev/null || get_membership "$leader_rpc" - else - echo " No leader reachable" - fi -} - -# ─── Entry Point ───────────────────────────────────────────────────────────── - -case "${1:-}" in - build) - log_info "Building test images (delegating to run-test.sh)..." - "$SCRIPT_DIR/run-test.sh" build - ;; - setup) - log_info "Setting up devnet (delegating to run-test.sh)..." - "$SCRIPT_DIR/run-test.sh" setup - ;; - start) - start_ha_cluster - ;; - test) - run_full_ha_test - ;; - stop) - cd "$DOCKER_DIR" - $COMPOSE_HA down 2>/dev/null || $COMPOSE_BASE down - remove_ha_override - ;; - clean) - cd "$DOCKER_DIR" - $COMPOSE_HA down -v 2>/dev/null || $COMPOSE_BASE down -v 2>/dev/null || true - remove_ha_override - rm -rf "$OPS_DIR/l2-genesis/.devnet" - rm -rf "$DOCKER_DIR/.devnet" - # Clean isolated-HA-cluster artifacts (geth nodekeys are kept in DOCKER_DIR). - rm -f "$DOCKER_DIR/ha-nodekey0" "$DOCKER_DIR/ha-nodekey1" "$DOCKER_DIR/ha-nodekey2" - # Clean L1 genesis (stale genesis causes beacon chain to stick at head_slot=0) - bash "$DOCKER_DIR/layer1/scripts/clean.sh" 2>/dev/null || true - log_success "Cleaned." - ;; - logs) - shift - cd "$DOCKER_DIR" - $COMPOSE_HA logs -f "$@" - ;; - status) - show_ha_status - ;; - api) - run_api_tests - print_summary - generate_report - ;; - failover) - run_failover_tests - print_summary - generate_report - ;; - *) - cat </dev/null || echo 0 -} - -wait_for_block() { - local target=$1 url="${2:-$L2_RPC}" max=${3:-300} waited=0 - while [ $waited -lt $max ]; do - local cur=$(get_block_number "$url") - if [ "$cur" -ge "$target" ]; then return 0; fi - echo -ne "\r block: $cur / $target" - sleep 3; waited=$((waited + 3)) - done - echo ""; return 1 -} - -wait_for_ha_leader() { - local max=${1:-60} waited=0 - while [ $waited -lt $max ]; do - for rpc in http://127.0.0.1:9501 http://127.0.0.1:9601 http://127.0.0.1:9701; do - local resp - resp=$(curl -sf -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"hakeeper_leader","params":[],"id":1}' \ - "$rpc" 2>/dev/null || true) - if echo "$resp" | grep -q '"result":true'; then - log_ok "HA leader found at $rpc" - return 0 - fi - done - sleep 3; waited=$((waited + 3)) - done - log_err "No HA leader found within ${max}s" - return 1 -} - -# ── Build ───────────────────────────────────────────────────────────────────── - -do_build() { - log_section "Building test images with perf instrumentation" - - cd "$MORPH_ROOT" - make go-ubuntu-builder - - cd "$BITGET_ROOT" - log_info "Building morph-geth-test..." - docker build -t morph-geth-test:latest \ - -f morph/ops/docker-sequencer-test/Dockerfile.l2-geth-test . - - log_info "Building morph-node-test..." - docker build -t morph-node-test:latest \ - -f morph/ops/docker-sequencer-test/Dockerfile.l2-node-test . - - log_ok "Test images built" -} - -# ── Setup ───────────────────────────────────────────────────────────────────── - -do_setup() { - log_section "Setting up devnet (L1 + contracts + L2 genesis)" - cd "$SCRIPT_DIR" - ./run-test.sh clean || true - ./run-test.sh setup - log_ok "Setup complete" -} - -# ── Start HA cluster ────────────────────────────────────────────────────────── - -do_start() { - log_section "Starting HA cluster" - cd "$DOCKER_DIR" - - # Copy override files - cp "$SCRIPT_DIR/docker-compose.override.yml" . - cp "$SCRIPT_DIR/docker-compose.ha-override.yml" . - source .env 2>/dev/null || true - - # Wait for L1 finalized - log_info "Waiting for L1 to finalize..." - local l1_latest - l1_latest=$(curl -sf -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - http://127.0.0.1:9545 2>/dev/null | grep -o '"result":"0x[^"]*"' | cut -d'"' -f4) - l1_latest=$(printf "%d" "$l1_latest" 2>/dev/null || echo 1) - - local waited=0 - while [ $waited -lt 120 ]; do - local fin - fin=$(curl -sf -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["finalized",false],"id":1}' \ - http://127.0.0.1:9545 2>/dev/null | grep -o '"number":"0x[^"]*"' | head -1 | cut -d'"' -f4) - local fin_dec=$(printf "%d" "$fin" 2>/dev/null || echo 0) - if [ "$fin_dec" -ge "$l1_latest" ]; then - log_ok "L1 finalized at $fin_dec" - break - fi - echo -ne "\r L1 finalized: $fin_dec / $l1_latest" - sleep 3; waited=$((waited + 3)) - done - - # Stop any existing - $COMPOSE_HA stop morph-geth-0 morph-geth-1 morph-geth-2 morph-geth-3 \ - node-0 node-1 node-2 node-3 2>/dev/null || true - - # Clean Raft state for fresh cluster - rm -rf .devnet/node0/raft .devnet/node1/raft .devnet/node2/raft 2>/dev/null || true - - # Start geth nodes - log_info "Starting geth nodes..." - $COMPOSE_HA up -d morph-geth-0 morph-geth-1 morph-geth-2 morph-geth-3 - sleep 5 - - # Start tendermint nodes - log_info "Starting tendermint nodes (node-0: bootstrap, node-1/2: join, node-3: plain)..." - $COMPOSE_HA up -d node-0 node-1 node-2 node-3 - - log_info "Waiting for L2 RPC..." - wait_for_rpc "$L2_RPC" 60 || { log_err "L2 RPC not ready"; return 1; } - - # Wait for upgrade height (PBFT → V2 switch) - log_info "Waiting for upgrade height ($UPGRADE_HEIGHT)..." - wait_for_block $UPGRADE_HEIGHT "$L2_RPC" 300 || { log_err "Upgrade height not reached"; return 1; } - echo "" - - # Wait for HA leader - log_info "Waiting for HA cluster formation..." - sleep 10 - wait_for_ha_leader 60 || { log_warn "HA leader not found, checking logs..."; } - - log_ok "HA cluster running" -} - -# ── TX Load Generator ──────────────────────────────────────────────────────── - -TX_GEN_PIDS=() -TXFLOOD_BIN="${SCRIPT_DIR}/txflood/txflood" - -start_tx_load() { - local num_senders=${TX_SENDERS:-5} - local dur="${PERF_DURATION:-120}s" - - # Build txflood if missing or stale - if [ ! -f "$TXFLOOD_BIN" ] || [ "$SCRIPT_DIR/txflood/main.go" -nt "$TXFLOOD_BIN" ]; then - log_info "Building txflood..." - (cd "$MORPH_ROOT" && go build -o "$TXFLOOD_BIN" ./ops/docker-sequencer-test/txflood/main.go) - log_ok "txflood built" - fi - - log_section "Starting TX load (Go txflood, ${num_senders} senders, ~${dur})" - - RPC_URL="$L2_RPC" SENDERS="$num_senders" DURATION="$dur" "$TXFLOOD_BIN" & - TX_GEN_PIDS+=($!) - - log_ok "txflood started (PID: ${TX_GEN_PIDS[*]})" -} - -stop_tx_load() { - if [ ${#TX_GEN_PIDS[@]} -gt 0 ]; then - for pid in "${TX_GEN_PIDS[@]}"; do - kill "$pid" 2>/dev/null || true - done - for pid in "${TX_GEN_PIDS[@]}"; do - wait "$pid" 2>/dev/null || true - done - TX_GEN_PIDS=() - log_info "txflood stopped" - fi -} - -# ── Log Analysis ────────────────────────────────────────────────────────────── - -do_analyze() { - log_section "Collecting and analyzing [PERF] logs" - cd "$DOCKER_DIR" - - local tmpdir=$(mktemp -d) - local since="${PERF_LOG_SINCE:-}" - - # Collect logs from all nodes - for node in node-0 node-1 node-2; do - if [ -n "$since" ]; then - docker logs --since "$since" "$node" 2>&1 | grep '\[PERF\]' > "$tmpdir/$node.log" 2>/dev/null || true - else - docker logs "$node" 2>&1 | grep '\[PERF\]' > "$tmpdir/$node.log" 2>/dev/null || true - fi - done - - # ── Summary per node ── - for node in node-0 node-1 node-2; do - local logfile="$tmpdir/$node.log" - local count=$(wc -l < "$logfile" | tr -d ' ') - - if [ "$count" -eq 0 ]; then - log_warn "$node: no [PERF] entries found" - continue - fi - - echo "" - echo -e "${BOLD}═══ $node ($count entries) ═══${NC}" - - # produceBlock (only on leader = node-0 typically) - local produce_count; produce_count=$(grep -c 'produceBlock' "$logfile" 2>/dev/null || true); produce_count=${produce_count:-0} - if [ "${produce_count}" -gt 0 ] 2>/dev/null; then - echo -e "\n${CYAN}[produceBlock] ($produce_count blocks)${NC}" - grep 'produceBlock' "$logfile" | awk ' - { - build=0; sign=0; commit=0; total=0; tx=0; gas=0 - for(i=1;i<=NF;i++) { - if($i ~ /build_ms=/) { split($i,a,"="); build=a[2]+0 } - if($i ~ /sign_ms=/) { split($i,a,"="); sign=a[2]+0 } - if($i ~ /raft_commit_ms=/) { split($i,a,"="); commit=a[2]+0 } - if($i ~ /apply_ms=/) { split($i,a,"="); commit=a[2]+0 } - if($i ~ /total_ms=/) { split($i,a,"="); total=a[2]+0 } - if($i ~ /txCount=/) { split($i,a,"="); tx=a[2]+0 } - if($i ~ /gasUsed=/) { split($i,a,"="); gas=a[2]+0 } - } - n++; s_build+=build; s_sign+=sign; s_commit+=commit; s_total+=total; s_tx+=tx; s_gas+=gas - if(build>max_build) max_build=build - if(commit>max_commit) max_commit=commit - if(total>max_total) max_total=total - if(n==1 || build0) { - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "build_ms:", s_build/n, min_build, max_build - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "sign_ms:", s_sign/n, 0, 0 - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "raft_commit_ms:", s_commit/n, min_commit, max_commit - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "total_ms:", s_total/n, min_total, max_total - printf " %-18s avg=%.1f\n", "txCount:", s_tx/n - printf " %-18s avg=%.0f\n", "gasUsed:", s_gas/n - } - }' - fi - - # HAService.Commit (only on leader) - local commit_count; commit_count=$(grep -c 'HAService.Commit' "$logfile" 2>/dev/null || true); commit_count=${commit_count:-0} - if [ "${commit_count}" -gt 0 ] 2>/dev/null; then - echo -e "\n${CYAN}[HAService.Commit] ($commit_count entries)${NC}" - grep 'HAService.Commit' "$logfile" | awk ' - { - enc=0; raft=0; total=0; bytes=0 - for(i=1;i<=NF;i++) { - if($i ~ /encode_ms=/) { split($i,a,"="); enc=a[2]+0 } - if($i ~ /raft_ms=/) { split($i,a,"="); raft=a[2]+0 } - if($i ~ /total_ms=/) { split($i,a,"="); total=a[2]+0 } - if($i ~ /dataBytes=/) { split($i,a,"="); bytes=a[2]+0 } - } - n++; s_enc+=enc; s_raft+=raft; s_total+=total; s_bytes+=bytes - if(raft>max_raft) max_raft=raft - if(n==1 || raft0) { - printf " %-18s avg=%-10.2f\n", "encode_ms:", s_enc/n - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "raft_ms:", s_raft/n, min_raft, max_raft - printf " %-18s avg=%-10.2f\n", "total_ms:", s_total/n - printf " %-18s avg=%.0f\n", "dataBytes:", s_bytes/n - } - }' - fi - - # BlockFSM.Apply (on all HA nodes) - local fsm_count=$(grep -c 'BlockFSM.Apply' "$logfile" 2>/dev/null || echo 0) - if [ "$fsm_count" -gt 0 ]; then - echo -e "\n${CYAN}[BlockFSM.Apply] ($fsm_count entries)${NC}" - grep 'BlockFSM.Apply' "$logfile" | awk ' - { - dec=0; applied=0; total=0 - for(i=1;i<=NF;i++) { - if($i ~ /decode_ms=/) { split($i,a,"="); dec=a[2]+0 } - if($i ~ /onApplied_ms=/) { split($i,a,"="); applied=a[2]+0 } - if($i ~ /total_ms=/) { split($i,a,"="); total=a[2]+0 } - } - n++; s_dec+=dec; s_applied+=applied; s_total+=total - if(applied>max_applied) max_applied=applied - if(total>max_total) max_total=total - if(n==1 || applied0) { - printf " %-18s avg=%-10.2f\n", "decode_ms:", s_dec/n - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "onApplied_ms:", s_applied/n, min_applied, max_applied - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "total_ms:", s_total/n, min_total, max_total - } - }' - fi - - # ApplyBlock (on all HA nodes) - local apply_count=$(grep -c 'ApplyBlock' "$logfile" | head -1 2>/dev/null || echo 0) - # Exclude produceBlock lines - local pure_apply=$(grep 'ApplyBlock' "$logfile" | grep -cv 'produceBlock' 2>/dev/null || echo 0) - if [ "$pure_apply" -gt 0 ]; then - echo -e "\n${CYAN}[ApplyBlock] ($pure_apply entries)${NC}" - grep 'ApplyBlock' "$logfile" | grep -v 'produceBlock' | awk ' - { - geth=0; sig=0; total=0 - for(i=1;i<=NF;i++) { - if($i ~ /geth_ms=/) { split($i,a,"="); geth=a[2]+0 } - if($i ~ /sigSave_ms=/) { split($i,a,"="); sig=a[2]+0 } - if($i ~ /total_ms=/) { split($i,a,"="); total=a[2]+0 } - } - n++; s_geth+=geth; s_sig+=sig; s_total+=total - if(geth>max_geth) max_geth=geth - if(n==1 || geth0) { - printf " %-18s avg=%-10.2f min=%-10.2f max=%.2f\n", "geth_ms:", s_geth/n, min_geth, max_geth - printf " %-18s avg=%-10.2f\n", "sigSave_ms:", s_sig/n - printf " %-18s avg=%-10.2f\n", "total_ms:", s_total/n - } - }' - fi - done - - # ── Raft overhead summary ── - echo "" - log_section "Raft Overhead Summary" - - local leader_raft_avg leader_fsm_avg - leader_raft_avg=$(grep 'HAService.Commit' "$tmpdir/node-0.log" 2>/dev/null | awk ' - { for(i=1;i<=NF;i++) if($i ~ /raft_ms=/) { split($i,a,"="); s+=a[2]+0; n++ } } - END { if(n>0) printf "%.2f", s/n; else print "N/A" }') - - leader_fsm_avg=$(grep 'BlockFSM.Apply' "$tmpdir/node-0.log" 2>/dev/null | awk ' - { for(i=1;i<=NF;i++) if($i ~ /onApplied_ms=/) { split($i,a,"="); s+=a[2]+0; n++ } } - END { if(n>0) printf "%.2f", s/n; else print "N/A" }') - - echo -e " Leader raft_ms avg: ${BOLD}${leader_raft_avg}${NC} ms" - echo -e " Leader onApplied_ms avg: ${BOLD}${leader_fsm_avg}${NC} ms" - - if [[ "$leader_raft_avg" != "N/A" && "$leader_fsm_avg" != "N/A" ]]; then - local overhead - overhead=$(awk "BEGIN { printf \"%.2f\", $leader_raft_avg - $leader_fsm_avg }") - echo -e " ${BOLD}Pure Raft overhead: ${RED}${overhead}${NC} ms${NC} (network + quorum + log write)" - fi - - # Follower comparison - for node in node-1 node-2; do - local f_avg - f_avg=$(grep 'BlockFSM.Apply' "$tmpdir/$node.log" 2>/dev/null | awk ' - { for(i=1;i<=NF;i++) if($i ~ /onApplied_ms=/) { split($i,a,"="); s+=a[2]+0; n++ } } - END { if(n>0) printf "%.2f", s/n; else print "N/A" }') - echo -e " $node onApplied_ms avg: ${BOLD}${f_avg}${NC} ms" - done - - rm -rf "$tmpdir" - echo "" -} - -# ── Run (full test cycle) ──────────────────────────────────────────────────── - -do_run() { - log_section "Running HA performance test (${PERF_DURATION}s)" - - local start_block=$(get_block_number "$L2_RPC") - log_info "Starting at block $start_block" - - start_tx_load - - local start_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) - - log_info "Collecting data for ${PERF_DURATION}s (txflood running)..." - # Wait for txflood to finish (it runs for PERF_DURATION then exits) - for pid in "${TX_GEN_PIDS[@]}"; do - wait "$pid" 2>/dev/null || true - done - TX_GEN_PIDS=() - - local end_block=$(get_block_number "$L2_RPC") - local blocks=$((end_block - start_block)) - log_ok "Collected $blocks blocks ($start_block → $end_block)" - - PERF_LOG_SINCE="$start_ts" do_analyze -} - -# ── Stop ────────────────────────────────────────────────────────────────────── - -do_stop() { - log_section "Stopping all containers" - stop_tx_load - cd "$DOCKER_DIR" - $COMPOSE_HA stop morph-geth-0 morph-geth-1 morph-geth-2 morph-geth-3 \ - node-0 node-1 node-2 node-3 2>/dev/null || true - log_ok "Stopped" -} - -# ── Clean ───────────────────────────────────────────────────────────────────── - -do_clean() { - log_section "Full cleanup" - - # 1. Clean L2 containers + data - cd "$SCRIPT_DIR" - ./run-test.sh clean || true - - # 2. Clean L1 volumes + genesis (MUST do this, otherwise beacon chain gets - # stuck at head_slot=0 with stale genesis on next setup) - cd "$DOCKER_DIR" - $COMPOSE_BASE down -v 2>/dev/null || true - bash "$OPS_DIR/docker/layer1/scripts/clean.sh" 2>/dev/null || true - - # 3. Clean tendermint + L2 genesis state - rm -rf "$DOCKER_DIR/.devnet" "$OPS_DIR/l2-genesis/.devnet" 2>/dev/null || true - - log_ok "Cleaned" -} - -# ── Main ────────────────────────────────────────────────────────────────────── - -case "${1:-help}" in - build) do_build ;; - setup) do_setup ;; - start) do_start ;; - load) start_tx_load; echo "Press Ctrl+C to stop"; wait ;; - run) do_run ;; - analyze) do_analyze ;; - all) - do_build - do_setup - do_start - do_run - ;; - stop) do_stop ;; - clean) do_clean ;; - *) - echo "Usage: $0 {build|setup|start|load|run|analyze|all|stop|clean}" - echo "" - echo " build - Rebuild test images with perf instrumentation" - echo " setup - Deploy L1 + contracts + L2 genesis" - echo " start - Start HA cluster (waits for upgrade + cluster formation)" - echo " load - Start TX load generator (interactive)" - echo " run - Start load + collect ${PERF_DURATION}s + analyze" - echo " analyze - Parse existing [PERF] logs and print summary" - echo " all - build + setup + start + run" - echo " stop - Stop L2 containers" - echo " clean - Full cleanup (L1 + L2 + data)" - ;; -esac diff --git a/ops/docker-sequencer-test/run-test.sh b/ops/docker-sequencer-test/run-test.sh deleted file mode 100755 index 11d668b20..000000000 --- a/ops/docker-sequencer-test/run-test.sh +++ /dev/null @@ -1,970 +0,0 @@ -#!/bin/bash -# Sequencer Upgrade Test Runner -# Reuses devnet-morph logic but with test-specific docker images - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MORPH_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -POLYREPO_ROOT="$(cd "$MORPH_ROOT/.." && pwd)" -OPS_DIR="$MORPH_ROOT/ops" -DOCKER_DIR="$OPS_DIR/docker" -DEVNET_DIR="$OPS_DIR/devnet-morph" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } -log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } - -# Configuration -# Upgrade is timestamp-driven (not height): UPGRADE_OFFSET_S controls how many seconds after L2 -# start the PBFT->sequencer switch fires (see start_l2_test, default 120s). -UPGRADE_OFFSET_S=${UPGRADE_OFFSET_S:-120} -L2_RPC="http://127.0.0.1:8545" -L2_RPC_NODE1="http://127.0.0.1:8645" - -# ========== Helper Functions ========== - -wait_for_rpc() { - local rpc_url="$1" - local max_retries=${2:-60} - local retry=0 - - log_info "Waiting for RPC at $rpc_url..." - while [ $retry -lt $max_retries ]; do - if curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - "$rpc_url" 2>/dev/null | grep -q "result"; then - log_success "RPC is ready!" - return 0 - fi - retry=$((retry + 1)) - sleep 2 - done - log_error "Timeout waiting for RPC" - return 1 -} - -get_block_number() { - local rpc_url="${1:-$L2_RPC}" - local result - result=$(curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - "$rpc_url" 2>/dev/null) - echo "$result" | grep -o '"result":"[^"]*"' | cut -d'"' -f4 | xargs printf "%d" 2>/dev/null || true -} - -wait_for_block() { - local target_height=$1 - local rpc_url="${2:-$L2_RPC}" - - log_info "Waiting for block $target_height..." - while true; do - local current=$(get_block_number "$rpc_url") - if [ "$current" -ge "$target_height" ]; then - log_success "Reached block $current" - return 0 - fi - echo -ne "\r Current block: $current / $target_height" - sleep 2 - done -} - -# ========== Setup Functions ========== - -# Build test images (with -test suffix) -# Uses the polyrepo root as build context to access local go-ethereum and tendermint -build_test_images() { - log_info "Building test Docker images..." - log_info "Using build context: $POLYREPO_ROOT" - - # Build go-ubuntu-builder if needed - cd "$MORPH_ROOT" - make go-ubuntu-builder - - # Build from the polyrepo root to access all repos - cd "$POLYREPO_ROOT" - - # # Copy go module cache to avoid network downloads - # if [ -d "$HOME/go/pkg/mod" ]; then - # log_info "Copying go module cache to build context..." - # rm -rf .gomodcache - # cp -r "$HOME/go/pkg/mod" .gomodcache - # else - # log_warn "Go module cache not found at $HOME/go/pkg/mod" - # log_warn "Build may fail due to network issues" - # fi - - # Build test execution image - log_info "Building morph-el-test (using local go-ethereum)..." - docker build -t morph-el-test:latest \ - -f morph/ops/docker-sequencer-test/Dockerfile.l2-geth-test . - - # Build test node image - log_info "Building morph-node-test (using local go-ethereum + tendermint)..." - docker build -t morph-node-test:latest \ - -f morph/ops/docker-sequencer-test/Dockerfile.l2-node-test . - - # # Cleanup go module cache copy - # rm -rf .gomodcache - - log_success "Test images built!" -} - -# Run full devnet setup (reusing existing logic, but skip L2 startup) -setup_devnet() { - log_info "Running devnet setup..." - cd "$MORPH_ROOT" - - # Note: the timestamp upgrade (SEQUENCER_UPGRADE_TIME) is configured at start, not at setup. - - # Step 1: Start L1 and setup tendermint nodes - # Note: main.py calls setup_devnet_nodes() before devnet.main() - log_info "Step 1: Starting L1 and setting up tendermint nodes..." - python3 "$DEVNET_DIR/main.py" --polyrepo-dir="$MORPH_ROOT" --only-l1 - - # Step 2: Deploy contracts and generate L2 genesis (without starting L2) - log_info "Step 2: Deploying contracts and generating L2 genesis..." - python3 -c " -import sys -import os -import time -import re -import fileinput -sys.path.insert(0, '$DEVNET_DIR') -import devnet -from devnet import run_command, read_json, write_json, test_port, log - -pjoin = os.path.join -polyrepo_dir = '$MORPH_ROOT' -L2_dir = pjoin(polyrepo_dir, 'ops', 'l2-genesis') -devnet_dir = pjoin(polyrepo_dir, 'ops', 'l2-genesis', '.devnet') -ops_dir = pjoin(polyrepo_dir, 'ops', 'docker') -contracts_dir = pjoin(polyrepo_dir, 'contracts') - -os.makedirs(devnet_dir, exist_ok=True) - -# Generate network config -devnet_cfg_orig = pjoin(L2_dir, 'deploy-config', 'devnet-deploy-config.json') -deploy_config = read_json(devnet_cfg_orig) -deploy_config['l1GenesisBlockTimestamp'] = '0x{:x}'.format(int(time.time())) -deploy_config['l1StartingBlockTag'] = 'earliest' -temp_deploy_config = pjoin(devnet_dir, 'deploy-config.json') -write_json(temp_deploy_config, deploy_config) - -# Deploy L1 contracts -deployment_dir = pjoin(devnet_dir, 'devnetL1.json') -run_command(['rm', '-f', deployment_dir], env={}, cwd=contracts_dir) -log.info('Deploying L1 Proxy contracts...') -run_command(['yarn', 'build'], env={}, cwd=contracts_dir) -run_command(['npx', 'hardhat', 'deploy', '--network', 'l1', '--storagepath', deployment_dir, '--concurrent', 'true'], env={}, cwd=contracts_dir) - -# Generate L2 genesis -log.info('Generating L2 genesis and rollup configs...') -run_command([ - 'env', 'CGO_ENABLED=1', 'CGO_LDFLAGS=\"-ldl\"', - 'go', 'run', 'cmd/main.go', 'genesis', 'l2', - '--l1-rpc', 'http://localhost:9545', - '--deploy-config', temp_deploy_config, - '--deployment-dir', deployment_dir, - '--outfile.l2', pjoin(devnet_dir, 'genesis-l2.json'), - '--outfile.genbatchheader', pjoin(devnet_dir, 'genesis-batch-header.json'), - '--outfile.rollup', pjoin(devnet_dir, 'rollup.json') -], cwd=L2_dir) - -# Initialize contracts -log.info('Deploying L1 Impl contracts and initialize...') -rollup_cfg = read_json(pjoin(devnet_dir, 'rollup.json')) -genesis_batch_header = rollup_cfg['genesis_batch_header'] -contracts_config = pjoin(contracts_dir, 'src', 'deploy-config', 'l1.ts') -pattern3 = re.compile(\"batchHeader: '.*'\") -for line in fileinput.input(contracts_config, inplace=True): - modified_line = re.sub(pattern3, f\"batchHeader: '{genesis_batch_header}'\", line) - print(modified_line, end='') -run_command(['npx', 'hardhat', 'initialize', '--network', 'l1', '--storagepath', deployment_dir, '--concurrent', 'true'], env={}, cwd=contracts_dir) - -# Staking -log.info('Staking sequencers...') -addresses = {} -deployment = read_json(deployment_dir) -for d in deployment: - addresses[d['name']] = d['address'] -for i in range(4): - run_command(['cast', 'send', addresses['Proxy__L1Staking'], - 'register(bytes32,bytes memory)', - deploy_config['l2StakingTmKeys'][i], - deploy_config['l2StakingBlsKeys'][i], - '--rpc-url', 'http://127.0.0.1:9545', - '--value', '1ether', - '--private-key', deploy_config['l2StakingPks'][i] - ]) - -# Initialize L1Sequencer for V2 mode -# Register the first sequencer (node-0's staking address). The sequencer is -# active from L2 block 0; the PBFT->single-sequencer switch is triggered by -# block timestamp (network-specific sequencerUpgradeTime flag), not by an on-chain height. -l1_sequencer_addr = addresses.get('Proxy__L1Sequencer', '') -if l1_sequencer_addr: - # Override for whitelist integration test: register the HA cluster's - # signer key (0xAb70...) so that ha-node-X can produce blocks after the - # PBFT->HA upgrade. Without this, isSequencerAt() always returns false - # for the HA leader and no blocks are produced. - sequencer_addr = os.environ.get('HA_SEQUENCER_ADDR', deploy_config['l2StakingAddresses'][0]) - deployer_pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' - log.info(f'Setting first L1Sequencer: sequencer={sequencer_addr} (active from block 0)') - try: - run_command([ - 'cast', 'send', l1_sequencer_addr, - 'setFirstSequencer(address)', - sequencer_addr, - '--rpc-url', 'http://127.0.0.1:9545', - '--private-key', deployer_pk - ]) - log.info('L1Sequencer first sequencer set successfully') - except Exception as e: - log.info(f'L1Sequencer setFirstSequencer failed (may already be initialized): {e}') - -# Update .env file -log.info('Updating .env file...') -env_file = pjoin(ops_dir, '.env') -env_data = {} -with open(env_file, 'r+') as envfile: - env_content = envfile.readlines() - for line in env_content: - line = line.strip() - if line and not line.startswith('#'): - parts = line.split('=', 1) - if len(parts) == 2: - env_data[parts[0].strip()] = parts[1].strip() - env_data['L1_CROSS_DOMAIN_MESSENGER'] = addresses['Proxy__L1CrossDomainMessenger'] - env_data['MORPH_PORTAL'] = addresses['Proxy__L1MessageQueueWithGasPriceOracle'] - env_data['MORPH_ROLLUP'] = addresses['Proxy__Rollup'] - env_data['MORPH_L1STAKING'] = addresses['Proxy__L1Staking'] - env_data['L1_SEQUENCER_CONTRACT'] = addresses.get('Proxy__L1Sequencer', '') - envfile.seek(0) - for key, value in env_data.items(): - envfile.write(f'{key}={value}\n') - envfile.truncate() - -log.info('Contract deployment and genesis generation complete!') -log.info('Skipping L2 startup - will be done with test images.') -" - - log_success "Devnet setup complete (L2 not started yet)" -} - -# Docker compose command with override file -# Note: -f must explicitly include override file when using non-default compose file name -COMPOSE_CMD="docker compose -f docker-compose-4nodes.yml -f docker-compose.override.yml" -COMPOSE_CMD_NO_OVERRIDE="docker compose -f docker-compose-4nodes.yml" - -# Copy override file to use test images -setup_override() { - log_info "Setting up docker-compose override for test images..." - cp "$SCRIPT_DIR/docker-compose.override.yml" "$DOCKER_DIR/docker-compose.override.yml" - log_success "Override file copied to $DOCKER_DIR/" -} - -# Remove override file -remove_override() { - rm -f "$DOCKER_DIR/docker-compose.override.yml" -} - -# Wait for L1 finalized block to reach at least the given height. -# This ensures contract data (e.g., setFirstSequencer) is visible via -# the finalized block tag when L2 nodes start their verifier sync. -wait_for_l1_finalized() { - local min_block=${1:-1} - local l1_rpc="${2:-http://127.0.0.1:9545}" - local max_wait=120 - local waited=0 - - log_info "Waiting for L1 finalized block >= $min_block..." - while [ $waited -lt $max_wait ]; do - local fin - fin=$(curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["finalized",false],"id":1}' \ - "$l1_rpc" 2>/dev/null | grep -o '"number":"0x[^"]*"' | head -1 | cut -d'"' -f4) - if [ -n "$fin" ]; then - local fin_dec - fin_dec=$(printf "%d" "$fin" 2>/dev/null || echo 0) - if [ "$fin_dec" -ge "$min_block" ]; then - log_success "L1 finalized block: $fin_dec (>= $min_block)" - return 0 - fi - echo -ne "\r L1 finalized: $fin_dec / $min_block" - fi - sleep 3 - waited=$((waited + 3)) - done - log_warn "Timeout waiting for L1 finalized >= $min_block (continuing anyway)" -} - -# Start L2 with test images -start_l2_test() { - log_info "Starting L2 with test images..." - cd "$DOCKER_DIR" - - # Setup override file - setup_override - - # Read the .env file to get contract addresses - source .env 2>/dev/null || true - - # Set sequencer private key - export SEQUENCER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - - # Wait for L1 to finalize past the contract deployment block. - # The verifier reads history via finalized tag; if L1 hasn't finalized - # the setFirstSequencer tx yet, the initial sync will miss it. - local l1_latest - l1_latest=$(curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - http://127.0.0.1:9545 2>/dev/null | grep -o '"result":"0x[^"]*"' | cut -d'"' -f4) - l1_latest=$(printf "%d" "$l1_latest" 2>/dev/null || echo 1) - wait_for_l1_finalized "$l1_latest" - - # Stop any existing L2 containers - $COMPOSE_CMD stop \ - morph-el-0 morph-el-1 morph-el-2 morph-el-3 \ - node-0 node-1 node-2 node-3 2>/dev/null || true - - # Note: Test images should already be built by build_test_images() - # Uncomment below if you need to rebuild during start - # log_info "Building L2 containers with test images..." - # $COMPOSE_CMD build morph-el-0 node-0 - - # Start L2 execution nodes - log_info "Starting L2 execution nodes..." - $COMPOSE_CMD up -d morph-el-0 morph-el-1 morph-el-2 morph-el-3 sentry-el-0 - - sleep 5 - - # Timestamp-driven upgrade: tendermint block.Time ~= wall clock, so set the upgrade time - # to (now + offset) ms. The chain runs PBFT for ~offset seconds, then the first block whose - # time crosses this value becomes the last PBFT block and node-1 takes over as sequencer. - local upgrade_offset_s=${UPGRADE_OFFSET_S:-120} - export SEQUENCER_UPGRADE_TIME=$(( ($(date +%s) + upgrade_offset_s) * 1000 )) - log_info "Sequencer upgrade time set to $SEQUENCER_UPGRADE_TIME ms (now + ${upgrade_offset_s}s)" - - # Start L2 tendermint nodes - log_info "Starting L2 tendermint nodes..." - $COMPOSE_CMD up -d node-0 node-1 node-2 node-3 sentry-node-0 - - wait_for_rpc "$L2_RPC" - log_success "L2 is running with test images!" -} - -# ========== Test Functions ========== - -test_pbft_mode() { - log_info "========== Phase 1: Testing PBFT Mode ==========" - - local start_block=$(get_block_number) - log_info "Starting block: $start_block" - - # Wait for some blocks - local target=$((start_block + 10)) - wait_for_block $target - - # Verify nodes in sync - local block0=$(get_block_number "$L2_RPC") - local block1=$(get_block_number "$L2_RPC_NODE1") - - local diff=$((block0 - block1)) - if [ ${diff#-} -le 2 ]; then - log_success "Nodes in sync (node0: $block0, node1: $block1)" - else - log_error "Nodes out of sync!" - return 1 - fi -} - -test_upgrade() { - log_info "========== Phase 2: Waiting for timestamp-driven Upgrade ==========" - cd "$DOCKER_DIR" - - # Upgrade is triggered by block timestamp, not height: poll node logs for the consensus - # switch instead of waiting for a fixed height. - local max_wait=${UPGRADE_WAIT_S:-240} - local waited=0 - while [ $waited -lt $max_wait ]; do - if $COMPOSE_CMD logs --tail=3000 node-0 node-1 node-2 node-3 2>/dev/null \ - | grep -q "switching to sequencer mode"; then - log_success "Upgrade triggered (timestamp boundary crossed) after ${waited}s" - break - fi - echo -ne "\r waiting for upgrade... ${waited}s/${max_wait}s" - sleep 5 - waited=$((waited + 5)) - done - if [ $waited -ge $max_wait ]; then - log_error "Upgrade did not trigger within ${max_wait}s" - return 1 - fi - - sleep 10 - # Verify network continues producing after upgrade (V2 blocks from node-1) - local post_upgrade=$(get_block_number) - wait_for_block $((post_upgrade + 5)) - - log_success "Upgrade completed! Network continues producing blocks." -} - -test_sequencer_mode() { - log_info "========== Phase 3: Testing Sequencer Mode ==========" - - local start_block=$(get_block_number) - wait_for_block $((start_block + 20)) - - local block0=$(get_block_number "$L2_RPC") - local block1=$(get_block_number "$L2_RPC_NODE1") - - local diff=$((block0 - block1)) - if [ ${diff#-} -le 2 ]; then - log_success "Nodes in sync after upgrade (node0: $block0, node1: $block1)" - else - log_error "Nodes out of sync after upgrade!" - return 1 - fi -} - -test_fullnode_sync() { - log_info "========== Phase 4: Testing Fullnode Sync ==========" - - local current_height=$(get_block_number) - log_info "Current height: $current_height" - - cd "$DOCKER_DIR" - - # Start sentry node (fullnode) - log_info "Starting fullnode (sentry-node-0)..." - $COMPOSE_CMD up -d sentry-el-0 sentry-node-0 - - sleep 10 - wait_for_rpc "http://127.0.0.1:8945" - - # Wait for sync - local target_sync=$((current_height - 5)) - local max_wait=300 - local waited=0 - - while [ $waited -lt $max_wait ]; do - local fn_block=$(get_block_number "http://127.0.0.1:8945") - if [ "$fn_block" -ge "$target_sync" ]; then - log_success "Fullnode synced to block $fn_block" - return 0 - fi - echo -ne "\r Fullnode: $fn_block / $target_sync" - sleep 5 - waited=$((waited + 5)) - done - - log_error "Fullnode sync timeout" - return 1 -} - -# ========== Transaction Generator ========== - -start_tx_generator() { - log_info "Starting transaction generator..." - - # Simple tx generator using cast - ( - while true; do - RANDOM_ADDR="0x$(openssl rand -hex 20)" - cast send --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ - --rpc-url "$L2_RPC" \ - --value 1wei \ - "$RANDOM_ADDR" 2>/dev/null || true - sleep ${TX_INTERVAL:-5} - done - ) & - TX_GEN_PID=$! - log_info "TX generator started (PID: $TX_GEN_PID)" -} - -stop_tx_generator() { - if [ -n "$TX_GEN_PID" ]; then - kill $TX_GEN_PID 2>/dev/null || true - log_info "TX generator stopped" - fi -} - -# ========== Cleanup ========== - -cleanup() { - log_info "Cleaning up..." - stop_tx_generator - cd "$DOCKER_DIR" - $COMPOSE_CMD_NO_OVERRIDE down -v 2>/dev/null || true - remove_override -} - -# ========== Main Commands ========== - -run_full_test() { - log_info "==========================================" - log_info " Sequencer Upgrade Test (timestamp-driven)" - log_info "==========================================" - - trap cleanup EXIT - - # Single sequencer after upgrade = node-1, signing with this key. setFirstSequencer must - # register this key's address so V2 blocks verify. 0xac09..ff80 -> 0xf39Fd6..2266 (anvil #0). - export SEQUENCER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - export HA_SEQUENCER_ADDR="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - - # Build test images - build_test_images - - # Setup devnet (L1 + contracts + L2 genesis) - setup_devnet - - # Start L2 with test images - start_l2_test - - # Start tx generator - start_tx_generator - - # Run tests - test_pbft_mode - test_upgrade - test_sequencer_mode - test_fullnode_sync - - stop_tx_generator - - log_success "==========================================" - log_success " ALL TESTS PASSED!" - log_success "==========================================" -} - -show_status() { - echo "Node 1: Block $(get_block_number http://127.0.0.1:8645)" - echo "Node 2: Block $(get_block_number http://127.0.0.1:8745)" - echo "Node 3: Block $(get_block_number http://127.0.0.1:8845)" - echo "Node 0 (seq-0): Block $(get_block_number http://127.0.0.1:8545)" - echo "Sentry: Block $(get_block_number http://127.0.0.1:8945 2>/dev/null || echo 'N/A')" -} - -show_logs() { - cd "$DOCKER_DIR" - $COMPOSE_CMD_NO_OVERRIDE logs -f "$@" -} - -# ========== Malicious Image Build ========== - -build_malicious_image() { - log_info "Building malicious node image from test/p2p-security branch..." - cd "$BITGET_ROOT" - - # Save current tendermint branch state - cd tendermint - local original_branch - original_branch=$(git branch --show-current) - git stash 2>/dev/null || true - - # Switch to malicious branch - git checkout test/p2p-security - cd "$BITGET_ROOT" - - # Build using same Dockerfile, different tag - docker build -t morph-node-malicious:latest \ - -f morph/ops/docker-sequencer-test/Dockerfile.l2-node-test . - - # Switch back - cd tendermint - git checkout "$original_branch" - git stash pop 2>/dev/null || true - cd "$BITGET_ROOT" - - log_success "Malicious image built!" -} - -# ========== P2P Security Test ========== - -L2_RPC_SENTRY="http://127.0.0.1:8945" - -# Swap sentry-node-0 to use malicious image, keeping its data. -# This is the practical approach: a malicious node must be synced first (fresh -# nodes from height 0 can't connect after PBFT->V2 upgrade). By swapping the -# sentry's image, the malicious node starts already synced and connected. -start_malicious_sentry() { - local mode="${1:-all}" - log_info "Swapping sentry-node-0 to malicious image (MALICIOUS_MODE=$mode)..." - cd "$DOCKER_DIR" - - # Stop sentry - $COMPOSE_CMD stop sentry-node-0 2>/dev/null || true - $COMPOSE_CMD rm -f sentry-node-0 2>/dev/null || true - - # Restart with malicious image via env override - export MALICIOUS_MODE="$mode" - SENTRY_IMAGE=morph-node-malicious:latest \ - docker compose -f docker-compose-4nodes.yml -f docker-compose.override.yml \ - run -d --name sentry-node-0-malicious \ - -e MALICIOUS_MODE="$mode" \ - --entrypoint "" \ - morph-node-malicious:latest \ - morphnode --home /data 2>/dev/null || true - - # Simpler: just modify the override to use malicious image for sentry - # and restart - $COMPOSE_CMD up -d sentry-node-0 -} - -# Actually, the simplest approach: temporarily edit docker-compose to use -# the malicious image for sentry-node-0, then restart it. -swap_sentry_to_malicious() { - local mode="${1:-all}" - log_info "Swapping sentry to malicious image (mode=$mode)..." - cd "$DOCKER_DIR" - - # Stop sentry - $COMPOSE_CMD stop sentry-node-0 2>/dev/null || true - $COMPOSE_CMD rm -f sentry-node-0 2>/dev/null || true - - # Create a temp override that changes sentry image to malicious. - # IMPORTANT: docker compose replaces the entire environment list, not merge. - # Must include ALL required env vars here. - cat > docker-compose.malicious-override.yml </dev/null || true - $COMPOSE_CMD rm -f sentry-node-0 2>/dev/null || true - rm -f docker-compose.malicious-override.yml - - # Restart with normal image - $COMPOSE_CMD up -d sentry-node-0 -} - -test_p2p_security() { - log_info "==========================================" - log_info " P2P Anti-Malicious Security Tests" - log_info "==========================================" - - cd "$DOCKER_DIR" - - # ========================================== - # Phase 0: Precondition checks - # ========================================== - local height - height=$(get_block_number "$L2_RPC") - - # Check 1: the timestamp-driven upgrade must have occurred (a node switched to sequencer mode). - # The boundary is decided by block timestamp, not a fixed height, so check the consensus log. - local upgraded - upgraded=$($COMPOSE_CMD logs node-0 node-1 node-2 node-3 2>/dev/null | grep -c "switching to sequencer mode" || true) - if [ "$upgraded" -lt 1 ]; then - log_error "Upgrade not triggered yet (no 'switching to sequencer mode' in logs). V2 not active." - return 1 - fi - - # Check 2: node-0 must be in V2 mode with signer - local node0_v2 - node0_v2=$($COMPOSE_CMD logs node-0 2>/dev/null | grep -c "StateV2 initialized.*hasSigner=true" || true) - if [ "$node0_v2" -lt 1 ]; then - log_error "node-0 not in V2 mode with signer. Check SEQUENCER_PRIVATE_KEY and L1 setFirstSequencer." - return 1 - fi - - # Check 3: sentry must be in V2 path (not PBFT consensus reactor) - local sentry_v2 - sentry_v2=$($COMPOSE_CMD logs sentry-node-0 2>/dev/null | grep -c "Starting block apply routine" || true) - if [ "$sentry_v2" -lt 1 ]; then - log_error "sentry-node-0 not in V2 path. Check L1 contract setFirstSequencer." - return 1 - fi - - log_info "Preconditions OK: height=$height, upgrade triggered, V2 active" - - local pass=0 - local fail=0 - local skip=0 - - # Strategy: swap sentry-node-0's image to the malicious one. - # The sentry is already synced, so the malicious node starts with full - # P2P connectivity and can immediately execute attacks. - # Other nodes (node-0~3) are the "victims" that should reject forged blocks. - - # ========================================== - # Phase 1: Active attacks (T-01 ~ T-05) - # ========================================== - log_info "---------- Phase 1: Active attacks ----------" - - # Record log baseline for all victim nodes - local log_baseline="/tmp/p2p_log_baseline_$$.txt" - $COMPOSE_CMD logs node-0 node-1 node-2 node-3 2>/dev/null | wc -l > "$log_baseline" - - swap_sentry_to_malicious "all" - log_info "Waiting for malicious routine (~40s)..." - sleep 40 - - # Dump logs - local mal_log="/tmp/mal_p2p_$$.log" - docker compose \ - -f docker-compose-4nodes.yml \ - -f docker-compose.override.yml \ - -f docker-compose.malicious-override.yml \ - logs sentry-node-0 2>/dev/null > "$mal_log" - - local victim_log="/tmp/victim_p2p_$$.log" - $COMPOSE_CMD logs node-0 node-1 node-2 node-3 2>/dev/null > "$victim_log" - - restore_sentry_to_normal - - # Check malicious node executed attacks - local mal_attacks - mal_attacks=$(grep -c "\[MALICIOUS\]" "$mal_log" 2>/dev/null || true) - log_info "Malicious node executed $mal_attacks attack log entries" - - # T-01/02/03: Signature attacks (check victim nodes) - local sig_reject - sig_reject=$(grep -c "Block signature verification failed" "$victim_log" 2>/dev/null || true) - if [ "$sig_reject" -ge 3 ]; then - log_success "T-01/02/03 Signature attacks: PASSED ($sig_reject blocks rejected)" - pass=$((pass + 1)) - else - log_error "T-01/02/03 Signature attacks: FAILED ($sig_reject rejections, expected >= 3)" - fail=$((fail + 1)) - fi - - # T-04: Unsolicited sync (check victim nodes) - local unsolicited - unsolicited=$(grep -c "Unsolicited sync response" "$victim_log" 2>/dev/null || true) - if [ "$unsolicited" -ge 1 ]; then - log_success "T-04 Unsolicited sync: PASSED ($unsolicited dropped)" - pass=$((pass + 1)) - else - # Unsolicited sync targets random peers, may not hit victim nodes - log_warn "T-04 Unsolicited sync: SKIPPED (no rejection logs on victim nodes)" - skip=$((skip + 1)) - fi - - # T-05: Duplicate flood (check victim nodes) - local dedup - dedup=$(grep -c "broadcast dedup" "$victim_log" 2>/dev/null || true) - if [ "$dedup" -ge 1 ]; then - log_success "T-05 Duplicate flood: PASSED ($dedup deduped)" - pass=$((pass + 1)) - else - log_warn "T-05 Duplicate flood: SKIPPED (debug log not visible)" - skip=$((skip + 1)) - fi - - rm -f "$mal_log" "$victim_log" "$log_baseline" - - # ========================================== - # Phase 2: BlockSync forge (T-06) - V1 main vulnerability - # ========================================== - log_info "---------- Phase 2: BlockSync forge (T-06) ----------" - log_info "Testing blocksync/reactor.go:respondToPeerV2 path (BlocksyncChannel 0x40)" - - # Step 1: Swap sentry to malicious image (blocksync-forge mode) - # The malicious sentry will respond to BlockSync requests with forged blocks - swap_sentry_to_malicious "blocksync-forge" - sleep 5 - - # Step 2: Stop node-3 to create a sync gap - log_info "Stopping node-3 to create BlockSync gap..." - $COMPOSE_CMD stop node-3 2>/dev/null || true - sleep 20 # Let chain advance while node-3 is down - - # Step 3: Restart node-3 — it will BlockSync from peers (including malicious sentry) - log_info "Restarting node-3 (will BlockSync from peers including malicious sentry)..." - $COMPOSE_CMD start node-3 - - # Step 4: Wait for node-3 to catch up - local target_height - target_height=$(get_block_number "$L2_RPC") - log_info "Waiting for node-3 to sync to ~$target_height..." - local max_wait=120 - local waited=0 - while [ $waited -lt $max_wait ]; do - local n3_height - n3_height=$(get_block_number "http://127.0.0.1:8845") - if [ "$n3_height" -ge "$((target_height - 3))" ]; then - log_info "node-3 synced to $n3_height" - break - fi - sleep 5 - waited=$((waited + 5)) - done - - # Step 5: Dump logs (separate files for isolation) - local mal_bs_log="/tmp/mal_blocksync_$$.log" - docker compose \ - -f docker-compose-4nodes.yml \ - -f docker-compose.override.yml \ - -f docker-compose.malicious-override.yml \ - logs sentry-node-0 2>/dev/null > "$mal_bs_log" - - local node3_log="/tmp/node3_blocksync_$$.log" - $COMPOSE_CMD logs node-3 2>/dev/null > "$node3_log" - - restore_sentry_to_normal - - # Step 6: Verify - local bs_forged - bs_forged=$(grep -c "\[MALICIOUS\] Sent forged blocksync response" "$mal_bs_log" 2>/dev/null || true) - local bs_rejected - bs_rejected=$(grep -c "Block signature verification failed" "$node3_log" 2>/dev/null || true) - local n3_final - n3_final=$(get_block_number "http://127.0.0.1:8845") - - rm -f "$mal_bs_log" "$node3_log" - - if [ "$bs_forged" -ge 1 ] && [ "$bs_rejected" -ge 1 ]; then - log_success "T-06 BlockSync forge: PASSED (sent $bs_forged forged, rejected $bs_rejected, node-3 at $n3_final)" - pass=$((pass + 1)) - elif [ "$bs_forged" -ge 1 ]; then - log_warn "T-06 BlockSync forge: PARTIAL (sent $bs_forged forged, but node-3 may not have queried malicious peer)" - skip=$((skip + 1)) - else - log_warn "T-06 BlockSync forge: SKIPPED (malicious sentry not queried via BlockSync)" - skip=$((skip + 1)) - fi - - # ========================================== - # Phase 3: Network resilience (T-07) - # ========================================== - log_info "---------- Phase 3: Network resilience ----------" - - local h1 - h1=$(get_block_number "$L2_RPC") - sleep 30 - local h2 - h2=$(get_block_number "$L2_RPC") - if [ "$h2" -gt "$h1" ]; then - log_success "T-07 Network resilience: PASSED ($h1 -> $h2)" - pass=$((pass + 1)) - else - log_error "T-07 Network resilience: FAILED (height stuck at $h1)" - fail=$((fail + 1)) - fi - - # ========================================== - # Results - # ========================================== - log_info "==========================================" - if [ "$fail" -eq 0 ]; then - log_success " P2P Security: $pass PASSED, $skip SKIPPED, $fail FAILED" - log_success "==========================================" - else - log_error " P2P Security: $pass PASSED, $skip SKIPPED, $fail FAILED" - log_error "==========================================" - return 1 - fi -} - -# ========== Command Parsing ========== - -case "${1:-}" in - build) - build_test_images - ;; - setup) - setup_devnet - ;; - start) - start_l2_test - ;; - stop) - cd "$DOCKER_DIR" - $COMPOSE_CMD_NO_OVERRIDE down - ;; - clean) - cleanup - # Also clean L2 genesis - rm -rf "$OPS_DIR/l2-genesis/.devnet" - rm -rf "$DOCKER_DIR/.devnet" - ;; - logs) - shift - show_logs "$@" - ;; - test) - run_full_test - ;; - tx) - start_tx_generator - wait - ;; - status) - show_status - ;; - build-malicious) - build_malicious_image - ;; - p2p-test) - test_p2p_security - ;; - *) - echo "Sequencer Upgrade Test Runner" - echo "" - echo "Usage: $0 {build|setup|start|stop|clean|logs|test|tx|status|build-malicious|p2p-test}" - echo "" - echo "Commands:" - echo " build - Build test Docker images (morph-el-test, morph-node-test)" - echo " build-malicious - Build malicious node image from test/p2p-security branch" - echo " setup - Run full devnet setup (L1 + contracts + L2 genesis)" - echo " start - Start L2 nodes with test images" - echo " stop - Stop all containers" - echo " clean - Stop and remove all containers and data" - echo " logs [service] - Show container logs" - echo " test - Run full upgrade test" - echo " p2p-test - Run P2P anti-malicious security tests" - echo " tx - Start transaction generator" - echo " status - Show current block numbers" - echo "" - echo "Environment Variables:" - echo " UPGRADE_OFFSET_S - Seconds after L2 start to trigger the timestamp upgrade (default: 120)" - echo " TX_INTERVAL - Seconds between txs (default: 5)" - echo " MALICIOUS_MODE - Attack mode for p2p-test (default: all)" - echo "" - echo "Test Flow:" - echo " 1. build - Build test images" - echo " 2. setup - Deploy L1, contracts, generate L2 genesis" - echo " 3. start - Start L2 with test images" - echo " 4. test - Run PBFT -> Upgrade -> Sequencer -> Fullnode tests" - echo " 5. p2p-test - Run P2P security tests (requires build-malicious)" - echo "" - echo "Quick Start:" - echo " UPGRADE_OFFSET_S=120 $0 test" - ;; -esac diff --git a/ops/docker-sequencer-test/scripts/tx-generator.sh b/ops/docker-sequencer-test/scripts/tx-generator.sh deleted file mode 100644 index d6ee40cdf..000000000 --- a/ops/docker-sequencer-test/scripts/tx-generator.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/sh -# Transaction Generator for Sequencer Test -# Sends random transactions to keep the network active - -set -e - -L2_RPC="${L2_RPC:-http://morph-el-0:8545}" -INTERVAL="${TX_INTERVAL:-5}" # seconds between txs -PRIVATE_KEY="${PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}" - -# Wait for L2 to be ready -echo "Waiting for L2 RPC to be ready..." -while true; do - if curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - "$L2_RPC" | grep -q "result"; then - echo "L2 RPC is ready!" - break - fi - echo "Waiting..." - sleep 2 -done - -# Get initial nonce -get_nonce() { - curl -s -X POST -H "Content-Type: application/json" \ - --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionCount\",\"params\":[\"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266\",\"latest\"],\"id\":1}" \ - "$L2_RPC" | grep -o '"result":"[^"]*"' | cut -d'"' -f4 -} - -# Get current block number -get_block_number() { - curl -s -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - "$L2_RPC" | grep -o '"result":"[^"]*"' | cut -d'"' -f4 -} - -echo "Starting transaction generator..." -echo "L2 RPC: $L2_RPC" -echo "Interval: ${INTERVAL}s" - -NONCE_HEX=$(get_nonce) -NONCE=$((NONCE_HEX)) -TX_COUNT=0 - -while true; do - BLOCK=$(get_block_number) - BLOCK_DEC=$((BLOCK)) - - # Generate random recipient address - RANDOM_SUFFIX=$(od -An -N4 -tx1 /dev/urandom | tr -d ' ') - TO_ADDR="0x000000000000000000000000000000${RANDOM_SUFFIX}" - - # Create and send transaction - NONCE_HEX=$(printf "0x%x" $NONCE) - TX_DATA="{\"jsonrpc\":\"2.0\",\"method\":\"eth_sendTransaction\",\"params\":[{\"from\":\"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266\",\"to\":\"${TO_ADDR}\",\"value\":\"0x1\",\"nonce\":\"${NONCE_HEX}\"}],\"id\":1}" - - RESULT=$(curl -s -X POST -H "Content-Type: application/json" --data "$TX_DATA" "$L2_RPC") - - if echo "$RESULT" | grep -q "result"; then - TX_HASH=$(echo "$RESULT" | grep -o '"result":"[^"]*"' | cut -d'"' -f4) - echo "[Block $BLOCK_DEC] TX #$TX_COUNT sent: $TX_HASH" - NONCE=$((NONCE + 1)) - TX_COUNT=$((TX_COUNT + 1)) - else - echo "[Block $BLOCK_DEC] TX failed: $RESULT" - # Refresh nonce in case of error - NONCE_HEX=$(get_nonce) - NONCE=$((NONCE_HEX)) - fi - - sleep "$INTERVAL" -done - From d553a70d1f0ec038c986c466775bd6fc84ededeb Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 15:00:37 +0800 Subject: [PATCH 07/11] fix devnet sequencer owner key --- ops/devnet-morph/devnet/__init__.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/ops/devnet-morph/devnet/__init__.py b/ops/devnet-morph/devnet/__init__.py index bd772acf2..1dfe71350 100644 --- a/ops/devnet-morph/devnet/__init__.py +++ b/ops/devnet-morph/devnet/__init__.py @@ -38,6 +38,12 @@ parser.add_argument('--sequencer-upgrade-offset-seconds', type=int, default=int(os.environ.get('SEQUENCER_UPGRADE_OFFSET_SECONDS', '0')), help='Seconds from now before single-sequencer mode activates') +parser.add_argument('--deployer-private-key', + default=os.environ.get( + 'DEPLOYER_PRIVATE_KEY', + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + ), + help='Private key for the L1 contract deployer/owner') # parser.add_argument('--deploy', help='Whether the contracts should be predeployed or deployed', action="store_true") parser.add_argument('--debugccc', help='Whether set the debug log level for ccc', action="store_true") @@ -336,13 +342,26 @@ def configure_l1_sequencer(paths, args, addresses, deploy_config): raise RuntimeError( f'sequencer private key derives {derived.stdout.strip()}, expected {args.sequencer_address}') + owner = run_command_capture_output([ + 'cast', 'call', l1_sequencer_addr, + 'owner()(address)', + '--rpc-url', 'http://127.0.0.1:9545', + ], cwd=paths.contracts_dir).stdout.strip().lower() + deployer = run_command_capture_output([ + 'cast', 'wallet', 'address', + '--private-key', args.deployer_private_key, + ], cwd=paths.contracts_dir).stdout.strip().lower() + if deployer != owner: + raise RuntimeError( + f'deployer private key derives {deployer}, but L1Sequencer owner is {owner}') + log.info(f'Setting first L1Sequencer: sequencer={args.sequencer_address} (active from block 0)') run_command([ 'cast', 'send', l1_sequencer_addr, 'setFirstSequencer(address)', args.sequencer_address, '--rpc-url', 'http://127.0.0.1:9545', - '--private-key', deploy_config['BLOCK_SIGNER_PRIVATE_KEY'], + '--private-key', args.deployer_private_key, ], cwd=paths.contracts_dir) latest_l1_block = eth_blockNumber('127.0.0.1:9545') or 1 From 95120a0165e28ff2481cae5e83a95e5343fd8d4d Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 30 Jun 2026 16:05:13 +0800 Subject: [PATCH 08/11] extend devnet single sequencer coverage Add a layer1-derivation fullnode to the default devnet and wire an optional HA sequencer cluster through the existing devnet entrypoints. Cache Go dependency downloads in devnet Dockerfiles and restore project-label volume cleanup so future compose services are covered. Constraint: Preserve the deleted sequencer-test functionality through the main devnet path. Confidence: medium Scope-risk: moderate Not-tested: Full make devnet-up or HA runtime startup; Docker builds were not run. --- Makefile | 17 +- ops/devnet-morph/devnet/__init__.py | 16 +- ops/devnet-morph/devnet/setup_nodes.py | 47 +++-- ops/devnet-morph/tests/test_devnet_config.py | 88 +++++++++ ops/docker/Dockerfile.l2-geth | 8 +- ops/docker/Dockerfile.l2-node | 14 +- ops/docker/docker-compose-cluster.yml | 195 +++++++++++++++++++ ops/docker/docker-compose-devnet.yml | 66 ++++++- ops/docker/docker-compose-reth.yml | 3 + ops/docker/node2/node_key.json | 1 + ops/docker/nodekey2 | 1 + ops/docker/static-nodes.json | 3 +- 12 files changed, 434 insertions(+), 25 deletions(-) create mode 100644 ops/devnet-morph/tests/test_devnet_config.py create mode 100644 ops/docker/docker-compose-cluster.yml create mode 100644 ops/docker/node2/node_key.json create mode 100644 ops/docker/nodekey2 diff --git a/Makefile b/Makefile index 5be878908..e29f7c1f7 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,8 @@ go-ubuntu-builder: ################## devnet 2 nodes #################### EXECUTION_CLIENT ?= geth +DEVNET_CLUSTER ?= false +DEVNET_CLUSTER_ENABLED := $(filter true 1 yes,$(DEVNET_CLUSTER)) DEVNET_SEQUENCER_PRIVATE_KEY ?= 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 DEVNET_SEQUENCER_ADDRESS ?= 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS ?= 0 @@ -172,20 +174,33 @@ endif else $(error unsupported EXECUTION_CLIENT "$(EXECUTION_CLIENT)", expected "geth" or "reth") endif +ifneq ($(DEVNET_CLUSTER_ENABLED),) +DEVNET_COMPOSE_FILES += -f docker-compose-cluster.yml +endif devnet-up: $(DEVNET_EXECUTION_DEPS) go-ubuntu-builder python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) \ + $(if $(DEVNET_CLUSTER_ENABLED),--cluster,) \ --sequencer-private-key=$(DEVNET_SEQUENCER_PRIVATE_KEY) \ --sequencer-address=$(DEVNET_SEQUENCER_ADDRESS) \ --sequencer-upgrade-offset-seconds=$(DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS) .PHONY: devnet-up +devnet-up-cluster: + $(MAKE) devnet-up DEVNET_CLUSTER=true +.PHONY: devnet-up-cluster + devnet-up-reth: $(MAKE) devnet-up EXECUTION_CLIENT=reth .PHONY: devnet-up-reth +devnet-up-cluster-reth: + $(MAKE) devnet-up EXECUTION_CLIENT=reth DEVNET_CLUSTER=true +.PHONY: devnet-up-cluster-reth + devnet-up-debugccc: $(DEVNET_EXECUTION_DEPS) go-ubuntu-builder python3 ops/devnet-morph/main.py --polyrepo-dir=. --execution-client=$(EXECUTION_CLIENT) --debugccc \ + $(if $(DEVNET_CLUSTER_ENABLED),--cluster,) \ --sequencer-private-key=$(DEVNET_SEQUENCER_PRIVATE_KEY) \ --sequencer-address=$(DEVNET_SEQUENCER_ADDRESS) \ --sequencer-upgrade-offset-seconds=$(DEVNET_SEQUENCER_UPGRADE_OFFSET_SECONDS) @@ -201,7 +216,7 @@ devnet-down-reth: devnet-clean-build: devnet-l1-clean cd ops/docker && docker compose $(DEVNET_COMPOSE_FILES) down --volumes --remove-orphans - docker volume rm docker_morph_data_0 docker_morph_data_1 docker_morph_data_2 docker_morph_data_3 docker_sentry_el_data docker_sentry_el_data_1 2>/dev/null || true + docker volume ls --filter label=com.docker.compose.project=docker --format='{{.Name}}' | xargs docker volume rm 2>/dev/null || true rm -rf ops/l2-genesis/.devnet rm -rf ops/docker/.devnet rm -rf ops/docker/consensus ops/docker/execution diff --git a/ops/devnet-morph/devnet/__init__.py b/ops/devnet-morph/devnet/__init__.py index 1dfe71350..020efb254 100644 --- a/ops/devnet-morph/devnet/__init__.py +++ b/ops/devnet-morph/devnet/__init__.py @@ -44,6 +44,9 @@ '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', ), help='Private key for the L1 contract deployer/owner') +parser.add_argument('--cluster', action="store_true", + default=os.environ.get('DEVNET_CLUSTER', '').lower() in ('1', 'true', 'yes'), + help='Start an HA sequencer cluster instead of making node-0 the sequencer') # parser.add_argument('--deploy', help='Whether the contracts should be predeployed or deployed', action="store_true") parser.add_argument('--debugccc', help='Whether set the debug log level for ccc', action="store_true") @@ -53,11 +56,13 @@ ETH = GWEI * GWEI -def compose_file_args(execution_client): +def compose_file_args(execution_client, cluster=False): """Return docker-compose -f flags for the chosen L2 execution client.""" args = ['-f', 'docker-compose-devnet.yml'] if execution_client == 'reth': args.extend(['-f', 'docker-compose-reth.yml']) + if cluster: + args.extend(['-f', 'docker-compose-cluster.yml']) return args @@ -269,8 +274,9 @@ def devnet_deploy(paths, args): configure_l1_sequencer(paths, args, addresses, deploy_config) sequencer_upgrade_time = int((time.time() + args.sequencer_upgrade_offset_seconds) * 1000) + active_sequencer_private_key = '' if args.cluster else args.sequencer_private_key log.info( - f'Single sequencer mode enabled: sequencer={args.sequencer_address}, ' + f'Single sequencer mode enabled: sequencer={args.sequencer_address}, cluster={args.cluster}, ' f'upgrade_time_ms={sequencer_upgrade_time}, ' f'upgrade_offset_seconds={args.sequencer_upgrade_offset_seconds}') @@ -295,6 +301,7 @@ def devnet_deploy(paths, args): env_data['MORPH_L1STAKING'] = addresses['Proxy__L1Staking'] env_data['L1_SEQUENCER_CONTRACT'] = addresses.get('Proxy__L1Sequencer', '') env_data['SEQUENCER_PRIVATE_KEY'] = args.sequencer_private_key + env_data['ACTIVE_SEQUENCER_PRIVATE_KEY'] = active_sequencer_private_key env_data['HA_SEQUENCER_ADDR'] = args.sequencer_address env_data['SEQUENCER_UPGRADE_TIME'] = str(sequencer_upgrade_time) envfile.seek(0) @@ -307,7 +314,7 @@ def devnet_deploy(paths, args): - run_command(['docker', 'compose', *compose_file_args(args.execution_client), 'up', '-d'], check=False, cwd=paths.ops_dir, + run_command(['docker', 'compose', *compose_file_args(args.execution_client, args.cluster), 'up', '-d'], check=False, cwd=paths.ops_dir, env={ 'MORPH_PORTAL': addresses['Proxy__L1MessageQueueWithGasPriceOracle'], 'MORPH_ROLLUP': addresses['Proxy__Rollup'], @@ -320,6 +327,7 @@ def devnet_deploy(paths, args): 'L1_BEACON_CHAIN_RPC': 'http://layer1-cl:4000', 'L1_SEQUENCER_CONTRACT': addresses.get('Proxy__L1Sequencer', ''), 'SEQUENCER_PRIVATE_KEY': args.sequencer_private_key, + 'ACTIVE_SEQUENCER_PRIVATE_KEY': active_sequencer_private_key, 'SEQUENCER_UPGRADE_TIME': str(sequencer_upgrade_time), }) wait_up(8545) @@ -399,7 +407,7 @@ def wait_for_l1_finalized(min_block, retries=120, wait_secs=3): log.info(f'Waiting for L1 finalized block >= {min_block} (current: {finalized})') time.sleep(wait_secs) - log.warning(f'Timeout waiting for L1 finalized block >= {min_block}; continuing') + raise RuntimeError(f'Timeout waiting for L1 finalized block >= {min_block}') def run_command(args, check=True, shell=False, cwd=None, env=None, output=None): diff --git a/ops/devnet-morph/devnet/setup_nodes.py b/ops/devnet-morph/devnet/setup_nodes.py index 6c4dd16c2..d2f2f4a54 100644 --- a/ops/devnet-morph/devnet/setup_nodes.py +++ b/ops/devnet-morph/devnet/setup_nodes.py @@ -32,9 +32,18 @@ def setup_devnet_nodes(): docker_dir = os.path.join(root_dir, "ops", "docker") devnet_dir = os.path.join(docker_dir, ".devnet") if os.path.exists(devnet_dir): - old_topology_paths = [os.path.join(devnet_dir, f"node{i}") for i in range(2, 6)] - if any(os.path.exists(path) for path in old_topology_paths): - print("Existing 6-node devnet detected. Regenerating 2-node single-sequencer config.") + old_topology_paths = [os.path.join(devnet_dir, f"node{i}") for i in range(3, 6)] + expected_paths = [ + os.path.join(devnet_dir, "node0"), + os.path.join(devnet_dir, "node1"), + os.path.join(devnet_dir, "node2"), + os.path.join(devnet_dir, "ha-node0"), + os.path.join(devnet_dir, "ha-node1"), + os.path.join(devnet_dir, "ha-node2"), + ] + if any(os.path.exists(path) for path in old_topology_paths) or any( + not os.path.exists(path) for path in expected_paths): + print("Existing stale devnet detected. Regenerating single-sequencer config.") shutil.rmtree(devnet_dir) else: print(".devnet directory already exists. Devnet nodes setup has already been completed. Exiting.") @@ -43,25 +52,38 @@ def setup_devnet_nodes(): # Run the Tendermint testnet command print("Setting up the devnet...") command = [ - "tendermint", "testnet", "--v", "1", "--n", "1", "--o", devnet_dir, + "tendermint", "testnet", "--v", "1", "--n", "5", "--o", devnet_dir, "--populate-persistent-peers", "--hostname", "node-0", "--hostname", "node-1", + "--hostname", "node-2", + "--hostname", "ha-node-0", + "--hostname", "ha-node-1", + "--hostname", "ha-node-2", ] if subprocess.call(command) != 0: print("Failed to set up devnet.") sys.exit(1) - # Modify config.toml files using toml library + # Rename generated non-validator directories to match the compose service names. + for generated, desired in (("node3", "ha-node0"), ("node4", "ha-node1"), ("node5", "ha-node2")): + generated_path = os.path.join(devnet_dir, generated) + desired_path = os.path.join(devnet_dir, desired) + if os.path.exists(generated_path): + os.rename(generated_path, desired_path) + + # Modify config.toml files. print("Modifying config.toml files...") config_files = [ - os.path.join(devnet_dir, f"node{i}/config/config.toml") for i in range(2) + os.path.join(devnet_dir, node, "config", "config.toml") + for node in ("node0", "node1", "node2", "ha-node0", "ha-node1", "ha-node2") ] persistent_peers_value = ( "93e27ea2306e158a8146d5f44caaab97496797d2@node-0:26656," - "7f78b7d7a7e6bad4faf68d5731d437f4288d96d0@node-1:26656" + "7f78b7d7a7e6bad4faf68d5731d437f4288d96d0@node-1:26656," + "06c699be2f9aeb9f7ec79f508a95ff80576deb12@node-2:26656" ) for i, config_file in enumerate(config_files): @@ -96,7 +118,7 @@ def setup_devnet_nodes(): # Copy key files to devnet node directories print("Copying key files...") - node_dirs = [f"node{i}" for i in range(2)] + node_dirs = ["node0", "node1", "node2", "ha-node0", "ha-node1", "ha-node2"] for node in node_dirs: source_dir = os.path.join(docker_dir, node) @@ -106,13 +128,10 @@ def setup_devnet_nodes(): print(f"Error: Missing destination directory for {node}. Exiting.") sys.exit(1) - if not os.path.isdir(source_dir): - print(f"Error: Missing source or destination directory for {node}. Exiting.") - sys.exit(1) - - shutil.copyfile(os.path.join(source_dir, "node_key.json"), os.path.join(dest_dir, "node_key.json")) + if os.path.isdir(source_dir): + shutil.copyfile(os.path.join(source_dir, "node_key.json"), os.path.join(dest_dir, "node_key.json")) - if node == "node0": + if node == "node0" and os.path.isdir(source_dir): shutil.copyfile(os.path.join(source_dir, "priv_validator_key.json"), os.path.join(dest_dir, "priv_validator_key.json")) else: priv_validator_key = os.path.join(dest_dir, "priv_validator_key.json") diff --git a/ops/devnet-morph/tests/test_devnet_config.py b/ops/devnet-morph/tests/test_devnet_config.py new file mode 100644 index 000000000..7b4f4dac3 --- /dev/null +++ b/ops/devnet-morph/tests/test_devnet_config.py @@ -0,0 +1,88 @@ +import importlib +import sys +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +DEVNET_PACKAGE = REPO_ROOT / "ops" / "devnet-morph" +DOCKER_DIR = REPO_ROOT / "ops" / "docker" + + +class DevnetConfigTest(unittest.TestCase): + def test_node_dockerfile_caches_go_dependencies_before_source_copy(self): + dockerfile = (DOCKER_DIR / "Dockerfile.l2-node").read_text() + + dependency_layer = dockerfile.index("RUN go mod download") + source_layer = dockerfile.index("COPY . /morph") + + self.assertLess(dependency_layer, source_layer) + self.assertIn("COPY go.work go.work.sum /morph/", dockerfile) + self.assertIn("COPY node/go.mod node/go.sum /morph/node/", dockerfile) + + def test_geth_dockerfile_caches_go_dependencies_before_source_copy(self): + dockerfile = (DOCKER_DIR / "Dockerfile.l2-geth").read_text() + + dependency_layer = dockerfile.index("RUN go mod download") + source_layer = dockerfile.index("COPY go-ethereum /go-ethereum") + + self.assertLess(dependency_layer, source_layer) + self.assertIn("COPY go-ethereum/go.mod go-ethereum/go.sum /go-ethereum/", dockerfile) + + def test_devnet_clean_removes_compose_project_volumes(self): + makefile = (REPO_ROOT / "Makefile").read_text() + + self.assertIn("--filter label=com.docker.compose.project=docker", makefile) + self.assertNotIn("docker_morph_data_0 docker_morph_data_1", makefile) + + def test_default_compose_includes_layer1_derivation_node(self): + compose = (DOCKER_DIR / "docker-compose-devnet.yml").read_text() + + self.assertIn("morph-el-2:", compose) + self.assertIn("node-2:", compose) + self.assertIn("MORPH_NODE_L2_ETH_RPC=http://morph-el-2:8545", compose) + self.assertIn("MORPH_NODE_DERIVATION_VERIFY_MODE=layer1", compose) + + def test_cluster_compose_defines_ha_services(self): + cluster_compose = DOCKER_DIR / "docker-compose-cluster.yml" + + self.assertTrue(cluster_compose.exists()) + compose = cluster_compose.read_text() + for service in ("ha-geth-0:", "ha-geth-1:", "ha-geth-2:", "ha-node-0:", "ha-node-1:", "ha-node-2:"): + self.assertIn(service, compose) + self.assertIn("MORPH_NODE_HA_ENABLED=true", compose) + self.assertIn("MORPH_NODE_HA_BOOTSTRAP=true", compose) + self.assertIn("MORPH_NODE_HA_JOIN=ha-node-0:9401", compose) + + def test_compose_file_args_can_enable_cluster_mode(self): + sys.path.insert(0, str(DEVNET_PACKAGE)) + try: + devnet = importlib.import_module("devnet") + importlib.reload(devnet) + finally: + sys.path.remove(str(DEVNET_PACKAGE)) + + self.assertEqual(devnet.compose_file_args("geth"), ["-f", "docker-compose-devnet.yml"]) + self.assertEqual( + devnet.compose_file_args("reth"), + ["-f", "docker-compose-devnet.yml", "-f", "docker-compose-reth.yml"], + ) + self.assertEqual( + devnet.compose_file_args("geth", cluster=True), + ["-f", "docker-compose-devnet.yml", "-f", "docker-compose-cluster.yml"], + ) + self.assertEqual( + devnet.compose_file_args("reth", cluster=True), + [ + "-f", + "docker-compose-devnet.yml", + "-f", + "docker-compose-reth.yml", + "-f", + "docker-compose-cluster.yml", + ], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ops/docker/Dockerfile.l2-geth b/ops/docker/Dockerfile.l2-geth index 5f959888d..bc8ae09c9 100644 --- a/ops/docker/Dockerfile.l2-geth +++ b/ops/docker/Dockerfile.l2-geth @@ -3,8 +3,12 @@ FROM golang:1.24.0-alpine as builder RUN apk add --no-cache gcc musl-dev linux-headers git make -COPY ./go-ethereum /go-ethereum -RUN cd /go-ethereum && go run build/ci.go install ./cmd/geth +COPY go-ethereum/go.mod go-ethereum/go.sum /go-ethereum/ +WORKDIR /go-ethereum +RUN go mod download + +COPY go-ethereum /go-ethereum +RUN go run build/ci.go install ./cmd/geth # Pull Geth into a second stage deploy alpine container FROM alpine:latest diff --git a/ops/docker/Dockerfile.l2-node b/ops/docker/Dockerfile.l2-node index f743dc1fb..cb5f638ba 100644 --- a/ops/docker/Dockerfile.l2-node +++ b/ops/docker/Dockerfile.l2-node @@ -1,7 +1,19 @@ # Build Stage FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu as builder -# Set working directory and copy necessary files +# Cache module downloads before copying frequently changing source files. +COPY go.work go.work.sum /morph/ +COPY bindings/go.mod bindings/go.sum /morph/bindings/ +COPY common/go.mod common/go.sum /morph/common/ +COPY contracts/go.mod contracts/go.sum /morph/contracts/ +COPY node/go.mod node/go.sum /morph/node/ +COPY ops/l2-genesis/go.mod ops/l2-genesis/go.sum /morph/ops/l2-genesis/ +COPY ops/tools/go.mod ops/tools/go.sum /morph/ops/tools/ +COPY token-price-oracle/go.mod token-price-oracle/go.sum /morph/token-price-oracle/ +COPY tx-submitter/go.mod tx-submitter/go.sum /morph/tx-submitter/ +WORKDIR /morph/node +RUN go mod download + COPY . /morph WORKDIR /morph/node diff --git a/ops/docker/docker-compose-cluster.yml b/ops/docker/docker-compose-cluster.yml new file mode 100644 index 000000000..f5c75e6fb --- /dev/null +++ b/ops/docker/docker-compose-cluster.yml @@ -0,0 +1,195 @@ +services: + ha-geth-0: + container_name: ha-geth-0 + depends_on: + morph-el-0: + condition: service_started + image: morph-geth:latest + restart: unless-stopped + ports: + - "9145:8545" + - "9146:8546" + - "8551" + - "6060" + - "30303" + volumes: + - ".devnet/ha-el0:/db" + - "${PWD}/jwt-secret.txt:/jwt-secret.txt" + - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" + - "${PWD}/ha-nodekey0:/db/geth/nodekey" + - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" + environment: + - RUST_LOG=${RUST_LOG} + entrypoint: + - "/bin/bash" + - "/entrypoint.sh" + + ha-geth-1: + container_name: ha-geth-1 + depends_on: + morph-el-0: + condition: service_started + image: morph-geth:latest + restart: unless-stopped + ports: + - "9245:8545" + - "9246:8546" + - "8551" + - "6060" + - "30303" + volumes: + - ".devnet/ha-el1:/db" + - "${PWD}/jwt-secret.txt:/jwt-secret.txt" + - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" + - "${PWD}/ha-nodekey1:/db/geth/nodekey" + - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" + environment: + - RUST_LOG=${RUST_LOG} + entrypoint: + - "/bin/bash" + - "/entrypoint.sh" + + ha-geth-2: + container_name: ha-geth-2 + depends_on: + morph-el-0: + condition: service_started + image: morph-geth:latest + restart: unless-stopped + ports: + - "9345:8545" + - "9346:8546" + - "8551" + - "6060" + - "30303" + volumes: + - ".devnet/ha-el2:/db" + - "${PWD}/jwt-secret.txt:/jwt-secret.txt" + - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" + - "${PWD}/ha-nodekey2:/db/geth/nodekey" + - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" + environment: + - RUST_LOG=${RUST_LOG} + entrypoint: + - "/bin/bash" + - "/entrypoint.sh" + + ha-node-0: + container_name: ha-node-0 + depends_on: + ha-geth-0: + condition: service_started + node-0: + condition: service_started + image: morph-node:latest + restart: unless-stopped + ports: + - "26656" + - "27657:26657" + - "26658" + - "26660" + - "9501:9401" + environment: + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} + - MORPH_NODE_L2_ETH_RPC=http://ha-geth-0:8545 + - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-0:8551 + - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} + - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} + - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} + - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} + - MORPH_NODE_L1_CONFIRMATIONS=0 + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} + - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_HA_ENABLED=true + - MORPH_NODE_HA_BOOTSTRAP=true + - MORPH_NODE_HA_SERVER_ID=ha-node-0 + - MORPH_NODE_HA_ADVERTISED_ADDR=ha-node-0:9400 + volumes: + - ".devnet/ha-node0:${NODE_DATA_DIR}" + - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" + command: > + morphnode + --home $NODE_DATA_DIR + + ha-node-1: + container_name: ha-node-1 + depends_on: + ha-node-0: + condition: service_started + ha-geth-1: + condition: service_started + image: morph-node:latest + restart: unless-stopped + ports: + - "26656" + - "27757:26657" + - "26658" + - "26660" + - "9601:9401" + environment: + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} + - MORPH_NODE_L2_ETH_RPC=http://ha-geth-1:8545 + - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-1:8551 + - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} + - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} + - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} + - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} + - MORPH_NODE_L1_CONFIRMATIONS=0 + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} + - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_HA_ENABLED=true + - MORPH_NODE_HA_JOIN=ha-node-0:9401 + - MORPH_NODE_HA_SERVER_ID=ha-node-1 + - MORPH_NODE_HA_ADVERTISED_ADDR=ha-node-1:9400 + volumes: + - ".devnet/ha-node1:${NODE_DATA_DIR}" + - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" + command: > + morphnode + --home $NODE_DATA_DIR + + ha-node-2: + container_name: ha-node-2 + depends_on: + ha-node-0: + condition: service_started + ha-geth-2: + condition: service_started + image: morph-node:latest + restart: unless-stopped + ports: + - "26656" + - "27857:26657" + - "26658" + - "26660" + - "9701:9401" + environment: + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} + - MORPH_NODE_L2_ETH_RPC=http://ha-geth-2:8545 + - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-2:8551 + - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} + - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} + - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} + - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} + - MORPH_NODE_L1_CONFIRMATIONS=0 + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} + - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_HA_ENABLED=true + - MORPH_NODE_HA_JOIN=ha-node-0:9401 + - MORPH_NODE_HA_SERVER_ID=ha-node-2 + - MORPH_NODE_HA_ADVERTISED_ADDR=ha-node-2:9400 + volumes: + - ".devnet/ha-node2:${NODE_DATA_DIR}" + - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" + command: > + morphnode + --home $NODE_DATA_DIR diff --git a/ops/docker/docker-compose-devnet.yml b/ops/docker/docker-compose-devnet.yml index 6f0c387a4..bc016e834 100644 --- a/ops/docker/docker-compose-devnet.yml +++ b/ops/docker/docker-compose-devnet.yml @@ -150,6 +150,30 @@ services: - "/bin/bash" - "/entrypoint.sh" + morph-el-2: + container_name: morph-el-2 + depends_on: + - morph-el-0 + image: morph-geth:latest + restart: unless-stopped + ports: + - "8745:8545" + - "8746:8546" + - "8551" + - "6060" + - "30303" + volumes: + - ".devnet/el2:/db" + - "${PWD}/jwt-secret.txt:/jwt-secret.txt" + - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" + - "${PWD}/nodekey2:/db/geth/nodekey" + - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" + environment: + - RUST_LOG=${RUST_LOG} + entrypoint: + - "/bin/bash" + - "/entrypoint.sh" + # ========== L2 Nodes ========== node-0: container_name: node-0 @@ -178,7 +202,7 @@ services: - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY} + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${ACTIVE_SEQUENCER_PRIVATE_KEY} - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} volumes: - ".devnet/node0:${NODE_DATA_DIR}" @@ -222,6 +246,42 @@ services: morphnode --home $NODE_DATA_DIR + node-2: + container_name: node-2 + depends_on: + morph-el-2: + condition: service_started + node-0: + condition: service_started + image: morph-node:latest + restart: unless-stopped + ports: + - "26656" + - "26657" + - "26658" + - "26660" + environment: + - EMPTY_BLOCK_DELAY=true + - MORPH_NODE_L2_ETH_RPC=http://morph-el-2:8545 + - MORPH_NODE_L2_ENGINE_RPC=http://morph-el-2:8551 + - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} + - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} + - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} + - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} + - MORPH_NODE_L1_CONFIRMATIONS=0 + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} + - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} + - MORPH_NODE_DERIVATION_VERIFY_MODE=layer1 + volumes: + - ".devnet/node2:${NODE_DATA_DIR}" + - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" + command: > + morphnode + --home $NODE_DATA_DIR + # ========== Submitter and Oracle ========== tx-submitter-0: container_name: tx-submitter-0 @@ -230,6 +290,8 @@ services: condition: service_started node-1: condition: service_started + node-2: + condition: service_started build: context: ../.. dockerfile: ops/docker/Dockerfile.submitter @@ -242,7 +304,7 @@ services: - TX_SUBMITTER_BUILD_ENV=dev - TX_SUBMITTER_L1_ETH_RPC=${L1_ETH_RPC} - TX_SUBMITTER_L1_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 - - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545 + - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545,http://morph-el-2:8545 - TX_SUBMITTER_TX_TIMEOUT=60s - TX_SUBMITTER_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - TX_SUBMITTER_FINALIZE=true diff --git a/ops/docker/docker-compose-reth.yml b/ops/docker/docker-compose-reth.yml index 6d18823f4..e2e803de2 100644 --- a/ops/docker/docker-compose-reth.yml +++ b/ops/docker/docker-compose-reth.yml @@ -30,3 +30,6 @@ services: morph-el-1: <<: *reth-service + + morph-el-2: + <<: *reth-service diff --git a/ops/docker/node2/node_key.json b/ops/docker/node2/node_key.json new file mode 100644 index 000000000..e92ef1f3c --- /dev/null +++ b/ops/docker/node2/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"wWQIhytUFWhzVK6AbH50mMNyilfJidIbu1p8hViNns1IBo1dw+ry31dKDNcEoouUmlemwE7meicMlZizUe4Xsw=="}} diff --git a/ops/docker/nodekey2 b/ops/docker/nodekey2 new file mode 100644 index 000000000..e8f14ad4a --- /dev/null +++ b/ops/docker/nodekey2 @@ -0,0 +1 @@ +b9b15801462f01ba61c1f6921c6214c0a4a87f010022086c1544b794fde6fc7e diff --git a/ops/docker/static-nodes.json b/ops/docker/static-nodes.json index 3cf330e1a..c29ed91ed 100644 --- a/ops/docker/static-nodes.json +++ b/ops/docker/static-nodes.json @@ -1,3 +1,4 @@ ["enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303", - "enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303" + "enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303", + "enode://c91a993ace50749c89d37d554f12b2f4937d2ecca0232695bb33772d95a01f53564ad9dd71465c229be21e231e5c46929c2adaa78bea9d5f0966c46fca327c46@morph-el-2:30303" ] From b712007e57c6fb28781201596b66fb036494c82b Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 30 Jun 2026 16:29:00 +0800 Subject: [PATCH 09/11] simplify devnet data cleanup Make devnet-clean-build clear the full devnet compose set so callers do not need to remember the cluster or reth flags, and remove the redundant reth-specific clean-build target. Constraint: Avoid committing local generated env/deploy-config changes. Confidence: high Scope-risk: narrow Not-tested: Runtime docker cleanup against live containers was not run. --- Makefile | 7 ++----- ops/devnet-morph/tests/test_devnet_config.py | 6 ++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index e29f7c1f7..08539960f 100644 --- a/Makefile +++ b/Makefile @@ -161,6 +161,7 @@ export MORPH_RETH_RUSTFLAGS export MORPH_RETH_DOCKER_TARGET export MORPH_RETH_ENTRYPOINT DEVNET_COMPOSE_FILES := -f docker-compose-devnet.yml +DEVNET_CLEAN_COMPOSE_FILES := -f docker-compose-devnet.yml -f docker-compose-reth.yml -f docker-compose-cluster.yml ifeq ($(EXECUTION_CLIENT),geth) DEVNET_EXECUTION_DEPS := submodules @@ -215,17 +216,13 @@ devnet-down-reth: .PHONY: devnet-down-reth devnet-clean-build: devnet-l1-clean - cd ops/docker && docker compose $(DEVNET_COMPOSE_FILES) down --volumes --remove-orphans + cd ops/docker && docker compose $(DEVNET_CLEAN_COMPOSE_FILES) down --volumes --remove-orphans docker volume ls --filter label=com.docker.compose.project=docker --format='{{.Name}}' | xargs docker volume rm 2>/dev/null || true rm -rf ops/l2-genesis/.devnet rm -rf ops/docker/.devnet rm -rf ops/docker/consensus ops/docker/execution .PHONY: devnet-clean-build -devnet-clean-build-reth: - $(MAKE) devnet-clean-build EXECUTION_CLIENT=reth -.PHONY: devnet-clean-build-reth - devnet-clean: devnet-clean-build docker image ls '*morph*' --format='{{.Repository}}' | xargs -r docker rmi docker image ls '*sentry-*' --format='{{.Repository}}' | xargs -r docker rmi diff --git a/ops/devnet-morph/tests/test_devnet_config.py b/ops/devnet-morph/tests/test_devnet_config.py index 7b4f4dac3..3d5dbd7d4 100644 --- a/ops/devnet-morph/tests/test_devnet_config.py +++ b/ops/devnet-morph/tests/test_devnet_config.py @@ -32,8 +32,14 @@ def test_geth_dockerfile_caches_go_dependencies_before_source_copy(self): def test_devnet_clean_removes_compose_project_volumes(self): makefile = (REPO_ROOT / "Makefile").read_text() + self.assertIn( + "DEVNET_CLEAN_COMPOSE_FILES := -f docker-compose-devnet.yml -f docker-compose-reth.yml -f docker-compose-cluster.yml", + makefile, + ) + self.assertIn("docker compose $(DEVNET_CLEAN_COMPOSE_FILES) down --volumes --remove-orphans", makefile) self.assertIn("--filter label=com.docker.compose.project=docker", makefile) self.assertNotIn("docker_morph_data_0 docker_morph_data_1", makefile) + self.assertNotIn("devnet-clean-build-reth", makefile) def test_default_compose_includes_layer1_derivation_node(self): compose = (DOCKER_DIR / "docker-compose-devnet.yml").read_text() From 3a64db4cae0f7ad9a4ca83b766d1dcc6bfa00162 Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 30 Jun 2026 16:40:57 +0800 Subject: [PATCH 10/11] remove redundant reth devnet clean target Drop the reth-specific devnet-clean wrapper now that devnet-clean-build always tears down the full devnet compose set, including reth and cluster overrides. Constraint: Keep cleanup entrypoints minimal after unifying compose cleanup. Confidence: high Scope-risk: narrow Not-tested: Runtime docker cleanup against live containers was not run. --- Makefile | 4 ---- ops/devnet-morph/tests/test_devnet_config.py | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 08539960f..3211c3ea7 100644 --- a/Makefile +++ b/Makefile @@ -228,10 +228,6 @@ devnet-clean: devnet-clean-build docker image ls '*sentry-*' --format='{{.Repository}}' | xargs -r docker rmi .PHONY: devnet-clean -devnet-clean-reth: - $(MAKE) devnet-clean EXECUTION_CLIENT=reth -.PHONY: devnet-clean-reth - devnet-l1: python3 ops/devnet-morph/main.py --polyrepo-dir=. --only-l1 diff --git a/ops/devnet-morph/tests/test_devnet_config.py b/ops/devnet-morph/tests/test_devnet_config.py index 3d5dbd7d4..dd58023d4 100644 --- a/ops/devnet-morph/tests/test_devnet_config.py +++ b/ops/devnet-morph/tests/test_devnet_config.py @@ -40,6 +40,7 @@ def test_devnet_clean_removes_compose_project_volumes(self): self.assertIn("--filter label=com.docker.compose.project=docker", makefile) self.assertNotIn("docker_morph_data_0 docker_morph_data_1", makefile) self.assertNotIn("devnet-clean-build-reth", makefile) + self.assertNotIn("devnet-clean-reth", makefile) def test_default_compose_includes_layer1_derivation_node(self): compose = (DOCKER_DIR / "docker-compose-devnet.yml").read_text() From b59e1633ca77a7ff825d5d027b9d42791e322f1f Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Tue, 30 Jun 2026 18:33:06 +0800 Subject: [PATCH 11/11] chore(devnet): drop third sync node --- ops/devnet-morph/tests/test_devnet_config.py | 8 ++- ops/docker/docker-compose-devnet.yml | 65 +------------------- ops/docker/docker-compose-reth.yml | 3 - ops/docker/static-nodes.json | 3 +- 4 files changed, 8 insertions(+), 71 deletions(-) diff --git a/ops/devnet-morph/tests/test_devnet_config.py b/ops/devnet-morph/tests/test_devnet_config.py index dd58023d4..f64532279 100644 --- a/ops/devnet-morph/tests/test_devnet_config.py +++ b/ops/devnet-morph/tests/test_devnet_config.py @@ -45,10 +45,12 @@ def test_devnet_clean_removes_compose_project_volumes(self): def test_default_compose_includes_layer1_derivation_node(self): compose = (DOCKER_DIR / "docker-compose-devnet.yml").read_text() - self.assertIn("morph-el-2:", compose) - self.assertIn("node-2:", compose) - self.assertIn("MORPH_NODE_L2_ETH_RPC=http://morph-el-2:8545", compose) + self.assertIn("node-1:", compose) + self.assertIn("MORPH_NODE_L2_ETH_RPC=http://morph-el-1:8545", compose) self.assertIn("MORPH_NODE_DERIVATION_VERIFY_MODE=layer1", compose) + self.assertNotIn("morph-el-2:", compose) + self.assertNotIn("node-2:", compose) + self.assertNotIn("morph-el-2:8545", compose) def test_cluster_compose_defines_ha_services(self): cluster_compose = DOCKER_DIR / "docker-compose-cluster.yml" diff --git a/ops/docker/docker-compose-devnet.yml b/ops/docker/docker-compose-devnet.yml index bc016e834..25321072b 100644 --- a/ops/docker/docker-compose-devnet.yml +++ b/ops/docker/docker-compose-devnet.yml @@ -150,30 +150,6 @@ services: - "/bin/bash" - "/entrypoint.sh" - morph-el-2: - container_name: morph-el-2 - depends_on: - - morph-el-0 - image: morph-geth:latest - restart: unless-stopped - ports: - - "8745:8545" - - "8746:8546" - - "8551" - - "6060" - - "30303" - volumes: - - ".devnet/el2:/db" - - "${PWD}/jwt-secret.txt:/jwt-secret.txt" - - "${PWD}/../l2-genesis/.devnet/genesis-l2.json:/genesis.json" - - "${PWD}/nodekey2:/db/geth/nodekey" - - "${PWD}/static-nodes.json:/db/geth/static-nodes.json" - environment: - - RUST_LOG=${RUST_LOG} - entrypoint: - - "/bin/bash" - - "/entrypoint.sh" - # ========== L2 Nodes ========== node-0: container_name: node-0 @@ -239,44 +215,9 @@ services: - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - volumes: - - ".devnet/node1:${NODE_DATA_DIR}" - - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" - command: > - morphnode - --home $NODE_DATA_DIR - - node-2: - container_name: node-2 - depends_on: - morph-el-2: - condition: service_started - node-0: - condition: service_started - image: morph-node:latest - restart: unless-stopped - ports: - - "26656" - - "26657" - - "26658" - - "26660" - environment: - - EMPTY_BLOCK_DELAY=true - - MORPH_NODE_L2_ETH_RPC=http://morph-el-2:8545 - - MORPH_NODE_L2_ENGINE_RPC=http://morph-el-2:8551 - - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} - - MORPH_NODE_L1_ETH_RPC=${L1_ETH_RPC} - - MORPH_NODE_L1_ETH_BEACON_RPC=${L1_BEACON_CHAIN_RPC} - - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - - MORPH_NODE_L1_CONFIRMATIONS=0 - - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - - MORPH_NODE_SYNC_START_HEIGHT=${MORPH_NODE_SYNC_START_HEIGHT:-1} - - MORPH_NODE_UPGRADE_BATCH_TIME=${BATCH_UPGRADE_TIME} - - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - MORPH_NODE_DERIVATION_VERIFY_MODE=layer1 volumes: - - ".devnet/node2:${NODE_DATA_DIR}" + - ".devnet/node1:${NODE_DATA_DIR}" - "${PWD}/jwt-secret.txt:${JWT_SECRET_PATH}" command: > morphnode @@ -290,8 +231,6 @@ services: condition: service_started node-1: condition: service_started - node-2: - condition: service_started build: context: ../.. dockerfile: ops/docker/Dockerfile.submitter @@ -304,7 +243,7 @@ services: - TX_SUBMITTER_BUILD_ENV=dev - TX_SUBMITTER_L1_ETH_RPC=${L1_ETH_RPC} - TX_SUBMITTER_L1_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 - - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545,http://morph-el-2:8545 + - TX_SUBMITTER_L2_ETH_RPCS=http://morph-el-0:8545,http://morph-el-1:8545 - TX_SUBMITTER_TX_TIMEOUT=60s - TX_SUBMITTER_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - TX_SUBMITTER_FINALIZE=true diff --git a/ops/docker/docker-compose-reth.yml b/ops/docker/docker-compose-reth.yml index e2e803de2..6d18823f4 100644 --- a/ops/docker/docker-compose-reth.yml +++ b/ops/docker/docker-compose-reth.yml @@ -30,6 +30,3 @@ services: morph-el-1: <<: *reth-service - - morph-el-2: - <<: *reth-service diff --git a/ops/docker/static-nodes.json b/ops/docker/static-nodes.json index c29ed91ed..3cf330e1a 100644 --- a/ops/docker/static-nodes.json +++ b/ops/docker/static-nodes.json @@ -1,4 +1,3 @@ ["enode://58e698ea2dd8a76e0cb185d13c1faabf223b60c89fef988c8b89496571056d6c2922109537bb291cd87f2ec09a23ac37d59bde2c7a4885d07b7b641cadff2921@morph-el-0:30303", - "enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303", - "enode://c91a993ace50749c89d37d554f12b2f4937d2ecca0232695bb33772d95a01f53564ad9dd71465c229be21e231e5c46929c2adaa78bea9d5f0966c46fca327c46@morph-el-2:30303" + "enode://bd755ce0bc8c06b4444b9013e8d1215a02e2b53f39f746f060c292ba2f6877d7b702374f006a49a7b1506bf1bc027b43824859d081283e6bac97c8600cdf3fee@morph-el-1:30303" ]