Skip to content

Commit 7c4c428

Browse files
authored
feat(api): add EIP-7966 eth_sendRawTransactionSync method (#4565)
## What ❔ Implement `eth_sendRawTransactionSync` API method to add [EIP-7966](https://eips.ethereum.org/EIPS/eip-7966) support to zksync era API. `sendRawTransactionSync` uses the existing new block pubsub broadcast under the hood to check for transaction inclusion, on new blocks, while racing a user specified timeout (with default and node-configurable max) for inclusion. <!-- What are the changes this PR brings about? --> <!-- Example: This PR adds a PR template to the repo. --> <!-- (For bigger PRs adding more context is appreciated) --> ## Why ❔ With the v29 release and fast (200ms) blocks, dapps can benefit greatly by using synchronous calls for transaction submission to receipt, instead of having to poll for the receipt. EIP-7966 has clear benefits for a number of onchain use cases: - Trading. DEXs, CLOBs, and other trading apps can double their fee revenue by processing twice as many transactions per second, with no modification to block times or gas limits. The increased speed also allows these apps to offer tighter spreads, reduce stale quotes, and increase order execution probability – all of these improve UX. - Gaming. Many onchain games translate rapid, repeated in-game actions (e.g. moves, item trades, crafting, etc.) into individual transactions, each of which generates a polling loop under today’s eth_sendRawTransaction paradigm. EIP-7966’s introduction of synchronous receipts reduces the load on RPCs by eliminating those loops and provides smoother gameplay for users. - Payments. Onchain payments apps with high volumes can process more payments – and therefore generate more fees – with EIP-7966’s eth_sendRawTransactionSync method. This method also allows them to confirm payments faster and eliminate the anxiety users get waiting for a big transfer to be confirmed – a huge UX win. <!-- Why are these changes done? What goal do they contribute to? What are the principles behind them? --> <!-- The `Why` has to be clear to non-Matter Labs entities running their own ZK Chain --> <!-- Example: PR templates ensure PR reviewers, observers, and future iterators are in context about the evolution of repos. --> ## Is this a breaking change? - [ ] Yes - [x] No ## Operational changes <!-- Any config changes? Any new flags? Any changes to any scripts? --> <!-- Please add anything that non-Matter Labs entities running their own ZK Chain may need to know --> Two new configuration values: - `send_raw_tx_sync_default_timeout_ms`: default 2000 (2 seconds) per recommendation in EIP - `send_raw_tx_sync_max_timeout_ms`: default 10000 (10 seconds) - prevents callers from very-long/infinite duration calls by specifying an unreasonable transaction inclusion timeout ## Checklist <!-- Check your PR fulfills the following items. --> <!-- For draft PRs check the boxes as you complete them. --> - [ ] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [ ] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. - [ ] Code has been formatted via `zkstack dev fmt` and `zkstack dev lint`.
1 parent 42c3f0f commit 7c4c428

File tree

18 files changed

+542
-29
lines changed

18 files changed

+542
-29
lines changed

core/bin/external_node/src/config/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ impl ExternalNodeConfig {
203203

204204
impl From<&LocalConfig> for InternalApiConfigBase {
205205
fn from(config: &LocalConfig) -> Self {
206+
let state_keeper_config = &config.state_keeper;
206207
let web3_rpc = &config.api.web3_json_rpc;
207208
Self {
208209
l1_chain_id: config.networks.l1_chain_id,
@@ -216,6 +217,11 @@ impl From<&LocalConfig> for InternalApiConfigBase {
216217
filters_disabled: web3_rpc.filters_disabled,
217218
l1_to_l2_txs_paused: false,
218219
eth_call_gas_cap: web3_rpc.eth_call_gas_cap,
220+
send_raw_tx_sync_max_timeout_ms: web3_rpc.send_raw_tx_sync_max_timeout_ms,
221+
send_raw_tx_sync_default_timeout_ms: web3_rpc.send_raw_tx_sync_default_timeout_ms,
222+
send_raw_tx_sync_poll_interval_ms: state_keeper_config
223+
.l2_block_commit_deadline
224+
.as_millis() as u64,
219225
}
220226
}
221227
}

core/bin/external_node/src/config/tests/config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ api:
8585
fee_history_limit: 100
8686
whitelisted_tokens_for_aa:
8787
- '0x0000000000000000000000000000000000000001'
88+
send_raw_tx_sync_max_timeout_ms: 10000
89+
send_raw_tx_sync_default_timeout_ms: 2000
8890

8991
contracts:
9092
l1:
@@ -93,6 +95,7 @@ state_keeper:
9395
l2_block_seal_queue_capacity: 20
9496
save_call_traces: false
9597
protective_reads_persistence_enabled: true
98+
l2_block_commit_deadline_ms: 1000
9699
commitment_generator:
97100
max_parallelism: 4
98101
timestamp_asserter:

core/bin/external_node/src/config/tests/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ fn parsing_from_full_env() {
257257
258258
EN_L2_BLOCK_SEAL_QUEUE_CAPACITY=20
259259
EN_PROTECTIVE_READS_PERSISTENCE_ENABLED=true
260+
EN_L2_BLOCK_COMMIT_DEADLINE_MS=1000
260261
# NEW PARAMS: From SharedStateKeeperConfig
261262
EN_SAVE_CALL_TRACES=false
262263
@@ -305,6 +306,9 @@ fn parsing_from_full_env() {
305306
EN_API_TREE_API_REMOTE_URL=http://tree/
306307
# Tree component config
307308
EN_TREE_API_PORT=2955
309+
310+
EN_API_WEB3_JSON_RPC_SEND_RAW_TX_SYNC_MAX_TIMEOUT_MS=10000
311+
EN_API_WEB3_JSON_RPC_SEND_RAW_TX_SYNC_DEFAULT_TIMEOUT_MS=2000
308312
"#;
309313
let env = smart_config::Environment::from_dotenv("test.env", env)
310314
.unwrap()

core/bin/zksync_server/src/node_builder.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,8 +462,9 @@ impl MainNodeBuilder {
462462
pruning_info_refresh_interval: Duration::from_secs(10),
463463
polling_interval: rpc_config.pubsub_polling_interval,
464464
};
465-
let base = InternalApiConfigBase::new(&self.genesis_config, &rpc_config)
466-
.with_l1_to_l2_txs_paused(self.configs.mempool_config.l1_to_l2_txs_paused);
465+
let base =
466+
InternalApiConfigBase::new(&self.genesis_config, &rpc_config, &state_keeper_config)
467+
.with_l1_to_l2_txs_paused(self.configs.mempool_config.l1_to_l2_txs_paused);
467468
Ok((base, optional_config))
468469
}
469470

core/lib/config/src/configs/api.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,12 @@ pub struct Web3JsonRpcConfig {
402402
/// (hundreds or thousands RPS).
403403
#[config(default, alias = "extended_rpc_tracing")]
404404
pub extended_api_tracing: bool,
405+
/// Maximum timeout for `eth_sendRawTransactionSync` in milliseconds.
406+
#[config(default_t = 10_000)]
407+
pub send_raw_tx_sync_max_timeout_ms: u64,
408+
/// Default timeout for `eth_sendRawTransactionSync` in milliseconds.
409+
#[config(default_t = 2_000)]
410+
pub send_raw_tx_sync_default_timeout_ms: u64,
405411
}
406412

407413
impl Web3JsonRpcConfig {
@@ -546,6 +552,8 @@ mod tests {
546552
extended_api_tracing: true,
547553
gas_price_scale_factor_open_batch: Some(1.3),
548554
eth_call_gas_cap: None,
555+
send_raw_tx_sync_max_timeout_ms: 10000,
556+
send_raw_tx_sync_default_timeout_ms: 2000,
549557
},
550558
healthcheck: HealthCheckConfig {
551559
port: 8081.into(),
@@ -592,6 +600,8 @@ mod tests {
592600
API_WEB3_JSON_RPC_WEBSOCKET_REQUESTS_PER_MINUTE_LIMIT=10
593601
API_WEB3_JSON_RPC_MEMPOOL_CACHE_SIZE=10000
594602
API_WEB3_JSON_RPC_MEMPOOL_CACHE_UPDATE_INTERVAL=50
603+
API_WEB3_JSON_RPC_SEND_RAW_TX_SYNC_MAX_TIMEOUT_MS=10000
604+
API_WEB3_JSON_RPC_SEND_RAW_TX_SYNC_DEFAULT_TIMEOUT_MS=2000
595605
API_CONTRACT_VERIFICATION_PORT="3070"
596606
API_CONTRACT_VERIFICATION_URL="http://127.0.0.1:3070"
597607
API_WEB3_JSON_RPC_TREE_API_URL="http://tree/"
@@ -658,6 +668,8 @@ mod tests {
658668
eth_call_gas_cap: null
659669
request_timeout_sec: 20
660670
tree_api_url: "http://tree/"
671+
send_raw_tx_sync_max_timeout_ms: 10000
672+
send_raw_tx_sync_default_timeout_ms: 2000
661673
prometheus:
662674
listener_port: 3312
663675
pushgateway_url: http://127.0.0.1:9091
@@ -721,6 +733,8 @@ mod tests {
721733
eth_call_gas_cap: null
722734
request_timeout: 20s
723735
tree_api_url: "http://tree/"
736+
send_raw_tx_sync_max_timeout_ms: 10000
737+
send_raw_tx_sync_default_timeout_ms: 2000
724738
prometheus:
725739
listener_port: 3312
726740
pushgateway_url: http://127.0.0.1:9091

core/lib/config/src/configs/chain.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ pub struct SharedStateKeeperConfig {
4040
#[config(default_t = 10)]
4141
pub l2_block_seal_queue_capacity: usize,
4242

43+
/// Deadline after which an L2 block should be sealed by the timeout sealer.
44+
#[config(deprecated = "miniblock_commit_deadline")]
45+
#[config(default_t = Duration::from_secs(1))]
46+
pub l2_block_commit_deadline: Duration,
47+
4348
/// Whether to save call traces when processing blocks in the state keeper.
4449
#[config(default_t = true)]
4550
pub save_call_traces: bool,
@@ -127,10 +132,6 @@ pub struct StateKeeperConfig {
127132
#[config(deprecated = "block_commit_deadline")]
128133
#[config(default_t = Duration::from_millis(2_500))]
129134
pub l1_batch_commit_deadline: Duration,
130-
/// Deadline after which an L2 block should be sealed by the timeout sealer.
131-
#[config(deprecated = "miniblock_commit_deadline")]
132-
#[config(default_t = Duration::from_secs(1))]
133-
pub l2_block_commit_deadline: Duration,
134135
/// The max payload size threshold that triggers sealing of an L2 block.
135136
#[config(deprecated = "miniblock_max_payload_size")]
136137
#[config(default_t = ByteSize(1_000_000), with = Fallback(SizeUnit::Bytes))]
@@ -177,10 +178,11 @@ impl StateKeeperConfig {
177178
/// Values mostly repeat the values used in the localhost environment.
178179
pub fn for_tests() -> Self {
179180
Self {
180-
shared: SharedStateKeeperConfig::default(),
181+
shared: SharedStateKeeperConfig {
182+
..SharedStateKeeperConfig::default()
183+
},
181184
seal_criteria: SealCriteriaConfig::for_tests(),
182185
l1_batch_commit_deadline: Duration::from_millis(2500),
183-
l2_block_commit_deadline: Duration::from_secs(1),
184186
l2_block_max_payload_size: ByteSize(1_000_000),
185187
max_single_tx_gas: 6000000,
186188
max_allowed_l2_tx_gas_limit: 4000000000,
@@ -281,6 +283,7 @@ mod tests {
281283
StateKeeperConfig {
282284
shared: SharedStateKeeperConfig {
283285
l2_block_seal_queue_capacity: 10,
286+
l2_block_commit_deadline: Duration::from_millis(1000),
284287
save_call_traces: false,
285288
protective_reads_persistence_enabled: true,
286289
},
@@ -296,7 +299,6 @@ mod tests {
296299
max_circuits_per_batch: 24100,
297300
},
298301
l1_batch_commit_deadline: Duration::from_millis(2500),
299-
l2_block_commit_deadline: Duration::from_millis(1000),
300302
l2_block_max_payload_size: ByteSize(1_000_000),
301303
max_single_tx_gas: 1_000_000,
302304
max_allowed_l2_tx_gas_limit: 2_000_000_000,

core/lib/web3_decl/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ pub enum Web3Error {
5050
InternalError(#[from] anyhow::Error),
5151
#[error("Server is shutting down")]
5252
ServerShuttingDown,
53+
#[error("Transaction {0:?} timeout waiting for receipt")]
54+
TransactionTimeout(zksync_types::H256),
55+
#[error("Transaction processing error: {0}")]
56+
TransactionUnready(String),
57+
#[error("Invalid timeout. Max timeout is {0}ms")]
58+
InvalidTimeout(u64),
5359
}
5460

5561
/// Client RPC error with additional details: the method name and arguments of the called method.

core/lib/web3_decl/src/namespaces/eth.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ pub trait EthNamespace {
153153
#[method(name = "sendRawTransaction")]
154154
async fn send_raw_transaction(&self, tx_bytes: Bytes) -> RpcResult<H256>;
155155

156+
#[method(name = "sendRawTransactionSync")]
157+
async fn send_raw_transaction_sync(
158+
&self,
159+
tx_bytes: Bytes,
160+
max_wait_ms: Option<U256>,
161+
) -> RpcResult<TransactionReceipt>;
162+
156163
#[method(name = "syncing")]
157164
async fn syncing(&self) -> RpcResult<SyncState>;
158165

core/node/api_server/src/web3/backend_jsonrpsee/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ impl MethodTracer {
4040
| Web3Error::TooManyTopics
4141
| Web3Error::FilterNotFound
4242
| Web3Error::InvalidFilterBlockHash
43+
| Web3Error::InvalidTimeout(_)
4344
| Web3Error::LogsLimitExceeded(_, _, _) => ErrorCode::InvalidParams.code(),
4445
Web3Error::SubmitTransactionError(_, _)
4546
| Web3Error::SerializationError(_)
4647
| Web3Error::ProxyError(_) => 3,
48+
Web3Error::TransactionTimeout(_) => 4,
49+
Web3Error::TransactionUnready(_) => 5,
4750
Web3Error::TreeApiUnavailable => 6,
4851
Web3Error::ServerShuttingDown => ErrorCode::ServerIsBusy.code(),
4952
};

core/node/api_server/src/web3/backend_jsonrpsee/namespaces/eth.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,16 @@ impl EthNamespaceServer for EthNamespace {
225225
.map_err(|err| self.current_method().map_err(err))
226226
}
227227

228+
async fn send_raw_transaction_sync(
229+
&self,
230+
tx_bytes: Bytes,
231+
max_wait_ms: Option<U256>,
232+
) -> RpcResult<TransactionReceipt> {
233+
self.send_raw_transaction_sync_impl(tx_bytes, max_wait_ms)
234+
.await
235+
.map_err(|err| self.current_method().map_err(err))
236+
}
237+
228238
async fn syncing(&self) -> RpcResult<SyncState> {
229239
Ok(self.syncing_impl())
230240
}

0 commit comments

Comments
 (0)