Skip to content
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

feat: upgraded encoding of transactions in consensus Payload. #2245

Merged
merged 10 commits into from
Jun 19, 2024
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions core/lib/basic_types/src/protocol_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ pub enum ProtocolVersionId {
}

impl ProtocolVersionId {
pub fn latest() -> Self {
pub const fn latest() -> Self {
Self::Version24
}

pub fn next() -> Self {
pub const fn next() -> Self {
Self::Version25
}

Expand Down
4 changes: 4 additions & 0 deletions core/lib/dal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,9 @@ strum = { workspace = true, features = ["derive"] }
tracing.workspace = true
chrono = { workspace = true, features = ["serde"] }

[dev-dependencies]
zksync_test_account.workspace = true
zksync_concurrency.workspace = true
pompon0 marked this conversation as resolved.
Show resolved Hide resolved

[build-dependencies]
zksync_protobuf_build.workspace = true
118 changes: 95 additions & 23 deletions core/lib/dal/src/consensus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use anyhow::{anyhow, Context as _};
use zksync_consensus_roles::validator;
use zksync_protobuf::{required, ProtoFmt, ProtoRepr};
use zksync_types::{
abi, ethabi,
fee::Fee,
l1::{OpProcessingType, PriorityQueueType},
l2::TransactionType,
Expand Down Expand Up @@ -38,38 +39,59 @@ pub struct Payload {
impl ProtoFmt for Payload {
type Proto = proto::Payload;

fn read(message: &Self::Proto) -> anyhow::Result<Self> {
let mut transactions = Vec::with_capacity(message.transactions.len());
for (i, tx) in message.transactions.iter().enumerate() {
transactions.push(tx.read().with_context(|| format!("transactions[{i}]"))?)
fn read(r: &Self::Proto) -> anyhow::Result<Self> {
let protocol_version = required(&r.protocol_version)
.and_then(|x| Ok(ProtocolVersionId::try_from(u16::try_from(*x)?)?))
.context("protocol_version")?;
let mut transactions = vec![];

match protocol_version {
v if v >= ProtocolVersionId::Version25 => {
moshababo marked this conversation as resolved.
Show resolved Hide resolved
anyhow::ensure!(
r.transactions.is_empty(),
"transactions should be empty in protocol_version {v}"
);
for (i, tx) in r.transactions_v25.iter().enumerate() {
transactions.push(
tx.read()
.with_context(|| format!("transactions_v25[{i}]"))?,
);
}
}
v => {
anyhow::ensure!(
r.transactions_v25.is_empty(),
"transactions_v25 should be empty in protocol_version {v}"
);
for (i, tx) in r.transactions.iter().enumerate() {
transactions.push(tx.read().with_context(|| format!("transactions[{i}]"))?)
}
}
}

Ok(Self {
protocol_version: required(&message.protocol_version)
.and_then(|x| Ok(ProtocolVersionId::try_from(u16::try_from(*x)?)?))
.context("protocol_version")?,
hash: required(&message.hash)
protocol_version,
hash: required(&r.hash)
.and_then(|h| parse_h256(h))
.context("hash")?,
l1_batch_number: L1BatchNumber(
*required(&message.l1_batch_number).context("l1_batch_number")?,
*required(&r.l1_batch_number).context("l1_batch_number")?,
),
timestamp: *required(&message.timestamp).context("timestamp")?,
l1_gas_price: *required(&message.l1_gas_price).context("l1_gas_price")?,
l2_fair_gas_price: *required(&message.l2_fair_gas_price)
.context("l2_fair_gas_price")?,
fair_pubdata_price: message.fair_pubdata_price,
virtual_blocks: *required(&message.virtual_blocks).context("virtual_blocks")?,
operator_address: required(&message.operator_address)
timestamp: *required(&r.timestamp).context("timestamp")?,
l1_gas_price: *required(&r.l1_gas_price).context("l1_gas_price")?,
l2_fair_gas_price: *required(&r.l2_fair_gas_price).context("l2_fair_gas_price")?,
fair_pubdata_price: r.fair_pubdata_price,
virtual_blocks: *required(&r.virtual_blocks).context("virtual_blocks")?,
operator_address: required(&r.operator_address)
.and_then(|a| parse_h160(a))
.context("operator_address")?,
transactions,
last_in_batch: *required(&message.last_in_batch).context("last_in_batch")?,
last_in_batch: *required(&r.last_in_batch).context("last_in_batch")?,
})
}

fn build(&self) -> Self::Proto {
Self::Proto {
let mut x = Self::Proto {
protocol_version: Some((self.protocol_version as u16).into()),
hash: Some(self.hash.as_bytes().into()),
l1_batch_number: Some(self.l1_batch_number.0),
Expand All @@ -80,13 +102,19 @@ impl ProtoFmt for Payload {
virtual_blocks: Some(self.virtual_blocks),
operator_address: Some(self.operator_address.as_bytes().into()),
// Transactions are stored in execution order, therefore order is deterministic.
transactions: self
.transactions
.iter()
.map(proto::Transaction::build)
.collect(),
transactions: vec![],
transactions_v25: vec![],
last_in_batch: Some(self.last_in_batch),
};
match self.protocol_version {
v if v >= ProtocolVersionId::Version25 => {
x.transactions_v25 = self.transactions.iter().map(ProtoRepr::build).collect();
}
_ => {
x.transactions = self.transactions.iter().map(ProtoRepr::build).collect();
}
}
x
}
}

Expand All @@ -100,6 +128,50 @@ impl Payload {
}
}

impl ProtoRepr for proto::TransactionV25 {
type Type = Transaction;

fn read(&self) -> anyhow::Result<Self::Type> {
use proto::transaction_v25::T;
let tx = match required(&self.t)? {
T::L1(l1) => abi::Transaction::L1 {
tx: required(&l1.rlp)
.and_then(|x| {
let tokens = ethabi::decode(&[abi::L2CanonicalTransaction::schema()], x)
.context("ethabi::decode()")?;
// Unwrap is safe because `ethabi::decode` does the verification.
let tx =
abi::L2CanonicalTransaction::decode(tokens.into_iter().next().unwrap())
.context("L2CanonicalTransaction::decode()")?;
Ok(tx)
})
.context("rlp")?
.into(),
factory_deps: l1.factory_deps.clone(),
eth_block: 0,
moshababo marked this conversation as resolved.
Show resolved Hide resolved
},
T::L2(l2) => abi::Transaction::L2(required(&l2.rlp).context("rlp")?.clone()),
};
tx.try_into()
}

fn build(tx: &Self::Type) -> Self {
let tx = abi::Transaction::try_from(tx.clone()).unwrap();
use proto::transaction_v25::T;
Self {
t: Some(match tx {
abi::Transaction::L1 {
tx, factory_deps, ..
} => T::L1(proto::L1Transaction {
rlp: Some(ethabi::encode(&[tx.encode()])),
moshababo marked this conversation as resolved.
Show resolved Hide resolved
factory_deps,
}),
abi::Transaction::L2(tx) => T::L2(proto::L2Transaction { rlp: Some(tx) }),
}),
}
}
}

impl ProtoRepr for proto::Transaction {
type Type = Transaction;

Expand Down
20 changes: 20 additions & 0 deletions core/lib/dal/src/consensus/proto/mod.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,30 @@ message Payload {
optional uint64 fair_pubdata_price = 11; // required since 1.4.1; gwei
optional uint32 virtual_blocks = 6; // required
optional bytes operator_address = 7; // required; H160
// Set for protocol_version < 25.
repeated Transaction transactions = 8;
// Set for protocol_version >= 25.
repeated TransactionV25 transactions_v25 = 12;
optional bool last_in_batch = 10; // required
}

message L1Transaction {
pompon0 marked this conversation as resolved.
Show resolved Hide resolved
optional bytes rlp = 1; // required; RLP encoded L2CanonicalTransaction
repeated bytes factory_deps = 2;
}

message L2Transaction {
optional bytes rlp = 1; // required; RLP encoded TransactionRequest
}

message TransactionV25 {
// required
oneof t {
L1Transaction l1 = 1;
L2Transaction l2 = 2;
}
}

message Transaction {
reserved 5;
reserved "received_timestamp_ms";
Expand Down
64 changes: 59 additions & 5 deletions core/lib/dal/src/consensus/tests.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,75 @@
use std::fmt::Debug;

use rand::Rng;
use zksync_concurrency::ctx;
use zksync_protobuf::{
repr::{decode, encode},
testonly::test_encode,
ProtoRepr,
};
use zksync_types::{web3::Bytes, Execute, ExecuteTransactionCommon, Transaction};
use zksync_test_account::Account;
use zksync_types::{
web3::Bytes, Execute, ExecuteTransactionCommon, L1BatchNumber, ProtocolVersionId, Transaction,
};

use super::{proto, Payload};
use crate::tests::mock_protocol_upgrade_transaction;

fn execute(rng: &mut impl Rng) -> Execute {
Execute {
contract_address: rng.gen(),
value: rng.gen::<u128>().into(),
calldata: (0..10 * 32).map(|_| rng.gen()).collect(),
// TODO: find a way to generate valid random bytecode.
pompon0 marked this conversation as resolved.
Show resolved Hide resolved
factory_deps: vec![],
}
}

use crate::tests::{mock_l1_execute, mock_l2_transaction, mock_protocol_upgrade_transaction};
fn l1_transaction(rng: &mut impl Rng) -> Transaction {
Account::random_using(rng).get_l1_tx(execute(rng), rng.gen())
}

fn l2_transaction(rng: &mut impl Rng) -> Transaction {
Account::random_using(rng).get_l2_tx_for_execute(execute(rng), None)
}

fn payload(rng: &mut impl Rng, protocol_version: ProtocolVersionId) -> Payload {
Payload {
protocol_version,
hash: rng.gen(),
l1_batch_number: L1BatchNumber(rng.gen()),
timestamp: rng.gen(),
l1_gas_price: rng.gen(),
l2_fair_gas_price: rng.gen(),
fair_pubdata_price: Some(rng.gen()),
virtual_blocks: rng.gen(),
operator_address: rng.gen(),
transactions: (0..10)
.map(|_| match rng.gen() {
true => l1_transaction(rng),
false => l2_transaction(rng),
})
.collect(),
last_in_batch: rng.gen(),
}
}

/// Tests struct <-> proto struct conversions.
#[test]
fn test_encoding() {
encode_decode::<super::proto::Transaction, ComparableTransaction>(mock_l1_execute().into());
encode_decode::<super::proto::Transaction, ComparableTransaction>(mock_l2_transaction().into());
encode_decode::<super::proto::Transaction, ComparableTransaction>(
let ctx = &ctx::test_root(&ctx::RealClock);
let rng = &mut ctx.rng();
encode_decode::<proto::TransactionV25, ComparableTransaction>(l1_transaction(rng));
encode_decode::<proto::TransactionV25, ComparableTransaction>(l2_transaction(rng));
encode_decode::<proto::Transaction, ComparableTransaction>(l1_transaction(rng));
encode_decode::<proto::Transaction, ComparableTransaction>(l2_transaction(rng));
encode_decode::<proto::Transaction, ComparableTransaction>(
mock_protocol_upgrade_transaction().into(),
);
let p = payload(rng, ProtocolVersionId::Version24);
test_encode(rng, &p);
let p = payload(rng, ProtocolVersionId::Version25);
test_encode(rng, &p);
}

fn encode_decode<P, C>(msg: P::Type)
Expand Down
1 change: 1 addition & 0 deletions core/node/consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ zksync_node_genesis.workspace = true
zksync_node_test_utils.workspace = true
zksync_node_api_server.workspace = true
zksync_test_account.workspace = true
zksync_contracts.workspace= true

tokio.workspace = true
test-casing.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion core/node/consensus/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use zksync_types::{commitment::L1BatchWithMetadata, L1BatchNumber, L2BlockNumber
use super::config;

#[cfg(test)]
mod testonly;
pub(crate) mod testonly;

/// Context-aware `zksync_dal::ConnectionPool<Core>` wrapper.
#[derive(Debug, Clone)]
Expand Down
Loading
Loading