//! Oracle transaction builder — constructs synthetic L2Tx for price updates.//!//! The State Keeper calls `build_oracle_update_tx()` at the start of each batch//! to inject Oracle price data as the first user-space transaction, immediately//! after any protocol upgrade tx.usezksync_contracts::ORACLE_HUB_ADDRESS;usezksync_types::{fee::Fee,l2::L2Tx,Address,Nonce,L1BatchNumber,U256,transaction_request::PaymasterParams,};/// Gas limit for the Oracle update transaction./// Generous limit — operator doesn't actually pay gas.constORACLE_TX_GAS_LIMIT:u64=5_000_000;/// Build an Oracle price update transaction.////// Returns a `Transaction` that calls `OracleHub.batchUpdatePrices()` with the/// pre-encoded ABI calldata from the Oracle service.////// The transaction uses L2Tx type (EIP-712) with:/// - sender: operator (fee_account)/// - target: ORACLE_HUB_ADDRESS (0x8016)/// - calldata: pre-encoded batchUpdatePrices(bytes32[],uint128[],uint64[],uint8[])/// - gas_limit: 5M (operator doesn't pay)/// - nonce: Nonce(0) — operator tx, not validated by mempoolpubfnbuild_oracle_update_tx(operator_address:Address,oracle_calldata:Vec<u8>,)->zksync_types::Transaction{letl2_tx=L2Tx::new(Some(ORACLE_HUB_ADDRESS),// to: OracleHub (0x8016)oracle_calldata,// calldata: batchUpdatePrices(...)Nonce(0),// nonce: operator tx, not validatedFee{gas_limit:U256::from(ORACLE_TX_GAS_LIMIT),max_fee_per_gas:U256::from(0u64),max_priority_fee_per_gas:U256::from(0u64),gas_per_pubdata_limit:U256::from(800u64),},operator_address,// from: operatorU256::zero(),// value: 0vec![],// factory_deps: nonePaymasterParams::default(),// no paymaster);l2_tx.into()}#[cfg(test)]mod tests {usesuper::*;usezksync_types::ExecuteTransactionCommon;#[test]fntest_build_oracle_tx_correct_target(){letoperator=Address::repeat_byte(0xAA);letcalldata=vec![0x01,0x02,0x03,0x04];lettx=build_oracle_update_tx(operator,calldata);assert_eq!(tx.execute.contract_address,Some(ORACLE_HUB_ADDRESS),"Oracle tx must target OracleHub (0x8016)");}#[test]fntest_build_oracle_tx_correct_sender(){letoperator=Address::repeat_byte(0xBB);letcalldata=vec![0xDE,0xAD];lettx=build_oracle_update_tx(operator,calldata.clone());match&tx.common_data {ExecuteTransactionCommon::L2(data)=>{assert_eq!(data.initiator_address,operator);}_=>panic!("Oracle tx must be L2 type"),}}#[test]fntest_build_oracle_tx_correct_calldata(){letoperator=Address::repeat_byte(0xCC);letcalldata=vec![0xAA,0xBB,0xCC,0xDD,0xEE];lettx=build_oracle_update_tx(operator,calldata.clone());assert_eq!(tx.execute.calldata,calldata);}#[test]fntest_build_oracle_tx_zero_value(){letoperator=Address::repeat_byte(0xDD);letcalldata=vec![0x01];lettx=build_oracle_update_tx(operator,calldata);assert_eq!(tx.execute.value,U256::zero());}#[test]fntest_build_oracle_tx_gas_limit(){letoperator=Address::repeat_byte(0xEE);letcalldata=vec![0x01];lettx=build_oracle_update_tx(operator,calldata);match&tx.common_data {ExecuteTransactionCommon::L2(data)=>{assert_eq!(data.fee.gas_limit,U256::from(ORACLE_TX_GAS_LIMIT));}_=>panic!("Oracle tx must be L2 type"),}}#[test]fntest_build_oracle_tx_large_calldata(){letoperator=Address::repeat_byte(0xFF);// 100 pairs × ~128 bytes = ~12800 bytes — far exceeds old 768 byte limitletcalldata=vec![0x42;12800];lettx=build_oracle_update_tx(operator,calldata.clone());assert_eq!(tx.execute.calldata.len(),12800);assert_eq!(tx.execute.calldata,calldata);}}
Step 2: Register module in lib.rs
Modify era-core/core/node/state_keeper/src/lib.rs — add pub mod oracle_tx; after line 16 (pub mod io;):
Note: If the crate path differs, search for existing usages: Run: grep -r baby_oracle_wiring era-core/core/ --include="Cargo.toml" | head -5
Step 6: Find and update all callers of MempoolIO::new()
Search for call sites:
Run: cd /Users/judybaby/CodeBase/github/Layer2 && grep -rn "MempoolIO::new(" era-core/ --include="*.rs"
For each call site, add None (or the actual SharedOracleService) as the last argument. The primary caller is in the node wiring layer — it will need the actual service. Test callers should pass None.
Step 7: Verify compilation
Run: cd /Users/judybaby/CodeBase/github/Layer2/era-core/core && cargo check -p zksync_state_keeper 2>&1 | tail -10 Expected: compilation succeeds (may need to fix callers first)
Step 1: Add get_oracle_tx_calldata to StateKeeperIO trait
Add to the StateKeeperIO trait in io/mod.rs after load_batch_state_hash() (line 254):
Step 2: Implement in MempoolIO
Add to impl StateKeeperIO for MempoolIO block (after any existing method, around line 495):
Step 3: Add process_oracle_tx method to StateKeeperInner
Add in keeper.rs after process_upgrade_tx() (after line 710):
Step 4: Inject Oracle tx in process_block()
Modify process_block() in keeper.rs around line 921. The current code:
Change to:
IMPORTANT: The original else branch (lines 926-948) handles the case where there's no protocol_upgrade_tx. After restructuring, we need to keep that logic but adjust the flow. The key is:
Protocol upgrade tx (if any)
Oracle tx (if any)
L2 block params for non-first blocks
Fictive block check
User tx loop
The exact restructuring needs care to preserve the original control flow. Read the full process_block() method carefully and ensure:
set_l2_block_params is still called for non-first blocks
Step 3: Remove oracle_calldata from MempoolIO batch construction
In mempool.rs:
Delete line 580 (oracle_calldata: ...)
Delete line 710 (oracle_calldata: ...)
Delete the get_oracle_calldata() helper method added in Task 2 (no longer needed)
Delete the oracle_service field from MempoolIO struct (no longer needed for batch params)
BUT KEEP the oracle_service field — it's still needed for get_oracle_tx_calldata() on the trait!
Wait — actually, with the new design, the oracle_calldata is no longer passed via L1BatchParams/L1BatchEnv. Instead, it's obtained via StateKeeperIO::get_oracle_tx_calldata() in keeper.rs. So:
Remove oracle_calldata from L1BatchParams (stop passing it through the batch env pipe)
Keep oracle_service field in MempoolIO (used by get_oracle_tx_calldata())
Remove get_oracle_calldata() private helper if it was only used for batch params
Step 4: Remove bootloader memory slot code
In era-core/core/lib/multivm/src/versions/vm_latest/bootloader/init.rs:
Step 1: Add operator storage + modifier to OracleHub.sol
In OracleHub.sol, add after line 44 (uint256 private constant BPS_DENOMINATOR = 10_000;):
Step 2: Change batchUpdatePrices modifier
In OracleHub.sol line 80, change:
To:
Step 3: Add setOperator function
Add after setConfig() (after line 156):
Step 4: Update initialize() to set initial operator
In initialize() (line 55), change from onlyCallFromBootloader to accept an operator param:
Actually, initialize() should stay onlyCallFromBootloader since it's called during genesis. But add operator initialization inside it:
Add before the closing } of initialize() (before line 69):
Step 5: Add OperatorUpdated event to IOracleHub.sol
In IOracleHub.sol, add after line 23 (ConfigUpdated event):
Add to the interface functions (after setConfig at line 41):
Step 6: Update comment in OracleHub.sol
Change the contract-level comment (lines 12-19) to reflect new design:
"Price data is injected by the operator via synthetic L2 transaction at the start of each L1 Batch"
Remove reference to "bootloader"
Step 7: Add Foundry tests
In contracts/test/OracleHub.t.sol, add operator permission tests:
Note: The exact test implementation depends on the existing test file structure. Read contracts/test/OracleHub.t.sol first to understand the existing test setup.
Step 8: Run Foundry tests
Expected: all tests pass (existing + new operator tests)
Step 9: Compile system contracts
Expected: compilation succeeds
Step 10: Commit
Task 7: 全量回归 + dev-log
Files:
Verify: all era-core crates compile
Verify: all Foundry tests pass
Verify: bootloader compiles
Modify: docs/dev-log.md (add Phase 2.6 entry)
Step 1: era-core full cargo check
Expected: all crates compile (200+)
Step 2: Run oracle_tx tests
Expected: 6 tests pass
Step 3: Run oracle-wiring tests
Expected: 38 tests pass
Step 4: Run Foundry tests
Expected: 241+ tests pass
Step 5: Verify bootloader compilation
Expected: bootloader preprocesses successfully
Step 6: Verify no remaining oracle_calldata references in Rust
pub mod executor;
mod health;
pub mod io;
mod keeper;
mod mempool_actor;
pub(crate) mod mempool_guard;
pub mod metrics;
pub mod node;
pub mod oracle_tx; // ← ADD THIS LINE
pub mod seal_criteria;
cd /Users/judybaby/CodeBase/github/Layer2
git add era-core/core/node/state_keeper/src/oracle_tx.rs era-core/core/node/state_keeper/src/lib.rs
git commit -m "feat: oracle_tx.rs — synthetic L2Tx builder for Oracle price injection"
use baby_oracle_wiring::wiring::SharedOracleService;
pub struct MempoolIO {
// ... existing fields ...
settlement_layer: Option<SettlementLayer>,
/// BabyDriver: Shared Oracle service for price data injection.
oracle_service: Option<SharedOracleService>,
}
/// Get Oracle calldata from the shared service.
/// Returns None if Oracle is not configured or has no price data.
fn get_oracle_calldata(&self) -> Option<Vec<u8>> {
let service = self.oracle_service.as_ref()?;
let svc = service.lock().ok()?;
svc.encode_oracle_calldata()
}
cd /Users/judybaby/CodeBase/github/Layer2
git add era-core/core/node/state_keeper/src/io/mempool.rs
git add era-core/core/node/state_keeper/Cargo.toml
# Add any caller files that were modified
git commit -m "feat: MempoolIO accepts SharedOracleService, wires oracle_calldata into batch params"
/// BabyDriver: Get Oracle calldata for the current batch.
/// Returns None if Oracle is not configured or has no data.
fn get_oracle_tx_calldata(&self) -> Option<Vec<u8>> {
None // default impl: no Oracle
}
/// BabyDriver: Process the Oracle price update transaction.
///
/// Similar to `process_upgrade_tx()`, but failure does NOT block the batch.
/// Oracle tx is injected as the first (or second, after upgrade tx) transaction.
async fn process_oracle_tx(
&mut self,
batch_executor: &mut dyn BatchExecutor<OwnedStorage>,
updates_manager: &mut UpdatesManager,
oracle_calldata: Vec<u8>,
) -> anyhow::Result<()> {
use crate::oracle_tx::build_oracle_update_tx;
let operator_address = updates_manager.l1_batch.fee_account;
let tx = build_oracle_update_tx(operator_address, oracle_calldata);
tracing::info!(
"Injecting Oracle price update tx into batch {}",
updates_manager.l1_batch.number
);
let (seal_resolution, exec_result) = self
.process_one_tx(batch_executor, updates_manager, tx.clone())
.await?;
match &seal_resolution {
SealResolution::NoSeal | SealResolution::IncludeAndSeal => {
let TxExecutionResult::Success {
tx_result,
tx_metrics: tx_execution_metrics,
call_tracer_result,
..
} = exec_result
else {
// Oracle tx rejected — warn but don't block batch
tracing::warn!("Oracle tx execution was not successful, skipping");
return Ok(());
};
if tx_result.result.is_failed() {
// Oracle tx reverted inside VM — warn but don't block batch
tracing::warn!(
"Oracle price update tx reverted: {:?}",
tx_result.result
);
return Ok(());
}
updates_manager.extend_from_executed_transaction(
tx,
*tx_result,
*tx_execution_metrics,
call_tracer_result,
);
tracing::info!("Oracle price update tx executed successfully");
}
SealResolution::ExcludeAndSeal => {
tracing::warn!("Oracle tx caused ExcludeAndSeal, skipping");
}
SealResolution::Unexecutable(reason) => {
tracing::warn!("Oracle tx is unexecutable: {reason}, skipping");
}
}
Ok(())
}
if let Some(protocol_upgrade_tx) = state.protocol_upgrade_tx.take() {
self.inner
.process_upgrade_tx(batch_executor, updates_manager, protocol_upgrade_tx)
.await?;
} else {
if let Some(protocol_upgrade_tx) = state.protocol_upgrade_tx.take() {
self.inner
.process_upgrade_tx(batch_executor, updates_manager, protocol_upgrade_tx)
.await?;
}
// BabyDriver: Inject Oracle price update tx after protocol upgrade tx
// (or as the first tx if no upgrade). Failure does not block the batch.
if updates_manager.pending_executed_transactions_len() <= 1 {
// Only inject on the first block of the batch
if let Some(oracle_calldata) = self.inner.io.get_oracle_tx_calldata() {
if let Err(e) = self.inner
.process_oracle_tx(batch_executor, updates_manager, oracle_calldata)
.await
{
tracing::warn!("Oracle tx injection failed: {e:#}, continuing batch");
}
}
}
if updates_manager.pending_executed_transactions_len() == 0 {
// No upgrade tx AND no oracle tx (or both skipped)
// Handle L2 block params for non-first block
cd /Users/judybaby/CodeBase/github/Layer2
git add era-core/core/node/state_keeper/src/keeper.rs
git add era-core/core/node/state_keeper/src/io/mod.rs
git commit -m "feat: keeper.rs injects Oracle tx at batch start (non-blocking)"
cd /Users/judybaby/CodeBase/github/Layer2
git add -A era-core/
git commit -m "refactor: remove oracle_calldata bootloader memory slot pipeline (19 files)"
// DELETE THIS BLOCK:
/// @dev BabyDriver: Oracle calldata starts at slot 8...
function ORACLE_CALLDATA_BEGIN_SLOT() -> ret {
ret := 8
}
function ORACLE_CALLDATA_BEGIN_BYTE() -> ret {
ret := mul(ORACLE_CALLDATA_BEGIN_SLOT(), 32)
}
/// @dev Maximum slots for Oracle calldata...
function ORACLE_CALLDATA_MAX_SLOTS() -> ret {
ret := 24
}
function SCRATCH_SPACE_BEGIN_SLOT() -> ret {
ret := add(ORACLE_CALLDATA_BEGIN_SLOT(), ORACLE_CALLDATA_MAX_SLOTS())
}
function SCRATCH_SPACE_BEGIN_SLOT() -> ret {
ret := 8
}
// DELETE:
/// @dev BabyDriver: Oracle Hub system contract address (0x8016)
function ORACLE_HUB_ADDR() -> ret {
ret := 0x0000000000000000000000000000000000008016
}
// BabyDriver: Update Oracle prices after batch initialization
updateOraclePrices()
// BabyDriver: Update Oracle prices after batch initialization
updateOraclePrices()
cd /Users/judybaby/CodeBase/github/Layer2/era-contracts-l1/system-contracts
nvm use 20 && npx hardhat run scripts/preprocess-bootloader.ts
cd /Users/judybaby/CodeBase/github/Layer2/era-contracts-l1/system-contracts
yarn preprocess:bootloader && yarn compile-yul compile-bootloader
cd /Users/judybaby/CodeBase/github/Layer2/era-contracts-l1/system-contracts
yarn preprocess:system-contracts
npx hardhat compile
// ==================== Operator ====================
/// @notice Operator address that can call batchUpdatePrices
address public operator;
/// @notice Only the operator can call this function
modifier onlyOperator() {
require(msg.sender == operator, "OracleHub: not operator");
_;
}
) external onlyCallFromBootloader {
) external onlyOperator {
/// @notice Set the operator address. Only callable by system call (admin).
function setOperator(address _operator) external onlySystemCall {
require(_operator != address(0), "OracleHub: zero operator");
operator = _operator;
emit OperatorUpdated(_operator);
}
// Set initial operator to the batch coinbase (operator)
operator = tx.origin;
emit OperatorUpdated(tx.origin);
event OperatorUpdated(address indexed operator);
function setOperator(address _operator) external;
function operator() external view returns (address);
function test_batchUpdatePrices_onlyOperator() public {
// Setup: set operator
// Call from operator: should succeed
// Call from non-operator: should revert
}
function test_setOperator_onlySystemCall() public {
// Only system call can set operator
}
function test_setOperator_rejectsZeroAddress() public {
// Zero address should be rejected
}
cd /Users/judybaby/CodeBase/github/Layer2/contracts
forge test --match-contract OracleHub -vvv 2>&1 | tail -30
cd /Users/judybaby/CodeBase/github/Layer2/era-contracts-l1/system-contracts
nvm use 20 && yarn preprocess:system-contracts && npx hardhat compile