Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/e2e-kind.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:
- name: RPC suite
script: test_rpc_suite.sh
foundry: true
go: true

steps:
- uses: actions/checkout@v7
Expand All @@ -110,6 +111,14 @@ jobs:
if: matrix.foundry
uses: foundry-rs/foundry-toolchain@v1

# The RPC suite's WS log-subscription test builds a small go-ethereum
# client (e2e/kind/tools/wslogs) on the host to drive eth_subscribe("logs").
- name: Set up Go
if: matrix.go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.25.10"

- name: Run ${{ matrix.name }}
env:
FW_SKIP_BUILD: "true"
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- (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.
- (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.
- (evm) [#291](https://github.com/mocachain/moca/pull/291) Reject native value sent to precompiles to prevent a balance-reconciliation mint.
- (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.
- (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).
- (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.
- (ci) [#65](https://github.com/mocachain/moca/pull/65) Resolve goreleaser CI failures for arm64 docker builds
Expand Down
284 changes: 284 additions & 0 deletions e2e/kind/tests/test_rpc_suite.sh

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions e2e/kind/tools/wslogs/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Command wslogs exercises the eth_subscribe("logs") WebSocket transport
// end-to-end. It dials an EVM JSON-RPC WebSocket endpoint, subscribes to the
// logs of a single contract address, and prints the first matching log as JSON.
//
// It exists so the e2e suite can drive rpc/websockets.go subscribeLogs (which
// rehydrates logs from the finalized block result) over the real WS push path,
// using only the go-ethereum client that moca already depends on — no external
// websocket CLI (websocat/wscat) is installed or required.
//
// Usage:
//
// wslogs <ws-url> <contract-address> [timeoutSeconds]
//
// It prints "SUBSCRIBED" to stderr as soon as the subscription is live. Because
// eth_subscribe is future-only, the caller must emit the log-producing tx only
// after seeing that line. On the first matching log it writes the log as JSON to
// stdout and exits 0; on a subscription error, dial failure, or timeout it writes
// a message to stderr and exits 1.
package main

import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"time"

ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)

const defaultTimeoutSeconds = 40

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "wslogs:", err)
os.Exit(1)
}
}

func run(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: wslogs <ws-url> <contract-address> [timeoutSeconds]")
}
wsURL := args[0]
if !common.IsHexAddress(args[1]) {
return fmt.Errorf("invalid contract address: %q", args[1])
}
addr := common.HexToAddress(args[1])

timeoutSeconds := defaultTimeoutSeconds
if len(args) >= 3 {
n, err := strconv.Atoi(args[2])
if err != nil || n <= 0 {
return fmt.Errorf("invalid timeout seconds: %q", args[2])
}
timeoutSeconds = n
}

ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)
defer cancel()

client, err := ethclient.DialContext(ctx, wsURL)
if err != nil {
return fmt.Errorf("dial %s: %w", wsURL, err)
}
defer client.Close()

query := ethereum.FilterQuery{Addresses: []common.Address{addr}}
logs := make(chan ethtypes.Log)
sub, err := client.SubscribeFilterLogs(ctx, query, logs)
if err != nil {
return fmt.Errorf("subscribe filter logs on %s: %w", wsURL, err)
}
defer sub.Unsubscribe()

// The subscription is now live. Signal the caller so it emits the
// log-producing tx only now — eth_subscribe delivers future logs only.
fmt.Fprintln(os.Stderr, "SUBSCRIBED")

select {
case vLog := <-logs:
out, err := json.Marshal(vLog)
if err != nil {
return fmt.Errorf("marshal log: %w", err)
}
fmt.Println(string(out))
return nil
case err := <-sub.Err():
if err == nil {
return fmt.Errorf("subscription closed before any matching log arrived")
}
return fmt.Errorf("subscription error: %w", err)
case <-ctx.Done():
return fmt.Errorf("timed out after %ds waiting for a matching log", timeoutSeconds)
}
}
12 changes: 3 additions & 9 deletions rpc/backend/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
tmrpctypes "github.com/cometbft/cometbft/rpc/core/types"
"github.com/cometbft/cometbft/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
evmtypes "github.com/cosmos/evm/x/vm/types"
"github.com/ethereum/go-ethereum/common"
"github.com/mocachain/moca/v2/rpc/backend/mocks"
rpc "github.com/mocachain/moca/v2/rpc/types"
Expand Down Expand Up @@ -178,18 +177,13 @@ func TestRegisterConsensusParams(t *testing.T) {

// BlockResults

// RegisterBlockResultsWithEventLog mocks a block result with one tx log in ExecTxResult.Data.
func RegisterBlockResultsWithEventLog(client *mocks.Client, height int64) (*tmrpctypes.ResultBlockResults, error) {
data, _ := buildLogTxResultData(height)
res := &tmrpctypes.ResultBlockResults{
Height: height,
TxsResults: []*abci.ExecTxResult{
{GasUsed: 0, Events: []abci.Event{{
Type: evmtypes.EventTypeEthereumTx,
Attributes: []abci.EventAttribute{{
Key: evmtypes.AttributeKeyTxLog,
Value: "{\"test\": \"hello\"}", // TODO refactor the value to unmarshall to a evmtypes.Log struct successfully
Index: true,
}},
}}},
{GasUsed: 0, Data: data},
},
}
client.On("BlockResults", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).
Expand Down
12 changes: 3 additions & 9 deletions rpc/backend/filters_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package backend

import (
"encoding/json"

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

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

testCases := []struct {
name string
Expand Down Expand Up @@ -70,7 +64,7 @@ func (suite *BackendTestSuite) TestGetLogs() {
suite.Require().NoError(err)
},
common.BytesToHash(block.Hash()),
[][]*ethtypes.Log{evmtypes.LogsToEthereum(logs)},
[][]*ethtypes.Log{expLogs},
true,
},
}
Expand Down
60 changes: 25 additions & 35 deletions rpc/backend/tx_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransac

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

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

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

cumulativeGasUsed := uint64(0)
Expand All @@ -183,31 +177,36 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{
} else {
status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful)
}
chainID, err := b.ChainID()
// GetSenderLegacy recovers sender from sig; historical txs have empty From.
var signer ethtypes.Signer
if ethTx.Protected() {
signer = ethtypes.LatestSignerForChainID(ethTx.ChainId())
} else {
signer = ethtypes.FrontierSigner{}
}
from, err := ethMsg.GetSenderLegacy(signer)
if err != nil {
return nil, err
}

// cosmos/evm v0.6.0: use GetSenderLegacy for txs loaded back from state —
// GetSender() returns the cached From, which is empty for a historical tx
// read from the store, whereas GetSenderLegacy recovers the sender from the
// signature via the signer when From is unset. Matches upstream rpc.
signer := ethtypes.LatestSignerForChainID(chainID.ToInt())
from, err := ethMsg.GetSenderLegacy(signer)
// Decode logs from tx response Data; v0.6.0 dropped per-log cosmos events.
height, err := types.SafeUint64(blockRes.Height)
if err != nil {
return nil, err
}

// parse tx logs from events
// res.MsgIndex is the EVM message's position within the tx. A MsgEthereumTx is
// only ever included in an all-EVM tx (the EVM ante rejects any non-EVM message
// and the cosmos ante's RejectMessagesDecorator rejects MsgEthereumTx elsewhere),
// so this index also matches the position in the MsgEthereumTxResponse slice that
// DecodeMsgLogs indexes into. Mirrors cosmos/evm v0.6.0 rpc/backend.
msgIndex := int(res.MsgIndex) // #nosec G701 -- checked for int overflow already
logs, err := TxLogsFromEvents(blockRes.TxsResults[res.TxIndex].Events, msgIndex)
logs, err := evmtypes.DecodeMsgLogs(blockRes.TxsResults[res.TxIndex].Data, msgIndex, height)
Comment thread
phenix3443 marked this conversation as resolved.
if err != nil {
b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error())
}

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

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

if logs == nil {
receipt["logs"] = [][]*ethtypes.Log{}
receipt["logs"] = []*ethtypes.Log{}
}

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

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

return receipt, nil
}
Expand Down
Loading
Loading