diff --git a/models/events.go b/models/events.go index 3aa68219d..057a11661 100644 --- a/models/events.go +++ b/models/events.go @@ -53,7 +53,8 @@ func NewCadenceEvents(events flow.BlockEvents) (*CadenceEvents, error) { return nil, err } - logIndex := uint(0) // todo revisit if we can reconstruct it simply like this + // Log index field holds the index position in the entire block + logIndex := uint(0) for i, rcp := range e.receipts { // add transaction hashes to the block e.block.TransactionHashes = append(e.block.TransactionHashes, rcp.TxHash) diff --git a/storage/index_testsuite.go b/storage/index_testsuite.go index 62de05fe4..f51620d97 100644 --- a/storage/index_testsuite.go +++ b/storage/index_testsuite.go @@ -185,6 +185,14 @@ func (s *ReceiptTestSuite) TestStoreReceipt() { mocks.NewReceipt(height, common.HexToHash("0x2")), mocks.NewReceipt(height, common.HexToHash("0x3")), } + // Log index field holds the index position in the entire block + logIndex := uint(0) + for _, receipt := range receipts { + for _, log := range receipt.Logs { + log.Index = logIndex + logIndex++ + } + } err := s.ReceiptIndexer.Store(receipts, nil) s.Require().NoError(err) diff --git a/storage/pebble/receipts.go b/storage/pebble/receipts.go index 8ce915956..7eac1c87e 100644 --- a/storage/pebble/receipts.go +++ b/storage/pebble/receipts.go @@ -143,14 +143,18 @@ func (r *Receipts) getByBlockHeight(height []byte, batch *pebble.Batch) ([]*mode ) } + // Log index field holds the index position in the entire block + logIndex := uint(0) for _, rcp := range receipts { // dynamically populate the values since they are not stored to save space - for i, l := range rcp.Logs { - l.BlockHash = rcp.BlockHash + for _, l := range rcp.Logs { l.BlockNumber = rcp.BlockNumber.Uint64() + l.BlockHash = rcp.BlockHash l.TxHash = rcp.TxHash l.TxIndex = rcp.TransactionIndex - l.Index = uint(i) + l.Index = logIndex + l.Removed = false + logIndex++ } } diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 8d7e0b01f..389754409 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -1,17 +1,27 @@ package tests import ( + _ "embed" "encoding/hex" "math/big" "testing" "github.com/onflow/cadence" "github.com/onflow/flow-emulator/emulator" + "github.com/onflow/flow-go/fvm/evm/testutils" "github.com/onflow/go-ethereum/common" "github.com/onflow/go-ethereum/crypto" "github.com/stretchr/testify/require" ) +var ( + //go:embed fixtures/storage.byte + storageByteCode string + + //go:embed fixtures/storageABI.json + storageABI string +) + func TestWeb3_E2E(t *testing.T) { t.Run("test setup sanity check", func(t *testing.T) { @@ -112,6 +122,71 @@ func TestWeb3_E2E(t *testing.T) { }) }) + t.Run("batch run transactions with logs", func(t *testing.T) { + // create multiple transactions that emit logs and batch run them before the test + runWeb3TestWithSetup(t, "eth_batch_tx_logs_test", func(emu emulator.Emulator) { + contractCode, err := hex.DecodeString(storageByteCode) + require.NoError(t, err) + storageContract := testutils.TestContract{ + ByteCode: contractCode, + ABI: storageABI, + } + + // create test account for contract deployment and contract calls + nonce := uint64(0) + accountKey, err := crypto.HexToECDSA("f6d5333177711e562cabf1f311916196ee6ffc2a07966d9d4628094073bd5442") + require.NoError(t, err) + + // contract deployment transaction + deployPayload, _, err := evmSign(big.NewInt(0), 350_000, accountKey, nonce, nil, contractCode) + require.NoError(t, err) + nonce += 1 + + const batchSize = 6 + payloads := make([][]byte, batchSize) + payloads[0] = deployPayload + contractAddress := common.HexToAddress("99a64c993965f8d69f985b5171bc20065cc32fab") + // contract call transactions, these emit logs/events + for i := 1; i <= 5; i++ { + callData := storageContract.MakeCallData(t, "sum", big.NewInt(10), big.NewInt(20)) + callPayload, _, err := evmSign(big.NewInt(0), 55_000, accountKey, nonce, &contractAddress, callData) + require.NoError(t, err) + payloads[i] = callPayload + nonce += 1 + } + encodedTxs := make([]cadence.Value, batchSize) + for i := range encodedTxs { + tx, err := cadence.NewString(hex.EncodeToString(payloads[i])) + require.NoError(t, err) + + encodedTxs[i] = tx + } + + res, err := flowSendTransaction( + emu, + `transaction(encodedTxs: [String]) { + prepare(signer: auth(Storage) &Account) { + var txs: [[UInt8]] = [] + for enc in encodedTxs { + txs.append(enc.decodeHex()) + } + + let txResults = EVM.batchRun( + txs: txs, + coinbase: EVM.EVMAddress(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + ) + for txResult in txResults { + assert(txResult.status == EVM.Status.successful, message: "failed to execute tx, error: ".concat(txResult.errorCode.toString())) + } + } + }`, + cadence.NewArray(encodedTxs), + ) + require.NoError(t, err) + require.NoError(t, res.Error) + }) + }) + t.Run("streaming of entities and subscription", func(t *testing.T) { runWeb3Test(t, "eth_streaming_test") }) diff --git a/tests/go.mod b/tests/go.mod index 0f9d9b1c8..fef867d00 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -81,6 +81,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-dap v0.11.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect @@ -232,6 +233,7 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools v2.2.0+incompatible // indirect lukechampine.com/blake3 v1.3.0 // indirect modernc.org/libc v1.37.6 // indirect modernc.org/mathutil v1.6.0 // indirect diff --git a/tests/web3js/eth_batch_tx_logs_test.js b/tests/web3js/eth_batch_tx_logs_test.js new file mode 100644 index 000000000..9d3f1d9e9 --- /dev/null +++ b/tests/web3js/eth_batch_tx_logs_test.js @@ -0,0 +1,54 @@ +const { assert } = require('chai') +const conf = require('./config') +const web3 = conf.web3 + +it('should retrieve batch transactions with logs', async () => { + // this test relies on the setup of batched transactions with logs found in ../e2e_web3js_test.go + + let latestHeight = await web3.eth.getBlockNumber() + let block = await web3.eth.getBlock(latestHeight) + + let batchSize = 6 + assert.lengthOf(block.transactions, batchSize) + + let blockLogs = [] + + for (let i = 0; i < block.transactions.length; i++) { + let tx = await web3.eth.getTransactionFromBlock(latestHeight, i) + let receipt = await web3.eth.getTransactionReceipt(tx.hash) + assert.equal(receipt.blockNumber, block.number, 'wrong block number') + assert.equal(receipt.blockHash, block.hash, 'wrong block hash') + assert.equal(receipt.type, 0, 'wrong tx type') + assert.equal(receipt.transactionIndex, i, 'wrong tx index') + assert.isBelow(i, batchSize, 'wrong batch size') + + // the contract deployment transaction has no logs + if (receipt.logs.length == 0) { + continue + } + + for (const log of receipt.logs) { + assert.equal(log.blockNumber, block.number, 'wrong block number') + assert.equal(log.blockHash, block.hash, 'wrong block hash') + assert.equal(log.transactionHash, tx.hash, 'wrong tx hash') + assert.equal(log.transactionIndex, i, 'wrong tx index') + // the 1st transaction contains the contract deployment + assert.equal(log.logIndex, log.transactionIndex - 1n, 'wrong log index') + assert.isFalse(log.removed, 'log should not be removed') + assert.equal(log.address, '0x99a64c993965f8d69f985b5171bc20065cc32fab', 'wrong log address') + assert.equal(log.topics.length, 4, 'wrong topics length') + assert.equal(log.data.length, 66, 'wrong data length') + + blockLogs.push(log) + } + } + + let logs = await web3.eth.getPastLogs({ + fromBlock: block.number, + toBlock: block.number + }) + + for (let i = 0; i < logs; i++) { + assert.deepEqual(logs[i], blockLogs[i]) + } +}) diff --git a/tests/web3js/eth_logs_filtering_test.js b/tests/web3js/eth_logs_filtering_test.js index bd58e4e48..7e96e3f4e 100644 --- a/tests/web3js/eth_logs_filtering_test.js +++ b/tests/web3js/eth_logs_filtering_test.js @@ -1,7 +1,6 @@ const { assert } = require('chai') const conf = require('./config') const helpers = require('./helpers') -const web3 = conf.web3 it('emit logs and retrieve them using different filters', async () => { setTimeout(() => process.exit(1), 19 * 1000) // hack if the ws connection is not closed