diff --git a/.gitignore b/.gitignore
index 09d78bca..c8f7d6ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,4 @@ tools/adventure/txhashes.log
**/build/
local
+.omc
diff --git a/devnet/0-all.sh b/devnet/0-all.sh
index 24d710b0..4708411f 100755
--- a/devnet/0-all.sh
+++ b/devnet/0-all.sh
@@ -6,4 +6,5 @@ set -e
./3-op-init.sh
./4-op-start-service.sh
./5-run-op-succinct.sh
-./6-run-kailua.sh
\ No newline at end of file
+./6-run-kailua.sh
+./7-run-tee-game.sh
diff --git a/devnet/5-run-op-succinct.sh b/devnet/5-run-op-succinct.sh
index 1e11dccc..3a3f0696 100755
--- a/devnet/5-run-op-succinct.sh
+++ b/devnet/5-run-op-succinct.sh
@@ -53,12 +53,14 @@ sed_inplace "s|^L2_NODE_RPC=.*|L2_NODE_RPC=$L2_NODE_RPC_URL_IN_DOCKER|" "$OP_SUC
sed_inplace "s|^FACTORY_ADDRESS=.*|FACTORY_ADDRESS=$DISPUTE_GAME_FACTORY_ADDRESS|" "$OP_SUCCINCT_DIR"/.env.deploy
sed_inplace "s|^OPTIMISM_PORTAL2_ADDRESS=.*|OPTIMISM_PORTAL2_ADDRESS=$OPTIMISM_PORTAL_PROXY_ADDRESS|" "$OP_SUCCINCT_DIR"/.env.deploy
sed_inplace "s|^ANCHOR_STATE_REGISTRY=.*|ANCHOR_STATE_REGISTRY=$ANCHOR_STATE_REGISTRY|" "$OP_SUCCINCT_DIR"/.env.deploy
-sed_inplace "s|^TRANSACTOR_ADDRESS=.*|TRANSACTOR_ADDRESS=$TRANSACTOR|" "$OP_SUCCINCT_DIR"/.env.deploy
+sed_inplace "s|^TRANSACTOR=.*|TRANSACTOR=$TRANSACTOR|" "$OP_SUCCINCT_DIR"/.env.deploy
+sed_inplace "s|^PROPOSER_ADDRESSES=.*|PROPOSER_ADDRESSES=$PROPOSER_ADDRESS|" "$OP_SUCCINCT_DIR"/.env.deploy
+sed_inplace "s|^CHALLENGER_ADDRESSES=.*|CHALLENGER_ADDRESSES=$CHALLENGER_ADDRESS|" "$OP_SUCCINCT_DIR"/.env.deploy
sed_inplace "s|^STARTING_L2_BLOCK_NUMBER=.*|STARTING_L2_BLOCK_NUMBER=$((FORK_BLOCK + 1))|" "$OP_SUCCINCT_DIR"/.env.deploy
sed_inplace "s|^OP_SUCCINCT_MOCK=.*|OP_SUCCINCT_MOCK=$PROOF_MOCK_MODE|" "$OP_SUCCINCT_DIR"/.env.deploy
-STARTING_L2_BLOCK_NUMBER=$(cast call "$ANCHOR_STATE_REGISTRY" "getAnchorRoot()(bytes32,uint256)" --json | jq -r '.[1]')
+STARTING_L2_BLOCK_NUMBER=$(cast call "$ANCHOR_STATE_REGISTRY" "getAnchorRoot()(bytes32,uint256)" --json -r "$L1_RPC_URL" | jq -r '.[1]')
sed_inplace "s|^STARTING_L2_BLOCK_NUMBER=.*|STARTING_L2_BLOCK_NUMBER=$STARTING_L2_BLOCK_NUMBER|" "$OP_SUCCINCT_DIR"/.env.deploy
# update .env.proposer
@@ -74,15 +76,26 @@ sed_inplace "s|^MOCK_MODE=.*|MOCK_MODE=$PROOF_MOCK_MODE|" "$OP_SUCCINCT_DIR"/.en
sed_inplace "s|^L1_RPC=.*|L1_RPC=$L1_RPC_URL_IN_DOCKER|" "$OP_SUCCINCT_DIR"/.env.challenger
sed_inplace "s|^L2_RPC=.*|L2_RPC=$L2_RPC_URL_IN_DOCKER|" "$OP_SUCCINCT_DIR"/.env.challenger
sed_inplace "s|^FACTORY_ADDRESS=.*|FACTORY_ADDRESS=$DISPUTE_GAME_FACTORY_ADDRESS|" "$OP_SUCCINCT_DIR"/.env.challenger
+grep -q "^RUST_LOG=" "$OP_SUCCINCT_DIR"/.env.challenger || echo "RUST_LOG=info" >> "$OP_SUCCINCT_DIR"/.env.challenger
docker compose up op-succinct-fetch-config
OP_DEPLOYER_ADDR=$(cast wallet a "$DEPLOYER_PRIVATE_KEY")
cast send --private-key "$RICH_L1_PRIVATE_KEY" --value 1ether "$OP_DEPLOYER_ADDR" --legacy --rpc-url "$L1_RPC_URL"
docker compose up op-succinct-contracts
-cast send "$ANCHOR_STATE_REGISTRY" "setRespectedGameType(uint32)" 42 --private-key="$DEPLOYER_PRIVATE_KEY"
+# Update ANCHOR_STATE_REGISTRY_ADDRESS in .env.proposer with the address from the newly deployed game implementation
+NEW_GAME_IMPL=$(cast call "$DISPUTE_GAME_FACTORY_ADDRESS" 'gameImpls(uint32)(address)' 42 -r "$L1_RPC_URL")
+NEW_ANCHOR_STATE_REGISTRY=$(cast call "$NEW_GAME_IMPL" 'anchorStateRegistry()(address)' -r "$L1_RPC_URL")
+grep -q "^ANCHOR_STATE_REGISTRY_ADDRESS=" "$OP_SUCCINCT_DIR"/.env.proposer \
+ && sed_inplace "s|^ANCHOR_STATE_REGISTRY_ADDRESS=.*|ANCHOR_STATE_REGISTRY_ADDRESS=$NEW_ANCHOR_STATE_REGISTRY|" "$OP_SUCCINCT_DIR"/.env.proposer \
+ || echo "ANCHOR_STATE_REGISTRY_ADDRESS=$NEW_ANCHOR_STATE_REGISTRY" >> "$OP_SUCCINCT_DIR"/.env.proposer
+grep -q "^ANCHOR_STATE_REGISTRY_ADDRESS=" "$OP_SUCCINCT_DIR"/.env.challenger \
+ && sed_inplace "s|^ANCHOR_STATE_REGISTRY_ADDRESS=.*|ANCHOR_STATE_REGISTRY_ADDRESS=$NEW_ANCHOR_STATE_REGISTRY|" "$OP_SUCCINCT_DIR"/.env.challenger \
+ || echo "ANCHOR_STATE_REGISTRY_ADDRESS=$NEW_ANCHOR_STATE_REGISTRY" >> "$OP_SUCCINCT_DIR"/.env.challenger
-TARGET_HEIGHT=$(cast call "$ANCHOR_STATE_REGISTRY" "getAnchorRoot()(bytes32,uint256)" --json | jq -r '.[1]')
+cast send "$ANCHOR_STATE_REGISTRY" "setRespectedGameType(uint32)" 42 --private-key="$DEPLOYER_PRIVATE_KEY" --rpc-url "$L1_RPC_URL"
+
+TARGET_HEIGHT=$(cast call "$ANCHOR_STATE_REGISTRY" "getAnchorRoot()(bytes32,uint256)" --json -r "$L1_RPC_URL" | jq -r '.[1]')
while true; do
CURRENT_HEIGHT=$(cast bn -r "$L2_RPC_URL" finalized 2>/dev/null || echo "0")
@@ -101,8 +114,8 @@ docker compose up -d op-succinct-proposer
echo " ā Proposer started"
if [ "$MIN_RUN" = "false" ]; then
- docker-compose down op-proposer
- docker-compose down op-challenger
+ docker compose down op-proposer
+ docker compose down op-challenger
echo " ā Older proposer and challenger stopped"
fi
diff --git a/devnet/7-run-tee-game.sh b/devnet/7-run-tee-game.sh
new file mode 100755
index 00000000..703f5b5e
--- /dev/null
+++ b/devnet/7-run-tee-game.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+set -e
+
+source .env
+
+PWD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SCRIPTS_DIR=$PWD_DIR/scripts
+
+# Check required images exist
+MISSING_IMAGES=()
+for IMG_VAR in OP_CONTRACTS_TEE_IMAGE_TAG OP_STACK_TEE_IMAGE_TAG MOCKTEERPC_IMAGE_TAG MOCKTEEPROVER_IMAGE_TAG; do
+ IMG="${!IMG_VAR}"
+ if ! docker image inspect "$IMG" > /dev/null 2>&1; then
+ MISSING_IMAGES+=("$IMG_VAR=$IMG")
+ fi
+done
+
+if [ ${#MISSING_IMAGES[@]} -gt 0 ]; then
+ echo "ā The following required Docker images are missing:"
+ for ENTRY in "${MISSING_IMAGES[@]}"; do
+ echo " - $ENTRY"
+ done
+ echo ""
+ echo "To build them, set the corresponding SKIP flags to 'false' in .env:"
+ for ENTRY in "${MISSING_IMAGES[@]}"; do
+ VAR_NAME="${ENTRY%%=*}"
+ case "$VAR_NAME" in
+ OP_CONTRACTS_TEE_IMAGE_TAG) echo " SKIP_OP_CONTRACTS_TEE_BUILD=false" ;;
+ OP_STACK_TEE_IMAGE_TAG) echo " SKIP_OP_STACK_TEE_BUILD=false" ;;
+ MOCKTEERPC_IMAGE_TAG) echo " SKIP_MOCKTEERPC_BUILD=false" ;;
+ MOCKTEEPROVER_IMAGE_TAG) echo " SKIP_MOCKTEEPROVER_BUILD=false" ;;
+ esac
+ done
+ echo ""
+ echo "Then run: bash init.sh"
+ echo ""
+ echo "After init.sh completes, re-run this script: bash 7-run-tee-game.sh"
+ exit 1
+fi
+
+ENCLAVE_ADDRESS=$(cast wallet address --private-key "$TEE_SIGNER_PRIVATE_KEY")
+
+echo "š§ Adding TEE game type..."
+echo " Image: $OP_CONTRACTS_TEE_IMAGE_TAG"
+echo " Enclave address: $ENCLAVE_ADDRESS"
+
+# Create a temp .env with L1_RPC_URL replaced by docker-internal URL,
+# because add-tee-game-type.sh sources .env internally and would overwrite -e overrides.
+TEMP_ENV=$(mktemp)
+trap "rm -f $TEMP_ENV" EXIT
+sed "s|^L1_RPC_URL=.*|L1_RPC_URL=$L1_RPC_URL_IN_DOCKER|" .env > "$TEMP_ENV"
+
+docker run --rm \
+ --network "$DOCKER_NETWORK" \
+ -v "$(pwd)/scripts:/devnet/scripts" \
+ -v "$TEMP_ENV:/devnet/.env" \
+ "$OP_CONTRACTS_TEE_IMAGE_TAG" \
+ bash /devnet/scripts/add-tee-game-type.sh \
+ --max-challenge-duration "$GAME_WINDOW" \
+ --max-prove-duration "$GAME_WINDOW" \
+ --mock-verifier \
+ --enclave "$ENCLAVE_ADDRESS" \
+ /app/packages/contracts-bedrock
+
+# Query L1 chain ID and TEE proof verifier address for EIP-712 domain separator
+export L1_CHAIN_ID=$(cast chain-id --rpc-url "$L1_RPC_URL")
+TEE_GAME_IMPL=$(cast call --rpc-url "$L1_RPC_URL" "$DISPUTE_GAME_FACTORY_ADDRESS" 'gameImpls(uint32)(address)' "$TEE_GAME_TYPE")
+export TEE_PROOF_VERIFIER_ADDRESS=$(cast call --rpc-url "$L1_RPC_URL" "$TEE_GAME_IMPL" 'teeProofVerifier()(address)')
+echo " L1 Chain ID: $L1_CHAIN_ID"
+echo " TEE Proof Verifier: $TEE_PROOF_VERIFIER_ADDRESS"
+
+echo "š Starting TEE services..."
+docker compose -f docker-compose.yml -f docker-compose-tee.yml up -d mockteerpc mockteeprover tee-proposer tee-challenger
+
+echo "ā
TEE game setup complete!"
+echo ""
+echo "To list all games, run:"
+echo " bash scripts/list-game.sh"
diff --git a/devnet/docker-compose-tee.yml b/devnet/docker-compose-tee.yml
new file mode 100644
index 00000000..d2c66a17
--- /dev/null
+++ b/devnet/docker-compose-tee.yml
@@ -0,0 +1,74 @@
+networks:
+ default:
+ name: ${DOCKER_NETWORK:-dev-op}
+
+services:
+ mockteerpc:
+ image: "${MOCKTEERPC_IMAGE_TAG}"
+ container_name: mockteerpc
+ command:
+ - --delay
+ - 1000ms
+ - --init-height
+ - "500000000"
+ ports:
+ - "8090:8090"
+
+ mockteeprover:
+ image: "${MOCKTEEPROVER_IMAGE_TAG}"
+ container_name: mockteeprover
+ environment:
+ - SIGNER_PRIVATE_KEY=${TEE_SIGNER_PRIVATE_KEY}
+ - CHAIN_ID=${L1_CHAIN_ID:-1337}
+ - VERIFYING_CONTRACT=${TEE_PROOF_VERIFIER_ADDRESS}
+ - TASK_DELAY=${TEE_TASK_DELAY:-2s}
+ ports:
+ - "8690:8690"
+
+ tee-proposer:
+ image: "${OP_STACK_TEE_IMAGE_TAG}"
+ container_name: tee-proposer
+ environment:
+ - DISPUTE_GAME_FACTORY_ADDRESS=${DISPUTE_GAME_FACTORY_ADDRESS}
+ - OP_PROPOSER_PRIVATE_KEY=${OP_PROPOSER_PRIVATE_KEY}
+ command:
+ - /app/op-proposer/bin/op-proposer
+ - --l1-eth-rpc=${L1_RPC_URL_IN_DOCKER}
+ - --tee-rollup-rpc=http://mockteerpc:8090
+ - --game-type=${TEE_GAME_TYPE:-1960}
+ - --game-factory-address=${DISPUTE_GAME_FACTORY_ADDRESS}
+ - --private-key=${OP_PROPOSER_PRIVATE_KEY}
+ - --poll-interval=2s
+ - --proposal-interval=35s
+ - --rpc.port=7302
+ - --log.level=info
+ depends_on:
+ - mockteerpc
+ - op-batcher
+
+ tee-challenger:
+ image: "${OP_STACK_TEE_IMAGE_TAG}"
+ container_name: tee-challenger
+ environment:
+ - DISPUTE_GAME_FACTORY_ADDRESS=${DISPUTE_GAME_FACTORY_ADDRESS}
+ - OP_PROPOSER_PRIVATE_KEY=${OP_PROPOSER_PRIVATE_KEY}
+ volumes:
+ - ./data/tee-challenger-data:/data
+ command:
+ - /app/op-challenger/bin/op-challenger
+ - --log.level=debug
+ - --l1-eth-rpc=${L1_RPC_URL_IN_DOCKER}
+ - --l1-beacon=${L1_BEACON_URL_IN_DOCKER}
+ - --game-factory-address=${DISPUTE_GAME_FACTORY_ADDRESS}
+ - --private-key=${OP_PROPOSER_PRIVATE_KEY}
+ - --game-types=tee
+ - --datadir=/data
+ - --http-poll-interval=2s
+ - --tee-prover-rpc=http://mockteeprover:8690
+ - --tee-prove-poll-interval=2s
+ - --tee-prove-timeout=120s
+ - --selective-claim-resolution
+ - --game-window=86400s
+ depends_on:
+ - mockteeprover
+ - tee-proposer
diff --git a/devnet/docker-compose.yml b/devnet/docker-compose.yml
index 0ead9d9b..5a509eb3 100644
--- a/devnet/docker-compose.yml
+++ b/devnet/docker-compose.yml
@@ -935,7 +935,7 @@ services:
volumes:
- ./op-succinct/.env.challenger:/app/.env
ports:
- - "9001:9001"
+ - "9002:9001"
command:
- challenger
- --env-file=/app/.env
diff --git a/devnet/example.env b/devnet/example.env
index 2461d30d..9523c417 100644
--- a/devnet/example.env
+++ b/devnet/example.env
@@ -2,6 +2,7 @@
# OP Stack Configuration
# ==============================================================================
OP_STACK_LOCAL_DIRECTORY=
+OP_STACK_BRANCH=dev
SKIP_OP_STACK_BUILD=true
OP_STACK_IMAGE_TAG=op-stack:latest
@@ -61,6 +62,29 @@ KAILUA_LOCAL_DIRECTORY=
SKIP_KAILUA_BUILD=true
KAILUA_IMAGE_TAG=kailua:latest
+# ==============================================================================
+# OP Stack TEE Configuration (independent from op-stack, built from tz/dev branch)
+# ==============================================================================
+OP_STACK_TEE_LOCAL_DIRECTORY=
+OP_STACK_TEE_BRANCH=tz/dev
+SKIP_OP_STACK_TEE_BUILD=true
+OP_STACK_TEE_IMAGE_TAG=op-stack:tee
+SKIP_OP_CONTRACTS_TEE_BUILD=true
+OP_CONTRACTS_TEE_IMAGE_TAG=op-contracts:tee
+
+# ==============================================================================
+# MockTeeRPC Configuration
+# ==============================================================================
+SKIP_MOCKTEERPC_BUILD=true
+MOCKTEERPC_IMAGE_TAG=mockteerpc:latest
+
+# ==============================================================================
+# MockTeeProver Configuration
+# ==============================================================================
+SKIP_MOCKTEEPROVER_BUILD=true
+MOCKTEEPROVER_IMAGE_TAG=mockteeprover:latest
+TEE_SIGNER_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
+
# ==============================================================================
# Build Configuration
# ==============================================================================
@@ -117,6 +141,7 @@ OP_CHALLENGER_PRIVATE_KEY=0x8b3a350cf5c34c9194ca9aa3f146b2b9afed22cd83d3c5f6a3f2
# ==============================================================================
DEPLOYER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
PROPOSER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
+CHALLENGER_ADDRESS=0x7d18A1B858253b5588f61fb5739d52e4b84e2cdA
ADMIN_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
SAFE_ADDRESS=0x0000000000000000000000000000000000000000
@@ -147,6 +172,7 @@ TEMP_GAME_WINDOW=60
MAX_CLOCK_DURATION=20
CLOCK_EXTENSION=10
GAME_WINDOW=60
+TEE_GAME_TYPE=1960
# AnchorStateRegistry configure
DISPUTE_GAME_FINALITY_DELAY_SECONDS=5
diff --git a/devnet/init.sh b/devnet/init.sh
index 64afbf2a..7d28c49a 100755
--- a/devnet/init.sh
+++ b/devnet/init.sh
@@ -24,6 +24,19 @@ function build_and_tag_image() {
cd -
}
+function git_switch_branch() {
+ local dir=$1
+ local branch=$2
+ local remote
+ cd "$dir"
+ remote=$(git remote | head -1)
+ echo "š Switching to branch: $branch (remote: $remote)"
+ git fetch "$remote"
+ git checkout "$branch"
+ git pull "$remote" "$branch"
+ cd -
+}
+
# Build OP_STACK image
if [ "$SKIP_OP_STACK_BUILD" = "true" ]; then
echo "āļø Skipping op-stack build"
@@ -32,6 +45,12 @@ else
echo "ā Please set OP_STACK_LOCAL_DIRECTORY in .env"
exit 1
else
+ if [ -n "$OP_STACK_BRANCH" ]; then
+ git_switch_branch "$OP_STACK_LOCAL_DIRECTORY" "$OP_STACK_BRANCH"
+ else
+ echo "š Using op-stack branch: $(cd "$OP_STACK_LOCAL_DIRECTORY" && git branch --show-current)"
+ fi
+
echo "šØ Building op-stack"
cd "$OP_STACK_LOCAL_DIRECTORY"
git submodule update --init --recursive
@@ -40,6 +59,40 @@ else
fi
fi
+# Build OP_STACK_TEE and OP_CONTRACTS_TEE images
+if [ "$SKIP_OP_STACK_TEE_BUILD" = "true" ] && [ "$SKIP_OP_CONTRACTS_TEE_BUILD" = "true" ]; then
+ echo "āļø Skipping op-stack-tee and op-contracts-tee build"
+else
+ # Use dedicated directory if set, otherwise fall back to OP_STACK_LOCAL_DIRECTORY
+ if [ -n "$OP_STACK_TEE_LOCAL_DIRECTORY" ]; then
+ OP_STACK_TEE_DIR="$OP_STACK_TEE_LOCAL_DIRECTORY"
+ elif [ -n "$OP_STACK_LOCAL_DIRECTORY" ]; then
+ OP_STACK_TEE_DIR="$OP_STACK_LOCAL_DIRECTORY"
+ else
+ echo "ā Please set OP_STACK_TEE_LOCAL_DIRECTORY or OP_STACK_LOCAL_DIRECTORY in .env"
+ exit 1
+ fi
+
+ git_switch_branch "$OP_STACK_TEE_DIR" "$OP_STACK_TEE_BRANCH"
+ cd "$OP_STACK_TEE_DIR"
+ git submodule update --init --recursive
+ cd -
+
+ if [ "$SKIP_OP_STACK_TEE_BUILD" = "true" ]; then
+ echo "āļø Skipping op-stack-tee build"
+ else
+ echo "šØ Building $OP_STACK_TEE_IMAGE_TAG"
+ build_and_tag_image "op-stack-tee" "$OP_STACK_TEE_IMAGE_TAG" "$OP_STACK_TEE_DIR" "Dockerfile-opstack"
+ fi
+
+ if [ "$SKIP_OP_CONTRACTS_TEE_BUILD" = "true" ]; then
+ echo "āļø Skipping op-contracts-tee build"
+ else
+ echo "šØ Building $OP_CONTRACTS_TEE_IMAGE_TAG"
+ build_and_tag_image "op-contracts-tee" "$OP_CONTRACTS_TEE_IMAGE_TAG" "$OP_STACK_TEE_DIR" "Dockerfile-contracts"
+ fi
+fi
+
# Build OP_GETH image
if [ "$SKIP_OP_GETH_BUILD" = "true" ]; then
echo "āļø Skipping op-geth build"
@@ -58,12 +111,7 @@ else
# Switch to specified branch if provided
if [ -n "$OP_GETH_BRANCH" ]; then
- echo "š Switching op-geth to branch: $OP_GETH_BRANCH"
- cd "$OP_GETH_DIR"
- git fetch origin
- git checkout "$OP_GETH_BRANCH"
- git pull origin "$OP_GETH_BRANCH"
- cd -
+ git_switch_branch "$OP_GETH_DIR" "$OP_GETH_BRANCH"
else
echo "š Using op-geth default branch"
fi
@@ -97,16 +145,11 @@ else
exit 1
else
echo "šØ Building $OP_RETH_IMAGE_TAG"
- cd "$OP_RETH_LOCAL_DIRECTORY"
if [ -n "$OP_RETH_BRANCH" ]; then
- echo "š Switching op-reth to branch: $OP_RETH_BRANCH"
- git fetch origin
- git checkout "$OP_RETH_BRANCH"
- git pull origin "$OP_RETH_BRANCH"
+ git_switch_branch "$OP_RETH_LOCAL_DIRECTORY" "$OP_RETH_BRANCH"
else
- echo "š Using op-reth branch: $(git branch --show-current)"
+ echo "š Using op-reth branch: $(cd "$OP_RETH_LOCAL_DIRECTORY" && git branch --show-current)"
fi
- cd -
# Check if profiling is enabled and build accordingly
if [ "$RETH_PROFILING_ENABLED" = "true" ]; then
@@ -147,8 +190,28 @@ else
exit 1
else
echo "šØ Building kailua image"
-
+
cd "$KAILUA_LOCAL_DIRECTORY"
build_and_tag_image "kailua" "$KAILUA_IMAGE_TAG" "$KAILUA_LOCAL_DIRECTORY" "Dockerfile.local"
fi
fi
+
+# Build MockTeeRPC image
+if [ "$SKIP_MOCKTEERPC_BUILD" = "true" ]; then
+ echo "āļø Skipping mockteerpc build"
+else
+ echo "šØ Building $MOCKTEERPC_IMAGE_TAG"
+ MOCKTEERPC_DIR="$PWD_DIR/../tools/mockteerpc"
+ build_and_tag_image "mockteerpc" "$MOCKTEERPC_IMAGE_TAG" "$MOCKTEERPC_DIR" "Dockerfile"
+fi
+
+# Build MockTeeProver image
+if [ "$SKIP_MOCKTEEPROVER_BUILD" = "true" ]; then
+ echo "āļø Skipping mockteeprover build"
+else
+ echo "šØ Building $MOCKTEEPROVER_IMAGE_TAG"
+ MOCKTEEPROVER_DIR="$PWD_DIR/../tools/mockteeprover"
+ build_and_tag_image "mockteeprover" "$MOCKTEEPROVER_IMAGE_TAG" "$MOCKTEEPROVER_DIR" "Dockerfile"
+fi
+
+
diff --git a/devnet/op-succinct/example.env.challenger b/devnet/op-succinct/example.env.challenger
index e1f01798..34212436 100644
--- a/devnet/op-succinct/example.env.challenger
+++ b/devnet/op-succinct/example.env.challenger
@@ -8,6 +8,8 @@ L2_RPC=http://op-geth-seq:8545
FACTORY_ADDRESS=0x57bbef86ab6744c67d5bf9a9f5d09dca71bd7453
+ANCHOR_STATE_REGISTRY_ADDRESS=
+
GAME_TYPE=42
PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
diff --git a/devnet/op-succinct/example.env.proposer b/devnet/op-succinct/example.env.proposer
index 0c906a99..f55b65d3 100644
--- a/devnet/op-succinct/example.env.proposer
+++ b/devnet/op-succinct/example.env.proposer
@@ -10,6 +10,8 @@ L2_NODE_RPC=http://op-seq:9545
FACTORY_ADDRESS=0x57bbef86ab6744c67d5bf9a9f5d09dca71bd7453
+ANCHOR_STATE_REGISTRY_ADDRESS=
+
GAME_TYPE=42
PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
diff --git a/devnet/scripts/add-tee-game-type.sh b/devnet/scripts/add-tee-game-type.sh
new file mode 100755
index 00000000..cf25c9ca
--- /dev/null
+++ b/devnet/scripts/add-tee-game-type.sh
@@ -0,0 +1,487 @@
+#!/bin/bash
+# add-tee-game-type.sh ā Register TeeDisputeGame on an existing devnet
+#
+# Usage:
+# ./scripts/add-tee-game-type.sh [FLAGS] [/path/to/tee-contracts]
+#
+# Flags:
+# --mock-verifier Deploy MockTeeProofVerifier instead of TeeProofVerifier.
+# Must be combined with --enclave
.
+# --enclave Enclave address to register as a valid signer on the
+# MockTeeProofVerifier via setRegistered(). Required when
+# --mock-verifier is set.
+# --max-challenge-duration Override MAX_CHALLENGE_DURATION (seconds). Falls back to
+# MAX_CLOCK_DURATION env var, then default 20.
+# --max-prove-duration Override MAX_PROVE_DURATION (seconds). Falls back to
+# MAX_CLOCK_DURATION env var, then default 20.
+#
+# If no path is given, defaults to /tee-contracts.
+# You can also set TEE_CONTRACTS_DIR explicitly:
+# TEE_CONTRACTS_DIR=/path/to/tee-contracts ./scripts/add-tee-game-type.sh
+
+set -e
+
+# āā Parse flags āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+USE_MOCK_VERIFIER=false
+MOCK_ENCLAVE_ADDRESS=""
+ARG_MAX_CHALLENGE_DURATION=""
+ARG_MAX_PROVE_DURATION=""
+POSITIONAL_ARGS=()
+i=1
+while [ $i -le $# ]; do
+ arg="${!i}"
+ case "$arg" in
+ --mock-verifier)
+ USE_MOCK_VERIFIER=true
+ ;;
+ --enclave)
+ i=$((i + 1))
+ next="${!i}"
+ if [ -z "$next" ] || [[ "$next" == --* ]]; then
+ echo "ā --enclave requires an address argument"
+ exit 1
+ fi
+ MOCK_ENCLAVE_ADDRESS="$next"
+ ;;
+ --max-challenge-duration)
+ i=$((i + 1))
+ ARG_MAX_CHALLENGE_DURATION="${!i}"
+ ;;
+ --max-prove-duration)
+ i=$((i + 1))
+ ARG_MAX_PROVE_DURATION="${!i}"
+ ;;
+ *) POSITIONAL_ARGS+=("$arg") ;;
+ esac
+ i=$((i + 1))
+done
+
+# Source environment variables
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DEVNET_DIR="$(dirname "$SCRIPT_DIR")"
+ENV_FILE="$DEVNET_DIR/.env"
+source "$ENV_FILE"
+
+# Resolve tee-contracts directory: positional arg > TEE_CONTRACTS_DIR env > default
+if [ "${#POSITIONAL_ARGS[@]}" -gt 0 ]; then
+ TEE_CONTRACTS_DIR="${POSITIONAL_ARGS[0]}"
+elif [ -z "$TEE_CONTRACTS_DIR" ]; then
+ TEE_CONTRACTS_DIR="$DEVNET_DIR/tee-contracts"
+fi
+
+if [ ! -f "$TEE_CONTRACTS_DIR/foundry.toml" ]; then
+ echo "ā Cannot find tee-contracts at: $TEE_CONTRACTS_DIR"
+ echo " Pass the directory as the first argument or set TEE_CONTRACTS_DIR."
+ exit 1
+fi
+
+echo "=== Using tee-contracts: $TEE_CONTRACTS_DIR ==="
+
+# TeeDisputeGame game type (must match TEE_DISPUTE_GAME_TYPE in AccessManager.sol)
+TEE_GAME_TYPE=1960
+
+# Validate OWNER_TYPE configuration
+if [ "$OWNER_TYPE" != "transactor" ] && [ "$OWNER_TYPE" != "safe" ]; then
+ echo "ā Error: Invalid OWNER_TYPE '$OWNER_TYPE'. Must be 'transactor' or 'safe'"
+ exit 1
+fi
+
+echo "=== Using OWNER_TYPE: $OWNER_TYPE ==="
+
+# Resolve on-chain addresses
+echo "=== Resolving on-chain addresses ==="
+DISPUTE_GAME_FACTORY_ADDR=$(cast call --rpc-url $L1_RPC_URL $SYSTEM_CONFIG_PROXY_ADDRESS 'disputeGameFactory()(address)')
+OPTIMISM_PORTAL_ADDR=$(cast call --rpc-url $L1_RPC_URL $SYSTEM_CONFIG_PROXY_ADDRESS 'optimismPortal()(address)')
+ANCHOR_STATE_REGISTRY_ADDR=$(cast call --rpc-url $L1_RPC_URL $OPTIMISM_PORTAL_ADDR 'anchorStateRegistry()(address)')
+
+echo " Existing DGF: $DISPUTE_GAME_FACTORY_ADDR"
+echo " OptimismPortal: $OPTIMISM_PORTAL_ADDR"
+echo " Existing ASR: $ANCHOR_STATE_REGISTRY_ADDR"
+echo ""
+
+# Export env vars consumed by DevnetAddTeeGame.s.sol
+export PRIVATE_KEY="$DEPLOYER_PRIVATE_KEY"
+export EXISTING_DGF="$DISPUTE_GAME_FACTORY_ADDR"
+export EXISTING_ASR="$ANCHOR_STATE_REGISTRY_ADDR"
+export SYSTEM_CONFIG_ADDRESS="$SYSTEM_CONFIG_PROXY_ADDRESS"
+export DISPUTE_GAME_FINALITY_DELAY_SECONDS="${DISPUTE_GAME_FINALITY_DELAY_SECONDS:-5}"
+
+
+# TEE game timing ā reuse devnet values for fast iteration
+export MAX_CHALLENGE_DURATION="${ARG_MAX_CHALLENGE_DURATION:-${MAX_CLOCK_DURATION:-20}}"
+export MAX_PROVE_DURATION="${ARG_MAX_PROVE_DURATION:-${MAX_CLOCK_DURATION:-20}}"
+
+# Bond defaults (0.01 ETH) and access-manager fallback timeout (1 hour)
+export CHALLENGER_BOND="${CHALLENGER_BOND:-10000000000000000}"
+export FALLBACK_TIMEOUT="${FALLBACK_TIMEOUT:-3600}"
+export INIT_BOND="${INIT_BOND:-10000000000000000}"
+
+if [ -n "$PROPOSER_ADDRESS" ]; then
+ export PROPOSER_ADDRESS
+else
+ unset PROPOSER_ADDRESS
+fi
+
+if [ -n "$CHALLENGER_ADDRESS" ]; then
+ export CHALLENGER_ADDRESS
+else
+ unset CHALLENGER_ADDRESS
+fi
+
+if [ "$USE_MOCK_VERIFIER" = "true" ] && [ -z "$MOCK_ENCLAVE_ADDRESS" ]; then
+ echo "ā --mock-verifier requires --enclave (e.g. --mock-verifier --enclave 0xABC...)"
+ exit 1
+fi
+
+export USE_MOCK_VERIFIER
+echo "=== Verifier mode: $([ "$USE_MOCK_VERIFIER" = "true" ] && echo "MockTeeProofVerifier (mock), enclave: $MOCK_ENCLAVE_ADDRESS" || echo "TeeProofVerifier + MockRiscZeroVerifier") ==="
+
+# āā Function: deploy TEE contracts via forge script āāāāāāāāāāāāāāāāāāāāāāāāāāā
+deploy_tee_contracts() {
+ echo "=== Deploying TEE contracts via forge script ==="
+
+ FORGE_LOG=$(mktemp)
+ # Ensure temp file is cleaned up on any exit (including set -e failures)
+ trap 'rm -f "${FORGE_LOG:-}"' RETURN
+
+ # Auto-detect scripts subdirectory: new-style repos use "scripts/", old-style use "script/"
+ if [ -f "$TEE_CONTRACTS_DIR/scripts/DevnetAddTeeGame.s.sol" ]; then
+ FORGE_SCRIPT_SUBDIR="scripts"
+ else
+ FORGE_SCRIPT_SUBDIR="script"
+ fi
+
+ # pushd/popd avoids persistent cwd change (plain `cd` inside a non-subshell
+ # function would affect the rest of the script)
+ pushd "$TEE_CONTRACTS_DIR" > /dev/null
+ forge script "${FORGE_SCRIPT_SUBDIR}/DevnetAddTeeGame.s.sol:DevnetAddTeeGame" \
+ --rpc-url "$L1_RPC_URL" \
+ --broadcast \
+ --legacy \
+ -vv 2>&1 | tee "$FORGE_LOG"
+ popd > /dev/null
+
+ if ! grep -q "ONCHAIN EXECUTION COMPLETE" "$FORGE_LOG"; then
+ echo ""
+ echo "ā Deployment failed ā check output above."
+ exit 1
+ fi
+
+ # Extract deployed addresses from forge log (grep for console2.log lines)
+ _addr() { grep "$1" "$FORGE_LOG" | grep -oE '0x[0-9a-fA-F]{40}' | head -1; }
+
+ TEE_GAME_IMPL=$(_addr "TeeDisputeGame impl")
+ NEW_ASR_ADDR=$(_addr "New AnchorStateRegistry")
+
+ echo ""
+ echo " TeeDisputeGame impl: $TEE_GAME_IMPL"
+ echo " New AnchorStateRegistry: $NEW_ASR_ADDR"
+ echo ""
+
+ if [ -z "$TEE_GAME_IMPL" ]; then
+ echo "ā Failed to extract TeeDisputeGame impl address from forge log."
+ exit 1
+ fi
+ if [ -z "$NEW_ASR_ADDR" ]; then
+ echo "ā Failed to extract New AnchorStateRegistry address from forge log."
+ exit 1
+ fi
+}
+
+# āā Function: register enclave with MockTeeProofVerifier āāāāāāāāāāāāāāāāāāāāāā
+register_enclave_with_mock_verifier() {
+ echo "=== Registering enclave address with MockTeeProofVerifier ==="
+
+ # Query the game impl to get the mock verifier address
+ MOCK_VERIFIER_ADDR=$(cast call --rpc-url "$L1_RPC_URL" "$TEE_GAME_IMPL" 'teeProofVerifier()(address)')
+ echo " MockTeeProofVerifier: $MOCK_VERIFIER_ADDR"
+ echo " Enclave address: $MOCK_ENCLAVE_ADDRESS"
+ echo ""
+
+ TX_OUTPUT=$(cast send \
+ --json \
+ --legacy \
+ --rpc-url "$L1_RPC_URL" \
+ --private-key "$DEPLOYER_PRIVATE_KEY" \
+ --from "$(cast wallet address --private-key "$DEPLOYER_PRIVATE_KEY")" \
+ "$MOCK_VERIFIER_ADDR" \
+ 'setRegistered(address,bool)' \
+ "$MOCK_ENCLAVE_ADDRESS" \
+ true)
+
+ TX_HASH=$(echo "$TX_OUTPUT" | jq -r '.transactionHash // empty')
+ TX_STATUS=$(echo "$TX_OUTPUT" | jq -r '.status // empty')
+ echo "Transaction sent, TX_HASH: $TX_HASH"
+
+ if [ "$TX_STATUS" = "0x1" ] || [ "$TX_STATUS" = "1" ]; then
+ echo " ā
setRegistered($MOCK_ENCLAVE_ADDRESS, true) completed successfully"
+ else
+ echo " ā Transaction failed with status: $TX_STATUS"
+ echo "Full output: $TX_OUTPUT"
+ exit 1
+ fi
+ echo ""
+}
+
+# āā Function: setRespectedGameType on new ASR āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+set_respected_game_type() {
+ echo "=== Setting Respected Game Type to $TEE_GAME_TYPE on new ASR ==="
+ echo " New ASR: $NEW_ASR_ADDR"
+ echo ""
+
+ # The deployer IS the guardian on devnet (SystemConfig.guardian() == deployer)
+ TX_OUTPUT=$(cast send \
+ --json \
+ --legacy \
+ --rpc-url $L1_RPC_URL \
+ --private-key $DEPLOYER_PRIVATE_KEY \
+ --from $(cast wallet address --private-key $DEPLOYER_PRIVATE_KEY) \
+ $NEW_ASR_ADDR \
+ 'setRespectedGameType(uint32)' \
+ $TEE_GAME_TYPE)
+
+ TX_HASH=$(echo "$TX_OUTPUT" | jq -r '.transactionHash // empty')
+ TX_STATUS=$(echo "$TX_OUTPUT" | jq -r '.status // empty')
+ echo "Transaction sent, TX_HASH: $TX_HASH"
+
+ if [ "$TX_STATUS" = "0x1" ] || [ "$TX_STATUS" = "1" ]; then
+ echo " ā
setRespectedGameType($TEE_GAME_TYPE) completed successfully"
+ else
+ echo " ā Transaction failed with status: $TX_STATUS"
+ echo "Full output: $TX_OUTPUT"
+ exit 1
+ fi
+ echo ""
+}
+
+# āā Function: register TEE game type via Transactor āāāāāāāāāāāāāāāāāāāāāāāāāāā
+add_tee_game_type_via_transactor() {
+ echo "=== Registering TeeDisputeGame (type $TEE_GAME_TYPE) via Transactor ==="
+ echo " Transactor: $TRANSACTOR"
+ echo " Existing DGF: $DISPUTE_GAME_FACTORY_ADDR"
+ echo " TeeDisputeGame impl: $TEE_GAME_IMPL"
+ echo " Sender: $(cast wallet address --private-key $DEPLOYER_PRIVATE_KEY)"
+ echo ""
+
+ # Build calldata for setImplementation(uint32,address)
+ SET_IMPL_CALLDATA=$(cast calldata 'setImplementation(uint32,address)' $TEE_GAME_TYPE $TEE_GAME_IMPL)
+ echo "setImplementation calldata: $SET_IMPL_CALLDATA"
+
+ echo "Executing CALL via Transactor (setImplementation)..."
+ TX_OUTPUT=$(cast send \
+ --json \
+ --legacy \
+ --rpc-url $L1_RPC_URL \
+ --private-key $DEPLOYER_PRIVATE_KEY \
+ --from $(cast wallet address --private-key $DEPLOYER_PRIVATE_KEY) \
+ $TRANSACTOR \
+ 'CALL(address,bytes,uint256)' \
+ $DISPUTE_GAME_FACTORY_ADDR \
+ $SET_IMPL_CALLDATA \
+ 0)
+
+ TX_HASH=$(echo "$TX_OUTPUT" | jq -r '.transactionHash // empty')
+ TX_STATUS=$(echo "$TX_OUTPUT" | jq -r '.status // empty')
+ echo ""
+ echo "Transaction sent, TX_HASH: $TX_HASH"
+
+ if [ "$TX_STATUS" = "0x1" ] || [ "$TX_STATUS" = "1" ]; then
+ echo " ā
setImplementation successful!"
+ else
+ echo " ā Transaction failed with status: $TX_STATUS"
+ echo "Full output: $TX_OUTPUT"
+ exit 1
+ fi
+ echo ""
+
+ # Build calldata for setInitBond(uint32,uint256)
+ SET_BOND_CALLDATA=$(cast calldata 'setInitBond(uint32,uint256)' $TEE_GAME_TYPE $INIT_BOND)
+ echo "setInitBond calldata: $SET_BOND_CALLDATA"
+
+ echo "Executing CALL via Transactor (setInitBond)..."
+ TX_OUTPUT=$(cast send \
+ --json \
+ --legacy \
+ --rpc-url $L1_RPC_URL \
+ --private-key $DEPLOYER_PRIVATE_KEY \
+ --from $(cast wallet address --private-key $DEPLOYER_PRIVATE_KEY) \
+ $TRANSACTOR \
+ 'CALL(address,bytes,uint256)' \
+ $DISPUTE_GAME_FACTORY_ADDR \
+ $SET_BOND_CALLDATA \
+ 0)
+
+ TX_HASH=$(echo "$TX_OUTPUT" | jq -r '.transactionHash // empty')
+ TX_STATUS=$(echo "$TX_OUTPUT" | jq -r '.status // empty')
+ echo ""
+ echo "Transaction sent, TX_HASH: $TX_HASH"
+
+ if [ "$TX_STATUS" = "0x1" ] || [ "$TX_STATUS" = "1" ]; then
+ echo " ā
setInitBond successful!"
+ else
+ echo " ā Transaction failed with status: $TX_STATUS"
+ echo "Full output: $TX_OUTPUT"
+ exit 1
+ fi
+ echo ""
+}
+
+# āā Function: register TEE game type via Safe āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+add_tee_game_type_via_safe() {
+ echo "=== Registering TeeDisputeGame (type $TEE_GAME_TYPE) via Safe ==="
+ echo " Safe: $SAFE_ADDRESS"
+ echo " Existing DGF: $DISPUTE_GAME_FACTORY_ADDR"
+ echo " TeeDisputeGame impl: $TEE_GAME_IMPL"
+ echo " Sender: $(cast wallet address --private-key $DEPLOYER_PRIVATE_KEY)"
+ echo ""
+
+ DEPLOYER_ADDRESS=$(cast wallet address --private-key $DEPLOYER_PRIVATE_KEY)
+
+ # Build signature like DeployOwnership.s.sol _callViaSafe method
+ DEPLOYER_ADDRESS_NO_PREFIX=${DEPLOYER_ADDRESS#0x}
+ ADDRESS_LENGTH=${#DEPLOYER_ADDRESS_NO_PREFIX}
+ ZEROS_NEEDED=$((64 - ADDRESS_LENGTH))
+ ZEROS=$(printf "%0${ZEROS_NEEDED}d" 0)
+ DEPLOYER_ADDRESS_PADDED="${ZEROS}${DEPLOYER_ADDRESS_NO_PREFIX}"
+
+ # Build signature: uint256(uint160(msg.sender)) + bytes32(0) + uint8(1)
+ PACKED_SIGNATURE="0x${DEPLOYER_ADDRESS_PADDED}000000000000000000000000000000000000000000000000000000000000000001"
+
+ echo "Deployer address: $DEPLOYER_ADDRESS"
+ echo "Signature (abi.encodePacked format): $PACKED_SIGNATURE"
+ echo ""
+
+ # setImplementation(uint32,address)
+ SET_IMPL_CALLDATA=$(cast calldata 'setImplementation(uint32,address)' $TEE_GAME_TYPE $TEE_GAME_IMPL)
+ echo "setImplementation calldata: $SET_IMPL_CALLDATA"
+
+ echo "Executing execTransaction on Safe (setImplementation)..."
+ TX_OUTPUT=$(cast send \
+ --json \
+ --legacy \
+ --rpc-url $L1_RPC_URL \
+ --private-key $DEPLOYER_PRIVATE_KEY \
+ --from $DEPLOYER_ADDRESS \
+ $SAFE_ADDRESS \
+ 'execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)' \
+ $DISPUTE_GAME_FACTORY_ADDR \
+ 0 \
+ $SET_IMPL_CALLDATA \
+ 0 \
+ 0 \
+ 0 \
+ 0 \
+ 0x0000000000000000000000000000000000000000 \
+ 0x0000000000000000000000000000000000000000 \
+ $PACKED_SIGNATURE)
+
+ TX_HASH=$(echo "$TX_OUTPUT" | jq -r '.transactionHash // empty')
+ TX_STATUS=$(echo "$TX_OUTPUT" | jq -r '.status // empty')
+ echo ""
+ echo "Transaction sent, TX_HASH: $TX_HASH"
+
+ if [ "$TX_STATUS" = "0x1" ] || [ "$TX_STATUS" = "1" ]; then
+ echo " ā
setImplementation successful!"
+ else
+ echo " ā Transaction failed with status: $TX_STATUS"
+ echo "Full output: $TX_OUTPUT"
+ exit 1
+ fi
+ echo ""
+
+ # setInitBond(uint32,uint256)
+ SET_BOND_CALLDATA=$(cast calldata 'setInitBond(uint32,uint256)' $TEE_GAME_TYPE $INIT_BOND)
+ echo "setInitBond calldata: $SET_BOND_CALLDATA"
+
+ echo "Executing execTransaction on Safe (setInitBond)..."
+ TX_OUTPUT=$(cast send \
+ --json \
+ --legacy \
+ --rpc-url $L1_RPC_URL \
+ --private-key $DEPLOYER_PRIVATE_KEY \
+ --from $DEPLOYER_ADDRESS \
+ $SAFE_ADDRESS \
+ 'execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)' \
+ $DISPUTE_GAME_FACTORY_ADDR \
+ 0 \
+ $SET_BOND_CALLDATA \
+ 0 \
+ 0 \
+ 0 \
+ 0 \
+ 0x0000000000000000000000000000000000000000 \
+ 0x0000000000000000000000000000000000000000 \
+ $PACKED_SIGNATURE)
+
+ TX_HASH=$(echo "$TX_OUTPUT" | jq -r '.transactionHash // empty')
+ TX_STATUS=$(echo "$TX_OUTPUT" | jq -r '.status // empty')
+ echo ""
+ echo "Transaction sent, TX_HASH: $TX_HASH"
+
+ if [ "$TX_STATUS" = "0x1" ] || [ "$TX_STATUS" = "1" ]; then
+ echo " ā
setInitBond successful!"
+ else
+ echo " ā Transaction failed with status: $TX_STATUS"
+ echo "Full output: $TX_OUTPUT"
+ exit 1
+ fi
+ echo ""
+}
+
+# āā Main execution āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
+ # 1. Deploy contracts via forge script
+ deploy_tee_contracts
+
+ # 2. If mock verifier, register the enclave address on the mock verifier
+ if [ "$USE_MOCK_VERIFIER" = "true" ]; then
+ register_enclave_with_mock_verifier
+ fi
+
+ # 3. setRespectedGameType(1960) on new ASR (deployer is guardian on devnet)
+ set_respected_game_type
+
+ # 4. setImplementation + setInitBond on existing DGF via TRANSACTOR or Safe
+ if [ "$OWNER_TYPE" = "transactor" ]; then
+ add_tee_game_type_via_transactor
+ elif [ "$OWNER_TYPE" = "safe" ]; then
+ add_tee_game_type_via_safe
+ fi
+
+ # 5. Verify
+ echo "=== Verifying TeeDisputeGame type was registered ==="
+ REGISTERED_IMPL=$(cast call --rpc-url $L1_RPC_URL $DISPUTE_GAME_FACTORY_ADDR 'gameImpls(uint32)(address)' $TEE_GAME_TYPE)
+ REGISTERED_BOND=$(cast call --rpc-url $L1_RPC_URL $DISPUTE_GAME_FACTORY_ADDR 'initBonds(uint32)(uint256)' $TEE_GAME_TYPE | awk '{print $1}')
+
+ if [ "$REGISTERED_IMPL" != "0x0000000000000000000000000000000000000000" ]; then
+ echo " ā
Success! TeeDisputeGame type $TEE_GAME_TYPE registered."
+ echo " Implementation: $REGISTERED_IMPL"
+ else
+ echo " ā Warning: Could not verify game type was registered. Check transaction status."
+ fi
+
+ if [ "$REGISTERED_BOND" = "$INIT_BOND" ]; then
+ echo " ā
initBond correctly set to $REGISTERED_BOND wei."
+ else
+ echo " ā Warning: initBond mismatch ā expected $INIT_BOND, got $REGISTERED_BOND."
+ fi
+
+ echo ""
+ echo "========================================"
+ echo " TEE Game Type ā Final Summary"
+ echo "========================================"
+ echo " Game Type: $TEE_GAME_TYPE"
+ echo " TeeDisputeGame impl: $TEE_GAME_IMPL"
+ echo " New AnchorStateRegistry: $NEW_ASR_ADDR"
+ echo " Existing DGF: $DISPUTE_GAME_FACTORY_ADDR"
+ if [ "$USE_MOCK_VERIFIER" = "true" ]; then
+ echo " Mock Verifier: $MOCK_VERIFIER_ADDR"
+ echo " Enclave address: $MOCK_ENCLAVE_ADDRESS"
+ fi
+ echo "========================================"
+ echo ""
+ echo "Verify:"
+ echo " cast call $DISPUTE_GAME_FACTORY_ADDR 'gameImpls(uint32)(address)' 1960"
+ echo " cast call $NEW_ASR_ADDR 'respectedGameType()(uint32)'"
+ echo " ā
add-tee-game-type completed successfully."
+fi
diff --git a/devnet/scripts/get-game.sh b/devnet/scripts/get-game.sh
new file mode 100755
index 00000000..ab291c5f
--- /dev/null
+++ b/devnet/scripts/get-game.sh
@@ -0,0 +1,312 @@
+#!/bin/bash
+# get-game.sh ā Show detailed info for a single dispute game by index.
+#
+# Usage:
+# ./get-game.sh
+#
+# Examples:
+# ./get-game.sh 0 # Show game at index 0
+# ./get-game.sh 42 # Show game at index 42
+
+set -euo pipefail
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Configuration
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DEVNET_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+[ -f "$DEVNET_DIR/.env" ] && source "$DEVNET_DIR/.env"
+
+FACTORY_ADDRESS=${DISPUTE_GAME_FACTORY_ADDRESS:-""}
+L1_RPC=${L1_RPC_URL:-"http://localhost:8545"}
+L2_RPC=${L2_RPC_URL:-"http://localhost:8123"}
+
+GENESIS_PARENT_INDEX=4294967295
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+BOLD='\033[1m'
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Usage / Arg Parsing
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+usage() {
+ echo "Usage: $0 "
+ echo ""
+ echo " game_id Index of the dispute game in the factory"
+ echo ""
+ echo "Examples:"
+ echo " $0 0"
+ echo " $0 42"
+ exit 1
+}
+
+if [[ $# -ne 1 ]] || ! [[ "$1" =~ ^[0-9]+$ ]]; then
+ usage
+fi
+
+GAME_ID="$1"
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Requirements
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+if ! command -v cast &>/dev/null; then
+ echo -e "${RED}Error: 'cast' not found. Install Foundry: https://getfoundry.sh${NC}"
+ exit 1
+fi
+
+if [[ -z "$FACTORY_ADDRESS" ]]; then
+ echo -e "${RED}Error: DISPUTE_GAME_FACTORY_ADDRESS not set in $DEVNET_DIR/.env${NC}"
+ exit 1
+fi
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Helpers
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+proposal_status_name() {
+ case "$1" in
+ 0) echo "Unchallenged" ;;
+ 1) echo "Challenged" ;;
+ 2) echo "UnchallengedAndValidProofProvided" ;;
+ 3) echo "ChallengedAndValidProofProvided" ;;
+ 4) echo "Resolved" ;;
+ *) echo "Unknown($1)" ;;
+ esac
+}
+
+game_status_name() {
+ case "$1" in
+ 0) echo "IN_PROGRESS" ;;
+ 1) echo "CHALLENGER_WINS" ;;
+ 2) echo "DEFENDER_WINS" ;;
+ *) echo "Unknown($1)" ;;
+ esac
+}
+
+bond_mode_name() {
+ case "$1" in
+ 0) echo "UNDECIDED" ;;
+ 1) echo "NORMAL" ;;
+ 2) echo "REFUND" ;;
+ *) echo "Unknown($1)" ;;
+ esac
+}
+
+fmt_ts_field() {
+ local raw="$1"
+ local num
+ num=$(echo "$raw" | awk '{print $1}')
+ if [[ "$num" == "0" || "$num" == "N/A" || -z "$num" ]]; then
+ echo "N/A"
+ return
+ fi
+ local human
+ human=$(date -r "$num" "+%Y-%m-%d %H:%M:%S" 2>/dev/null \
+ || date -d "@$num" "+%Y-%m-%d %H:%M:%S" 2>/dev/null \
+ || echo "?")
+ echo "${num} (${human})"
+}
+
+fmt_duration() {
+ local secs="$1"
+ if [[ "$secs" == "N/A" ]]; then echo "N/A"; return; fi
+ printf "%dh %dm %ds" "$((secs/3600))" "$(((secs%3600)/60))" "$((secs%60))"
+}
+
+row() { printf " %-32s %s\n" "$1" "$2"; }
+section() { echo " āāāā $1"; }
+section_end() { echo " ā$(printf 'ā%.0s' {1..100})ā"; }
+phase() { printf " āā %s\n" "$1"; }
+trow() { printf " ā %-26s %s\n" "$1" "$2"; }
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Header
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+echo -e "${BLUE}${BOLD}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+echo -e "${BLUE}${BOLD} Dispute Game Inspector${NC}"
+echo -e "${BLUE}${BOLD}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+echo ""
+echo -e " ${BOLD}Factory:${NC} ${FACTORY_ADDRESS:0:10}...${FACTORY_ADDRESS: -8}"
+echo -e " ${BOLD}L1 RPC:${NC} $L1_RPC"
+echo -e " ${BOLD}L2 RPC:${NC} $L2_RPC"
+echo -e " ${BOLD}Game ID:${NC} $GAME_ID"
+echo ""
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Validate game ID within range
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+TOTAL=$(cast call "$FACTORY_ADDRESS" "gameCount()(uint256)" --rpc-url "$L1_RPC")
+echo -e " ${CYAN}Total games in factory: $TOTAL${NC}"
+echo ""
+
+if [[ "$TOTAL" -eq 0 ]]; then
+ echo -e "${YELLOW}No games yet.${NC}"
+ exit 0
+fi
+
+if [[ "$GAME_ID" -ge "$TOTAL" ]]; then
+ echo -e "${RED}Error: game ID $GAME_ID out of range (0 ā $((TOTAL-1)))${NC}"
+ exit 1
+fi
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Factory record
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+INFO=$(cast call "$FACTORY_ADDRESS" "gameAtIndex(uint256)(uint8,uint64,address)" "$GAME_ID" --rpc-url "$L1_RPC")
+GAME_TYPE=$(echo "$INFO" | awk 'NR==1')
+ADDR=$(echo "$INFO" | awk 'NR==3')
+
+echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+printf "ā GAME #%-6s ā GameType: %-33sā\n" "$GAME_ID" "$GAME_TYPE"
+echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Fetch all fields
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+# Immutables
+MAX_CHAL_DUR=$(cast call "$ADDR" "maxChallengeDuration()(uint64)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+MAX_PROVE_DUR=$(cast call "$ADDR" "maxProveDuration()(uint64)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+CHAL_BOND=$( cast call "$ADDR" "challengerBond()(uint256)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+
+# Identity
+GAME_CREATOR=$( cast call "$ADDR" "gameCreator()(address)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+PROPOSER_ADDR=$( cast call "$ADDR" "proposer()(address)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+WAS_RESPECTED=$( cast call "$ADDR" "wasRespectedGameTypeWhenCreated()(bool)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+
+# Proposal range
+L2_BLOCK=$( cast call "$ADDR" "l2BlockNumber()(uint256)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+PARENT_IDX=$( cast call "$ADDR" "parentIndex()(uint32)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+STARTING_BN=$( cast call "$ADDR" "startingBlockNumber()(uint256)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+STARTING_HASH=$(cast call "$ADDR" "startingRootHash()(bytes32)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+ROOT_CLAIM=$( cast call "$ADDR" "rootClaim()(bytes32)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+BLOCK_HASH=$( cast call "$ADDR" "blockHash()(bytes32)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+STATE_HASH=$( cast call "$ADDR" "stateHash()(bytes32)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+
+# ClaimData struct: (uint32 parentIndex, address counteredBy, address prover,
+# bytes32 claim, uint8 status, uint64 deadline)
+CLAIM_RAW=$(cast call "$ADDR" "claimData()(uint32,address,address,bytes32,uint8,uint64)" \
+ --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+if [[ "$CLAIM_RAW" != "N/A" ]]; then
+ CD_COUNTERED=$( echo "$CLAIM_RAW" | awk 'NR==2')
+ CD_PROVER=$( echo "$CLAIM_RAW" | awk 'NR==3')
+ CD_STATUS_RAW=$( echo "$CLAIM_RAW" | awk 'NR==5')
+ CD_DEADLINE=$( echo "$CLAIM_RAW" | awk 'NR==6')
+else
+ CD_COUNTERED="N/A"; CD_PROVER="N/A"; CD_STATUS_RAW="N/A"; CD_DEADLINE="N/A"
+fi
+
+# Game-level state
+GAME_STATUS_RAW=$(cast call "$ADDR" "status()(uint8)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+CREATED_AT_RAW=$( cast call "$ADDR" "createdAt()(uint64)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+RESOLVED_AT_RAW=$(cast call "$ADDR" "resolvedAt()(uint64)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+BOND_MODE_RAW=$( cast call "$ADDR" "bondDistributionMode()(uint8)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+GAME_OVER=$( cast call "$ADDR" "gameOver()(bool)" --rpc-url "$L1_RPC" 2>/dev/null || echo "N/A")
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Derived values
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+CD_STATUS=$(proposal_status_name "$CD_STATUS_RAW")
+GAME_STATUS=$(game_status_name "$GAME_STATUS_RAW")
+BOND_MODE=$(bond_mode_name "$BOND_MODE_RAW")
+
+CREATED_AT_FMT=$( fmt_ts_field "$CREATED_AT_RAW")
+RESOLVED_AT_FMT=$(fmt_ts_field "$RESOLVED_AT_RAW")
+DEADLINE_FMT=$( fmt_ts_field "$(echo "$CD_DEADLINE" | awk '{print $1}')")
+
+MAX_CHAL_FMT="N/A"
+MAX_PROVE_FMT="N/A"
+if [[ "$MAX_CHAL_DUR" != "N/A" ]]; then
+ MAX_CHAL_NUM=$(echo "$MAX_CHAL_DUR" | awk '{print $1}')
+ MAX_CHAL_FMT="${MAX_CHAL_NUM}s ($(fmt_duration "$MAX_CHAL_NUM"))"
+fi
+if [[ "$MAX_PROVE_DUR" != "N/A" ]]; then
+ MAX_PROVE_NUM=$(echo "$MAX_PROVE_DUR" | awk '{print $1}')
+ MAX_PROVE_FMT="${MAX_PROVE_NUM}s ($(fmt_duration "$MAX_PROVE_NUM"))"
+fi
+
+CHAL_BOND_FMT="N/A"
+if [[ "$CHAL_BOND" != "N/A" ]]; then
+ CHAL_BOND_ETH=$(cast to-unit "$CHAL_BOND" ether 2>/dev/null || echo "?")
+ CHAL_BOND_FMT="${CHAL_BOND_ETH} ETH (${CHAL_BOND} wei)"
+fi
+
+# Parent block range
+PARENT_DISPLAY="$PARENT_IDX"
+BLOCK_RANGE="N/A"
+if [[ "$PARENT_IDX" == "$GENESIS_PARENT_INDEX" ]]; then
+ PARENT_DISPLAY="genesis"
+ BLOCK_RANGE="? ā ${L2_BLOCK}"
+else
+ PARENT_DATA=$(cast call "$FACTORY_ADDRESS" "gameAtIndex(uint256)(uint8,uint64,address)" \
+ "$PARENT_IDX" --rpc-url "$L1_RPC" 2>/dev/null || echo "")
+ if [[ -n "$PARENT_DATA" ]]; then
+ PARENT_ADDR=$(echo "$PARENT_DATA" | awk 'NR==3')
+ PARENT_L2=$(cast call "$PARENT_ADDR" "l2BlockNumber()(uint256)" --rpc-url "$L1_RPC" 2>/dev/null | awk '{print $1}')
+ BLOCK_RANGE="${PARENT_L2} ā ${L2_BLOCK} ($(( $(echo "$L2_BLOCK" | awk '{print $1}') - PARENT_L2 )) blocks)"
+ fi
+fi
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Output
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+# Section 1: Identity & Config
+echo ""
+section "[1] Identity & Config āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+phase "Identity"
+trow "Address:" "$ADDR"
+trow "GameType:" "$GAME_TYPE"
+trow "GameCreator:" "$GAME_CREATOR"
+trow "Proposer:" "$PROPOSER_ADDR"
+trow "WasRespectedGameType:" "$WAS_RESPECTED"
+phase "Config"
+trow "MaxChallengeDuration:" "$MAX_CHAL_FMT"
+trow "MaxProveDuration:" "$MAX_PROVE_FMT"
+trow "ChallengerBond:" "$CHAL_BOND_FMT"
+section_end
+
+# Section 2: Proposal
+echo ""
+section "[2] Proposal āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+phase "Starting State"
+trow "ParentIndex:" "$PARENT_DISPLAY"
+trow "StartingBlockNumber:" "$STARTING_BN"
+trow "StartingRootHash:" "$STARTING_HASH"
+phase "Target State"
+trow "L2BlockNumber:" "$L2_BLOCK"
+trow "BlockRange:" "$BLOCK_RANGE"
+trow "BlockHash:" "$BLOCK_HASH"
+trow "StateHash:" "$STATE_HASH"
+trow "RootClaim:" "$ROOT_CLAIM"
+section_end
+
+# Section 3: Lifecycle State
+echo ""
+section "[3] Lifecycle State āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+phase "Initialize"
+trow "CreatedAt:" "$CREATED_AT_FMT"
+phase "Challenge Window"
+trow "CounteredBy:" "$CD_COUNTERED"
+trow "ClaimData.status:" "$CD_STATUS"
+trow "ClaimData.deadline:" "$DEADLINE_FMT"
+phase "Prove"
+trow "Prover:" "$CD_PROVER"
+trow "GameOver:" "$GAME_OVER"
+phase "Resolve"
+trow "GameStatus:" "$GAME_STATUS"
+trow "ResolvedAt:" "$RESOLVED_AT_FMT"
+phase "CloseGame/ClaimCredit"
+trow "BondDistributionMode:" "$BOND_MODE"
+section_end
+
+echo ""
+echo -e "${BLUE}${BOLD}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+echo -e "${GREEN}${BOLD}Done.${NC}"
diff --git a/devnet/scripts/list-game.sh b/devnet/scripts/list-game.sh
new file mode 100755
index 00000000..9992ac44
--- /dev/null
+++ b/devnet/scripts/list-game.sh
@@ -0,0 +1,234 @@
+#!/bin/bash
+# Dispute Game List Tool
+# List dispute games by game type
+#
+# Usage:
+# ./list-game.sh [game_type] [start_index end_index]
+#
+# Examples:
+# ./list-game.sh # Prompt for game type, show all games
+# ./list-game.sh 42 # List all games of type 42
+# ./list-game.sh 1960 # List all games of type 1960
+# ./list-game.sh 42 10 20 # List type 42 games from index 10 to 20
+
+set -e
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Configuration
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DEVNET_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Load environment variables
+[ -f "$DEVNET_DIR/.env" ] && source "$DEVNET_DIR/.env"
+
+# Network configuration
+FACTORY_ADDRESS=${DISPUTE_GAME_FACTORY_ADDRESS:-""}
+L1_RPC=${L1_RPC_URL:-"http://localhost:8545"}
+L2_RPC=${L2_RPC_URL:-"http://localhost:8123"}
+
+# Constants
+GENESIS_PARENT_INDEX=4294967295
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+BOLD='\033[1m'
+
+# Maps indexed by game index, populated during list_games pass 1
+declare -A BLOCK_BY_INDEX # index -> l2BlockNumber
+declare -A ADDR_BY_INDEX # index -> contract address
+declare -A TYPE_BY_INDEX # index -> game type
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Requirement Checking
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+check_basic_requirements() {
+ if ! command -v cast &> /dev/null; then
+ echo -e "${RED}Error: 'cast' not found. Install Foundry: https://getfoundry.sh${NC}"
+ exit 1
+ fi
+
+ if [ -z "$FACTORY_ADDRESS" ]; then
+ echo -e "${RED}Error: DISPUTE_GAME_FACTORY_ADDRESS not set in $DEVNET_DIR/.env${NC}"
+ exit 1
+ fi
+}
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Helper Functions
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+# Get game status
+get_game_status() {
+ local addr=$1
+ local claim_hex=$(cast call $addr "claimData()" --rpc-url $L1_RPC 2>/dev/null)
+ local status_hex="0x$(echo "$claim_hex" | cut -c259-322)"
+ local status=$(cast --to-dec "$status_hex" 2>/dev/null || echo "0")
+
+ case $status in
+ 0) echo "Unchallenged|0" ;;
+ 1) echo "Challenged|1" ;;
+ 2) echo "Unchal+Proof|2" ;;
+ 3) echo "Chal+Proof|3" ;;
+ 4) echo "Resolved|4" ;;
+ *) echo "Unknown|$status" ;;
+ esac
+}
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Display Functions
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+show_header() {
+ local game_type=$1
+ echo -e "${BLUE}${BOLD}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo -e "${BLUE}${BOLD} Dispute Game List Tool${NC}"
+ echo -e "${BLUE}${BOLD}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo ""
+ echo -e " ${BOLD}Factory:${NC} ${FACTORY_ADDRESS:0:10}...${FACTORY_ADDRESS: -8}"
+ echo -e " ${BOLD}L1 RPC:${NC} $L1_RPC"
+ echo -e " ${BOLD}L2 RPC:${NC} $L2_RPC"
+ echo -e " ${BOLD}Game Type:${NC} ${game_type:-All}"
+ echo ""
+}
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Core Functions
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+list_games() {
+ local game_type=$1
+ local filter_start=$2
+ local filter_end=$3
+
+ local total=$(cast call $FACTORY_ADDRESS "gameCount()(uint256)" --rpc-url $L1_RPC 2>/dev/null)
+
+ if [ -z "$total" ] || [ "$total" = "0" ]; then
+ echo -e "${YELLOW}No games found.${NC}"
+ return 1
+ fi
+
+ local type_label="${game_type:-All Types}"
+ if [ -n "$filter_start" ]; then
+ echo -e "${BLUE}${BOLD} Games (Type $type_label) [Showing index $filter_start-$filter_end]${NC}"
+ else
+ echo -e "${BLUE}${BOLD} Games (Type $type_label)${NC}"
+ fi
+ echo -e "${CYAN}Total games in factory: $total${NC}"
+ echo ""
+
+ # Table header
+ printf "${BOLD}%-6s %-8s %-12s %-25s %-10s %-20s${NC}\n" \
+ "Index" "Type" "Parent" "Block Range" "Blocks" "Status"
+ echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+
+ BLOCK_BY_INDEX=()
+ local count=0
+
+ # Single reverse pass: newest game first, print immediately
+ for ((i=total-1; i>=0; i--)); do
+ local game_data=$(cast call $FACTORY_ADDRESS "gameAtIndex(uint256)((uint32,uint64,address))" $i --rpc-url $L1_RPC 2>/dev/null)
+ [ -z "$game_data" ] && continue
+
+ local type=$(echo "$game_data" | grep -oE '\([0-9]+' | head -1 | tr -d '(')
+ [ -n "$game_type" ] && [ "$type" != "$game_type" ] && continue
+
+ local addr=$(echo "$game_data" | grep -oE '0x[a-fA-F0-9]{40}')
+ local l2_block=$(cast call $addr "l2BlockNumber()(uint256)" --rpc-url $L1_RPC 2>/dev/null | awk '{print $1}')
+ [ -z "$l2_block" ] && l2_block=0
+ BLOCK_BY_INDEX[$i]=$l2_block
+
+ if [ -n "$filter_start" ]; then
+ [ $i -lt $filter_start ] || [ $i -gt $filter_end ] && continue
+ fi
+
+ local parent=$(cast call $addr "parentIndex()(uint32)" --rpc-url $L1_RPC 2>/dev/null | awk '{print $1}')
+ [ -z "$parent" ] && parent=$GENESIS_PARENT_INDEX
+
+ local start blocks parent_display
+ if [ "$parent" = "$GENESIS_PARENT_INDEX" ]; then
+ start="?"; blocks="?"; parent_display="genesis"
+ else
+ parent_display="$parent"
+ if [ -z "${BLOCK_BY_INDEX[$parent]+x}" ]; then
+ # Parent not yet visited (lower index) ā fetch its l2Block on demand
+ local pd=$(cast call $FACTORY_ADDRESS "gameAtIndex(uint256)((uint32,uint64,address))" $parent --rpc-url $L1_RPC 2>/dev/null)
+ local pa=$(echo "$pd" | grep -oE '0x[a-fA-F0-9]{40}')
+ local pb=$(cast call $pa "l2BlockNumber()(uint256)" --rpc-url $L1_RPC 2>/dev/null | awk '{print $1}')
+ BLOCK_BY_INDEX[$parent]=${pb:-0}
+ fi
+ start=${BLOCK_BY_INDEX[$parent]}
+ blocks=$((l2_block - start))
+ fi
+
+ local status_info=$(get_game_status $addr)
+ local status_text=$(echo "$status_info" | cut -d'|' -f1)
+
+ printf "%-6s %-8s %-12s %-25s %-10s %-20s\n" \
+ "$i" "$type" "$parent_display" "$start-$l2_block" "$blocks" "$status_text"
+ count=$((count + 1))
+ done
+
+ echo ""
+ if [ $count -eq 0 ]; then
+ echo -e "${YELLOW}No games of type ${game_type:-any} found.${NC}"
+ return 1
+ fi
+ echo -e "${CYAN}Total: $count games${NC}"
+ return 0
+}
+
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+# Main Entry Point
+# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+check_basic_requirements
+
+# Parse arguments
+GAME_TYPE=""
+FILTER_START=""
+FILTER_END=""
+
+if [ $# -eq 0 ]; then
+ # No args: show all game types
+ GAME_TYPE=""
+elif [ $# -eq 1 ]; then
+ if ! [[ "$1" =~ ^[0-9]+$ ]]; then
+ echo -e "${RED}Error: Game type must be a number${NC}"
+ exit 1
+ fi
+ GAME_TYPE="$1"
+elif [ $# -eq 3 ]; then
+ if ! [[ "$1" =~ ^[0-9]+$ ]] || ! [[ "$2" =~ ^[0-9]+$ ]] || ! [[ "$3" =~ ^[0-9]+$ ]]; then
+ echo -e "${RED}Error: All arguments must be numbers${NC}"
+ exit 1
+ fi
+ if [ "$2" -gt "$3" ]; then
+ echo -e "${RED}Error: Start index must be <= end index${NC}"
+ exit 1
+ fi
+ GAME_TYPE="$1"
+ FILTER_START="$2"
+ FILTER_END="$3"
+else
+ echo -e "${RED}Error: Invalid arguments${NC}"
+ echo ""
+ echo "Usage: $0 [game_type] [start_index end_index]"
+ echo ""
+ echo "Examples:"
+ echo " $0 # List all games (all types)"
+ echo " $0 42 # List all games of type 42"
+ echo " $0 1960 # List all games of type 1960"
+ echo " $0 42 10 20 # List type 42 games from index 10 to 20"
+ echo ""
+ exit 1
+fi
+
+show_header "$GAME_TYPE"
+list_games "$GAME_TYPE" "$FILTER_START" "$FILTER_END"
diff --git a/tools/mockteeprover/Dockerfile b/tools/mockteeprover/Dockerfile
new file mode 100644
index 00000000..16e1d94e
--- /dev/null
+++ b/tools/mockteeprover/Dockerfile
@@ -0,0 +1,15 @@
+FROM golang:1.22-alpine AS builder
+
+WORKDIR /app
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN CGO_ENABLED=0 go build -o /mock-tee-prover .
+
+FROM alpine:3.19
+RUN apk add --no-cache ca-certificates wget
+COPY --from=builder /mock-tee-prover /usr/local/bin/mock-tee-prover
+
+EXPOSE 8690
+
+ENTRYPOINT ["mock-tee-prover"]
diff --git a/tools/mockteeprover/README.md b/tools/mockteeprover/README.md
new file mode 100644
index 00000000..5bc3a012
--- /dev/null
+++ b/tools/mockteeprover/README.md
@@ -0,0 +1,91 @@
+# mockteeprover
+
+Standalone mock TEE Prover HTTP server for local development and testing.
+
+---
+
+## Mock TEE Prover Server
+
+Simulates the TEE Prover task API used by `op-challenger`. Accepts prove requests, generates mock batch proofs with ECDSA signatures, and returns them after a configurable delay.
+
+**Behavior:**
+- `POST /task/` ā creates a prove task, returns `taskId`
+- `GET /task/{taskId}` ā polls task status (`Running` ā `Finished` after delay)
+- `GET /health` ā health check
+- Admin endpoints for testing: `/admin/fail-next`, `/admin/never-finish`, `/admin/reset`, `/admin/stats`
+
+**Proof generation:**
+- Signs `keccak256(abi.encode(startBlockHash, startStateHash, endBlockHash, endStateHash, l2Block))` with the configured ECDSA key
+- Returns ABI-encoded `BatchProof[]` matching `TeeDisputeGame.sol`
+
+---
+
+## How to Run
+
+### Option 1: Direct `go run`
+
+```bash
+cd tools/mockteeprover
+
+# Requires SIGNER_PRIVATE_KEY (the TEE signer key registered in TeeProofVerifier)
+SIGNER_PRIVATE_KEY=0x... go run .
+
+# Custom listen address and task delay
+SIGNER_PRIVATE_KEY=0x... LISTEN_ADDR=:9000 TASK_DELAY=5s go run .
+```
+
+### Option 2: Docker
+
+```bash
+cd tools/mockteeprover
+docker build -t mockteeprover:latest .
+docker run --rm -p 8690:8690 -e SIGNER_PRIVATE_KEY=0x... mockteeprover:latest
+```
+
+---
+
+## curl Testing
+
+```bash
+# Health check
+curl -s http://localhost:8690/health | jq .
+
+# Submit a prove task
+curl -s -X POST http://localhost:8690/task/ \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "startBlkHeight": 100,
+ "endBlkHeight": 200,
+ "startBlkHash": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "endBlkHash": "0x0000000000000000000000000000000000000000000000000000000000000002",
+ "startBlkStateHash": "0x0000000000000000000000000000000000000000000000000000000000000003",
+ "endBlkStateHash": "0x0000000000000000000000000000000000000000000000000000000000000004"
+ }' | jq .
+
+# Poll task status (replace TASK_ID)
+curl -s http://localhost:8690/task/TASK_ID | jq .
+
+# View stats
+curl -s http://localhost:8690/admin/stats | jq .
+```
+
+---
+
+## Environment Variables
+
+| Variable | Default | Description |
+|----------------------|----------|----------------------------------------------------------|
+| `SIGNER_PRIVATE_KEY` | required | ECDSA private key for signing batch proofs (hex, with or without 0x prefix) |
+| `LISTEN_ADDR` | `:8690` | Listen address |
+| `TASK_DELAY` | `2s` | Time before a task transitions from Running to Finished |
+
+---
+
+## Admin Endpoints
+
+| Endpoint | Method | Description |
+|------------------------|--------|--------------------------------------------------|
+| `/admin/fail-next` | POST | Next created task will immediately fail (one-shot)|
+| `/admin/never-finish` | POST | New tasks stay Running forever until reset |
+| `/admin/reset` | POST | Clear all control flags |
+| `/admin/stats` | GET | Show submitted request count and control flags |
diff --git a/tools/mockteeprover/go.mod b/tools/mockteeprover/go.mod
new file mode 100644
index 00000000..7686b3a9
--- /dev/null
+++ b/tools/mockteeprover/go.mod
@@ -0,0 +1,15 @@
+module github.com/okx/xlayer-toolkit/tools/mockteeprover
+
+go 1.22.0
+
+require (
+ github.com/ethereum/go-ethereum v1.14.12
+ github.com/google/uuid v1.6.0
+)
+
+require (
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
+ github.com/holiman/uint256 v1.3.1 // indirect
+ golang.org/x/crypto v0.22.0 // indirect
+ golang.org/x/sys v0.22.0 // indirect
+)
diff --git a/tools/mockteeprover/go.sum b/tools/mockteeprover/go.sum
new file mode 100644
index 00000000..d9f43d0e
--- /dev/null
+++ b/tools/mockteeprover/go.sum
@@ -0,0 +1,24 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
+github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4=
+github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
+github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/tools/mockteeprover/main.go b/tools/mockteeprover/main.go
new file mode 100644
index 00000000..4080b98e
--- /dev/null
+++ b/tools/mockteeprover/main.go
@@ -0,0 +1,337 @@
+package main
+
+import (
+ "crypto/ecdsa"
+ "encoding/json"
+ "fmt"
+ "log"
+ "math/big"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/google/uuid"
+)
+
+// ProveRequest matches op-challenger/game/tee/prover_client.go ProveRequest.
+type ProveRequest struct {
+ StartBlkHeight uint64 `json:"startBlkHeight"`
+ EndBlkHeight uint64 `json:"endBlkHeight"`
+ StartBlkHash string `json:"startBlkHash"`
+ EndBlkHash string `json:"endBlkHash"`
+ StartBlkStateHash string `json:"startBlkStateHash"`
+ EndBlkStateHash string `json:"endBlkStateHash"`
+
+ // Optional EIP-712 domain hints. If present, override system defaults.
+ ChainID *uint64 `json:"chainId,omitempty"`
+ TeeProofVerifierAddr *string `json:"teeProofVerifierAddr,omitempty"`
+}
+
+type task struct {
+ ID string
+ Status string // "Running", "Finished", "Failed"
+ CreatedAt time.Time
+ FinishAt time.Time // when the task should transition to Finished
+ Request ProveRequest
+ ProofBytes []byte // set when Finished
+ FailCode int // error code when Failed
+}
+
+type response struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data interface{} `json:"data"`
+}
+
+type MockTEEProver struct {
+ mu sync.Mutex
+ tasks map[string]*task
+ signerKey *ecdsa.PrivateKey
+ defaultDomainCfg EIP712DomainConfig
+ defaultDomainSep common.Hash
+ taskDelay time.Duration
+
+ // control flags
+ failNext bool
+ neverFinish bool
+
+ // stats
+ submittedRequests []ProveRequest
+}
+
+func NewMockTEEProver(signerKey *ecdsa.PrivateKey, domainCfg EIP712DomainConfig, taskDelay time.Duration) *MockTEEProver {
+ return &MockTEEProver{
+ tasks: make(map[string]*task),
+ signerKey: signerKey,
+ defaultDomainCfg: domainCfg,
+ defaultDomainSep: computeDomainSeparator(domainCfg),
+ taskDelay: taskDelay,
+ }
+}
+
+// resolveDomainSep returns a domain separator based on request overrides or defaults.
+func (m *MockTEEProver) resolveDomainSep(req ProveRequest) common.Hash {
+ if req.ChainID == nil && req.TeeProofVerifierAddr == nil {
+ return m.defaultDomainSep
+ }
+ cfg := m.defaultDomainCfg
+ if req.ChainID != nil {
+ cfg.ChainID = new(big.Int).SetUint64(*req.ChainID)
+ }
+ if req.TeeProofVerifierAddr != nil {
+ cfg.VerifyingContract = common.HexToAddress(*req.TeeProofVerifierAddr)
+ }
+ return computeDomainSeparator(cfg)
+}
+
+// POST /task/
+func (m *MockTEEProver) handleCreateTask(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ writeJSON(w, response{Code: -1, Message: "method not allowed"})
+ return
+ }
+
+ var req ProveRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeJSON(w, response{Code: 10001, Message: fmt.Sprintf("invalid request: %v", err)})
+ return
+ }
+
+ m.mu.Lock()
+ m.submittedRequests = append(m.submittedRequests, req)
+
+ id := uuid.New().String()
+ t := &task{
+ ID: id,
+ Status: "Running",
+ CreatedAt: time.Now(),
+ FinishAt: time.Now().Add(m.taskDelay),
+ Request: req,
+ }
+
+ if m.failNext {
+ t.Status = "Failed"
+ t.FailCode = 10000 // retryable
+ m.failNext = false
+ } else if m.neverFinish {
+ // stays Running forever
+ t.FinishAt = time.Now().Add(24 * time.Hour)
+ }
+
+ m.tasks[id] = t
+ m.mu.Unlock()
+
+ log.Printf("[POST /task/] created task %s for blocks %dā%d (status=%s)", id, req.StartBlkHeight, req.EndBlkHeight, t.Status)
+
+ writeJSON(w, response{
+ Code: 0,
+ Message: "ok",
+ Data: map[string]string{"taskId": id},
+ })
+}
+
+// GET /task/{taskId}
+func (m *MockTEEProver) handleGetTask(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ writeJSON(w, response{Code: -1, Message: "method not allowed"})
+ return
+ }
+
+ taskID := extractTaskID(r.URL.Path)
+ if taskID == "" {
+ writeJSON(w, response{Code: 10004, Message: "task not found"})
+ return
+ }
+
+ m.mu.Lock()
+ t, ok := m.tasks[taskID]
+ if !ok {
+ m.mu.Unlock()
+ writeJSON(w, response{Code: 10004, Message: "task not found"})
+ return
+ }
+
+ // Transition Running ā Finished if delay has elapsed
+ if t.Status == "Running" && time.Now().After(t.FinishAt) {
+ domainSep := m.resolveDomainSep(t.Request)
+ proofBytes, err := generateProofBytes(t.Request, m.signerKey, domainSep)
+ if err != nil {
+ log.Printf("[GET /task/%s] ERROR generating proof: %v", taskID, err)
+ t.Status = "Failed"
+ t.FailCode = 20001
+ } else {
+ t.Status = "Finished"
+ t.ProofBytes = proofBytes
+ }
+ }
+
+ // Build response data
+ data := map[string]interface{}{
+ "taskId": t.ID,
+ "status": t.Status,
+ }
+ if t.Status == "Finished" {
+ data["proofBytes"] = hexutil.Encode(t.ProofBytes)
+ }
+ if t.Status == "Failed" {
+ data["detail"] = fmt.Sprintf("mock failure (code=%d)", t.FailCode)
+ }
+ m.mu.Unlock()
+
+ log.Printf("[GET /task/%s] status=%s", taskID, t.Status)
+
+ writeJSON(w, response{Code: 0, Message: "ok", Data: data})
+}
+
+// POST /admin/fail-next
+func (m *MockTEEProver) handleFailNext(w http.ResponseWriter, r *http.Request) {
+ m.mu.Lock()
+ m.failNext = true
+ m.mu.Unlock()
+ log.Println("[admin] fail-next enabled")
+ writeJSON(w, response{Code: 0, Message: "fail-next enabled"})
+}
+
+// POST /admin/never-finish
+func (m *MockTEEProver) handleNeverFinish(w http.ResponseWriter, r *http.Request) {
+ m.mu.Lock()
+ m.neverFinish = true
+ m.mu.Unlock()
+ log.Println("[admin] never-finish enabled")
+ writeJSON(w, response{Code: 0, Message: "never-finish enabled"})
+}
+
+// POST /admin/reset
+func (m *MockTEEProver) handleReset(w http.ResponseWriter, r *http.Request) {
+ m.mu.Lock()
+ m.failNext = false
+ m.neverFinish = false
+ m.mu.Unlock()
+ log.Println("[admin] reset all control flags")
+ writeJSON(w, response{Code: 0, Message: "reset"})
+}
+
+// GET /admin/stats
+func (m *MockTEEProver) handleStats(w http.ResponseWriter, r *http.Request) {
+ m.mu.Lock()
+ stats := map[string]interface{}{
+ "task_count": len(m.submittedRequests),
+ "requests": m.submittedRequests,
+ "fail_next": m.failNext,
+ "never_finish": m.neverFinish,
+ "active_tasks": len(m.tasks),
+ }
+ m.mu.Unlock()
+ writeJSON(w, stats)
+}
+
+// GET /health
+func (m *MockTEEProver) handleHealth(w http.ResponseWriter, r *http.Request) {
+ writeJSON(w, map[string]string{"status": "ok"})
+}
+
+func (m *MockTEEProver) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+
+ switch {
+ case path == "/health":
+ m.handleHealth(w, r)
+ case path == "/tee/task/" && r.Method == http.MethodPost:
+ m.handleCreateTask(w, r)
+ case strings.HasPrefix(path, "/tee/task/") && r.Method == http.MethodGet:
+ m.handleGetTask(w, r)
+ case path == "/admin/fail-next":
+ m.handleFailNext(w, r)
+ case path == "/admin/never-finish":
+ m.handleNeverFinish(w, r)
+ case path == "/admin/reset":
+ m.handleReset(w, r)
+ case path == "/admin/stats":
+ m.handleStats(w, r)
+ default:
+ http.NotFound(w, r)
+ }
+}
+
+func extractTaskID(path string) string {
+ // /task/{taskId} or /task/{taskId}/
+ path = strings.TrimPrefix(path, "/tee/task/")
+ path = strings.TrimSuffix(path, "/")
+ if path == "" {
+ return ""
+ }
+ return path
+}
+
+func writeJSON(w http.ResponseWriter, v interface{}) {
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(v); err != nil {
+ log.Printf("ERROR writing JSON response: %v", err)
+ }
+}
+
+func main() {
+ signerKeyHex := os.Getenv("SIGNER_PRIVATE_KEY")
+ if signerKeyHex == "" {
+ log.Fatal("SIGNER_PRIVATE_KEY environment variable is required")
+ }
+ signerKeyHex = strings.TrimPrefix(signerKeyHex, "0x")
+
+ signerKey, err := crypto.HexToECDSA(signerKeyHex)
+ if err != nil {
+ log.Fatalf("invalid SIGNER_PRIVATE_KEY: %v", err)
+ }
+
+ signerAddr := crypto.PubkeyToAddress(signerKey.PublicKey)
+ log.Printf("Mock TEE Prover starting, signer address: %s", signerAddr.Hex())
+
+ // EIP-712 domain config
+ chainIDStr := os.Getenv("CHAIN_ID")
+ if chainIDStr == "" {
+ log.Fatal("CHAIN_ID environment variable is required")
+ }
+ chainID, err := strconv.ParseInt(chainIDStr, 10, 64)
+ if err != nil {
+ log.Fatalf("invalid CHAIN_ID: %v", err)
+ }
+
+ verifyingContractHex := os.Getenv("VERIFYING_CONTRACT")
+ if verifyingContractHex == "" {
+ log.Fatal("VERIFYING_CONTRACT environment variable is required")
+ }
+ verifyingContract := common.HexToAddress(verifyingContractHex)
+
+ domainCfg := EIP712DomainConfig{
+ ChainID: big.NewInt(chainID),
+ VerifyingContract: verifyingContract,
+ }
+ log.Printf("EIP-712 default domain: chainId=%d, verifyingContract=%s, separator=%s",
+ chainID, verifyingContract.Hex(), computeDomainSeparator(domainCfg).Hex())
+
+ listenAddr := os.Getenv("LISTEN_ADDR")
+ if listenAddr == "" {
+ listenAddr = ":8690"
+ }
+
+ taskDelay := 2 * time.Second
+ if d := os.Getenv("TASK_DELAY"); d != "" {
+ parsed, err := time.ParseDuration(d)
+ if err != nil {
+ log.Fatalf("invalid TASK_DELAY: %v", err)
+ }
+ taskDelay = parsed
+ }
+
+ prover := NewMockTEEProver(signerKey, domainCfg, taskDelay)
+
+ log.Printf("Listening on %s (task_delay=%s)", listenAddr, taskDelay)
+ if err := http.ListenAndServe(listenAddr, prover); err != nil {
+ log.Fatalf("server error: %v", err)
+ }
+}
diff --git a/tools/mockteeprover/main_test.go b/tools/mockteeprover/main_test.go
new file mode 100644
index 00000000..45f7e0d0
--- /dev/null
+++ b/tools/mockteeprover/main_test.go
@@ -0,0 +1,207 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+func setupTestServer(t *testing.T) (*MockTEEProver, *httptest.Server) {
+ t.Helper()
+ key, err := crypto.GenerateKey()
+ if err != nil {
+ t.Fatalf("failed to generate key: %v", err)
+ }
+ prover := NewMockTEEProver(key, 100*time.Millisecond)
+ server := httptest.NewServer(prover)
+ t.Cleanup(server.Close)
+ return prover, server
+}
+
+func postTask(t *testing.T, serverURL string, req ProveRequest) string {
+ t.Helper()
+ body, _ := json.Marshal(req)
+ resp, err := http.Post(serverURL+"/task/", "application/json", bytes.NewReader(body))
+ if err != nil {
+ t.Fatalf("POST /task/ failed: %v", err)
+ }
+ defer resp.Body.Close()
+
+ var r response
+ json.NewDecoder(resp.Body).Decode(&r)
+ if r.Code != 0 {
+ t.Fatalf("POST /task/ returned code %d: %s", r.Code, r.Message)
+ }
+ data := r.Data.(map[string]interface{})
+ return data["taskId"].(string)
+}
+
+func getTask(t *testing.T, serverURL, taskID string) map[string]interface{} {
+ t.Helper()
+ resp, err := http.Get(serverURL + "/task/" + taskID)
+ if err != nil {
+ t.Fatalf("GET /task/%s failed: %v", taskID, err)
+ }
+ defer resp.Body.Close()
+
+ var r response
+ json.NewDecoder(resp.Body).Decode(&r)
+ return r.Data.(map[string]interface{})
+}
+
+func TestHealthEndpoint(t *testing.T) {
+ _, server := setupTestServer(t)
+
+ resp, err := http.Get(server.URL + "/health")
+ if err != nil {
+ t.Fatalf("GET /health failed: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ t.Errorf("expected 200, got %d", resp.StatusCode)
+ }
+}
+
+func TestCreateAndPollTask(t *testing.T) {
+ _, server := setupTestServer(t)
+
+ req := ProveRequest{
+ StartBlkHeight: 100,
+ EndBlkHeight: 200,
+ StartBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000001",
+ EndBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000002",
+ StartBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000003",
+ EndBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000004",
+ }
+
+ taskID := postTask(t, server.URL, req)
+ if taskID == "" {
+ t.Fatal("got empty taskId")
+ }
+
+ // Immediately should be Running
+ data := getTask(t, server.URL, taskID)
+ if data["status"] != "Running" {
+ t.Errorf("expected Running, got %s", data["status"])
+ }
+
+ // Wait for task delay to elapse
+ time.Sleep(200 * time.Millisecond)
+
+ // Should be Finished now
+ data = getTask(t, server.URL, taskID)
+ if data["status"] != "Finished" {
+ t.Errorf("expected Finished, got %s", data["status"])
+ }
+ if data["proofBytes"] == nil || data["proofBytes"] == "" {
+ t.Error("expected proofBytes to be set")
+ }
+}
+
+func TestFailNext(t *testing.T) {
+ _, server := setupTestServer(t)
+
+ // Enable fail-next
+ http.Post(server.URL+"/admin/fail-next", "", nil)
+
+ req := ProveRequest{
+ StartBlkHeight: 1,
+ EndBlkHeight: 2,
+ StartBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000001",
+ EndBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000002",
+ StartBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000003",
+ EndBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000004",
+ }
+
+ taskID := postTask(t, server.URL, req)
+ data := getTask(t, server.URL, taskID)
+ if data["status"] != "Failed" {
+ t.Errorf("expected Failed, got %s", data["status"])
+ }
+
+ // Next task should succeed (fail-next is one-shot)
+ taskID2 := postTask(t, server.URL, req)
+ time.Sleep(200 * time.Millisecond)
+ data2 := getTask(t, server.URL, taskID2)
+ if data2["status"] != "Finished" {
+ t.Errorf("expected Finished for second task, got %s", data2["status"])
+ }
+}
+
+func TestNeverFinish(t *testing.T) {
+ _, server := setupTestServer(t)
+
+ http.Post(server.URL+"/admin/never-finish", "", nil)
+
+ req := ProveRequest{
+ StartBlkHeight: 1,
+ EndBlkHeight: 2,
+ StartBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000001",
+ EndBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000002",
+ StartBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000003",
+ EndBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000004",
+ }
+
+ taskID := postTask(t, server.URL, req)
+ time.Sleep(200 * time.Millisecond)
+
+ data := getTask(t, server.URL, taskID)
+ if data["status"] != "Running" {
+ t.Errorf("expected Running (never-finish), got %s", data["status"])
+ }
+
+ // Reset and verify new tasks finish normally
+ http.Post(server.URL+"/admin/reset", "", nil)
+ taskID2 := postTask(t, server.URL, req)
+ time.Sleep(200 * time.Millisecond)
+ data2 := getTask(t, server.URL, taskID2)
+ if data2["status"] != "Finished" {
+ t.Errorf("expected Finished after reset, got %s", data2["status"])
+ }
+}
+
+func TestStats(t *testing.T) {
+ _, server := setupTestServer(t)
+
+ req := ProveRequest{
+ StartBlkHeight: 10,
+ EndBlkHeight: 20,
+ StartBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000001",
+ EndBlkHash: "0x0000000000000000000000000000000000000000000000000000000000000002",
+ StartBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000003",
+ EndBlkStateHash: "0x0000000000000000000000000000000000000000000000000000000000000004",
+ }
+
+ postTask(t, server.URL, req)
+ postTask(t, server.URL, req)
+
+ resp, _ := http.Get(server.URL + "/admin/stats")
+ defer resp.Body.Close()
+
+ var stats map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&stats)
+
+ count := int(stats["task_count"].(float64))
+ if count != 2 {
+ t.Errorf("expected 2 tasks in stats, got %d", count)
+ }
+}
+
+func TestTaskNotFound(t *testing.T) {
+ _, server := setupTestServer(t)
+
+ resp, _ := http.Get(server.URL + "/task/nonexistent-id")
+ defer resp.Body.Close()
+
+ var r response
+ json.NewDecoder(resp.Body).Decode(&r)
+ if r.Code != 10004 {
+ t.Errorf("expected code 10004, got %d", r.Code)
+ }
+}
diff --git a/tools/mockteeprover/proof.go b/tools/mockteeprover/proof.go
new file mode 100644
index 00000000..b65318bf
--- /dev/null
+++ b/tools/mockteeprover/proof.go
@@ -0,0 +1,201 @@
+package main
+
+import (
+ "crypto/ecdsa"
+ "fmt"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+// EIP-712 constants matching TeeDisputeGame.sol
+var (
+ // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
+ domainTypehash = crypto.Keccak256Hash([]byte("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"))
+ // keccak256("TeeDisputeGame")
+ domainNameHash = crypto.Keccak256Hash([]byte("TeeDisputeGame"))
+ // keccak256("1")
+ domainVersionHash = crypto.Keccak256Hash([]byte("1"))
+ // keccak256("BatchProof(bytes32 startBlockHash,bytes32 startStateHash,bytes32 endBlockHash,bytes32 endStateHash,uint256 l2Block)")
+ batchProofTypehash = crypto.Keccak256Hash([]byte("BatchProof(bytes32 startBlockHash,bytes32 startStateHash,bytes32 endBlockHash,bytes32 endStateHash,uint256 l2Block)"))
+)
+
+// EIP712DomainConfig holds the chain-specific EIP-712 domain parameters.
+type EIP712DomainConfig struct {
+ ChainID *big.Int
+ VerifyingContract common.Address
+}
+
+// BatchProof mirrors the Solidity struct in TeeDisputeGame.sol:
+//
+// struct BatchProof {
+// bytes32 startBlockHash;
+// bytes32 startStateHash;
+// bytes32 endBlockHash;
+// bytes32 endStateHash;
+// uint256 l2Block;
+// bytes signature;
+// }
+type BatchProof struct {
+ StartBlockHash [32]byte
+ StartStateHash [32]byte
+ EndBlockHash [32]byte
+ EndStateHash [32]byte
+ L2Block *big.Int
+ Signature []byte
+}
+
+// batchProofABIType is the ABI type for BatchProof[] used in abi.encode.
+var batchProofABIType abi.Arguments
+
+func init() {
+ tupleType, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{
+ {Name: "startBlockHash", Type: "bytes32"},
+ {Name: "startStateHash", Type: "bytes32"},
+ {Name: "endBlockHash", Type: "bytes32"},
+ {Name: "endStateHash", Type: "bytes32"},
+ {Name: "l2Block", Type: "uint256"},
+ {Name: "signature", Type: "bytes"},
+ })
+ if err != nil {
+ panic(fmt.Sprintf("failed to create BatchProof ABI type: %v", err))
+ }
+ batchProofABIType = abi.Arguments{{Type: tupleType}}
+}
+
+// structHashABIArgs is used to compute keccak256(abi.encode(BATCH_PROOF_TYPEHASH, ...fields...)).
+var structHashABIArgs abi.Arguments
+
+func init() {
+ bytes32Ty, _ := abi.NewType("bytes32", "", nil)
+ uint256Ty, _ := abi.NewType("uint256", "", nil)
+ structHashABIArgs = abi.Arguments{
+ {Type: bytes32Ty}, // BATCH_PROOF_TYPEHASH
+ {Type: bytes32Ty}, // startBlockHash
+ {Type: bytes32Ty}, // startStateHash
+ {Type: bytes32Ty}, // endBlockHash
+ {Type: bytes32Ty}, // endStateHash
+ {Type: uint256Ty}, // l2Block
+ }
+}
+
+// domainSeparatorABIArgs is used to compute the EIP-712 domain separator.
+var domainSeparatorABIArgs abi.Arguments
+
+func init() {
+ bytes32Ty, _ := abi.NewType("bytes32", "", nil)
+ uint256Ty, _ := abi.NewType("uint256", "", nil)
+ addressTy, _ := abi.NewType("address", "", nil)
+ domainSeparatorABIArgs = abi.Arguments{
+ {Type: bytes32Ty}, // DOMAIN_TYPEHASH
+ {Type: bytes32Ty}, // nameHash
+ {Type: bytes32Ty}, // versionHash
+ {Type: uint256Ty}, // chainId
+ {Type: addressTy}, // verifyingContract
+ }
+}
+
+// computeDomainSeparator computes the EIP-712 domain separator matching TeeDisputeGame._domainSeparator().
+func computeDomainSeparator(cfg EIP712DomainConfig) common.Hash {
+ packed, err := domainSeparatorABIArgs.Pack(
+ [32]byte(domainTypehash),
+ [32]byte(domainNameHash),
+ [32]byte(domainVersionHash),
+ cfg.ChainID,
+ cfg.VerifyingContract,
+ )
+ if err != nil {
+ panic(fmt.Sprintf("failed to pack domain separator: %v", err))
+ }
+ return crypto.Keccak256Hash(packed)
+}
+
+// computeEIP712Digest computes the full EIP-712 digest:
+// keccak256("\x19\x01" || domainSeparator || structHash)
+// where structHash = keccak256(abi.encode(BATCH_PROOF_TYPEHASH, startBlockHash, startStateHash, endBlockHash, endStateHash, l2Block))
+func computeEIP712Digest(domainSep common.Hash, startBlockHash, startStateHash, endBlockHash, endStateHash [32]byte, l2Block *big.Int) common.Hash {
+ // structHash = keccak256(abi.encode(BATCH_PROOF_TYPEHASH, ...))
+ packed, err := structHashABIArgs.Pack(
+ [32]byte(batchProofTypehash),
+ startBlockHash,
+ startStateHash,
+ endBlockHash,
+ endStateHash,
+ l2Block,
+ )
+ if err != nil {
+ panic(fmt.Sprintf("failed to pack struct hash: %v", err))
+ }
+ structHash := crypto.Keccak256Hash(packed)
+
+ // EIP-712: keccak256("\x19\x01" || domainSeparator || structHash)
+ raw := make([]byte, 2+32+32)
+ raw[0] = 0x19
+ raw[1] = 0x01
+ copy(raw[2:34], domainSep.Bytes())
+ copy(raw[34:66], structHash.Bytes())
+ return crypto.Keccak256Hash(raw)
+}
+
+// signBatchDigest signs a batch digest with the given ECDSA private key.
+// Returns 65-byte signature [r(32) || s(32) || v(1)] with v = 27 or 28 (Solidity ecrecover convention).
+func signBatchDigest(digest common.Hash, key *ecdsa.PrivateKey) ([]byte, error) {
+ sig, err := crypto.Sign(digest.Bytes(), key)
+ if err != nil {
+ return nil, fmt.Errorf("failed to sign batch digest: %w", err)
+ }
+ // go-ethereum Sign returns v as 0/1; Solidity ecrecover expects 27/28
+ sig[64] += 27
+ return sig, nil
+}
+
+// generateProofBytes constructs an ABI-encoded BatchProof[] from a ProveRequest.
+// It creates a single BatchProof covering the full range, signs it with EIP-712, and encodes.
+func generateProofBytes(req ProveRequest, signerKey *ecdsa.PrivateKey, domainSep common.Hash) ([]byte, error) {
+ startBlockHash := common.HexToHash(req.StartBlkHash)
+ startStateHash := common.HexToHash(req.StartBlkStateHash)
+ endBlockHash := common.HexToHash(req.EndBlkHash)
+ endStateHash := common.HexToHash(req.EndBlkStateHash)
+ l2Block := new(big.Int).SetUint64(req.EndBlkHeight)
+
+ digest := computeEIP712Digest(
+ domainSep,
+ [32]byte(startBlockHash),
+ [32]byte(startStateHash),
+ [32]byte(endBlockHash),
+ [32]byte(endStateHash),
+ l2Block,
+ )
+
+ sig, err := signBatchDigest(digest, signerKey)
+ if err != nil {
+ return nil, err
+ }
+
+ // Build a single-element BatchProof array
+ proofs := []struct {
+ StartBlockHash [32]byte `abi:"startBlockHash"`
+ StartStateHash [32]byte `abi:"startStateHash"`
+ EndBlockHash [32]byte `abi:"endBlockHash"`
+ EndStateHash [32]byte `abi:"endStateHash"`
+ L2Block *big.Int `abi:"l2Block"`
+ Signature []byte `abi:"signature"`
+ }{
+ {
+ StartBlockHash: [32]byte(startBlockHash),
+ StartStateHash: [32]byte(startStateHash),
+ EndBlockHash: [32]byte(endBlockHash),
+ EndStateHash: [32]byte(endStateHash),
+ L2Block: l2Block,
+ Signature: sig,
+ },
+ }
+
+ encoded, err := batchProofABIType.Pack(proofs)
+ if err != nil {
+ return nil, fmt.Errorf("failed to ABI-encode BatchProof[]: %w", err)
+ }
+ return encoded, nil
+}
diff --git a/tools/mockteeprover/proof_test.go b/tools/mockteeprover/proof_test.go
new file mode 100644
index 00000000..29701e98
--- /dev/null
+++ b/tools/mockteeprover/proof_test.go
@@ -0,0 +1,126 @@
+package main
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+func TestGenerateProofBytes(t *testing.T) {
+ // Generate a test signer key
+ signerKey, err := crypto.GenerateKey()
+ if err != nil {
+ t.Fatalf("failed to generate key: %v", err)
+ }
+ signerAddr := crypto.PubkeyToAddress(signerKey.PublicKey)
+
+ req := ProveRequest{
+ StartBlkHeight: 100,
+ EndBlkHeight: 200,
+ StartBlkHash: "0x" + common.Bytes2Hex(common.LeftPadBytes([]byte{0x01}, 32)),
+ EndBlkHash: "0x" + common.Bytes2Hex(common.LeftPadBytes([]byte{0x02}, 32)),
+ StartBlkStateHash: "0x" + common.Bytes2Hex(common.LeftPadBytes([]byte{0x03}, 32)),
+ EndBlkStateHash: "0x" + common.Bytes2Hex(common.LeftPadBytes([]byte{0x04}, 32)),
+ }
+
+ proofBytes, err := generateProofBytes(req, signerKey)
+ if err != nil {
+ t.Fatalf("generateProofBytes failed: %v", err)
+ }
+
+ if len(proofBytes) == 0 {
+ t.Fatal("proofBytes is empty")
+ }
+
+ // Decode using abi.ConvertType to a known Go type
+ decoded, err := batchProofABIType.Unpack(proofBytes)
+ if err != nil {
+ t.Fatalf("failed to unpack proofBytes: %v", err)
+ }
+
+ var proofs []BatchProof
+ if err := batchProofABIType.Copy(&proofs, decoded); err != nil {
+ t.Fatalf("failed to copy decoded proofs: %v", err)
+ }
+
+ if len(proofs) != 1 {
+ t.Fatalf("expected 1 proof, got %d", len(proofs))
+ }
+
+ proof := proofs[0]
+
+ // Verify l2Block
+ if proof.L2Block.Uint64() != 200 {
+ t.Errorf("expected l2Block=200, got %d", proof.L2Block.Uint64())
+ }
+
+ // Verify signature by recovering signer
+ digest := computeBatchDigest(
+ proof.StartBlockHash,
+ proof.StartStateHash,
+ proof.EndBlockHash,
+ proof.EndStateHash,
+ proof.L2Block,
+ )
+
+ // Convert v back from 27/28 to 0/1 for crypto.Ecrecover
+ sig := make([]byte, 65)
+ copy(sig, proof.Signature)
+ sig[64] -= 27
+
+ pubKey, err := crypto.Ecrecover(digest.Bytes(), sig)
+ if err != nil {
+ t.Fatalf("Ecrecover failed: %v", err)
+ }
+ recoveredAddr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:])
+
+ if recoveredAddr != signerAddr {
+ t.Errorf("recovered address %s != signer %s", recoveredAddr.Hex(), signerAddr.Hex())
+ }
+
+ t.Logf("proofBytes length: %d bytes", len(proofBytes))
+ t.Logf("signer: %s, recovered: %s", signerAddr.Hex(), recoveredAddr.Hex())
+}
+
+func TestComputeBatchDigest(t *testing.T) {
+ // Deterministic inputs
+ startBlockHash := [32]byte{0x01}
+ startStateHash := [32]byte{0x02}
+ endBlockHash := [32]byte{0x03}
+ endStateHash := [32]byte{0x04}
+ l2Block := big.NewInt(100)
+
+ d1 := computeBatchDigest(startBlockHash, startStateHash, endBlockHash, endStateHash, l2Block)
+ d2 := computeBatchDigest(startBlockHash, startStateHash, endBlockHash, endStateHash, l2Block)
+
+ if d1 != d2 {
+ t.Error("digest should be deterministic")
+ }
+
+ // Different input should produce different digest
+ d3 := computeBatchDigest(startBlockHash, startStateHash, endBlockHash, endStateHash, big.NewInt(101))
+ if d1 == d3 {
+ t.Error("different input should produce different digest")
+ }
+}
+
+func TestSignBatchDigest(t *testing.T) {
+ key, _ := crypto.GenerateKey()
+ digest := common.HexToHash("0xdeadbeef")
+
+ sig, err := signBatchDigest(digest, key)
+ if err != nil {
+ t.Fatalf("signBatchDigest failed: %v", err)
+ }
+
+ if len(sig) != 65 {
+ t.Fatalf("expected 65 byte signature, got %d", len(sig))
+ }
+
+ // v should be 27 or 28
+ if sig[64] != 27 && sig[64] != 28 {
+ t.Errorf("v byte should be 27 or 28, got %d", sig[64])
+ }
+}
diff --git a/tools/mockteerpc/Dockerfile b/tools/mockteerpc/Dockerfile
new file mode 100644
index 00000000..79f75e9e
--- /dev/null
+++ b/tools/mockteerpc/Dockerfile
@@ -0,0 +1,21 @@
+FROM golang:1.23-alpine AS builder
+
+WORKDIR /app
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+RUN CGO_ENABLED=0 GOOS=linux go build -o mockteerpc ./cmd/mockteerpc
+
+FROM alpine:3.21
+
+RUN apk --no-cache add ca-certificates
+
+WORKDIR /app
+COPY --from=builder /app/mockteerpc .
+
+EXPOSE 8090
+
+ENTRYPOINT ["./mockteerpc"]
+CMD ["--addr", ":8090"]
diff --git a/tools/mockteerpc/Makefile b/tools/mockteerpc/Makefile
new file mode 100644
index 00000000..c0f3f8f4
--- /dev/null
+++ b/tools/mockteerpc/Makefile
@@ -0,0 +1,29 @@
+BINARY := mockteerpc
+IMAGE := mockteerpc
+TAG ?= latest
+
+.PHONY: build install run test docker docker-build docker-run clean
+
+build:
+ go build -o bin/$(BINARY) ./cmd/mockteerpc
+
+install:
+ go install ./cmd/mockteerpc
+
+run:
+ go run ./cmd/mockteerpc
+
+test:
+ go test ./...
+
+docker:
+ docker build -t $(IMAGE):latest .
+
+docker-build:
+ docker build -t $(IMAGE):$(TAG) .
+
+docker-run:
+ docker run --rm -p 8090:8090 $(IMAGE):$(TAG)
+
+clean:
+ rm -rf bin/
diff --git a/tools/mockteerpc/README.md b/tools/mockteerpc/README.md
new file mode 100644
index 00000000..de4690c2
--- /dev/null
+++ b/tools/mockteerpc/README.md
@@ -0,0 +1,141 @@
+# mockteerpc
+
+Standalone mock TeeRollup HTTP server for local development and testing.
+
+---
+
+## Mock TeeRollup Server
+
+Simulates the `GET /chain/confirmed_block_info` REST endpoint provided by a real TeeRollup service.
+
+**Behavior:**
+- Starts at block height 1000 (configurable)
+- Increments height by a random delta in **[1, 50]** every second
+- `appHash` = `keccak256(big-endian uint64 of height)`, `"0x"` prefix, 66 characters
+- `blockHash` = `keccak256(appHash)`, `"0x"` prefix, 66 characters
+
+---
+
+## How to Run
+
+### Option 1: Direct `go run`
+
+```bash
+cd tools/mockteerpc
+go run ./cmd/mockteerpc
+
+# Custom listen address and initial height
+go run ./cmd/mockteerpc --addr :9000 --init-height 5000
+
+# 30% error rate + max 500ms delay
+go run ./cmd/mockteerpc --error-rate 0.3 --delay 500ms
+```
+
+### Option 2: Build then run
+
+```bash
+cd tools/mockteerpc
+make build
+./bin/mockteerpc --addr :8090
+```
+
+### Option 3: Docker
+
+```bash
+cd tools/mockteerpc
+
+# Build image (mockteerpc:latest)
+make docker
+
+# Run container (exposes :8090)
+make docker-run
+
+# Custom flags
+docker run --rm -p 9000:9000 mockteerpc:latest --addr :9000 --init-height 5000
+```
+
+Startup output example:
+```
+mock TeeRollup server listening on :8090
+initial height: 1000
+error rate: 0.0%
+max delay: 1s
+endpoint: GET /chain/confirmed_block_info
+
+tick: height=1023 delta=23
+tick: height=1058 delta=35
+...
+```
+
+---
+
+## curl Testing
+
+```bash
+# Query current confirmed block info
+curl -s http://localhost:8090/chain/confirmed_block_info | jq .
+```
+
+Example response:
+```json
+{
+ "code": 0,
+ "message": "OK",
+ "data": {
+ "height": 1023,
+ "appHash": "0x3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
+ "blockHash": "0x1234abcd..."
+ }
+}
+```
+
+### Observe height growth continuously
+
+```bash
+watch -n 0.5 'curl -s http://localhost:8090/chain/confirmed_block_info | jq .data'
+```
+
+---
+
+## Usage in Tests
+
+```go
+import mockteerpc "github.com/okx/xlayer-toolkit/tools/mockteerpc"
+
+func TestMyFeature(t *testing.T) {
+ srv := mockteerpc.NewTeeRollupServer(t) // t.Cleanup closes automatically
+
+ baseURL := srv.Addr() // e.g. "http://127.0.0.1:12345"
+
+ height, appHash, blockHash := srv.CurrentInfo()
+ _ = height
+ _ = appHash
+ _ = blockHash
+}
+```
+
+---
+
+## CLI flags
+
+| Flag | Default | Description |
+|----------------|---------|--------------------------------------------------------------------------|
+| `--addr` | `:8090` | Listen address |
+| `--init-height`| `1000` | Initial block height |
+| `--error-rate` | `0` | Error response probability [0.0, 1.0], 0 means no errors |
+| `--delay` | `1s` | Maximum random response delay, actual delay is random in [0, delay] |
+
+---
+
+## Makefile targets
+
+| Target | Description |
+|---------------------|------------------------------------------|
+| `make build` | Build binary to `bin/mockteerpc` |
+| `make install` | Install binary to `$GOPATH/bin` |
+| `make run` | Run via `go run` |
+| `make test` | Run all tests |
+| `make docker` | Build Docker image `mockteerpc:latest` |
+| `make docker-build` | Build Docker image with custom `TAG` |
+| `make docker-run` | Run Docker container on :8090 |
+| `make clean` | Remove `bin/` directory |
diff --git a/tools/mockteerpc/cmd/mockteerpc/main.go b/tools/mockteerpc/cmd/mockteerpc/main.go
new file mode 100644
index 00000000..9b458a06
--- /dev/null
+++ b/tools/mockteerpc/cmd/mockteerpc/main.go
@@ -0,0 +1,163 @@
+// Command mockteerpc runs a standalone mock TeeRollup HTTP server for local development and curl testing.
+//
+// Usage:
+//
+// go run ./mock/cmd/mockteerpc [--addr :8090]
+package main
+
+import (
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "math/rand"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+type response struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data *data `json:"data"`
+}
+
+type data struct {
+ Height uint64 `json:"height"`
+ AppHash string `json:"appHash"`
+ BlockHash string `json:"blockHash"`
+}
+
+type server struct {
+ mu sync.RWMutex
+ height uint64
+ errorRate float64
+ maxDelay time.Duration
+}
+
+func (s *server) tick() {
+ ticker := time.NewTicker(time.Second)
+ defer ticker.Stop()
+ for range ticker.C {
+ delta := uint64(rand.Intn(50) + 1)
+ s.mu.Lock()
+ s.height += delta
+ s.mu.Unlock()
+ s.mu.RLock()
+ log.Printf("tick: height=%d delta=%d", s.height, delta)
+ s.mu.RUnlock()
+ }
+}
+
+func computeAppHash(height uint64) [32]byte {
+ var buf [8]byte
+ binary.BigEndian.PutUint64(buf[:], height)
+ return crypto.Keccak256Hash(buf[:])
+}
+
+func computeBlockHash(appHash [32]byte) [32]byte {
+ return crypto.Keccak256Hash(appHash[:])
+}
+
+func (s *server) handleConfirmedBlockInfo(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ log.Printf("[mockteerpc] received %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
+
+ if r.Method != http.MethodGet {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Random delay in [0, maxDelay].
+ if s.maxDelay > 0 {
+ delay := time.Duration(rand.Int63n(int64(s.maxDelay) + 1))
+ time.Sleep(delay)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+
+ if s.errorRate > 0 && rand.Float64() < s.errorRate {
+ writeErrorResponse(w)
+ log.Printf("[mockteerpc] responded with error (took %s)", time.Since(start))
+ return
+ }
+
+ s.mu.RLock()
+ h := s.height
+ s.mu.RUnlock()
+
+ appHash := computeAppHash(h)
+ blockHash := computeBlockHash(appHash)
+
+ appHashStr := "0x" + hex.EncodeToString(appHash[:])
+ resp := response{
+ Code: 0,
+ Message: "OK",
+ Data: &data{
+ Height: h,
+ AppHash: appHashStr,
+ BlockHash: "0x" + hex.EncodeToString(blockHash[:]),
+ },
+ }
+ _ = json.NewEncoder(w).Encode(resp)
+ log.Printf("[mockteerpc] responded height=%d appHash=%s (took %s)", h, appHashStr[:10]+"...", time.Since(start))
+}
+
+func writeErrorResponse(w http.ResponseWriter) {
+ type nullableData struct {
+ Height *uint64 `json:"height"`
+ AppHash *string `json:"appHash"`
+ BlockHash *string `json:"blockHash"`
+ }
+ type respNoData struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ }
+ type respWithData struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data *nullableData `json:"data"`
+ }
+
+ switch rand.Intn(3) {
+ case 0: // code != 0, no data field
+ _ = json.NewEncoder(w).Encode(respNoData{Code: 1, Message: "internal server error"})
+ case 1: // code == 0, data is null
+ _ = json.NewEncoder(w).Encode(respWithData{Code: 0, Message: "OK", Data: nil})
+ case 2: // code == 0, data present but all fields null
+ _ = json.NewEncoder(w).Encode(respWithData{Code: 0, Message: "OK", Data: &nullableData{}})
+ }
+}
+
+func main() {
+ addr := flag.String("addr", ":8090", "listen address")
+ initHeight := flag.Uint64("init-height", 1000, "initial block height")
+ errorRate := flag.Float64("error-rate", 0, "probability [0.0, 1.0] of returning an error response")
+ maxDelay := flag.Duration("delay", time.Second, "maximum random response delay (actual delay is random in [0, delay])")
+ flag.Parse()
+
+ if *errorRate < 0 || *errorRate > 1 {
+ log.Fatalf("--error-rate must be in [0.0, 1.0], got %f", *errorRate)
+ }
+
+ s := &server{height: *initHeight, errorRate: *errorRate, maxDelay: *maxDelay}
+ go s.tick()
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/chain/confirmed_block_info", s.handleConfirmedBlockInfo)
+
+ fmt.Printf("mock TeeRollup server listening on %s\n", *addr)
+ fmt.Printf("initial height: %d\n", *initHeight)
+ fmt.Printf("error rate: %.1f%%\n", *errorRate*100)
+ fmt.Printf("max delay: %s\n", *maxDelay)
+ fmt.Println("endpoint: GET /chain/confirmed_block_info")
+ fmt.Println()
+
+ if err := http.ListenAndServe(*addr, mux); err != nil {
+ log.Fatalf("server error: %v", err)
+ }
+}
diff --git a/tools/mockteerpc/go.mod b/tools/mockteerpc/go.mod
new file mode 100644
index 00000000..3eb05084
--- /dev/null
+++ b/tools/mockteerpc/go.mod
@@ -0,0 +1,20 @@
+module github.com/okx/xlayer-toolkit/tools/mockteerpc
+
+go 1.23.0
+
+toolchain go1.24.1
+
+require (
+ github.com/ethereum/go-ethereum v1.15.7
+ github.com/stretchr/testify v1.10.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
+ github.com/holiman/uint256 v1.3.2 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/crypto v0.35.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/tools/mockteerpc/go.sum b/tools/mockteerpc/go.sum
new file mode 100644
index 00000000..b03df42e
--- /dev/null
+++ b/tools/mockteerpc/go.sum
@@ -0,0 +1,22 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
+github.com/ethereum/go-ethereum v1.15.7 h1:vm1XXruZVnqtODBgqFaTclzP0xAvCvQIDKyFNUA1JpY=
+github.com/ethereum/go-ethereum v1.15.7/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0=
+github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
+github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/tools/mockteerpc/mock_tee_rollup_server.go b/tools/mockteerpc/mock_tee_rollup_server.go
new file mode 100644
index 00000000..60dce99b
--- /dev/null
+++ b/tools/mockteerpc/mock_tee_rollup_server.go
@@ -0,0 +1,227 @@
+package mockteerpc
+
+import (
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "log"
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+// TeeRollupResponse is the normal JSON shape returned by GET /chain/confirmed_block_info.
+type TeeRollupResponse struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data struct {
+ Height uint64 `json:"height"`
+ AppHash string `json:"appHash"`
+ BlockHash string `json:"blockHash"`
+ } `json:"data"`
+}
+
+// Option configures a TeeRollupServer.
+type Option func(*TeeRollupServer)
+
+// WithErrorRate sets the probability [0.0, 1.0] that any given RPC call returns an error.
+// Three error types are equally likely when an error occurs:
+// 1. code != 0, no data field, only message.
+// 2. code == 0, data is null.
+// 3. code == 0, data is present but all fields (height, appHash, blockHash) are null.
+func WithErrorRate(rate float64) Option {
+ return func(s *TeeRollupServer) {
+ s.errorRate = rate
+ }
+}
+
+// WithMaxDelay sets the maximum random response delay. Each request sleeps for a
+// random duration in [0, maxDelay]. Default is 1s.
+func WithMaxDelay(d time.Duration) Option {
+ return func(s *TeeRollupServer) {
+ s.maxDelay = d
+ }
+}
+
+// TeeRollupServer is a mock TeeRollup HTTP server for testing.
+// Height starts at 1000 and increments by a random value in [1, 50] every second.
+type TeeRollupServer struct {
+ server *httptest.Server
+ mu sync.RWMutex
+ height uint64
+ errorRate float64
+ maxDelay time.Duration
+ stopCh chan struct{}
+ doneCh chan struct{}
+ closeOnce sync.Once
+}
+
+// NewTeeRollupServer starts the mock server and its background tick goroutine.
+// Close() is registered via t.Cleanup so callers need not call it explicitly.
+func NewTeeRollupServer(t *testing.T, opts ...Option) *TeeRollupServer {
+ t.Helper()
+
+ m := &TeeRollupServer{
+ height: 1000,
+ maxDelay: time.Second,
+ stopCh: make(chan struct{}),
+ doneCh: make(chan struct{}),
+ }
+ for _, opt := range opts {
+ opt(m)
+ }
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/chain/confirmed_block_info", m.handleConfirmedBlockInfo)
+
+ m.server = httptest.NewServer(mux)
+
+ go m.tick()
+
+ t.Cleanup(m.Close)
+ return m
+}
+
+// Addr returns the base URL (scheme + host) of the test server.
+func (m *TeeRollupServer) Addr() string {
+ return m.server.URL
+}
+
+// Close stops the tick goroutine and shuts down the HTTP server.
+// Safe to call multiple times.
+func (m *TeeRollupServer) Close() {
+ m.closeOnce.Do(func() {
+ close(m.stopCh)
+ <-m.doneCh
+ m.server.Close()
+ })
+}
+
+// CurrentInfo returns the current height, appHash and blockHash snapshot.
+// Useful for assertions in tests without making an HTTP round-trip.
+func (m *TeeRollupServer) CurrentInfo() (height uint64, appHash, blockHash [32]byte) {
+ m.mu.RLock()
+ h := m.height
+ m.mu.RUnlock()
+
+ appHash = ComputeAppHash(h)
+ blockHash = ComputeBlockHash(appHash)
+ return h, appHash, blockHash
+}
+
+// tick increments height by random(1, 50) every second until Close() is called.
+func (m *TeeRollupServer) tick() {
+ defer close(m.doneCh)
+
+ ticker := time.NewTicker(time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-m.stopCh:
+ return
+ case <-ticker.C:
+ delta := uint64(rand.Intn(50) + 1) // [1, 50]
+ m.mu.Lock()
+ m.height += delta
+ m.mu.Unlock()
+ }
+ }
+}
+
+// handleConfirmedBlockInfo serves GET /chain/confirmed_block_info.
+func (m *TeeRollupServer) handleConfirmedBlockInfo(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ log.Printf("[mockteerpc] received %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
+
+ // Random delay in [0, maxDelay].
+ if m.maxDelay > 0 {
+ delay := time.Duration(rand.Int63n(int64(m.maxDelay) + 1))
+ time.Sleep(delay)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+
+ // Inject error according to configured error rate.
+ if m.errorRate > 0 && rand.Float64() < m.errorRate {
+ writeErrorResponse(w)
+ log.Printf("[mockteerpc] responded with error (took %s)", time.Since(start))
+ return
+ }
+
+ m.mu.RLock()
+ h := m.height
+ m.mu.RUnlock()
+
+ appHash := ComputeAppHash(h)
+ blockHash := ComputeBlockHash(appHash)
+
+ resp := TeeRollupResponse{Code: 0, Message: "OK"}
+ resp.Data.Height = h
+ resp.Data.AppHash = "0x" + hex.EncodeToString(appHash[:])
+ resp.Data.BlockHash = "0x" + hex.EncodeToString(blockHash[:])
+
+ _ = json.NewEncoder(w).Encode(resp)
+ log.Printf("[mockteerpc] responded height=%d appHash=%s (took %s)", h, resp.Data.AppHash[:10]+"...", time.Since(start))
+}
+
+// writeErrorResponse writes one of three error shapes, chosen at random.
+//
+// Type 0: code != 0, no data field.
+// Type 1: code == 0, data is null.
+// Type 2: code == 0, data present but all fields are null.
+func writeErrorResponse(w http.ResponseWriter) {
+ type nullableFields struct {
+ Height *uint64 `json:"height"`
+ AppHash *string `json:"appHash"`
+ BlockHash *string `json:"blockHash"`
+ }
+ // type 0: no data field
+ type respNoData struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ }
+ // type 1 & 2: has data field (null or with null fields)
+ type respWithData struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data *nullableFields `json:"data"`
+ }
+
+ switch rand.Intn(3) {
+ case 0: // code != 0, no data field
+ _ = json.NewEncoder(w).Encode(respNoData{
+ Code: 1,
+ Message: "internal server error",
+ })
+ case 1: // code == 0, data is null
+ _ = json.NewEncoder(w).Encode(respWithData{
+ Code: 0,
+ Message: "OK",
+ Data: nil,
+ })
+ case 2: // code == 0, data present but all fields are null
+ _ = json.NewEncoder(w).Encode(respWithData{
+ Code: 0,
+ Message: "OK",
+ Data: &nullableFields{}, // all pointer fields are nil ā JSON null
+ })
+ }
+}
+
+// ComputeAppHash returns keccak256(big-endian uint64 bytes of height).
+func ComputeAppHash(height uint64) [32]byte {
+ var buf [8]byte
+ binary.BigEndian.PutUint64(buf[:], height)
+ return crypto.Keccak256Hash(buf[:])
+}
+
+// ComputeBlockHash returns keccak256(appHash[:]).
+func ComputeBlockHash(appHash [32]byte) [32]byte {
+ return crypto.Keccak256Hash(appHash[:])
+}
diff --git a/tools/mockteerpc/mock_tee_rollup_server_test.go b/tools/mockteerpc/mock_tee_rollup_server_test.go
new file mode 100644
index 00000000..18e62e77
--- /dev/null
+++ b/tools/mockteerpc/mock_tee_rollup_server_test.go
@@ -0,0 +1,62 @@
+package mockteerpc_test
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "net/http"
+ "testing"
+ "time"
+
+ mockteerpc "github.com/okx/xlayer-toolkit/tools/mockteerpc"
+ "github.com/stretchr/testify/require"
+)
+
+func TestTeeRollupServer_Basic(t *testing.T) {
+ srv := mockteerpc.NewTeeRollupServer(t)
+
+ // --- first request ---
+ resp, err := http.Get(srv.Addr() + "/chain/confirmed_block_info") //nolint:noctx
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var body mockteerpc.TeeRollupResponse
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
+
+ require.Equal(t, 0, body.Code)
+ require.Equal(t, "OK", body.Message)
+ require.GreaterOrEqual(t, body.Data.Height, uint64(1000))
+ require.Equal(t, 66, len(body.Data.AppHash), "appHash should be 0x + 64 hex chars")
+ require.Equal(t, 66, len(body.Data.BlockHash), "blockHash should be 0x + 64 hex chars")
+
+ firstHeight := body.Data.Height
+
+ // --- wait for at least one tick ---
+ time.Sleep(1500 * time.Millisecond)
+
+ resp2, err := http.Get(srv.Addr() + "/chain/confirmed_block_info") //nolint:noctx
+ require.NoError(t, err)
+ defer resp2.Body.Close()
+
+ var body2 mockteerpc.TeeRollupResponse
+ require.NoError(t, json.NewDecoder(resp2.Body).Decode(&body2))
+
+ require.Greater(t, body2.Data.Height, firstHeight, "height should have increased after 1.5s")
+
+ // --- verify CurrentInfo height is >= last observed HTTP height ---
+ h, _, _ := srv.CurrentInfo()
+ require.GreaterOrEqual(t, h, body2.Data.Height,
+ "CurrentInfo height should be >= last HTTP response height")
+
+ // --- verify hash determinism ---
+ appHash := mockteerpc.ComputeAppHash(body2.Data.Height)
+ require.Equal(t, "0x"+hex.EncodeToString(appHash[:]), body2.Data.AppHash)
+ blockHash := mockteerpc.ComputeBlockHash(appHash)
+ require.Equal(t, "0x"+hex.EncodeToString(blockHash[:]), body2.Data.BlockHash)
+}
+
+func TestTeeRollupServer_DoubleClose(t *testing.T) {
+ srv := mockteerpc.NewTeeRollupServer(t)
+ // Explicit close before t.Cleanup runs ā must not panic.
+ require.NotPanics(t, srv.Close)
+}