Skip to content

Commit 983c6a8

Browse files
ebadierenatanasow
andauthored
fix: populate synthetic transactions on getBlock... routes (#2784) (#2786)
* chore: populate synthetic transaction hashes * chore: fix naming * chore: rename tests * chore: remove .only * chore: rename test * chore: remove code duplication in the test --------- Signed-off-by: nikolay <[email protected]> Signed-off-by: ebadiere <[email protected]> Co-authored-by: Nikolay Atanasow <[email protected]>
1 parent 1a32cf5 commit 983c6a8

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed

packages/relay/src/lib/eth.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,6 +2188,33 @@ export class EthImpl implements Eth {
21882188
return gas;
21892189
}
21902190

2191+
populateSyntheticTransactions(
2192+
showDetails: boolean,
2193+
logs: Log[],
2194+
transactionsArray: Array<any>,
2195+
requestIdPrefix?: string,
2196+
): Array<any> {
2197+
let filteredLogs: Log[];
2198+
if (showDetails) {
2199+
filteredLogs = logs.filter(
2200+
(log) => !transactionsArray.some((transaction) => transaction.hash === log.transactionHash),
2201+
);
2202+
filteredLogs.forEach((log) => {
2203+
const transaction: Transaction1559 = this.createTransactionFromLog(log);
2204+
transactionsArray.push(transaction);
2205+
});
2206+
} else {
2207+
filteredLogs = logs.filter((log) => !transactionsArray.includes(log.transactionHash));
2208+
filteredLogs.forEach((log) => {
2209+
transactionsArray.push(log.transactionHash);
2210+
});
2211+
}
2212+
2213+
this.logger.trace(`${requestIdPrefix} Synthetic transaction hashes will be populated in the block response`);
2214+
2215+
return transactionsArray;
2216+
}
2217+
21912218
/**
21922219
* Gets the block with the given hash.
21932220
* Given an ethereum transaction hash, call the mirror node to get the block info.
@@ -2243,6 +2270,8 @@ export class EthImpl implements Eth {
22432270
transactionArray.push(showDetails ? formatContractResult(contractResult) : contractResult.hash);
22442271
}
22452272

2273+
transactionArray = this.populateSyntheticTransactions(showDetails, logs, transactionArray, requestIdPrefix);
2274+
22462275
const blockHash = toHash32(blockResponse.hash);
22472276
return new Block({
22482277
baseFeePerGas: await this.gasPrice(requestIdPrefix),
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
/*-
2+
*
3+
* Hedera JSON RPC Relay
4+
*
5+
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*
19+
*/
20+
21+
import path from 'path';
22+
import dotenv from 'dotenv';
23+
import MockAdapter from 'axios-mock-adapter';
24+
import { expect, use } from 'chai';
25+
import chaiAsPromised from 'chai-as-promised';
26+
import { Registry } from 'prom-client';
27+
28+
dotenv.config({ path: path.resolve(__dirname, '../test.env') });
29+
import { EthImpl } from '../../src/lib/eth';
30+
import { MirrorNodeClient } from '../../src/lib/clients/mirrorNodeClient';
31+
32+
import pino from 'pino';
33+
import constants from '../../src/lib/constants';
34+
import HAPIService from '../../src/lib/services/hapiService/hapiService';
35+
import HbarLimit from '../../src/lib/hbarlimiter';
36+
import { Log, Transaction } from '../../src/lib/model';
37+
import { nullableNumberTo0x, numberTo0x, nanOrNumberTo0x, toHash32 } from '../../../../packages/relay/src/formatters';
38+
import { CacheService } from '../../src/lib/services/cacheService/cacheService';
39+
import * as sinon from 'sinon';
40+
import { RedisInMemoryServer } from '../redisInMemoryServer';
41+
import { defaultDetailedContractResults } from '../helpers';
42+
43+
use(chaiAsPromised);
44+
45+
const logger = pino();
46+
const registry = new Registry();
47+
48+
let restMock: MockAdapter;
49+
let mirrorNodeInstance: MirrorNodeClient;
50+
let hapiServiceInstance: HAPIService;
51+
let cacheService: CacheService;
52+
let redisInMemoryServer: RedisInMemoryServer;
53+
let mirrorNodeCache;
54+
55+
const blockHashTrimmed = '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b';
56+
const blockHash = `${blockHashTrimmed}999fc7e86699f60f2a3fb3ed9a646c6b`;
57+
const blockNumber = 3;
58+
const firstTransactionTimestampSeconds = '1653077541';
59+
const contractTimestamp1 = `${firstTransactionTimestampSeconds}.983983199`;
60+
const contractHash1 = '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392';
61+
const contractHash2 = '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6393';
62+
const contractHash3 = '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6394';
63+
const contractId1 = '0.0.1375';
64+
65+
const defaultLogTopics = [
66+
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
67+
'0x0000000000000000000000000000000000000000000000000000000000000000',
68+
'0x000000000000000000000000000000000000000000000000000000000208fa13',
69+
'0x0000000000000000000000000000000000000000000000000000000000000005',
70+
];
71+
72+
const logBloom1 = '0x1111';
73+
const logBloom2 = '0x2222';
74+
const defaultLogs1 = [
75+
{
76+
address: '0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69',
77+
bloom: logBloom1,
78+
contract_id: contractId1,
79+
data: '0x',
80+
index: 0,
81+
topics: defaultLogTopics,
82+
root_contract_id: '0.0.34806097',
83+
timestamp: contractTimestamp1,
84+
block_hash: blockHash,
85+
block_number: blockNumber,
86+
transaction_hash: contractHash1,
87+
transaction_index: 1,
88+
},
89+
{
90+
address: '0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69',
91+
bloom: logBloom2,
92+
contract_id: contractId1,
93+
data: '0x',
94+
index: 1,
95+
topics: defaultLogTopics,
96+
root_contract_id: '0.0.34806097',
97+
timestamp: contractTimestamp1,
98+
block_hash: blockHash,
99+
block_number: blockNumber,
100+
transaction_hash: contractHash2,
101+
transaction_index: 1,
102+
},
103+
{
104+
address: '0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69',
105+
bloom: logBloom2,
106+
contract_id: contractId1,
107+
data: '0x',
108+
index: 2,
109+
topics: defaultLogTopics,
110+
root_contract_id: '0.0.34806097',
111+
timestamp: contractTimestamp1,
112+
block_hash: blockHash,
113+
block_number: blockNumber,
114+
transaction_hash: contractHash3,
115+
transaction_index: 1,
116+
},
117+
];
118+
119+
describe('eth_getBlockBy', async function () {
120+
this.timeout(10000);
121+
let ethImpl: EthImpl;
122+
const sandbox = sinon.createSandbox();
123+
124+
this.beforeAll(async () => {
125+
//done in order to be able to use cache
126+
process.env.TEST = 'false';
127+
process.env.REDIS_ENABLED = 'true';
128+
redisInMemoryServer = new RedisInMemoryServer(logger.child({ name: `in-memory redis server` }), 5031);
129+
await redisInMemoryServer.start();
130+
process.env.REDIS_URL = 'redis://127.0.0.1:5031';
131+
cacheService = new CacheService(logger.child({ name: `cache` }), registry);
132+
133+
// @ts-ignore
134+
mirrorNodeInstance = new MirrorNodeClient(
135+
process.env.MIRROR_NODE_URL,
136+
logger.child({ name: `mirror-node` }),
137+
registry,
138+
cacheService,
139+
);
140+
141+
// @ts-ignore
142+
mirrorNodeCache = mirrorNodeInstance.cache;
143+
144+
// @ts-ignore
145+
restMock = new MockAdapter(mirrorNodeInstance.getMirrorNodeRestInstance(), { onNoMatch: 'throwException' });
146+
147+
const duration = constants.HBAR_RATE_LIMIT_DURATION;
148+
const total = constants.HBAR_RATE_LIMIT_TINYBAR;
149+
const hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration, registry);
150+
151+
hapiServiceInstance = new HAPIService(logger, registry, hbarLimiter, cacheService);
152+
153+
process.env.ETH_FEE_HISTORY_FIXED = 'false';
154+
155+
// @ts-ignore
156+
ethImpl = new EthImpl(hapiServiceInstance, mirrorNodeInstance, logger, '0x12a', registry, cacheService);
157+
});
158+
159+
this.beforeEach(() => {
160+
cacheService.clear();
161+
restMock.reset();
162+
});
163+
164+
afterEach(function () {
165+
sandbox.restore();
166+
});
167+
168+
this.afterAll(async () => {
169+
process.env.REDIS_ENABLED = 'false';
170+
await cacheService.disconnectRedisClient();
171+
await redisInMemoryServer.stop();
172+
});
173+
174+
const mirrorLogToModelLog = (mirrorLog) => {
175+
return new Log({
176+
address: mirrorLog.address,
177+
blockHash: mirrorLog.block_hash,
178+
blockNumber: mirrorLog.block_number,
179+
data: mirrorLog.data,
180+
logIndex: mirrorLog.index,
181+
topics: mirrorLog.topics,
182+
transactionHash: mirrorLog.transaction_hash,
183+
transactionIndex: mirrorLog.transaction_index,
184+
});
185+
};
186+
187+
const modelLog1 = mirrorLogToModelLog(defaultLogs1[0]);
188+
const modelLog2 = mirrorLogToModelLog(defaultLogs1[1]);
189+
const modelLog3 = mirrorLogToModelLog(defaultLogs1[2]);
190+
const referenceLogs = [modelLog1, modelLog2, modelLog3];
191+
describe('populateSyntheticTransactions w showDetails=false', () => {
192+
const showDetails = false;
193+
194+
it('populateSyntheticTransactions with no dupes in empty transactionHashes', async function () {
195+
const initHashes = [];
196+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, initHashes, '1');
197+
expect(initHashes.length).to.equal(defaultLogs1.length);
198+
expect(initHashes[0]).to.equal(modelLog1.transactionHash);
199+
expect(initHashes[1]).to.equal(modelLog2.transactionHash);
200+
expect(initHashes[2]).to.equal(modelLog3.transactionHash);
201+
});
202+
203+
it('populateSyntheticTransactions with no dupes in non empty transactionHashes', async function () {
204+
const initHashes = ['txHash1', 'txHash2'];
205+
const txHashes = initHashes.slice();
206+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, txHashes, '1');
207+
expect(txHashes.length).to.equal(initHashes.length + defaultLogs1.length);
208+
expect(txHashes[initHashes.length + 0]).to.equal(modelLog1.transactionHash);
209+
expect(txHashes[initHashes.length + 1]).to.equal(modelLog2.transactionHash);
210+
expect(txHashes[initHashes.length + 2]).to.equal(modelLog3.transactionHash);
211+
});
212+
213+
it('populateSyntheticTransactions with 1 transaction dupes in transactionHashes', async function () {
214+
const initHashes = [modelLog2.transactionHash];
215+
const txHashes = initHashes.slice();
216+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, txHashes, '1');
217+
expect(txHashes.length).to.equal(referenceLogs.length);
218+
expect(txHashes[0]).to.equal(contractHash2);
219+
expect(txHashes[1]).to.equal(modelLog1.transactionHash);
220+
expect(txHashes[2]).to.equal(modelLog3.transactionHash);
221+
});
222+
223+
it('populateSyntheticTransactions with all dupes in transactionHashes', async function () {
224+
const initHashes = [modelLog1.transactionHash, modelLog2.transactionHash, modelLog3.transactionHash];
225+
const txHashes = initHashes.slice();
226+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, txHashes, '1');
227+
expect(txHashes.length).to.equal(referenceLogs.length);
228+
expect(txHashes[0]).to.equal(modelLog1.transactionHash);
229+
expect(txHashes[1]).to.equal(modelLog2.transactionHash);
230+
expect(txHashes[2]).to.equal(modelLog3.transactionHash);
231+
});
232+
});
233+
234+
describe('populateSyntheticTransactions w showDetails=true', () => {
235+
const getTransactionModel = (transactionHash) => {
236+
return new Transaction({
237+
accessList: undefined, // we don't support access lists for now, so punt
238+
blockHash: toHash32(defaultDetailedContractResults.block_hash),
239+
blockNumber: numberTo0x(defaultDetailedContractResults.block_number),
240+
chainId: defaultDetailedContractResults.chain_id,
241+
from: defaultDetailedContractResults.from.substring(0, 42),
242+
gas: nanOrNumberTo0x(defaultDetailedContractResults.gas_used),
243+
gasPrice: null,
244+
hash: transactionHash,
245+
input: defaultDetailedContractResults.function_parameters,
246+
maxPriorityFeePerGas: null,
247+
maxFeePerGas: null,
248+
nonce: nanOrNumberTo0x(defaultDetailedContractResults.nonce),
249+
r: EthImpl.zeroHex,
250+
s: EthImpl.zeroHex,
251+
to: defaultDetailedContractResults.to.substring(0, 42),
252+
transactionIndex: nullableNumberTo0x(defaultDetailedContractResults.transaction_index),
253+
type: nullableNumberTo0x(defaultDetailedContractResults.type),
254+
v: nanOrNumberTo0x(defaultDetailedContractResults.v),
255+
value: nanOrNumberTo0x(defaultDetailedContractResults.amount),
256+
});
257+
};
258+
259+
const showDetails = true;
260+
it('populateSyntheticTransactions with no dupes in empty txObjects', async function () {
261+
const initTxObjects: Transaction[] = [];
262+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, initTxObjects, '1');
263+
expect(initTxObjects.length).to.equal(defaultLogs1.length);
264+
expect(initTxObjects[0].hash).to.equal(modelLog1.transactionHash);
265+
expect(initTxObjects[1].hash).to.equal(modelLog2.transactionHash);
266+
expect(initTxObjects[2].hash).to.equal(modelLog3.transactionHash);
267+
});
268+
269+
it('populateSyntheticTransactions with no dupes in non empty txObjects', async function () {
270+
const initTxObjects = [getTransactionModel('txHash1'), getTransactionModel('txHash2')];
271+
const txObjects = initTxObjects.slice();
272+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, txObjects, '1');
273+
expect(txObjects.length).to.equal(initTxObjects.length + defaultLogs1.length);
274+
expect(txObjects[initTxObjects.length + 0].hash).to.equal(modelLog1.transactionHash);
275+
expect(txObjects[initTxObjects.length + 1].hash).to.equal(modelLog2.transactionHash);
276+
expect(txObjects[initTxObjects.length + 2].hash).to.equal(modelLog3.transactionHash);
277+
});
278+
279+
it('populateSyntheticTransactions with 1 transaction dupes in txObjects', async function () {
280+
const initTxObjects = [getTransactionModel(modelLog2.transactionHash)];
281+
const txObjects = initTxObjects.slice();
282+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, txObjects, '1');
283+
expect(txObjects.length).to.equal(referenceLogs.length);
284+
expect(txObjects[0].hash).to.equal(contractHash2);
285+
expect(txObjects[1].hash).to.equal(modelLog1.transactionHash);
286+
expect(txObjects[2].hash).to.equal(modelLog3.transactionHash);
287+
});
288+
289+
it('populateSyntheticTransactions with all dupes in txObjects', async function () {
290+
const initTxObjects = [
291+
getTransactionModel(modelLog1.transactionHash),
292+
getTransactionModel(modelLog2.transactionHash),
293+
getTransactionModel(modelLog3.transactionHash),
294+
];
295+
const txObjects = initTxObjects.slice();
296+
ethImpl.populateSyntheticTransactions(showDetails, referenceLogs, txObjects, '1');
297+
expect(txObjects.length).to.equal(referenceLogs.length);
298+
expect(txObjects[0].hash).to.equal(modelLog1.transactionHash);
299+
expect(txObjects[1].hash).to.equal(modelLog2.transactionHash);
300+
expect(txObjects[2].hash).to.equal(modelLog3.transactionHash);
301+
});
302+
});
303+
});

0 commit comments

Comments
 (0)