Skip to content
Draft
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 crates/stratus_metrics/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub fn metrics(input: TokenStream) -> TokenStream {
///
/// ```ignore
/// #[timed(
/// executor_external_transaction,
/// executor_imported_transaction,
/// labels(
/// contract = |input| contract_name(&input.execution_info.to),
/// function = |input| function_sig(&input.execution_info.input),
Expand Down
8 changes: 4 additions & 4 deletions crates/stratus_metrics/src/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ metrics! {
},

group: executor {
"Time executing and persist an external block with all transactions."
histogram_duration executor_external_block{},
"Time executing and persist an imported stratus block with all transactions."
histogram_duration executor_imported_block{},

"Time executing an external transaction."
histogram_duration executor_external_transaction{contract, function},
"Time executing an imported stratus transaction."
histogram_duration executor_imported_transaction{contract, function},

"Time executing a local transaction."
histogram_duration executor_local_transaction{success, contract, function},
Expand Down
31 changes: 16 additions & 15 deletions e2e/test/external/e2e-json-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ import {
toPaddedHex,
} from "../helpers/rpc";

// The block DTO serializes hashes as byte arrays; convert them to hex for assertions.
const bytesToHex = (bytes: number[]) => "0x" + Buffer.from(bytes).toString("hex");

// The block DTO serializes block numbers as byte-order-swapped u32; swap them back for assertions.
const swapU32 = (value: number) =>
((value & 0xff) << 24) | ((value & 0xff00) << 8) | ((value >>> 8) & 0xff00) | (value >>> 24);

describe("JSON-RPC", () => {
before(() => {
expect(currentBlockMode()).eq(BlockMode.External, "Wrong block mining mode is used");
Expand Down Expand Up @@ -165,24 +172,18 @@ describe("JSON-RPC", () => {
const block = await send("eth_getBlockByNumber", [blockNumber, true]);
const blockHash = block.hash;

// Get individual block and receipt
const individualBlock = await send("eth_getBlockByHash", [blockHash, true]);
const individualReceipt = await send("eth_getTransactionReceipt", [txHash, true]);

// Get block and receipts using stratus endpoint
const response = await send("stratus_getBlockAndReceipts", [blockHash]);

// Validate block
expect(response.block).to.not.be.null;
expect(response.block).to.deep.equal(individualBlock);

// Validate receipt
expect(response.receipts).to.have.length(1);
const combinedReceipt = response.receipts[0];
const safeIndividualReceipt = individualReceipt!;

// Compare receipt fields
expect(combinedReceipt).to.deep.equal(safeIndividualReceipt);
// The response is the stratus block DTO, with receipts embedded in each transaction
expect(response.block).to.equal(undefined);
expect(response.receipts).to.equal(undefined);
expect(bytesToHex(response.header.hash)).to.equal(blockHash);
expect(swapU32(response.header.number)).to.equal(parseInt(blockNumber, 16));
expect(response.transactions).to.have.length(1);
expect(bytesToHex(response.transactions[0].input.hash)).to.equal(txHash);
expect(response.transactions[0].execution).to.not.equal(undefined);
expect(response.transactions[0].logs).to.be.an("array");
});
});
describe("stratus_getBlockByTimestamp", () => {
Expand Down
50 changes: 37 additions & 13 deletions e2e/test/follower/e2e-pagination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,25 @@ import { FOLLOWER_URL, rpcCall, waitForFollowerBlock, waitForReceipt } from "./h
const MAX_RESPONSE_BYTES = 8192;
const FAT_TX_DATA_BYTES = 50_000;

// The block DTO serializes hashes as byte arrays; convert them to hex for assertions.
const bytesToHex = (bytes: number[]) => "0x" + Buffer.from(bytes).toString("hex");

// The block DTO serializes block numbers as byte-order-swapped u32; swap them back for assertions.
const swapU32 = (value: number) =>
((value & 0xff) << 24) | ((value & 0xff00) << 8) | ((value >>> 8) & 0xff00) | (value >>> 24);

describe("Pagination", () => {
it("paginates oversized importer responses and keeps the follower syncing", async () => {
// a fitting response is served normally, with no envelope, so old followers are unaffected
// a fitting response is served normally, with no envelope
const earlyBlock = await send("eth_getBlockByNumber", ["0x1", false]);
expect(earlyBlock).to.not.be.null;
expect(earlyBlock).to.not.equal(null);
const small = await send("stratus_getBlockAndReceipts", [earlyBlock.hash]);
expect(small.stratus_paginated).to.be.undefined;
expect(small.block.number).to.equal("0x1");
expect(small.stratus_paginated).to.equal(undefined);
expect(small.block).to.equal(undefined);
expect(small.receipts).to.equal(undefined);
expect(bytesToHex(small.header.hash)).to.equal(earlyBlock.hash);
expect(swapU32(small.header.number)).to.equal(1);
expect(small.transactions).to.be.an("array");

// fat contract deployment: the code always fails, but the fat data makes the response oversized
const nonce = await send("eth_getTransactionCount", [ALICE.address]);
Expand All @@ -38,15 +49,15 @@ describe("Pagination", () => {
// the old single-parameter call fails with the oversized response error (-32008),
// which is exactly what would stall an importer before pagination existed
const legacy = await sendAndGetFullResponse("stratus_getBlockAndReceipts", [fatBlockHash]);
expect(legacy.data.error).to.not.be.undefined;
expect(legacy.data.error).to.not.equal(undefined);
expect(legacy.data.error.code).to.equal(-32008);

// paginated reassembly; the chunk size is decided by the leader's response size limit
let assembled: Buffer = Buffer.alloc(0);
let total = 0;
for (let offset = 0; total === 0 || assembled.length < total; offset = assembled.length) {
const envelope = await send("stratus_getBlockAndReceipts", [fatBlockHash, { offset: offset }]);
expect(envelope.stratus_paginated).to.not.be.undefined;
expect(envelope.stratus_paginated).to.not.equal(undefined);
total = envelope.stratus_paginated.total;
const chunk = Buffer.from(envelope.stratus_paginated.chunk, "base64");
expect(chunk.length).to.be.greaterThan(0);
Expand All @@ -55,18 +66,31 @@ describe("Pagination", () => {
expect(assembled.length).to.equal(total);
expect(total).to.be.greaterThan(MAX_RESPONSE_BYTES, "the block response should be oversized");

// the reassembled content matches the block
// the reassembled content is the block DTO, with receipts embedded
const response = JSON.parse(assembled.toString("utf8"));
expect(response.block.hash).to.equal(fatBlockHash);
expect(parseInt(response.block.number, 16)).to.equal(fatBlockNumber);
expect(response.block.transactions).to.have.length(1);
expect(response.receipts).to.have.length(1);
expect(response.receipts[0].transactionHash).to.equal(txHash);
expect(response.block).to.equal(undefined);
expect(response.receipts).to.equal(undefined);
expect(bytesToHex(response.header.hash)).to.equal(fatBlockHash);
expect(swapU32(response.header.number)).to.equal(fatBlockNumber);
expect(response.transactions).to.have.length(1);
expect(bytesToHex(response.transactions[0].input.hash)).to.equal(txHash);
expect(response.transactions[0].execution).to.not.equal(undefined);
expect(response.transactions[0].logs).to.not.equal(undefined);

// the follower imports the fat block through the paginated importer
await waitForFollowerBlock(fatBlockNumber);

// the follower serves the same block content; the leader block is requested thin because
// its response limit rejects the full fat block over `eth_getBlockByNumber`
const leaderBlock = await send("eth_getBlockByNumber", [receipt.blockNumber, false]);
const followerBlock = await rpcCall(FOLLOWER_URL, "eth_getBlockByNumber", [receipt.blockNumber, true]);
expect(followerBlock.result.hash).to.equal(leaderBlock.hash);
expect(followerBlock.result.transactions).to.have.lengthOf(leaderBlock.transactions.length);
expect(followerBlock.result.transactions[0].hash).to.equal(txHash);

// and the transaction receipt is available on the follower
const followerReceipt = await rpcCall(FOLLOWER_URL, "eth_getTransactionReceipt", [txHash]);
expect(followerReceipt.result).to.not.be.null;
expect(followerReceipt.result).to.not.equal(null);
expect(followerReceipt.result.blockNumber).to.equal(receipt.blockNumber);
});
});
3 changes: 0 additions & 3 deletions src/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

use alloy_primitives::B256;

use crate::eth::types::ExternalTransaction;

// -----------------------------------------------------------------------------
// Serde
// -----------------------------------------------------------------------------
Expand All @@ -14,7 +12,6 @@ pub type JsonValue = serde_json::Value;
// -----------------------------------------------------------------------------
pub type AlloyBlockVoid = alloy_rpc_types_eth::Block<()>;
pub type AlloyBlockAlloyTransaction = alloy_rpc_types_eth::Block<alloy_rpc_types_eth::Transaction>;
pub type AlloyBlockExternalTransaction = alloy_rpc_types_eth::Block<ExternalTransaction>;
pub type AlloyBlockB256 = alloy_rpc_types_eth::Block<B256>;
pub type AlloyBytes = alloy_primitives::Bytes;
pub type AlloyLog = alloy_rpc_types_eth::Log;
Expand Down
Loading
Loading