11#! /usr/bin/env bash
2- # Trace Call Comparison Test - Compares Forest's trace_call with Anvil's debug_traceCall
3- # Usage: ./trace_call_integration_test.sh [--deploy] [--verbose]
42set -e
53
6- # --- Parse Flags ---
74DEPLOY_CONTRACT=false
85VERBOSE=false
96while [[ $# -gt 0 ]]; do
1714# --- Configuration ---
1815FOREST_RPC_URL=" ${FOREST_RPC_URL:- http:// localhost: 2345/ rpc/ v1} "
1916ANVIL_RPC_URL=" ${ANVIL_RPC_URL:- http:// localhost: 8545} "
20- FOREST_ACCOUNT=" ${FOREST_ACCOUNT:- " 0xb7aa1e9c847cda5f60f1ae6f65c3eae44848d41f" } "
21- FOREST_CONTRACT=" ${FOREST_CONTRACT:- " 0x8724d2eb7f86ebaef34e050b02fac6c268e56775 " } "
22- ANVIL_ACCOUNT=" ${ANVIL_ACCOUNT:- " 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" } "
23- ANVIL_CONTRACT=" ${ANVIL_CONTRACT:- " 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 " } "
24- ANVIL_PRIVATE_KEY=" ${ANVIL_PRIVATE_KEY:- " " } "
17+ FOREST_ACCOUNT=" ${FOREST_ACCOUNT:- 0xb7aa1e9c847cda5f60f1ae6f65c3eae44848d41f} "
18+ FOREST_CONTRACT=" ${FOREST_CONTRACT:- 0x73a43475aa2ccb14246613708b399f4b2ba546c7 } "
19+ ANVIL_ACCOUNT=" ${ANVIL_ACCOUNT:- 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266} "
20+ ANVIL_CONTRACT=" ${ANVIL_CONTRACT:- 0x5FbDB2315678afecb367f032d93F642f64180aa3 } "
21+ ANVIL_PRIVATE_KEY=" ${ANVIL_PRIVATE_KEY:- 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 } "
2522
2623GREEN=' \033[0;32m' RED=' \033[0;31m' BLUE=' \033[0;34m' YELLOW=' \033[0;33m' NC=' \033[0m'
2724PASS_COUNT=0 FAIL_COUNT=0
2825
29- # --- Dependency Check ---
3026command -v jq & > /dev/null || { echo " Error: jq is required" ; exit 1; }
3127command -v curl & > /dev/null || { echo " Error: curl is required" ; exit 1; }
3228
33- # --- Unified RPC Dispatcher ---
34- # Single entry point for all RPC calls - removes JSON-RPC boilerplate from test logic
3529call_rpc () {
3630 local url=" $1 " method=" $2 " params=" $3 "
3731 curl -s -X POST " $url " \
3832 -H " Content-Type: application/json" \
3933 -d " {\" jsonrpc\" :\" 2.0\" ,\" id\" :1,\" method\" :\" $method \" ,\" params\" :$params }"
4034}
4135
42- # --- RPC Health Check ---
4336check_rpc () {
4437 local name=" $1 " url=" $2 "
4538 local resp=$( call_rpc " $url " " eth_chainId" " []" )
@@ -53,7 +46,6 @@ check_rpc() {
5346check_rpc " Forest" " $FOREST_RPC_URL " || exit 1
5447check_rpc " Anvil" " $ANVIL_RPC_URL " || exit 1
5548
56- # --- Deploy Contract (if requested) ---
5749if [ " $DEPLOY_CONTRACT " = true ]; then
5850 command -v forge & > /dev/null || { echo " Error: forge is required for --deploy" ; exit 1; }
5951 echo -e " ${YELLOW} Deploying Tracer contract on Anvil...${NC} "
@@ -65,26 +57,16 @@ if [ "$DEPLOY_CONTRACT" = true ]; then
6557 echo -e " Deployed to: ${GREEN} $ANVIL_CONTRACT ${NC} "
6658fi
6759
68- # --- Normalization Helpers ---
69- # Convert different node outputs into a standard format for comparison
70-
71- # Normalize empty values: null, "", "0x" -> "0x"
7260normalize_empty () {
7361 local val=" $1 "
7462 [[ " $val " == " null" || -z " $val " ]] && echo " 0x" || echo " $val "
7563}
7664
77- # Get balance change type from Forest's Parity Delta format
78- # Returns: "unchanged", "changed", "added", or "removed"
79- get_balance_type () {
65+ get_delta_type () {
8066 local val=" $1 "
81- # Handle unchanged cases
8267 if [[ " $val " == " =" || " $val " == " \" =\" " || " $val " == " null" || -z " $val " ]]; then
8368 echo " unchanged"
84- return
85- fi
86- # Check for Delta types
87- if echo " $val " | jq -e ' has("*")' & > /dev/null; then
69+ elif echo " $val " | jq -e ' has("*")' & > /dev/null; then
8870 echo " changed"
8971 elif echo " $val " | jq -e ' has("+")' & > /dev/null; then
9072 echo " added"
@@ -97,8 +79,6 @@ get_balance_type() {
9779
9880assert_eq () {
9981 local label=" $1 " f_val=" $2 " a_val=" $3 "
100-
101- # Normalize: lowercase and treat null/0x/empty as equivalent
10282 local f_norm=$( echo " $f_val " | tr ' [:upper:]' ' [:lower:]' )
10383 local a_norm=$( echo " $a_val " | tr ' [:upper:]' ' [:lower:]' )
10484 [[ " $f_norm " == " null" || -z " $f_norm " ]] && f_norm=" 0x"
@@ -116,36 +96,30 @@ assert_eq() {
11696assert_both_have_error () {
11797 local f_err=" $1 " a_err=" $2 "
11898 if [[ -n " $f_err " && " $f_err " != " null" ]] && [[ -n " $a_err " && " $a_err " != " null" ]]; then
119- echo -e " ${GREEN} [PASS]${NC} Error: both have error"
99+ echo -e " ${GREEN} [PASS]${NC} Both have error"
120100 PASS_COUNT=$(( PASS_COUNT + 1 ))
121101 else
122- echo -e " ${RED} [FAIL]${NC} Error: (Forest: '$f_err ' | Anvil: '$a_err ')"
102+ echo -e " ${RED} [FAIL]${NC} Error mismatch (Forest: '$f_err ' | Anvil: '$a_err ')"
123103 FAIL_COUNT=$(( FAIL_COUNT + 1 ))
124104 fi
125105}
126106
127- # Compares Forest's trace_call [trace] with Anvil's debug_traceCall (callTracer)
128- # Types: "standard" (default), "revert", "deep"
129107test_trace () {
130108 local name=" $1 " data=" $2 " type=" ${3:- standard} "
131109 echo -e " ${BLUE} --- $name ---${NC} "
132110
133- # Forest: trace_call with trace
134111 local f_params=" [{\" from\" :\" $FOREST_ACCOUNT \" ,\" to\" :\" $FOREST_CONTRACT \" ,\" data\" :\" $data \" },[\" trace\" ],\" latest\" ]"
135112 local f_resp=$( call_rpc " $FOREST_RPC_URL " " trace_call" " $f_params " )
136113
137- # Anvil: debug_traceCall with callTracer
138114 local a_params=" [{\" from\" :\" $ANVIL_ACCOUNT \" ,\" to\" :\" $ANVIL_CONTRACT \" ,\" data\" :\" $data \" },\" latest\" ,{\" tracer\" :\" callTracer\" }]"
139115 local a_resp=$( call_rpc " $ANVIL_RPC_URL " " debug_traceCall" " $a_params " )
140116
141117 [[ " $VERBOSE " = true ]] && echo -e " ${YELLOW} Forest:${NC} $f_resp \n${YELLOW} Anvil:${NC} $a_resp "
142118
143- # Extract & compare input (common to all types)
144119 local f_input=$( echo " $f_resp " | jq -r ' .result.trace[0].action.input' )
145120 local a_input=$( echo " $a_resp " | jq -r ' .result.input' )
146121 assert_eq " Input" " $f_input " " $a_input "
147122
148- # Type-specific comparisons
149123 case $type in
150124 revert)
151125 local f_err=$( echo " $f_resp " | jq -r ' .result.trace[0].error // empty' )
@@ -169,42 +143,114 @@ test_trace() {
169143 echo " "
170144}
171145
172- # Compares Forest's trace_call [stateDiff] with Anvil's prestateTracer (diffMode)
173- test_state_diff () {
146+ test_balance_diff () {
174147 local name=" $1 " data=" $2 " value=" ${3:- 0x0} " expect=" ${4:- unchanged} "
175- echo -e " ${BLUE} --- $name (stateDiff ) ---${NC} "
148+ echo -e " ${BLUE} --- $name (balance ) ---${NC} "
176149
177- # Forest: trace_call with stateDiff
178150 local f_params=" [{\" from\" :\" $FOREST_ACCOUNT \" ,\" to\" :\" $FOREST_CONTRACT \" ,\" data\" :\" $data \" ,\" value\" :\" $value \" },[\" stateDiff\" ],\" latest\" ]"
179151 local f_resp=$( call_rpc " $FOREST_RPC_URL " " trace_call" " $f_params " )
180152
181- # Anvil: prestateTracer with diffMode
182153 local a_params=" [{\" from\" :\" $ANVIL_ACCOUNT \" ,\" to\" :\" $ANVIL_CONTRACT \" ,\" data\" :\" $data \" ,\" value\" :\" $value \" },\" latest\" ,{\" tracer\" :\" prestateTracer\" ,\" tracerConfig\" :{\" diffMode\" :true}}]"
183154 local a_resp=$( call_rpc " $ANVIL_RPC_URL " " debug_traceCall" " $a_params " )
184155
185156 [[ " $VERBOSE " = true ]] && echo -e " ${YELLOW} Forest:${NC} $f_resp \n${YELLOW} Anvil:${NC} $a_resp "
186157
187- # Extract contract addresses (lowercase for jq lookup)
188158 local f_contract_lower=$( echo " $FOREST_CONTRACT " | tr ' [:upper:]' ' [:lower:]' )
189159 local a_contract_lower=$( echo " $ANVIL_CONTRACT " | tr ' [:upper:]' ' [:lower:]' )
190160
191- # Extract Forest stateDiff balance
192- local f_diff=$( echo " $f_resp " | jq ' .result.stateDiff // {}' )
193- local f_bal=$( echo " $f_diff " | jq -r --arg a " $f_contract_lower " ' .[$a].balance // "="' )
194- local f_type=$( get_balance_type " $f_bal " )
161+ local f_bal=$( echo " $f_resp " | jq -r --arg a " $f_contract_lower " ' .result.stateDiff[$a].balance // "="' )
162+ local f_type=$( get_delta_type " $f_bal " )
195163
196- # Extract Anvil pre/post balance and determine change type
197164 local a_pre_bal=$( echo " $a_resp " | jq -r --arg a " $a_contract_lower " ' .result.pre[$a].balance // "0x0"' )
198165 local a_post_bal=$( echo " $a_resp " | jq -r --arg a " $a_contract_lower " ' .result.post[$a].balance // "0x0"' )
199166 local a_type=" unchanged"
200167 [[ " $a_pre_bal " != " $a_post_bal " ]] && a_type=" changed"
201168
202- # Semantic assertions - compare intent, not raw values
203- assert_eq " Forest matches Expected" " $f_type " " $expect "
204- assert_eq " Forest matches Anvil" " $f_type " " $a_type "
169+ assert_eq " BalanceChange" " $f_type " " $expect "
170+ assert_eq " ForestMatchesAnvil" " $f_type " " $a_type "
205171 echo " "
206172}
207173
174+ test_storage_diff () {
175+ local name=" $1 " data=" $2 " slot=" $3 " expect_type=" ${4:- changed} "
176+ echo -e " ${BLUE} --- $name (storage) ---${NC} "
177+
178+ local f_params=" [{\" from\" :\" $FOREST_ACCOUNT \" ,\" to\" :\" $FOREST_CONTRACT \" ,\" data\" :\" $data \" },[\" stateDiff\" ],\" latest\" ]"
179+ local f_resp=$( call_rpc " $FOREST_RPC_URL " " trace_call" " $f_params " )
180+
181+ local a_params=" [{\" from\" :\" $ANVIL_ACCOUNT \" ,\" to\" :\" $ANVIL_CONTRACT \" ,\" data\" :\" $data \" },\" latest\" ,{\" tracer\" :\" prestateTracer\" ,\" tracerConfig\" :{\" diffMode\" :true}}]"
182+ local a_resp=$( call_rpc " $ANVIL_RPC_URL " " debug_traceCall" " $a_params " )
183+
184+ [[ " $VERBOSE " = true ]] && echo -e " ${YELLOW} Forest:${NC} $f_resp \n${YELLOW} Anvil:${NC} $a_resp "
185+
186+ local f_contract_lower=$( echo " $FOREST_CONTRACT " | tr ' [:upper:]' ' [:lower:]' )
187+ local a_contract_lower=$( echo " $ANVIL_CONTRACT " | tr ' [:upper:]' ' [:lower:]' )
188+
189+ # Forest: Extract storage slot delta and values
190+ local f_slot_data=$( echo " $f_resp " | jq -r --arg a " $f_contract_lower " --arg s " $slot " ' .result.stateDiff[$a].storage[$s] // null' )
191+ local f_type=$( get_delta_type " $f_slot_data " )
192+ local f_to_val=" "
193+ if [ " $f_type " = " changed" ]; then
194+ f_to_val=$( echo " $f_slot_data " | jq -r ' .["*"].to // empty' )
195+ elif [ " $f_type " = " added" ]; then
196+ f_to_val=$( echo " $f_slot_data " | jq -r ' .["+"] // empty' )
197+ fi
198+
199+ # Anvil: Extract storage slot pre/post values
200+ local a_pre_val=$( echo " $a_resp " | jq -r --arg a " $a_contract_lower " --arg s " $slot " ' .result.pre[$a].storage[$s] // "0x0"' )
201+ local a_post_val=$( echo " $a_resp " | jq -r --arg a " $a_contract_lower " --arg s " $slot " ' .result.post[$a].storage[$s] // "0x0"' )
202+ local a_type=" unchanged"
203+ if [[ " $a_pre_val " == " 0x0" || " $a_pre_val " == " null" ]] && [[ " $a_post_val " != " 0x0" && " $a_post_val " != " null" ]]; then
204+ a_type=" changed" # Added (was zero, now non-zero)
205+ elif [[ " $a_pre_val " != " 0x0" && " $a_pre_val " != " null" ]] && [[ " $a_post_val " != " $a_pre_val " ]]; then
206+ a_type=" changed" # Modified
207+ fi
208+
209+ assert_eq " StorageChangeType" " $f_type " " $expect_type "
210+ assert_eq " ForestMatchesAnvil" " $f_type " " $a_type "
211+
212+ # Compare actual values if both have the slot
213+ if [[ -n " $f_to_val " && -n " $a_post_val " && " $a_post_val " != " null" ]]; then
214+ assert_eq " StorageValue" " $f_to_val " " $a_post_val "
215+ fi
216+ echo " "
217+ }
218+
219+ test_storage_multiple () {
220+ local name=" $1 " data=" $2 "
221+ shift 2
222+ local slots=(" $@ " )
223+ echo -e " ${BLUE} --- $name (multi-storage) ---${NC} "
224+
225+ local f_params=" [{\" from\" :\" $FOREST_ACCOUNT \" ,\" to\" :\" $FOREST_CONTRACT \" ,\" data\" :\" $data \" },[\" stateDiff\" ],\" latest\" ]"
226+ local f_resp=$( call_rpc " $FOREST_RPC_URL " " trace_call" " $f_params " )
227+
228+ local a_params=" [{\" from\" :\" $ANVIL_ACCOUNT \" ,\" to\" :\" $ANVIL_CONTRACT \" ,\" data\" :\" $data \" },\" latest\" ,{\" tracer\" :\" prestateTracer\" ,\" tracerConfig\" :{\" diffMode\" :true}}]"
229+ local a_resp=$( call_rpc " $ANVIL_RPC_URL " " debug_traceCall" " $a_params " )
230+
231+ [[ " $VERBOSE " = true ]] && echo -e " ${YELLOW} Forest:${NC} $f_resp \n${YELLOW} Anvil:${NC} $a_resp "
232+
233+ local f_contract_lower=$( echo " $FOREST_CONTRACT " | tr ' [:upper:]' ' [:lower:]' )
234+ local a_contract_lower=$( echo " $ANVIL_CONTRACT " | tr ' [:upper:]' ' [:lower:]' )
235+
236+ local f_slot_count=$( echo " $f_resp " | jq -r --arg a " $f_contract_lower " ' .result.stateDiff[$a].storage | length' )
237+ local a_slot_count=$( echo " $a_resp " | jq -r --arg a " $a_contract_lower " ' .result.post[$a].storage | length' )
238+ assert_eq " SlotCount" " $f_slot_count " " $a_slot_count "
239+
240+ for slot in " ${slots[@]} " ; do
241+ local f_slot_data=$( echo " $f_resp " | jq -r --arg a " $f_contract_lower " --arg s " $slot " ' .result.stateDiff[$a].storage[$s] // null' )
242+ local f_to_val=$( echo " $f_slot_data " | jq -r ' .["*"].to // .["+"] // empty' )
243+ local a_post_val=$( echo " $a_resp " | jq -r --arg a " $a_contract_lower " --arg s " $slot " ' .result.post[$a].storage[$s] // empty' )
244+
245+ local slot_short=" ${slot: -4} "
246+ assert_eq " Slot$slot_short " " $f_to_val " " $a_post_val "
247+ done
248+ echo " "
249+ }
250+
251+ # =============================================================================
252+ # Main Execution
253+ # =============================================================================
208254echo " =============================================="
209255echo " Trace Call Comparison: Forest vs Anvil"
210256echo " =============================================="
@@ -233,20 +279,43 @@ test_trace "deepTrace(3)" \
233279 " 0x0f3a17b80000000000000000000000000000000000000000000000000000000000000003" \
234280 " deep"
235281
236- # --- StateDiff Tests ---
237- echo -e " ${BLUE} === StateDiff Tests ===${NC} "
282+ # --- Balance Diff Tests ---
283+ echo -e " ${BLUE} === Balance Diff Tests ===${NC} "
238284echo " "
239285
240- test_state_diff " deposit() with 1 ETH" \
286+ test_balance_diff " deposit() with 1 ETH" \
241287 " 0xd0e30db0" \
242288 " 0xde0b6b3a7640000" \
243289 " changed"
244290
245- test_state_diff " setX(42) no value" \
291+ test_balance_diff " setX(42) no value" \
246292 " 0x4018d9aa000000000000000000000000000000000000000000000000000000000000002a" \
247293 " 0x0" \
248294 " unchanged"
249295
296+ # --- Storage Diff Tests ---
297+ echo -e " ${BLUE} === Storage Diff Tests ===${NC} "
298+ echo " "
299+
300+ # Slot 0: x variable (initialized to 42)
301+ test_storage_diff " setX(123) - change slot 0" \
302+ " 0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" \
303+ " 0x0000000000000000000000000000000000000000000000000000000000000000" \
304+ " changed"
305+
306+ # Slot 2: storageTestA (starts empty)
307+ test_storage_diff " storageAdd(100) - add slot 2" \
308+ " 0x55cb64b40000000000000000000000000000000000000000000000000000000000000064" \
309+ " 0x0000000000000000000000000000000000000000000000000000000000000002" \
310+ " changed"
311+
312+ # Multiple slots: storageTestA(10), storageTestB(20), storageTestC(30)
313+ test_storage_multiple " storageMultiple(10,20,30) - slots 2,3,4" \
314+ " 0x310af204000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e" \
315+ " 0x0000000000000000000000000000000000000000000000000000000000000002" \
316+ " 0x0000000000000000000000000000000000000000000000000000000000000003" \
317+ " 0x0000000000000000000000000000000000000000000000000000000000000004"
318+
250319# --- Results ---
251320echo " =============================================="
252321echo -e " Results: ${GREEN} Passed: $PASS_COUNT ${NC} | ${RED} Failed: $FAIL_COUNT ${NC} "
0 commit comments