Skip to content

Commit 8ad01d8

Browse files
authored
Merge pull request #292 from mocachain/fix/rpc-evm-logs
fix(rpc): decode EVM logs from tx data + align receipt/nonce fields with cosmos/evm v0.6.0
2 parents 085fac8 + 4302090 commit 8ad01d8

14 files changed

Lines changed: 706 additions & 147 deletions

File tree

.github/workflows/e2e-kind.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ jobs:
8787
- name: RPC suite
8888
script: test_rpc_suite.sh
8989
foundry: true
90+
go: true
9091

9192
steps:
9293
- uses: actions/checkout@v7
@@ -110,6 +111,14 @@ jobs:
110111
if: matrix.foundry
111112
uses: foundry-rs/foundry-toolchain@v1
112113

114+
# The RPC suite's WS log-subscription test builds a small go-ethereum
115+
# client (e2e/kind/tools/wslogs) on the host to drive eth_subscribe("logs").
116+
- name: Set up Go
117+
if: matrix.go
118+
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
119+
with:
120+
go-version: "1.25.10"
121+
113122
- name: Run ${{ matrix.name }}
114123
env:
115124
FW_SKIP_BUILD: "true"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
7272
- (app/upgrades) [#289](https://github.com/mocachain/moca/pull/289) Pin v2 feemarket `min_gas_price` to 20 gwei (moca's intended floor) so upgraded chains match genesis.
7373
- (evm) [#290](https://github.com/mocachain/moca/pull/290) Restore `CallEVM` error-context wrap (method + contract in error message), fix copy-pasted "evil token" comment in `erc721NonTransferable.go`, update stale geth v1.15→v1.16 comments, remove unreachable `AddBalance` blocks in distribution precompile, fix grammar in precompile Run() default cases, and drop dead test-helper expressions.
7474
- (evm) [#291](https://github.com/mocachain/moca/pull/291) Reject native value sent to precompiles to prevent a balance-reconciliation mint.
75+
- (rpc) [#292](https://github.com/mocachain/moca/pull/292) Decode EVM tx logs from the tx response data (fixes empty `eth_getLogs`, receipt `logs`/`logsBloom`, and log subscriptions) and align receipt signer, `effectiveGasPrice` (now set on all receipt types), and pending-nonce signer with cosmos/evm v0.6.0.
7576
- (x/payment) [#287](https://github.com/mocachain/moca/pull/287) Fix an Int64-overflow panic in the settle-timestamp math of `UpdateStreamRecord`/`TryResumeStreamRecord` (a chain-halt DoS reachable via a large balance + tiny netflow rate): compute in `sdkmath.Int` and bound to int64 range — reject an over-funding user deposit (`ErrSettleTimestampOverflow`), saturate on forced/EndBlocker paths so the chain can't halt (MOCA-385).
7677
- (x/challenge) [#286](https://github.com/mocachain/moca/pull/286) Retire a challenge from the active set once it is attested, making attestation idempotent so duplicate submissions (e.g. redundant relayers or resubmissions by the in-turn submitter) are rejected instead of re-running heartbeat rewards and re-emitting attestation events.
7778
- (ci) [#65](https://github.com/mocachain/moca/pull/65) Resolve goreleaser CI failures for arm64 docker builds

e2e/kind/tests/test_rpc_suite.sh

Lines changed: 284 additions & 0 deletions
Large diffs are not rendered by default.

e2e/kind/tools/wslogs/main.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Command wslogs exercises the eth_subscribe("logs") WebSocket transport
2+
// end-to-end. It dials an EVM JSON-RPC WebSocket endpoint, subscribes to the
3+
// logs of a single contract address, and prints the first matching log as JSON.
4+
//
5+
// It exists so the e2e suite can drive rpc/websockets.go subscribeLogs (which
6+
// rehydrates logs from the finalized block result) over the real WS push path,
7+
// using only the go-ethereum client that moca already depends on — no external
8+
// websocket CLI (websocat/wscat) is installed or required.
9+
//
10+
// Usage:
11+
//
12+
// wslogs <ws-url> <contract-address> [timeoutSeconds]
13+
//
14+
// It prints "SUBSCRIBED" to stderr as soon as the subscription is live. Because
15+
// eth_subscribe is future-only, the caller must emit the log-producing tx only
16+
// after seeing that line. On the first matching log it writes the log as JSON to
17+
// stdout and exits 0; on a subscription error, dial failure, or timeout it writes
18+
// a message to stderr and exits 1.
19+
package main
20+
21+
import (
22+
"context"
23+
"encoding/json"
24+
"fmt"
25+
"os"
26+
"strconv"
27+
"time"
28+
29+
ethereum "github.com/ethereum/go-ethereum"
30+
"github.com/ethereum/go-ethereum/common"
31+
ethtypes "github.com/ethereum/go-ethereum/core/types"
32+
"github.com/ethereum/go-ethereum/ethclient"
33+
)
34+
35+
const defaultTimeoutSeconds = 40
36+
37+
func main() {
38+
if err := run(os.Args[1:]); err != nil {
39+
fmt.Fprintln(os.Stderr, "wslogs:", err)
40+
os.Exit(1)
41+
}
42+
}
43+
44+
func run(args []string) error {
45+
if len(args) < 2 {
46+
return fmt.Errorf("usage: wslogs <ws-url> <contract-address> [timeoutSeconds]")
47+
}
48+
wsURL := args[0]
49+
if !common.IsHexAddress(args[1]) {
50+
return fmt.Errorf("invalid contract address: %q", args[1])
51+
}
52+
addr := common.HexToAddress(args[1])
53+
54+
timeoutSeconds := defaultTimeoutSeconds
55+
if len(args) >= 3 {
56+
n, err := strconv.Atoi(args[2])
57+
if err != nil || n <= 0 {
58+
return fmt.Errorf("invalid timeout seconds: %q", args[2])
59+
}
60+
timeoutSeconds = n
61+
}
62+
63+
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)
64+
defer cancel()
65+
66+
client, err := ethclient.DialContext(ctx, wsURL)
67+
if err != nil {
68+
return fmt.Errorf("dial %s: %w", wsURL, err)
69+
}
70+
defer client.Close()
71+
72+
query := ethereum.FilterQuery{Addresses: []common.Address{addr}}
73+
logs := make(chan ethtypes.Log)
74+
sub, err := client.SubscribeFilterLogs(ctx, query, logs)
75+
if err != nil {
76+
return fmt.Errorf("subscribe filter logs on %s: %w", wsURL, err)
77+
}
78+
defer sub.Unsubscribe()
79+
80+
// The subscription is now live. Signal the caller so it emits the
81+
// log-producing tx only now — eth_subscribe delivers future logs only.
82+
fmt.Fprintln(os.Stderr, "SUBSCRIBED")
83+
84+
select {
85+
case vLog := <-logs:
86+
out, err := json.Marshal(vLog)
87+
if err != nil {
88+
return fmt.Errorf("marshal log: %w", err)
89+
}
90+
fmt.Println(string(out))
91+
return nil
92+
case err := <-sub.Err():
93+
if err == nil {
94+
return fmt.Errorf("subscription closed before any matching log arrived")
95+
}
96+
return fmt.Errorf("subscription error: %w", err)
97+
case <-ctx.Done():
98+
return fmt.Errorf("timed out after %ds waiting for a matching log", timeoutSeconds)
99+
}
100+
}

rpc/backend/client_test.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
tmrpctypes "github.com/cometbft/cometbft/rpc/core/types"
1515
"github.com/cometbft/cometbft/types"
1616
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
17-
evmtypes "github.com/cosmos/evm/x/vm/types"
1817
"github.com/ethereum/go-ethereum/common"
1918
"github.com/mocachain/moca/v2/rpc/backend/mocks"
2019
rpc "github.com/mocachain/moca/v2/rpc/types"
@@ -178,18 +177,13 @@ func TestRegisterConsensusParams(t *testing.T) {
178177

179178
// BlockResults
180179

180+
// RegisterBlockResultsWithEventLog mocks a block result with one tx log in ExecTxResult.Data.
181181
func RegisterBlockResultsWithEventLog(client *mocks.Client, height int64) (*tmrpctypes.ResultBlockResults, error) {
182+
data, _ := buildLogTxResultData(height)
182183
res := &tmrpctypes.ResultBlockResults{
183184
Height: height,
184185
TxsResults: []*abci.ExecTxResult{
185-
{GasUsed: 0, Events: []abci.Event{{
186-
Type: evmtypes.EventTypeEthereumTx,
187-
Attributes: []abci.EventAttribute{{
188-
Key: evmtypes.AttributeKeyTxLog,
189-
Value: "{\"test\": \"hello\"}", // TODO refactor the value to unmarshall to a evmtypes.Log struct successfully
190-
Index: true,
191-
}},
192-
}}},
186+
{GasUsed: 0, Data: data},
193187
},
194188
}
195189
client.On("BlockResults", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).

rpc/backend/filters_test.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package backend
22

33
import (
4-
"encoding/json"
5-
64
tmtypes "github.com/cometbft/cometbft/types"
7-
evmtypes "github.com/cosmos/evm/x/vm/types"
85
"github.com/ethereum/go-ethereum/common"
96
ethtypes "github.com/ethereum/go-ethereum/core/types"
107
"github.com/mocachain/moca/v2/rpc/backend/mocks"
@@ -14,12 +11,9 @@ import (
1411
func (suite *BackendTestSuite) TestGetLogs() {
1512
_, bz := suite.buildEthereumTx()
1613
block := tmtypes.MakeBlock(1, []tmtypes.Tx{bz}, nil, nil)
17-
logs := make([]*evmtypes.Log, 0, 1)
18-
var log evmtypes.Log
19-
err := json.Unmarshal([]byte("{\"test\": \"hello\"}"), &log) // TODO refactor this to unmarshall to a log struct successfully
20-
suite.Require().NoError(err)
2114

22-
logs = append(logs, &log)
15+
// logs come from tx response Data; see buildLogTxResultData.
16+
_, expLogs := buildLogTxResultData(ethrpc.BlockNumber(1).Int64())
2317

2418
testCases := []struct {
2519
name string
@@ -70,7 +64,7 @@ func (suite *BackendTestSuite) TestGetLogs() {
7064
suite.Require().NoError(err)
7165
},
7266
common.BytesToHash(block.Hash()),
73-
[][]*ethtypes.Log{evmtypes.LogsToEthereum(logs)},
67+
[][]*ethtypes.Log{expLogs},
7468
true,
7569
},
7670
}

rpc/backend/tx_info.go

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransac
5353

5454
if res.EthTxIndex == -1 {
5555
// Fallback to find tx index by iterating all valid eth transactions.
56-
// cosmos/evm v0.6.0: MsgEthereumTx.Hash is a method (common.Hash);
57-
// compare its hex against the lookup key.
5856
msgs := b.EthMsgsFromTendermintBlock(block, blockRes)
5957
for i := range msgs {
6058
if msgs[i].Hash().Hex() == hexTx {
@@ -106,7 +104,6 @@ func (b *Backend) getTransactionByHashPending(txHash common.Hash) (*rpctypes.RPC
106104
continue
107105
}
108106

109-
// cosmos/evm v0.6.0: Hash is a method now.
110107
if msg.Hash().Hex() == hexTx {
111108
// use zero block values since it's not included in a block yet
112109
rpcTx, err := rpctypes.NewTransactionFromMsg(
@@ -161,9 +158,6 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{
161158
}
162159
ethMsg := tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx)
163160

164-
// cosmos/evm v0.6.0 retired evmtypes.UnpackTxData and the MsgEthereumTx.Data
165-
// field; the raw tx is now accessed via MsgEthereumTx.AsTransaction() which
166-
// exposes equivalent accessors (GasPrice, Gas, To, Nonce, Type).
167161
ethTx := ethMsg.AsTransaction()
168162

169163
cumulativeGasUsed := uint64(0)
@@ -183,31 +177,36 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{
183177
} else {
184178
status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful)
185179
}
186-
chainID, err := b.ChainID()
180+
// GetSenderLegacy recovers sender from sig; historical txs have empty From.
181+
var signer ethtypes.Signer
182+
if ethTx.Protected() {
183+
signer = ethtypes.LatestSignerForChainID(ethTx.ChainId())
184+
} else {
185+
signer = ethtypes.FrontierSigner{}
186+
}
187+
from, err := ethMsg.GetSenderLegacy(signer)
187188
if err != nil {
188189
return nil, err
189190
}
190191

191-
// cosmos/evm v0.6.0: use GetSenderLegacy for txs loaded back from state —
192-
// GetSender() returns the cached From, which is empty for a historical tx
193-
// read from the store, whereas GetSenderLegacy recovers the sender from the
194-
// signature via the signer when From is unset. Matches upstream rpc.
195-
signer := ethtypes.LatestSignerForChainID(chainID.ToInt())
196-
from, err := ethMsg.GetSenderLegacy(signer)
192+
// Decode logs from tx response Data; v0.6.0 dropped per-log cosmos events.
193+
height, err := types.SafeUint64(blockRes.Height)
197194
if err != nil {
198195
return nil, err
199196
}
200-
201-
// parse tx logs from events
197+
// res.MsgIndex is the EVM message's position within the tx. A MsgEthereumTx is
198+
// only ever included in an all-EVM tx (the EVM ante rejects any non-EVM message
199+
// and the cosmos ante's RejectMessagesDecorator rejects MsgEthereumTx elsewhere),
200+
// so this index also matches the position in the MsgEthereumTxResponse slice that
201+
// DecodeMsgLogs indexes into. Mirrors cosmos/evm v0.6.0 rpc/backend.
202202
msgIndex := int(res.MsgIndex) // #nosec G701 -- checked for int overflow already
203-
logs, err := TxLogsFromEvents(blockRes.TxsResults[res.TxIndex].Events, msgIndex)
203+
logs, err := evmtypes.DecodeMsgLogs(blockRes.TxsResults[res.TxIndex].Data, msgIndex, height)
204204
if err != nil {
205205
b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error())
206206
}
207207

208208
if res.EthTxIndex == -1 {
209209
// Fallback to find tx index by iterating all valid eth transactions.
210-
// cosmos/evm v0.6.0: MsgEthereumTx.Hash is a method.
211210
msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes)
212211
for i := range msgs {
213212
if msgs[i].Hash().Hex() == hexTx {
@@ -225,9 +224,7 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{
225224
// Consensus fields: These fields are defined by the Yellow Paper
226225
"status": status,
227226
"cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed),
228-
// geth v1.15 removed ethtypes.LogsBloom([]*Log); the modern API
229-
// computes bloom from a single *Receipt. Wrap our logs in a transient
230-
// receipt so we get the same bytes as before.
227+
// geth v1.15: CreateBloom takes *Receipt; wrap logs accordingly.
231228
"logsBloom": ethtypes.CreateBloom(&ethtypes.Receipt{Logs: logs}),
232229
"logs": logs,
233230

@@ -250,38 +247,31 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{
250247
}
251248

252249
if logs == nil {
253-
receipt["logs"] = [][]*ethtypes.Log{}
250+
receipt["logs"] = []*ethtypes.Log{}
254251
}
255252

256-
// If the To address is nil it's a contract creation; the contract
257-
// address is deterministic from sender + nonce.
253+
// nil To = contract creation.
258254
if ethTx.To() == nil {
259255
receipt["contractAddress"] = crypto.CreateAddress(from, ethTx.Nonce())
260256
}
261257

262-
// cosmos/evm v0.6.0 retired the *evmtypes.DynamicFeeTx wrapper; identify
263-
// dynamic-fee txs via the underlying tx type and compute the effective
264-
// gas price directly from the geth tx accessors.
258+
// effectiveGasPrice is required for all tx types (JSON-RPC spec).
259+
effectiveGasPrice := ethTx.GasPrice()
265260
if ethTx.Type() == ethtypes.DynamicFeeTxType {
266261
baseFee, err := b.BaseFee(blockRes)
267262
if err != nil {
268-
// tolerate the error for pruned node.
263+
// tolerate the error for pruned node; fall back to the gas price.
269264
b.logger.Error("fetch baseFee failed, node is pruned?", "height", res.Height, "error", err)
270265
} else {
271-
// effective gas price = baseFee + min(gasTipCap, gasFeeCap - baseFee)
272-
// geth v1.15: EffectiveGasTipValue was renamed EffectiveGasTip
273-
// and returns (*big.Int, error). The error covers the underpriced
274-
// case (basefee > gasFeeCap) which we already filtered upstream
275-
// in the antehandler, so it's safe to ignore for receipt
276-
// formatting.
266+
// underpriced error ignored; antehandler already rejects these txs.
277267
tip, _ := ethTx.EffectiveGasTip(baseFee)
278268
if tip == nil {
279269
tip = new(big.Int)
280270
}
281-
effective := new(big.Int).Add(baseFee, tip)
282-
receipt["effectiveGasPrice"] = hexutil.Big(*effective)
271+
effectiveGasPrice = new(big.Int).Add(baseFee, tip)
283272
}
284273
}
274+
receipt["effectiveGasPrice"] = hexutil.Big(*effectiveGasPrice)
285275

286276
return receipt, nil
287277
}

0 commit comments

Comments
 (0)