Skip to content

chore: Adds prestate tracer to traceTransaction endpoint #3898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
2 changes: 1 addition & 1 deletion docs/openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -1563,7 +1563,7 @@
"TracerType": {
"title": "Tracer type",
"type": "string",
"enum": ["callTracer", "opcodeLogger"]
"enum": ["callTracer", "opcodeLogger", "prestateTracer"]
},
"TracerConfigWrapper": {
"title": "Tracer config wrapper",
Expand Down
9 changes: 5 additions & 4 deletions packages/relay/src/lib/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,6 @@ export class DebugImpl implements Debug {
tracerObject: TransactionTracerConfig,
requestDetails: RequestDetails,
): Promise<any> {
if (tracerObject?.tracer === TracerType.PrestateTracer) {
throw predefined.INVALID_PARAMETER(1, 'Prestate tracer is not yet supported on debug_traceTransaction');
}

if (this.logger.isLevelEnabled('trace')) {
this.logger.trace(`${requestDetails.formattedRequestId} traceTransaction(${transactionIdOrHash})`);
}
Expand All @@ -134,6 +130,11 @@ export class DebugImpl implements Debug {
return await this.callTracer(transactionIdOrHash, tracerConfig as ICallTracerConfig, requestDetails);
}

if (tracer === TracerType.PrestateTracer) {
const onlyTopCall = (tracerObject?.tracerConfig as ICallTracerConfig)?.onlyTopCall ?? false;
return await this.prestateTracer(transactionIdOrHash, onlyTopCall, requestDetails);
}

if (!ConfigService.get('OPCODELOGGER_ENABLED')) {
throw predefined.UNSUPPORTED_METHOD;
}
Expand Down
94 changes: 94 additions & 0 deletions packages/relay/tests/lib/debug.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,100 @@ describe('Debug API Test Suite', async function () {
}
});

describe('prestateTracer', async function () {
const prestateTracer: TracerType = TracerType.PrestateTracer;
const mockPrestateResult = {
'0xc37f417fa09933335240fca72dd257bfbde9c275': {
balance: '0x100000000',
nonce: 2,
code: '0x',
storage: {},
},
'0x637a6a8e5a69c087c24983b05261f63f64ed7e9b': {
balance: '0x200000000',
nonce: 1,
code: '0x608060405234801561001057600080fd5b50600436106100415760003560e01c8063',
storage: {
'0x0': '0x1',
'0x1': '0x2',
},
},
};

beforeEach(() => {
sinon.stub(debugService, 'prestateTracer').resolves(mockPrestateResult);
});

afterEach(() => {
sinon.restore();
});

it('should successfully trace transaction with prestateTracer', async function () {
const tracerObject = { tracer: prestateTracer };
const result = await debugService.traceTransaction(transactionHash, tracerObject, requestDetails);

expect(result).to.deep.equal(mockPrestateResult);
expect(result).to.be.an('object');
expect(Object.keys(result)).to.have.lengthOf(2);

for (const address of Object.keys(result)) {
expect(result[address]).to.have.all.keys(['balance', 'nonce', 'code', 'storage']);
expect(result[address].nonce).to.be.a('number');
expect(result[address].code).to.exist;
expect(result[address].storage).to.be.an('object');
}
});

it('should trace transaction with prestateTracer and onlyTopCall=true', async function () {
const tracerObject = { tracer: prestateTracer, tracerConfig: { onlyTopCall: true } };
const result = await debugService.traceTransaction(transactionHash, tracerObject, requestDetails);

expect(result).to.deep.equal(mockPrestateResult);

// Verify that prestateTracer was called with onlyTopCall=true
const prestateTracerStub = debugService.prestateTracer as sinon.SinonStub;
expect(prestateTracerStub.calledOnce).to.be.true;
expect(prestateTracerStub.calledWith(transactionHash, true, requestDetails)).to.be.true;
});

it('should trace transaction with prestateTracer and onlyTopCall=false (default)', async function () {
const tracerObject = { tracer: prestateTracer, tracerConfig: { onlyTopCall: false } };
const result = await debugService.traceTransaction(transactionHash, tracerObject, requestDetails);

expect(result).to.deep.equal(mockPrestateResult);

// Verify that prestateTracer was called with onlyTopCall=false
const prestateTracerStub = debugService.prestateTracer as sinon.SinonStub;
expect(prestateTracerStub.calledOnce).to.be.true;
expect(prestateTracerStub.calledWith(transactionHash, false, requestDetails)).to.be.true;
});

it('should handle empty prestate result', async function () {
const emptyResult = {};
(debugService.prestateTracer as sinon.SinonStub).resolves(emptyResult);

const tracerObject = { tracer: prestateTracer };
const result = await debugService.traceTransaction(transactionHash, tracerObject, requestDetails);

expect(result).to.deep.equal(emptyResult);
expect(result).to.be.an('object');
expect(Object.keys(result)).to.have.lengthOf(0);
});

it('should propagate errors from prestateTracer', async function () {
const expectedError = predefined.RESOURCE_NOT_FOUND('Failed to retrieve contract results');
(debugService.prestateTracer as sinon.SinonStub).rejects(expectedError);

const tracerObject = { tracer: prestateTracer };

await RelayAssertions.assertRejection(expectedError, debugService.traceTransaction, true, debugService, [
transactionHash,
tracerObject,
requestDetails,
]);
});
});

describe('Invalid scenarios', async function () {
let notFound: { _status: { messages: { message: string }[] } };

Expand Down
74 changes: 65 additions & 9 deletions packages/server/tests/acceptance/debug.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,71 @@ describe('@debug API Acceptance Tests', function () {
});
});

describe('PrestateTracer', () => {
it('should trace a transaction using PrestateTracer', async function () {
const result = await relay.call(
DEBUG_TRACE_TRANSACTION,
[createChildTx.hash, TRACER_CONFIGS.PRESTATE_TRACER],
requestId,
);

expect(result).to.be.an('object');
expect(Object.keys(result).length).to.be.at.least(1);

// Check that the result contains prestate information for at least the contract and sender
const keys = Object.keys(result);
expect(keys.length).to.be.at.least(2);

// For each address in the result, check it has the expected fields
for (const address of keys) {
const state = result[address];
Assertions.validatePrestateTracerResult(state);
}
});

it('should trace a transaction using PrestateTracer with onlyTopCall=true', async function () {
const result = await relay.call(
DEBUG_TRACE_TRANSACTION,
[createChildTx.hash, TRACER_CONFIGS.PRESTATE_TRACER_TOP_ONLY],
requestId,
);

expect(result).to.be.an('object');
expect(Object.keys(result).length).to.be.at.least(1);

// Check that the result contains prestate information for at least the contract and sender
const keys = Object.keys(result);
expect(keys.length).to.be.at.least(2);

// For each address in the result, check it has the expected fields
for (const address of keys) {
const state = result[address];
Assertions.validatePrestateTracerResult(state);
}
});

it('should trace a transaction using PrestateTracer with onlyTopCall=false', async function () {
const result = await relay.call(
DEBUG_TRACE_TRANSACTION,
[createChildTx.hash, TRACER_CONFIGS.PRESTATE_TRACER_TOP_ONLY_FALSE],
requestId,
);

expect(result).to.be.an('object');
expect(Object.keys(result).length).to.be.at.least(1);

// Check that the result contains prestate information for at least the contract and sender
const keys = Object.keys(result);
expect(keys.length).to.be.at.least(2);

// For each address in the result, check it has the expected fields
for (const address of keys) {
const state = result[address];
Assertions.validatePrestateTracerResult(state);
}
});
});

describe('OpcodeLogger', () => {
it('@release should trace a successful transaction using OpcodeLogger (default when no tracer specified)', async function () {
const result = await relay.call(DEBUG_TRACE_TRANSACTION, [createChildTx.hash], requestId);
Expand Down Expand Up @@ -546,15 +611,6 @@ describe('@debug API Acceptance Tests', function () {
);
});

it('should fail with INVALID_PARAMETER when using PrestateTracer', async function () {
await relay.callFailing(
DEBUG_TRACE_TRANSACTION,
[createChildTx.hash, TRACER_CONFIGS.PRESTATE_TRACER],
predefined.INVALID_PARAMETER(1, 'Prestate tracer is not yet supported on debug_traceTransaction'),
requestId,
);
});

it('should fail with INVALID_PARAMETER when given an invalid tracer type', async function () {
const invalidTracerConfig = { tracer: 'InvalidTracer' };
await relay.callFailing(
Expand Down
22 changes: 22 additions & 0 deletions packages/server/tests/integration/server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,28 @@ describe('RPC Server', function () {
).to.not.throw;
});

it('should execute with PrestateTracer type and valid PrestateTracerConfig', async () => {
expect(
await testClient.post('/', {
jsonrpc: '2.0',
method: 'debug_traceTransaction',
params: [contractHash1, { tracer: TracerType.PrestateTracer }],
id: 1,
}),
).to.not.throw;
});

it('should execute with PrestateTracer type and onlyTopCall option', async () => {
expect(
await testClient.post('/', {
jsonrpc: '2.0',
method: 'debug_traceTransaction',
params: [contractHash1, { tracer: TracerType.PrestateTracer, tracerConfig: { onlyTopCall: true } }],
id: 1,
}),
).to.not.throw;
});

it('should execute with valid hash', async () => {
expect(
await testClient.post('/', {
Expand Down
Loading