diff --git a/evmrpc/filter_test.go b/evmrpc/filter_test.go index df5daeadad..a7ff2b1323 100644 --- a/evmrpc/filter_test.go +++ b/evmrpc/filter_test.go @@ -199,12 +199,10 @@ func getCommonFilterLogTests() []GetFilterLogTests { } func TestFilterGetLogs(t *testing.T) { - t.Skip() testFilterGetLogs(t, "eth", getCommonFilterLogTests()) } func TestFilterSeiGetLogs(t *testing.T) { - t.Skip() // make sure we pass all the eth_ namespace tests testFilterGetLogs(t, "sei", getCommonFilterLogTests()) @@ -245,7 +243,6 @@ func TestFilterSeiGetLogs(t *testing.T) { } func TestFilterEthEndpointReturnsNormalEvmLogEvenIfSyntheticLogIsInSameBlock(t *testing.T) { - t.Skip() testFilterGetLogs(t, "eth", []GetFilterLogTests{ { name: "normal evm log is returned even if synthetic log is in the same block", @@ -300,7 +297,6 @@ func testFilterGetLogs(t *testing.T, namespace string, tests []GetFilterLogTests } func TestFilterGetFilterLogs(t *testing.T) { - t.Skip() filterCriteria := map[string]interface{}{ "fromBlock": "0x2", "toBlock": "0x2", @@ -324,7 +320,6 @@ func TestFilterGetFilterLogs(t *testing.T) { } func TestFilterGetFilterChanges(t *testing.T) { - t.Skip() filterCriteria := map[string]interface{}{ "fromBlock": "0x2", } @@ -333,8 +328,7 @@ func TestFilterGetFilterChanges(t *testing.T) { resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, filterId) logs := resObj["result"].([]interface{}) - // After tightening block/receipt matching, fromBlock=0x2 now yields 5 logs total - require.Equal(t, 5, len(logs)) + require.Equal(t, 7, len(logs)) logObj := logs[0].(map[string]interface{}) require.Equal(t, "0x2", logObj["blockNumber"].(string)) @@ -417,7 +411,6 @@ func TestFilterGetFilterChangesKeepsFilterAlive(t *testing.T) { } func TestGetLogsBlockHashIsNotZero(t *testing.T) { - t.Skip() t.Parallel() // Test that eth_getLogs returns logs with correct blockHash (not zero hash) filterCriteria := map[string]interface{}{ diff --git a/evmrpc/info_test.go b/evmrpc/info_test.go index bd1660b0a3..2a5dc036c7 100644 --- a/evmrpc/info_test.go +++ b/evmrpc/info_test.go @@ -33,7 +33,7 @@ func newInfoAPIWithWatermarks(ctxProvider func(int64) sdk.Context) *evmrpc.InfoA func TestBlockNumber(t *testing.T) { resObj := sendRequestGood(t, "blockNumber") result := resObj["result"].(string) - require.Equal(t, fmt.Sprintf("0x%x", MockHeight8), result) + require.Equal(t, fmt.Sprintf("0x%x", MockHeight103), result) } func TestChainID(t *testing.T) { @@ -91,13 +91,13 @@ func TestFeeHistory(t *testing.T) { Ctx = Ctx.WithBlockHeight(1) // Simulate context with a specific block height - latestHex := fmt.Sprintf("0x%x", MockHeight8) + latestHex := fmt.Sprintf("0x%x", MockHeight103) testCases := []feeHistoryTestCase{ {name: "Valid request by number", blockCount: 1, lastBlock: latestHex, rewardPercentiles: []interface{}{0.5}, expectedOldest: latestHex}, {name: "Valid request by latest", blockCount: 1, lastBlock: "latest", rewardPercentiles: []interface{}{0.5}, expectedOldest: latestHex}, {name: "Valid request by earliest", blockCount: 1, lastBlock: "earliest", rewardPercentiles: []interface{}{0.5}, expectedOldest: "0x1"}, {name: "Request on the same block", blockCount: 1, lastBlock: "0x1", rewardPercentiles: []interface{}{0.5}, expectedOldest: "0x1"}, - {name: "Request on future block", blockCount: 1, lastBlock: fmt.Sprintf("0x%x", MockHeight8+1), rewardPercentiles: []interface{}{0.5}, expectedError: fmt.Errorf("requested last block %d is not yet available; safe latest is %d", MockHeight8+1, MockHeight8)}, + {name: "Request on future block", blockCount: 1, lastBlock: fmt.Sprintf("0x%x", MockHeight103+1), rewardPercentiles: []interface{}{0.5}, expectedError: fmt.Errorf("requested last block %d is not yet available; safe latest is %d", MockHeight103+1, MockHeight103)}, {name: "Block count truncates", blockCount: 1025, lastBlock: "latest", rewardPercentiles: []interface{}{25}, expectedOldest: "0x1"}, {name: "Too many percentiles", blockCount: 10, lastBlock: "latest", rewardPercentiles: make([]interface{}, 101), expectedError: errors.New("rewardPercentiles length must be less than or equal to 100")}, {name: "Invalid percentiles order", blockCount: 10, lastBlock: "latest", rewardPercentiles: []interface{}{99, 1}, expectedError: errors.New("invalid reward percentiles: must be ascending and between 0 and 100")}, diff --git a/evmrpc/setup_test.go b/evmrpc/setup_test.go index b88fc4272b..d3b5669628 100644 --- a/evmrpc/setup_test.go +++ b/evmrpc/setup_test.go @@ -610,16 +610,20 @@ func init() { _ = store.SetEarliestVersion(1) } ctxProvider := func(height int64) sdk.Context { + // All branches set ClosestUpgradeName so any upgrade-name-sensitive + // gate (e.g. isReceiptFromAnteError) hits the production post-v5.8.0 + // path; see LatestCtxUpgradeName above. if height == MockHeight2 { - return MultiTxCtx.WithIsTracing(true) + return MultiTxCtx.WithIsTracing(true).WithClosestUpgradeName(LatestCtxUpgradeName) } if height == evmrpc.LatestCtxHeight { - // See LatestCtxUpgradeName above — make the latest ctx look - // post-v5.8.0 so any consumer that branches on upgrade name - // sees the production path, not the pre-v5.8.0 fallback. - return baseCtx.WithIsTracing(true).WithClosestUpgradeName(LatestCtxUpgradeName) + // Report MockHeight103 so the WatermarkManager's latest watermark + // covers every block the mocks produce — taking the minimum + // across height sources, a stale height here would cap log + // queries below the mocked blocks. + return baseCtx.WithBlockHeight(MockHeight103).WithIsTracing(true).WithClosestUpgradeName(LatestCtxUpgradeName) } - return Ctx.WithIsTracing(true) + return Ctx.WithIsTracing(true).WithClosestUpgradeName(LatestCtxUpgradeName) } // Start good http server goodConfig := evmrpcconfig.DefaultConfig @@ -1062,7 +1066,13 @@ func setupLogs() { Topics: []string{"0x0000000000000000000000000000000000000000000000000000000000000234", "0x0000000000000000000000000000000000000000000000000000000000000789"}, Synthetic: true, }}, - EffectiveGasPrice: 0, + // Non-zero EffectiveGasPrice avoids the Status==0 && EGP==0 + // ante-error nonce-continuity check in filterTransactions, which + // would otherwise wrongly exclude this synthetic receipt because the + // test sender's nonce state isn't seeded for synthetic-tx ordering. + // The TxType=ShellEVMTxType marker still gates eth-namespace + // inclusion, so eth-side tests keep filtering it out. + EffectiveGasPrice: 100, }) // Also create a normal (non-synthetic) receipt in block 100 with two logs CtxBlock100 := Ctx.WithBlockHeight(MockHeight100) @@ -1082,18 +1092,35 @@ func setupLogs() { EffectiveGasPrice: 100, }) CtxMock = Ctx.WithBlockHeight(MockHeight103) + // Synthetic log at latest (MockHeight103) so default-range eth_getLogs + // queries — which resolve to [latest, latest] — can exercise the + // sei-namespace synthetic-inclusion path. + bloomSynth103 := ethtypes.CreateBloom(ðtypes.Receipt{Logs: []*ethtypes.Log{{ + Address: common.HexToAddress("0x1111111111111111111111111111111111111116"), + Topics: []common.Hash{ + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000234"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000789"), + }, + }}}) EVMKeeper.MockReceipt(CtxMock, common.HexToHash(TestSyntheticTxHash), &types.Receipt{ TxType: types.ShellEVMTxType, BlockNumber: MockHeight103, TransactionIndex: 2, TxHashHex: TestSyntheticTxHash, + LogsBloom: bloomSynth103[:], + Logs: []*types.Log{{ + Address: "0x1111111111111111111111111111111111111116", + Topics: []string{"0x0000000000000000000000000000000000000000000000000000000000000234", "0x0000000000000000000000000000000000000000000000000000000000000789"}, + Synthetic: true, + }}, + // Non-zero EGP so this synthetic receipt isn't excluded by the + // Status==0 && EGP==0 ante-error nonce-continuity gate in + // filterTransactions; see the block-100 synth receipt above for the + // same rationale. + EffectiveGasPrice: 100, }) - CtxDebugTrace := Ctx.WithBlockHeight(MockHeight8) - EVMKeeper.MockReceipt(CtxDebugTrace, common.HexToHash(DebugTraceHashHex), &types.Receipt{ - BlockNumber: MockHeight8, - TransactionIndex: 0, - TxHashHex: DebugTraceHashHex, - }) + // DebugTraceHashHex == tx1.Hash(); tx1's receipt (mocked earlier with full + // Logs) is what debug_traceTransaction and log-filter queries both rely on. CtxDebugTracePanic := Ctx.WithBlockHeight(MockHeight103) EVMKeeper.MockReceipt(CtxDebugTracePanic, common.HexToHash(TestNonPanicTxHash), &types.Receipt{ BlockNumber: MockHeight103, diff --git a/evmrpc/subscribe_test.go b/evmrpc/subscribe_test.go index e3a229c561..8286dc0327 100644 --- a/evmrpc/subscribe_test.go +++ b/evmrpc/subscribe_test.go @@ -187,10 +187,14 @@ func TestSubscribeEmptyLogs(t *testing.T) { } func TestSubscribeNewLogs(t *testing.T) { - t.Skip() + // Query MockHeight8 directly: the mocks place a matching log there + // (tx1's receipt with address 0x1111…1111 and topic 0x1111…1111). + // Using a fixed historical range avoids the FromBlock=0+ToBlock=latest + // rewrite path so this test stays decoupled from "latest" placement; + // TestSubscribeEmptyLogs covers the empty-filter handshake path. data := map[string]interface{}{ - "fromBlock": "0x0", - "toBlock": "latest", + "fromBlock": fmt.Sprintf("0x%x", MockHeight8), + "toBlock": fmt.Sprintf("0x%x", MockHeight8), "address": []common.Address{ common.HexToAddress("0x1111111111111111111111111111111111111111"), }, @@ -233,16 +237,12 @@ func TestSubscribeNewLogs(t *testing.T) { t.Fatal("Subscription ID does not match") } resultMap := paramMap["result"].(map[string]interface{}) - if resultMap["address"] != "0x1111111111111111111111111111111111111111" && resultMap["address"] != "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" { - t.Fatalf("Unexpected address, got %v", resultMap["address"]) - } + require.Equal(t, "0x1111111111111111111111111111111111111111", resultMap["address"]) firstTopic := resultMap["topics"].([]interface{})[0].(string) - if firstTopic != "0x1111111111111111111111111111111111111111111111111111111111111111" { - t.Fatalf("Unexpected topic, got %v", firstTopic) - } + require.Equal(t, "0x1111111111111111111111111111111111111111111111111111111111111111", firstTopic) case <-timer.C: if !receivedSubMsg || !receivedEvents { - t.Fatal("No message received within 5 seconds") + t.Fatal("No subscription ack or log notification within 2 seconds") } return } diff --git a/integration_test/evm_module/scripts/contracts/emitter.sol b/integration_test/evm_module/scripts/contracts/emitter.sol new file mode 100644 index 0000000000..8e91a289f5 --- /dev/null +++ b/integration_test/evm_module/scripts/contracts/emitter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Contract used by the ws_test eth_subscribe(logs) integration test. The +// constructor emits one LOG1 with a fixed 32-byte topic; the deploy tx +// receipt therefore contains exactly one log, whose address equals the +// newly-created contract address. The test subscribes to the deploy +// block + emitter address and asserts the log is delivered over the WS. +// +// emitter_contract.hex is hand-crafted (no solc required). Init code: +// +// 7f<32-byte topic> PUSH32 topic (0x4242…4242, easy to spot in logs) +// 6000 PUSH1 0 ; LOG data size +// 6000 PUSH1 0 ; LOG data offset +// a1 LOG1 +// 6001 PUSH1 1 ; runtime size +// 6000 PUSH1 0 ; runtime offset (memory is zero-init, +// so memory[0] = 0x00 = STOP) +// f3 RETURN +// +// Solidity reference (not compiled — only the .hex is used at deploy time): + +pragma solidity ^0.8.0; + +contract Emitter { + constructor() { + assembly { + log1(0, 0, 0x4242424242424242424242424242424242424242424242424242424242424242) + } + } +} diff --git a/integration_test/evm_module/scripts/contracts/emitter_contract.hex b/integration_test/evm_module/scripts/contracts/emitter_contract.hex new file mode 100644 index 0000000000..ea8b8732a3 --- /dev/null +++ b/integration_test/evm_module/scripts/contracts/emitter_contract.hex @@ -0,0 +1 @@ +7f424242424242424242424242424242424242424242424242424242424242424260006000a160016000f3 diff --git a/integration_test/evm_module/scripts/evm_rpc_tests.sh b/integration_test/evm_module/scripts/evm_rpc_tests.sh index 26505191e1..5a83a7036f 100755 --- a/integration_test/evm_module/scripts/evm_rpc_tests.sh +++ b/integration_test/evm_module/scripts/evm_rpc_tests.sh @@ -11,6 +11,7 @@ RECIPIENT="${SEI_EVM_IO_TX_RECIPIENT:-0xF87A299e6bC7bEba58dbBe5a5Aa21d49bCD16D52 PROJECT_ROOT="${SEI_EVM_IO_PROJECT_ROOT:-/sei-protocol/sei-chain}" CONTRACT_HEX="${PROJECT_ROOT}/integration_test/evm_module/scripts/contracts/minimal_contract.hex" REVERTER_HEX="${PROJECT_ROOT}/integration_test/evm_module/scripts/contracts/reverter_contract.hex" +EMITTER_HEX="${PROJECT_ROOT}/integration_test/evm_module/scripts/contracts/emitter_contract.hex" EVM_RPC_URL="${SEI_EVM_RPC_URL:-http://localhost:8545}" KEYRING_ARGS=() if [[ -n "${SEI_EVM_IO_KEYRING_BACKEND:-}" ]]; then @@ -63,6 +64,31 @@ if [[ -z "${SEI_EVM_IO_REVERTER_ADDRESS:-}" ]]; then echo "WARNING: Reverter contract not deployed (deploy or receipt lookup failed). Tests using __REVERTER__ will be skipped." >&2 fi +# Deploy emitter contract (constructor emits one LOG1 with topic 0x4242…4242); +# export SEI_EVM_WS_EMITTER_ADDRESS + SEI_EVM_WS_EMITTER_BLOCK so the WS +# eth_subscribe(logs) integration test can subscribe to the deploy block + emitter +# address and assert end-to-end log delivery. +docker exec "$CONTAINER" /bin/bash -c "tr -d '[:space:]' < \"$EMITTER_HEX\" > /tmp/emitter_contract.hex" +EMITTER_OUT=$(run seid tx evm deploy /tmp/emitter_contract.hex --from "$FROM" "${KEYRING_ARGS[@]}" --chain-id sei --evm-rpc "$EVM_RPC_URL" -b block -y 2>&1) || true +EMITTER_TX=$(echo "$EMITTER_OUT" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) +if [[ -n "$EMITTER_TX" ]]; then + sleep 2 + for _ in 1 2 3 4 5 6 7 8 9 10; do + ERESP=$(docker exec "$CONTAINER" curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$EMITTER_TX\"]}" "$EVM_RPC_URL" 2>/dev/null) || true + EMITTER_ADDR=$(echo "$ERESP" | grep -o '"contractAddress":"[^"]*"' | head -1 | cut -d'"' -f4) + EMITTER_BLOCK=$(echo "$ERESP" | grep -o '"blockNumber":"[^"]*"' | head -1 | cut -d'"' -f4) + [[ -n "$EMITTER_ADDR" && -n "$EMITTER_BLOCK" ]] && break + sleep 1 + done + if [[ -n "$EMITTER_ADDR" && -n "$EMITTER_BLOCK" ]]; then + export SEI_EVM_WS_EMITTER_ADDRESS="$EMITTER_ADDR" + export SEI_EVM_WS_EMITTER_BLOCK="$EMITTER_BLOCK" + fi +fi +if [[ -z "${SEI_EVM_WS_EMITTER_ADDRESS:-}" || -z "${SEI_EVM_WS_EMITTER_BLOCK:-}" ]]; then + echo "WARNING: Emitter contract not deployed (deploy or receipt lookup failed). eth_subscribe(logs) integration test will be skipped." >&2 +fi + export SEI_EVM_IO_RUN_INTEGRATION=1 go test ./integration_test/evm_module/rpc_io_test/ -v -count=1 diff --git a/integration_test/evm_module/ws_test/ws_test.go b/integration_test/evm_module/ws_test/ws_test.go index 13e9896508..1429cf3e96 100644 --- a/integration_test/evm_module/ws_test/ws_test.go +++ b/integration_test/evm_module/ws_test/ws_test.go @@ -1,19 +1,25 @@ // Package ws_test exercises WebSocket JSON-RPC subscriptions against a // live Sei EVM RPC. The test is consensus-mode agnostic: it dials the -// EVM WS port and asserts that eth_subscribe("newHeads") delivers a -// head notification. It runs under both standard CometBFT clusters and -// Autobahn clusters — the producer hook differs between them (legacy -// event bus vs in-process notifier), but the externally observable -// behaviour must be identical. +// EVM WS port and asserts that eth_subscribe("newHeads") and +// eth_subscribe("logs") deliver notifications. It runs under both +// standard CometBFT clusters and Autobahn clusters — the newHeads +// producer hook differs between them (legacy event bus vs in-process +// notifier), but the externally observable behaviour must be identical. // // Env: // - SEI_EVM_WS_RUN_INTEGRATION=1 to run (set by integration scripts/CI). // Otherwise the test skips so `go test ./...` stays cheap. // - SEI_EVM_WS_URL overrides the default ws://127.0.0.1:8546. +// - SEI_EVM_WS_EMITTER_ADDRESS + SEI_EVM_WS_EMITTER_BLOCK: address + +// deploy block of the emitter contract whose constructor emits one +// LOG1; required by TestEthSubscribeLogs. The integration script +// deploys the contract and exports both vars; missing vars cause the +// logs test to skip rather than fail. package ws_test import ( "os" + "strings" "testing" "time" @@ -107,3 +113,100 @@ func TestEthSubscribeNewHeads(t *testing.T) { } t.Logf("received head: number=%v hash=%v", header["number"], header["hash"]) } + +func TestEthSubscribeLogs(t *testing.T) { + if os.Getenv("SEI_EVM_WS_RUN_INTEGRATION") != "1" { + t.Skip("EVM WS integration tests skipped (set SEI_EVM_WS_RUN_INTEGRATION=1 to run)") + } + emitterAddr := os.Getenv("SEI_EVM_WS_EMITTER_ADDRESS") + emitterBlock := os.Getenv("SEI_EVM_WS_EMITTER_BLOCK") + if emitterAddr == "" || emitterBlock == "" { + t.Skip("emitter contract address/block not set (script-side deploy failed?); skipping eth_subscribe(logs) integration test") + } + + url := wsURL() + conn, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + t.Fatalf("dial %s: %v", url, err) + } + defer conn.Close() + + // Subscribe to logs in the emitter's deploy block. The constructor + // emits one LOG1 with topic 0x4242…4242 from the new contract's + // address, so the deploy block receipt contains exactly one matching + // log. + if err = conn.WriteJSON(map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_subscribe", + "params": []interface{}{ + "logs", + map[string]interface{}{ + "fromBlock": emitterBlock, + "toBlock": emitterBlock, + "address": emitterAddr, + }, + }, + }); err != nil { + t.Fatalf("write subscribe: %v", err) + } + + if err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { + t.Fatalf("set deadline: %v", err) + } + var ack struct { + Result string `json:"result"` + Error map[string]interface{} `json:"error"` + } + if err = conn.ReadJSON(&ack); err != nil { + t.Fatalf("read subscribe ack: %v", err) + } + if ack.Error != nil { + t.Fatalf("subscribe error: %v", ack.Error) + } + if ack.Result == "" { + t.Fatalf("subscribe returned empty subscription id") + } + t.Logf("subscription id: %s", ack.Result) + + // Server polls historical block ranges on the first iteration with no + // leading sleep, so the queued log for the deploy block should arrive + // well within the deadline. + if err = conn.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil { + t.Fatalf("set deadline: %v", err) + } + var note struct { + Method string `json:"method"` + Params struct { + Subscription string `json:"subscription"` + Result map[string]interface{} `json:"result"` + } `json:"params"` + } + if err = conn.ReadJSON(¬e); err != nil { + t.Fatalf("read log notification: %v", err) + } + if note.Method != "eth_subscription" { + t.Fatalf("expected eth_subscription, got %q", note.Method) + } + if note.Params.Subscription != ack.Result { + t.Fatalf("subscription id mismatch: got %q want %q", + note.Params.Subscription, ack.Result) + } + log := note.Params.Result + gotAddr, _ := log["address"].(string) + if !strings.EqualFold(gotAddr, emitterAddr) { + t.Fatalf("log address: got %q want %q", gotAddr, emitterAddr) + } + if gotBlock, _ := log["blockNumber"].(string); !strings.EqualFold(gotBlock, emitterBlock) { + t.Fatalf("log blockNumber: got %q want %q", gotBlock, emitterBlock) + } + topics, ok := log["topics"].([]interface{}) + if !ok || len(topics) != 1 { + t.Fatalf("expected exactly 1 topic, got %+v", log["topics"]) + } + const wantTopic = "0x4242424242424242424242424242424242424242424242424242424242424242" + if gotTopic, _ := topics[0].(string); !strings.EqualFold(gotTopic, wantTopic) { + t.Fatalf("log topic: got %q want %q", gotTopic, wantTopic) + } + t.Logf("received log: addr=%v block=%v topic=%v", log["address"], log["blockNumber"], topics[0]) +}