Skip to content

Commit

Permalink
Tx Circuit (scroll-tech#220)
Browse files Browse the repository at this point in the history
* feat: RLP encoding verification circuit

* fix: compilation and tx rlp encode tests

* fix: refactor redundant columns, is_first and is_last are advices

* fix: assign dummy rows front and back

* feat: assign multiple inputs to rlp circuit

* feat: add tx table lookup from rlp circuit

* fix: lookup for all fields of tx

* fix: calldata rlc lookup

* hash of rlp encoding

* refactor: remove receipt related verification

* fix: remove lookups from rlp circuit

* refactor: separate out rlp table and embed in circuit

* feat: add eip-155 support for unsigned tx

* chore: refactor tag_index into RLP table

* chore: refactor constraints into TxSign and TxHash

* feat: signed tx support

* feat: verify sig_r and sig_s fields

* fix: add missing check for only one tag

* chore: remove unused gadget

* fix: randomness | add rlp table to tx circuit

* feat: tx circuit lookup to rlp table

* feat: configure lookups from tx circuit

* feat: add calldata length and gas cost to rlp table

* fix: lookups from tx circuit and tests

* fix: handle calldata length == 0 case in lookups

* fix: account for chainid and 0, 0

* fix: clippy

* fix: constraints for tx_id

* fix: additional constraints around last row

* chore: rename rlp circuit based on #650

* fix: calldatalength and gas cost in tx circuit

* feat: RLP encoding verification circuit

* fix: compilation and tx rlp encode tests

* fix: refactor redundant columns, is_first and is_last are advices

* fix: assign dummy rows front and back

* feat: assign multiple inputs to rlp circuit

* feat: add tx table lookup from rlp circuit

* fix: lookup for all fields of tx

* fix: calldata rlc lookup

* hash of rlp encoding

* refactor: remove receipt related verification

* fix: remove lookups from rlp circuit

* refactor: separate out rlp table and embed in circuit

* feat: add eip-155 support for unsigned tx

* chore: refactor tag_index into RLP table

* chore: refactor constraints into TxSign and TxHash

* feat: signed tx support

* feat: verify sig_r and sig_s fields

* fix: add missing check for only one tag

* chore: remove unused gadget

* fix: randomness | add rlp table to tx circuit

* fix: next tx id assignment

* fix: lookup for call data bytes

* fix: make assignments to tx table

* feat: lookups for msg len/rlc and sig fields

* fix: add constraint for tx_id increment at nonce row

* feat: tag equality check

* fix: resolve issues after merging

* feat: lookup to check that call data bytes exist in tx table

* rewrite pi circuit to use rlp-based approach

* pi circuit assignment, clippy fixes

* lookup to rlp table for tx hash in tx circuit

* chore: clippy fix

* add copy constraints between pi and block/tx table

* add lookup to keccak for final public input in pi circuit

* chore: clippy fix

* fix: compilation after update against upstream

* feat: updates to RLP circuit/table

* fix: compilation

* chore: clippy fix

* refactor pi circuit to use challenge api and expose keccak (hi,lo)

* refactor pi circuit to use challenge api and change pi to keccak hi&lo

* refactor witness of rlp circuit to use value api

* refactor rlp_circuit to use single set of constraints to handle tags

* fix clippy errors

* fix

* add padding constraints

* reduce degree to 9

* fix

* add rlp_circuit to super_circuit

* disable lt/cmp chips for padding rows to reduce witness assign time

* fix: get chain_id from block header

* fix

* fix clippy error

* skip tx without sigs

* fmt

* skip tx/block table load in pi_circuit's synthesize_sub

* add max_inner_blocks

* fix clippy

* feat: padding blocks will not increase keccak input's size

* add TODO in pi circuit

* enable pi circuit in super_circuit

* reduce lookups and fix other issues in tx circuit

* chore: fix comment

* fix

* fix wrong tx hash

* add tx circuit to super circuit

* reduce degree of tx circuit

* add feature to decide if we enable sign_verify synthesis

* reduce lookups and fix other issues in tx circuit

* chore: fix comment

* fix

* fix wrong tx hash

* add tx circuit to super circuit

* reduce degree of tx circuit

* add feature to decide if we enable sign_verify synthesis

* fix: handle the case txs.len() < max_tx

* fix

* add padding tx in rlp circuit and add padding tx to keccak_inputs

* fix tx circuit witness gen bugs

* fix

* fmt

* Allow tx.to address to be different from call.callee_address

* Fix caller -> callee typo

* Use tx.is_create instead of another IsZeroGadget

* clippy

* change tag in tx table to fixed col

* fix

* fix: state circuit load u16 table

* add contract deploy tx test in super circuit

* bug-fix: the rlp(tx.to) is 0x80 when tx.to is zero

* fix clippy

Co-authored-by: kunxian-xia <xiakunxian130@gmail.com>
Co-authored-by: Zhang Zhuo <mycinbrin@gmail.com>
Co-authored-by: Mason Liang <mason@scroll.io>
  • Loading branch information
4 people authored Jan 11, 2023
1 parent 8ef74e6 commit dabfbca
Show file tree
Hide file tree
Showing 19 changed files with 1,475 additions and 904 deletions.
41 changes: 33 additions & 8 deletions bus-mapping/src/circuit_input_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ use eth_types::sign_types::{pk_bytes_le, pk_bytes_swap_endianness, SignData};
use eth_types::{self, Address, GethExecStep, GethExecTrace, ToWord, Word, H256, U256};
use eth_types::{geth_types, ToBigEndian};
use ethers_core::k256::ecdsa::SigningKey;
use ethers_core::types::transaction::eip2718::TypedTransaction;
use ethers_core::types::{Bytes, Signature, TransactionRequest};
use ethers_core::types::{Bytes, NameOrAddress, Signature, TransactionRequest};
use ethers_providers::JsonRpcClient;
pub use execution::{
CopyDataType, CopyEvent, CopyStep, ExecState, ExecStep, ExpEvent, ExpStep, NumberOrHash,
Expand Down Expand Up @@ -480,14 +479,14 @@ pub fn keccak_inputs_sign_verify(sigs: &[SignData]) -> Vec<Vec<u8>> {
/// Generate a dummy tx in which
/// (nonce=0, gas=0, gas_price=0, to=0, value=0, data="", chain_id)
/// using the dummy private key = 1
pub fn get_dummy_tx(chain_id: u64) -> (TypedTransaction, Signature) {
pub fn get_dummy_tx(chain_id: u64) -> (TransactionRequest, Signature) {
let mut sk_be_scalar = [0u8; 32];
sk_be_scalar[31] = 1_u8;

let sk = SigningKey::from_bytes(&sk_be_scalar).expect("sign key = 1");
let wallet = ethers_signers::Wallet::from(sk);

let tx_req = TransactionRequest::new()
let tx = TransactionRequest::new()
.nonce(0)
.gas(0)
.gas_price(U256::zero())
Expand All @@ -496,8 +495,8 @@ pub fn get_dummy_tx(chain_id: u64) -> (TypedTransaction, Signature) {
.data(Bytes::default())
.chain_id(chain_id);

let tx = tx_req.into();
let sig = wallet.sign_transaction_sync(&tx);
// FIXME: need to check if this is deterministic which means sig is fixed.
let sig = wallet.sign_transaction_sync(&tx.clone().into());

(tx, sig)
}
Expand All @@ -508,7 +507,11 @@ pub fn get_dummy_tx_hash(chain_id: u64) -> H256 {
let (tx, sig) = get_dummy_tx(chain_id);

let tx_hash = keccak256(tx.rlp_signed(&sig));
log::debug!("dummy tx hash: {}", hex::encode(tx_hash));
log::debug!(
"DUMMY TX HASH for CHAIN_ID({}): {}",
chain_id,
hex::encode(tx_hash)
);

H256(tx_hash)
}
Expand Down Expand Up @@ -582,11 +585,24 @@ pub fn keccak_inputs_tx_circuit(
s: tx.s,
v: tx.v,
};
let tx: TransactionRequest = tx.into();
let mut tx: TransactionRequest = tx.into();
if tx.to.is_some() {
let to = tx.to.clone().unwrap();
match to {
NameOrAddress::Name(_) => {}
NameOrAddress::Address(addr) => {
// the rlp of zero addr is 0x80
if addr == Address::zero() {
tx.to = None;
}
}
}
}
tx.rlp_signed(&sig).to_vec()
})
.collect::<Vec<Vec<u8>>>();
let dummy_hash_data = {
// dummy tx is a legacy tx.
let (dummy_tx, dummy_sig) = get_dummy_tx(chain_id);
dummy_tx.rlp_signed(&dummy_sig).to_vec()
};
Expand All @@ -609,6 +625,15 @@ pub fn keccak_inputs_tx_circuit(
// Keccak inputs from SignVerify Chip
let sign_verify_inputs = keccak_inputs_sign_verify(&sign_datas);
inputs.extend_from_slice(&sign_verify_inputs);

// Since the SignData::default() already includes pk = [1]G which is also the
// one that we use in get_dummy_tx, so we only need to include the tx sign
// hash of the dummy tx.
let dummy_sign_input = {
let (dummy_tx, _) = get_dummy_tx(chain_id);
dummy_tx.rlp().to_vec()
};
inputs.push(dummy_sign_input);
// NOTE: We don't verify the Tx Hash in the circuit yet, so we don't have more
// hash inputs.
Ok(inputs)
Expand Down
16 changes: 13 additions & 3 deletions bus-mapping/src/circuit_input_builder/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ pub struct Transaction {
pub value: Word,
/// Input / Call Data
pub input: Vec<u8>,
/// Chain_id
pub chain_id: u64,
/// Signature
pub signature: Signature,
/// Calls made in the transaction
Expand All @@ -210,6 +212,7 @@ pub struct Transaction {
impl From<&Transaction> for geth_types::Transaction {
fn from(tx: &Transaction) -> geth_types::Transaction {
geth_types::Transaction {
hash: tx.hash,
from: tx.from,
to: Some(tx.to),
nonce: Word::from(tx.nonce),
Expand All @@ -236,6 +239,7 @@ impl Transaction {
to: Address::zero(),
value: Word::zero(),
input: Vec::new(),
chain_id: 0,
signature: Signature {
r: Word::zero(),
s: Word::zero(),
Expand Down Expand Up @@ -306,18 +310,24 @@ impl Transaction {
}
};

log::debug!(
"eth_tx's type: {:?}, idx: {:?}, hash: {:?}, tx: {:?}",
eth_tx.transaction_type,
eth_tx.transaction_index,
eth_tx.hash,
eth_tx
);
Ok(Self {
block_num: eth_tx.block_number.unwrap().as_u64(),
hash: eth_tx.hash,
nonce: eth_tx.nonce.as_u64(),
gas: eth_tx.gas.as_u64(),
gas_price: eth_tx.gas_price.unwrap_or_default(),
from: eth_tx.from,
to: eth_tx
.to
.unwrap_or_else(|| get_contract_address(eth_tx.from, eth_tx.nonce)),
to: eth_tx.to.unwrap_or_default(),
value: eth_tx.value,
input: eth_tx.input.to_vec(),
chain_id: eth_tx.chain_id.unwrap_or_default().as_u64(), // FIXME
calls: vec![call],
steps: Vec::new(),
signature: Signature {
Expand Down
6 changes: 5 additions & 1 deletion bus-mapping/src/evm/opcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use eth_types::{
evm_types::{GasCost, MAX_REFUND_QUOTIENT_OF_GAS_USED},
GethExecStep, ToAddress, ToWord, Word,
};
use ethers_core::utils::get_contract_address;
use keccak256::EMPTY_HASH;
use log::warn;

Expand Down Expand Up @@ -443,7 +444,10 @@ pub fn gen_begin_tx_ops(state: &mut CircuitInputStateRef) -> Result<ExecStep, Er
CallContextField::CallerAddress,
call.caller_address.to_word(),
),
(CallContextField::CalleeAddress, call.address.to_word()),
(
CallContextField::CalleeAddress,
get_contract_address(call.caller_address, nonce_prev).to_word(),
),
(
CallContextField::CallDataOffset,
call.call_data_offset.into(),
Expand Down
18 changes: 17 additions & 1 deletion eth-types/src/geth_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,20 @@ impl Transaction {
Error::Signature(libsecp256k1::Error::InvalidSignature),
)?;
// msg = rlp([nonce, gasPrice, gas, to, value, data, chain_id, 0, 0])
let req: TransactionRequest = self.into();
let mut req: TransactionRequest = self.into();
if req.to.is_some() {
let to = req.to.clone().unwrap();
match to {
NameOrAddress::Name(_) => {}
NameOrAddress::Address(addr) => {
if addr == Address::zero() {
// the rlp of zero addr is 0x80 instead of
// [0x94, 0, ..., 0]
req.to = None;
}
}
}
}
let msg = req.chain_id(chain_id).rlp();
let msg_hash: [u8; 32] = Keccak256::digest(&msg)
.as_slice()
Expand Down Expand Up @@ -270,6 +283,9 @@ impl GethData {
tx.v = U64::from(sig.v);
tx.r = sig.r;
tx.s = sig.s;
// The previous tx.hash is calculated without signature.
// Therefore we need to update tx.hash.
tx.hash = tx.hash();
}
}
}
9 changes: 9 additions & 0 deletions eth-types/src/sign_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ lazy_static! {
let pk = generator * sk;
let pk = pk.to_affine();
let msg = b"1";
// let msg = TransactionRequest::new()
// .nonce(0)
// .gas(0)
// .gas_price(U256::zero())
// .to(Address::zero())
// .value(U256::zero())
// .data(Bytes::default())
// .chain_id(1)
// .rlp().to_vec();
let msg_hash: [u8; 32] = Keccak256::digest(msg)
.as_slice()
.to_vec()
Expand Down
4 changes: 3 additions & 1 deletion mock/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ lazy_static! {
.nonce(word!("0x103"))
.value(word!("0x3e8"))
.gas_price(word!("0x4d2"))
.input(vec![1, 2, 3, 4, 5, 6, 7, 8, 9].into())
.input(vec![1, 2, 3, 4, 5, 0, 6, 7, 8, 9].into()) // call data gas cost of 0 is 4
.build(),
MockTransaction::default()
.transaction_idx(2u64)
Expand Down Expand Up @@ -354,6 +354,8 @@ impl MockTransaction {
// Compute tx hash in case is not already set
if self.hash.is_none() {
let tmp_tx = Transaction::from(self.to_owned());
// FIXME: Note that tmp_tx does not have sigs if self.from.is_wallet() = false.
// This means tmp_tx.hash() is not correct.
self.hash(tmp_tx.hash());
}

Expand Down
1 change: 1 addition & 0 deletions zkevm-circuits/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ pretty_assertions = "1.0.0"
default = []
test = ["ethers-signers", "mock"]
onephase = []
enable-sign-verify = []
codehash = []
43 changes: 38 additions & 5 deletions zkevm-circuits/src/evm_circuit/execution/begin_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ use crate::{
Transition::{Delta, To},
},
math_gadget::{IsEqualGadget, IsZeroGadget, MulWordByU64Gadget, RangeCheckGadget},
CachedRegion, Cell, RandomLinearCombination, Word,
not, CachedRegion, Cell, RandomLinearCombination, Word,
},
witness::{Block, Call, ExecStep, Transaction},
},
table::{AccountFieldTag, CallContextFieldTag, TxFieldTag as TxContextFieldTag},
util::Expr,
};
use eth_types::{Field, ToLittleEndian, ToScalar};
use ethers_core::utils::get_contract_address;
use halo2_proofs::circuit::Value;
use halo2_proofs::plonk::Error;
use keccak256::EMPTY_HASH_LE;
Expand All @@ -32,6 +33,7 @@ pub(crate) struct BeginTxGadget<F> {
tx_caller_address: Cell<F>,
tx_caller_address_is_zero: IsZeroGadget<F>,
tx_callee_address: Cell<F>,
call_callee_address: Cell<F>,
tx_is_create: Cell<F>,
tx_value: Word<F>,
tx_call_data_length: Cell<F>,
Expand Down Expand Up @@ -79,6 +81,20 @@ impl<F: Field> ExecutionGadget<F> for BeginTxGadget<F> {
TxContextFieldTag::CallDataGasCost,
]
.map(|field_tag| cb.tx_context(tx_id.expr(), field_tag, None));

let call_callee_address = cb.query_cell();
cb.condition(tx_is_create.expr(), |_cb| {
// TODO: require call_callee_address to be
// address(keccak(rlp([tx_caller_address, tx_nonce])))
});
cb.condition(not::expr(tx_is_create.expr()), |cb| {
cb.require_equal(
"Tx to non-zero address",
tx_callee_address.expr(),
call_callee_address.expr(),
);
});

let tx_caller_address_is_zero = IsZeroGadget::construct(cb, tx_caller_address.expr());
cb.require_equal(
"CallerAddress != 0 (not a padding tx)",
Expand Down Expand Up @@ -133,7 +149,7 @@ impl<F: Field> ExecutionGadget<F> for BeginTxGadget<F> {
);
cb.account_access_list_write(
tx_id.expr(),
tx_callee_address.expr(),
call_callee_address.expr(),
1.expr(),
0.expr(),
None,
Expand All @@ -143,7 +159,7 @@ impl<F: Field> ExecutionGadget<F> for BeginTxGadget<F> {
let transfer_with_gas_fee = TransferWithGasFeeGadget::construct(
cb,
tx_caller_address.expr(),
tx_callee_address.expr(),
call_callee_address.expr(),
tx_value.clone(),
mul_gas_fee_by_gas.product().clone(),
&mut reversion_info,
Expand All @@ -155,7 +171,7 @@ impl<F: Field> ExecutionGadget<F> for BeginTxGadget<F> {
// Read code_hash of callee
let code_hash = cb.query_cell();
cb.account_write(
tx_callee_address.expr(),
call_callee_address.expr(),
AccountFieldTag::CodeHash,
code_hash.expr(),
code_hash.expr(),
Expand Down Expand Up @@ -206,7 +222,10 @@ impl<F: Field> ExecutionGadget<F> for BeginTxGadget<F> {
for (field_tag, value) in [
(CallContextFieldTag::Depth, 1.expr()),
(CallContextFieldTag::CallerAddress, tx_caller_address.expr()),
(CallContextFieldTag::CalleeAddress, tx_callee_address.expr()),
(
CallContextFieldTag::CalleeAddress,
call_callee_address.expr(),
),
(CallContextFieldTag::CallDataOffset, 0.expr()),
(
CallContextFieldTag::CallDataLength,
Expand Down Expand Up @@ -270,6 +289,7 @@ impl<F: Field> ExecutionGadget<F> for BeginTxGadget<F> {
tx_caller_address,
tx_caller_address_is_zero,
tx_callee_address,
call_callee_address,
tx_is_create,
tx_value,
tx_call_data_length,
Expand Down Expand Up @@ -327,6 +347,19 @@ impl<F: Field> ExecutionGadget<F> for BeginTxGadget<F> {
.expect("unexpected Address -> Scalar conversion failure"),
),
)?;
self.call_callee_address.assign(
region,
offset,
Value::known(
if tx.is_create {
get_contract_address(tx.caller_address, tx.nonce)
} else {
tx.callee_address
}
.to_scalar()
.expect("unexpected Address -> Scalar conversion failure"),
),
)?;
self.tx_is_create
.assign(region, offset, Value::known(F::from(tx.is_create as u64)))?;
self.tx_call_data_length.assign(
Expand Down
Loading

0 comments on commit dabfbca

Please sign in to comment.