package requests import ( "encoding/json" "fmt" "strconv" "strings" libcommon "github.com/ledgerwatch/erigon-lib/common" "github.com/ledgerwatch/erigon-lib/common/hexutility" "github.com/ledgerwatch/erigon/common/hexutil" "github.com/ledgerwatch/log/v3" ) // Log represents a contract log event. These events are generated by the LOG opcode and // stored/indexed by the node. type Log struct { //nolint // Consensus fields: // address of the contract that generated the event Address libcommon.Address `json:"address" gencodec:"required"` // list of topics provided by the contract. Topics []libcommon.Hash `json:"topics" gencodec:"required"` // supplied by the contract, usually ABI-encoded Data hexutility.Bytes `json:"data" gencodec:"required"` // Derived fields. These fields are filled in by the node // but not secured by consensus. // block in which the transaction was included BlockNumber hexutil.Uint64 `json:"blockNumber"` // hash of the transaction TxHash libcommon.Hash `json:"transactionHash" gencodec:"required"` // index of the transaction in the block TxIndex hexutil.Uint `json:"transactionIndex" gencodec:"required"` // hash of the block in which the transaction was included BlockHash libcommon.Hash `json:"blockHash"` // index of the log in the receipt Index hexutil.Uint `json:"logIndex" gencodec:"required"` // The Removed field is true if this log was reverted due to a chain reorganisation. // You must pay attention to this field if you receive logs through a filter query. Removed bool `json:"removed"` } type EthGetLogs struct { CommonResponse Result []Log `json:"result"` } func BuildLog(hash libcommon.Hash, blockNum string, address libcommon.Address, topics []libcommon.Hash, data hexutility.Bytes, txIndex hexutil.Uint, blockHash libcommon.Hash, index hexutil.Uint, removed bool) Log { return Log{ Address: address, Topics: topics, Data: data, BlockNumber: hexutil.Uint64(HexToInt(blockNum)), TxHash: hash, TxIndex: txIndex, BlockHash: blockHash, Index: index, Removed: removed, } } // HexToInt converts a hexadecimal string to uint64 func HexToInt(hexStr string) uint64 { cleaned := strings.ReplaceAll(hexStr, "0x", "") // remove the 0x prefix result, _ := strconv.ParseUint(cleaned, 16, 64) return result } func (reqGen *RequestGenerator) GetAndCompareLogs(fromBlock uint64, toBlock uint64, expected Log) error { reqGen.logger.Info("GETTING AND COMPARING LOGS") var b EthGetLogs method, body := reqGen.getLogs(fromBlock, toBlock, expected.Address) if res := reqGen.call(method, body, &b); res.Err != nil { return fmt.Errorf("failed to fetch logs: %v", res.Err) } if len(b.Result) == 0 { return fmt.Errorf("logs result should not be empty") } eventLog := b.Result[0] actual := BuildLog(eventLog.TxHash, strconv.FormatUint(uint64(eventLog.BlockNumber), 10), eventLog.Address, eventLog.Topics, eventLog.Data, eventLog.TxIndex, eventLog.BlockHash, eventLog.Index, eventLog.Removed) // compare the log events errs, ok := compareLogEvents(expected, actual) if !ok { reqGen.logger.Error("Log result is incorrect", "errors", errs) return fmt.Errorf("incorrect logs: %v", errs) } _, err := parseResponse(b) if err != nil { return fmt.Errorf("error parsing response: %v", err) } log.Info("SUCCESS => Logs compared successfully, no discrepancies") return nil } // ParseResponse converts any of the models interfaces to a string for readability func parseResponse(resp interface{}) (string, error) { result, err := json.Marshal(resp) if err != nil { return "", fmt.Errorf("error trying to marshal response: %v", err) } return string(result), nil } func compareLogEvents(expected, actual Log) ([]error, bool) { var errs []error switch { case expected.Address != actual.Address: errs = append(errs, fmt.Errorf("expected address: %v, actual address %v", expected.Address, actual.Address)) case expected.TxHash != actual.TxHash: errs = append(errs, fmt.Errorf("expected txhash: %v, actual txhash %v", expected.TxHash, actual.TxHash)) case expected.BlockHash != actual.BlockHash: errs = append(errs, fmt.Errorf("expected blockHash: %v, actual blockHash %v", expected.BlockHash, actual.BlockHash)) case expected.BlockNumber != actual.BlockNumber: errs = append(errs, fmt.Errorf("expected blockNumber: %v, actual blockNumber %v", expected.BlockNumber, actual.BlockNumber)) case expected.TxIndex != actual.TxIndex: errs = append(errs, fmt.Errorf("expected txIndex: %v, actual txIndex %v", expected.TxIndex, actual.TxIndex)) case !hashSlicesAreEqual(expected.Topics, actual.Topics): errs = append(errs, fmt.Errorf("expected topics: %v, actual topics %v", expected.Topics, actual.Topics)) } return errs, len(errs) == 0 } func hashSlicesAreEqual(s1, s2 []libcommon.Hash) bool { if len(s1) != len(s2) { return false } for i := 0; i < len(s1); i++ { if s1[i] != s2[i] { return false } } return true } func (req *RequestGenerator) getLogs(fromBlock, toBlock uint64, address libcommon.Address) (RPCMethod, string) { const template = `{"jsonrpc":"2.0","method":%q,"params":[{"fromBlock":"0x%x","toBlock":"0x%x","address":"0x%x"}],"id":%d}` return Methods.ETHGetLogs, fmt.Sprintf(template, Methods.ETHGetLogs, fromBlock, toBlock, address, req.reqID) }