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(node): support legacy transactions #1235

Merged
merged 12 commits into from
Dec 20, 2024
Merged
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
15 changes: 13 additions & 2 deletions fendermint/eth/api/examples/ethers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ use ethers_core::{
abi::Abi,
types::{
transaction::eip2718::TypedTransaction, Address, BlockId, BlockNumber, Bytes,
Eip1559TransactionRequest, Filter, Log, SyncingStatus, TransactionReceipt, TxHash, H256,
U256, U64,
Eip1559TransactionRequest, Filter, Log, SyncingStatus, TransactionReceipt,
TransactionRequest, TxHash, H256, U256, U64,
},
};
use tracing::Level;
Expand Down Expand Up @@ -706,6 +706,17 @@ where
})?;
}

// Check legacy transactions
// Set up the transaction details for a legacy transaction
let tx = TransactionRequest::new()
.to(to.eth_addr) // Replace with recipient address
.value(U256::from(1u64)); // Gas limit for standard ETH transfer

// Send the transaction
let pending_tx = mw.send_transaction(tx, None).await?;
let receipt = pending_tx.await?.unwrap();
tracing::info!("legacy transaction sent: {:?}", receipt.transaction_hash);

Ok(())
}

Expand Down
47 changes: 31 additions & 16 deletions fendermint/eth/api/src/apis/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use fendermint_vm_actor_interface::evm;
use fendermint_vm_message::chain::ChainMessage;
use fendermint_vm_message::query::FvmQueryHeight;
use fendermint_vm_message::signed::SignedMessage;
use fil_actors_evm_shared::uints;
use futures::FutureExt;
use fvm_ipld_encoding::RawBytes;
use fvm_shared::address::Address;
Expand All @@ -36,17 +37,15 @@ use tendermint_rpc::{
Client,
};

use fil_actors_evm_shared::uints;

use crate::conv::from_eth::{self, to_fvm_message};
use crate::conv::from_eth::{self, derive_origin_kind, to_fvm_message};
use crate::conv::from_tm::{self, msg_hash, to_chain_message, to_cumulative, to_eth_block_zero};
use crate::error::{error_with_revert, OutOfSequence};
use crate::filters::{matches_topics, FilterId, FilterKind, FilterRecords};
use crate::{
conv::{
from_eth::to_fvm_address,
from_fvm::to_eth_tokens,
from_tm::{to_eth_receipt, to_eth_transaction},
from_tm::{to_eth_receipt, to_eth_transaction_response},
},
error, JsonRpcData, JsonRpcResult,
};
Expand Down Expand Up @@ -427,7 +426,8 @@ where
C: Client + Sync + Send,
{
// Check in the pending cache first.
if let Some(tx) = data.tx_cache.get(&tx_hash) {
if let Some((tx, sig)) = data.tx_cache.get(&tx_hash) {
let tx = from_eth::to_eth_transaction_response(&tx, sig)?;
Ok(Some(tx))
} else if let Some(res) = data.tx_by_hash(tx_hash).await? {
let msg = to_chain_message(&res.tx)?;
Expand All @@ -439,8 +439,11 @@ where
.state_params(FvmQueryHeight::Height(header.header.height.value()))
.await?;
let chain_id = ChainID::from(sp.value.chain_id);

let hash = msg_hash(&res.tx_result.events, &res.tx);
let mut tx = to_eth_transaction(msg, chain_id, hash)?;
let mut tx = to_eth_transaction_response(msg, chain_id)?;
debug_assert_eq!(hash, tx.hash);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we want to crash the process here? Wouldn't it be best to return an internal error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they should be the same, debug_assert_eq should be fine for release build though, more like a sanity check for tests.


tx.transaction_index = Some(et::U64::from(res.index));
tx.block_hash = Some(et::H256::from_slice(header.header.hash().as_bytes()));
tx.block_number = Some(et::U64::from(res.height.value()));
Expand Down Expand Up @@ -612,6 +615,14 @@ pub async fn get_uncle_by_block_number_and_index<C>(
Ok(None)
}

fn normalize_signature(sig: &mut et::Signature) -> JsonRpcResult<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use Signature::as_signature(), which returns a normalized recoverable signature?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's not exposed...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, a lot of methods are not exposed. Could be helpful util tools.

sig.v = sig
.recovery_id()
.context("cannot normalize eth signature")?
.to_byte() as u64;
Ok(())
}

/// Creates new message call transaction or a contract creation for signed transactions.
pub async fn send_raw_transaction<C>(
data: JsonRpcData<C>,
Expand All @@ -621,23 +632,23 @@ where
C: Client + Sync + Send,
{
let rlp = rlp::Rlp::new(tx.as_ref());
let (tx, sig): (TypedTransaction, et::Signature) = TypedTransaction::decode_signed(&rlp)
let (tx, mut sig): (TypedTransaction, et::Signature) = TypedTransaction::decode_signed(&rlp)
.context("failed to decode RLP as signed TypedTransaction")?;

// for legacy eip155 transactions, the chain id is encoded in it. The `v` most likely will not
// be normalized, normalize to ensure consistent txn hash calculation.
normalize_signature(&mut sig)?;

let sighash = tx.sighash();
let msghash = et::TxHash::from(ethers_core::utils::keccak256(rlp.as_raw()));
let msghash = tx.hash(&sig);
tracing::debug!(?sighash, eth_hash = ?msghash, ?tx, "received raw transaction");

if let Some(tx) = tx.as_eip1559_ref() {
let tx = from_eth::to_eth_transaction(tx.clone(), sig, msghash);
data.tx_cache.insert(msghash, tx);
}
Comment on lines -631 to -634
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I reading right that prior to this PR, if a transaction failed the checks (broadcast_tx_sync or others), it would remain forever in the cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't remain forever as it's using LRU cache. It will just stay for some time, depending on the configuration.


let msg = to_fvm_message(tx, false)?;
let msg = to_fvm_message(tx.clone())?;
let sender = msg.from;
let nonce = msg.sequence;

let msg = SignedMessage {
origin_kind: derive_origin_kind(&tx)?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to implement TryFrom<TypedTransaction> for OriginKind instead? Then you can do tx.try_into()? here?

message: msg,
signature: Signature::new_secp256k1(sig.to_vec()),
};
Expand All @@ -648,6 +659,8 @@ where
// but not the execution results - those will have to be polled with get_transaction_receipt.
let res: tx_sync::Response = data.tm().broadcast_tx_sync(bz).await?;
if res.code.is_ok() {
data.tx_cache.insert(msghash, (tx, sig));

// The following hash would be okay for ethers-rs,and we could use it to look up the TX with Tendermint,
// but ethers.js would reject it because it doesn't match what Ethereum would use.
// Ok(et::TxHash::from_slice(res.hash.as_bytes()))
Expand All @@ -671,6 +684,8 @@ where
tracing::debug!(eth_hash = ?msghash, expected = oos.expected, got = oos.got, is_admissible, "out-of-sequence transaction received");

if is_admissible {
data.tx_cache.insert(msghash, (tx, sig));

data.tx_buffer.insert(sender, nonce, msg);
return Ok(msghash);
}
Expand All @@ -688,7 +703,7 @@ pub async fn call<C>(
where
C: Client + Sync + Send,
{
let msg = to_fvm_message(tx.into(), true)?;
let msg = to_fvm_message(tx.into())?;
let is_create = msg.to == EAM_ACTOR_ADDR;
let height = data.query_height(block_id).await?;
let response = data.client.call(msg, height).await?;
Expand Down Expand Up @@ -737,7 +752,7 @@ where
EstimateGasParams::Two((tx, block_id)) => (tx, block_id),
};

let msg = to_fvm_message(tx.into(), true).context("failed to convert to FVM message")?;
let msg = to_fvm_message(tx.into()).context("failed to convert to FVM message")?;

let height = data
.query_height(block_id)
Expand Down
158 changes: 110 additions & 48 deletions fendermint/eth/api/src/conv/from_eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,66 +5,128 @@

use ethers_core::types as et;
use ethers_core::types::transaction::eip2718::TypedTransaction;
use ethers_core::types::{Eip1559TransactionRequest, TransactionRequest};

pub use fendermint_vm_message::conv::from_eth::*;
use fendermint_vm_message::signed::OriginKind;
use fvm_shared::{error::ExitCode, message::Message};

use crate::{error, JsonRpcResult};
use crate::error::error_with_revert;
use crate::JsonRpcResult;

pub fn to_fvm_message(tx: TypedTransaction, accept_legacy: bool) -> JsonRpcResult<Message> {
fn handle_typed_txn<
R,
F1: Fn(&TransactionRequest) -> JsonRpcResult<R>,
F2: Fn(&Eip1559TransactionRequest) -> JsonRpcResult<R>,
>(
tx: &TypedTransaction,
handle_legacy: F1,
handle_eip1559: F2,
) -> JsonRpcResult<R> {
match tx {
TypedTransaction::Eip1559(ref tx) => {
Ok(fendermint_vm_message::conv::from_eth::to_fvm_message(tx)?)
}
TypedTransaction::Legacy(_) if accept_legacy => {
// legacy transactions are only accepted for gas estimation purposes
// (when accept_legacy is explicitly set)
// eth_sendRawTransaction should fail for legacy transactions.
// For this purpose it os OK to not set `max_fee_per_gas` and
// `max_priority_fee_per_gas`. Legacy transactions don't include
// that information
Ok(fendermint_vm_message::conv::from_eth::to_fvm_message(
&tx.into(),
)?)
}
TypedTransaction::Legacy(_) | TypedTransaction::Eip2930(_) => error(
TypedTransaction::Legacy(ref t) => handle_legacy(t),
TypedTransaction::Eip1559(ref t) => handle_eip1559(t),
_ => error_with_revert(
ExitCode::USR_ILLEGAL_ARGUMENT,
"unexpected transaction type",
"txn type not supported",
None::<Vec<u8>>,
),
}
}

pub fn derive_origin_kind(tx: &TypedTransaction) -> JsonRpcResult<OriginKind> {
handle_typed_txn(
tx,
|_| Ok(OriginKind::EthereumLegacy),
|_| Ok(OriginKind::EthereumEIP1559),
)
}

pub fn to_fvm_message(tx: TypedTransaction) -> JsonRpcResult<Message> {
handle_typed_txn(
&tx,
|r| Ok(fvm_message_from_legacy(r)?),
|r| Ok(fvm_message_from_eip1559(r)?),
)
}

/// Turn a request into the DTO returned by the API.
pub fn to_eth_transaction(
tx: et::Eip1559TransactionRequest,
pub fn to_eth_transaction_response(
tx: &TypedTransaction,
sig: et::Signature,
hash: et::TxHash,
) -> et::Transaction {
et::Transaction {
hash,
nonce: tx.nonce.unwrap_or_default(),
block_hash: None,
block_number: None,
transaction_index: None,
from: tx.from.unwrap_or_default(),
to: tx.to.and_then(|to| to.as_address().cloned()),
value: tx.value.unwrap_or_default(),
gas: tx.gas.unwrap_or_default(),
max_fee_per_gas: tx.max_fee_per_gas,
max_priority_fee_per_gas: tx.max_priority_fee_per_gas,
// Strictly speaking a "Type 2" transaction should not need to set this, but we do because Blockscout
// has a database constraint that if a transaction is included in a block this can't be null.
gas_price: Some(
tx.max_fee_per_gas.unwrap_or_default()
+ tx.max_priority_fee_per_gas.unwrap_or_default(),
),
input: tx.data.unwrap_or_default(),
chain_id: tx.chain_id.map(|x| et::U256::from(x.as_u64())),
v: et::U64::from(sig.v),
r: sig.r,
s: sig.s,
transaction_type: Some(2u64.into()),
access_list: Some(tx.access_list),
other: Default::default(),
) -> JsonRpcResult<et::Transaction> {
macro_rules! essential_txn_response {
($tx: expr, $hash: expr) => {{
let mut r = et::Transaction::default();

r.nonce = $tx.nonce.unwrap_or_default();
r.hash = $hash;
r.from = $tx.from.unwrap_or_default();
r.to = $tx.to.clone().and_then(|to| to.as_address().cloned());
r.value = $tx.value.unwrap_or_default();
r.gas = $tx.gas.unwrap_or_default();
r.input = $tx.data.clone().unwrap_or_default();
r.chain_id = $tx.chain_id.map(|x| et::U256::from(x.as_u64()));
r.v = et::U64::from(sig.v);
r.r = sig.r;
r.s = sig.s;

r
}};
}

let hash = tx.hash(&sig);

handle_typed_txn(
tx,
|tx| {
let mut r = essential_txn_response!(tx, hash);
r.gas_price = tx.gas_price;
r.transaction_type = Some(0u64.into());
Ok(r)
},
|tx| {
let mut r = essential_txn_response!(tx, hash);
r.max_fee_per_gas = tx.max_fee_per_gas;
r.max_priority_fee_per_gas = tx.max_priority_fee_per_gas;
// Strictly speaking a "Type 2" transaction should not need to set this, but we do because Blockscout
// has a database constraint that if a transaction is included in a block this can't be null.
r.gas_price = Some(
tx.max_fee_per_gas.unwrap_or_default()
+ tx.max_priority_fee_per_gas.unwrap_or_default(),
);
r.transaction_type = Some(2u64.into());
r.access_list = Some(tx.access_list.clone());
Ok(r)
},
)
}

#[cfg(test)]
mod tests {
use crate::conv::from_eth::to_fvm_message;
use ethers_core::types::transaction::eip2718::TypedTransaction;
use ethers_core::types::Signature;
use ethers_core::utils::rlp;
use fendermint_vm_message::signed::{OriginKind, SignedMessage};
use fvm_shared::chainid::ChainID;

#[test]
fn test_legacy_transaction() {
let raw_tx = "f8ac821dac850df8475800830186a09465292eeadf1426cd2df1c4793a3d7519f253913b80b844a9059cbb000000000000000000000000cd50511c4e355f2bc3c084d854253cc17b2230bf00000000000000000000000000000000000000000000148a616ad7f95aa0000025a0a4f3a70a01cfb3969c4a12510ebccd7d08250a4d34181123bebae3f865392643a063116147193f2badc611fa20dfa1c339bca299f50e470353ee4f676bc236479d";
let raw_tx = hex::decode(raw_tx).unwrap();

let rlp = rlp::Rlp::new(raw_tx.as_ref());
let (tx, sig): (TypedTransaction, Signature) =
TypedTransaction::decode_signed(&rlp).unwrap();

let msg = to_fvm_message(tx).unwrap();

let signed_msg = SignedMessage {
origin_kind: OriginKind::EthereumLegacy,
message: msg,
signature: fvm_shared::crypto::signature::Signature::new_secp256k1(sig.to_vec()),
};
assert!(signed_msg.verify(&ChainID::from(1)).is_ok());
}
}
13 changes: 7 additions & 6 deletions fendermint/eth/api/src/conv/from_tm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Context};
use ethers_core::types::{self as et};
use fendermint_vm_actor_interface::eam::EthAddress;
use fendermint_vm_message::conv::from_fvm::to_eth_transaction_request;
use fendermint_vm_message::conv::from_fvm::to_eth_typed_transaction;
use fendermint_vm_message::{chain::ChainMessage, signed::SignedMessage};
use fvm_shared::address::Address;
use fvm_shared::bigint::Zero;
Expand Down Expand Up @@ -160,9 +160,11 @@ pub fn to_eth_block(
if let ChainMessage::Signed(msg) = msg {
let hash = msg_hash(&result.events, data);

let mut tx = to_eth_transaction(msg, chain_id, hash)
let mut tx = to_eth_transaction_response(msg, chain_id)
.context("failed to convert to eth transaction")?;

debug_assert_eq!(hash, tx.hash);

tx.transaction_index = Some(et::U64::from(idx));
tx.block_hash = Some(et::H256::from_slice(block.header.hash().as_bytes()));
tx.block_number = Some(et::U64::from(block.header.height.value()));
Expand Down Expand Up @@ -205,20 +207,19 @@ pub fn to_eth_block(
Ok(block)
}

pub fn to_eth_transaction(
pub fn to_eth_transaction_response(
msg: SignedMessage,
chain_id: ChainID,
hash: et::TxHash,
) -> anyhow::Result<et::Transaction> {
// Based on https://github.com/filecoin-project/lotus/blob/6cc506f5cf751215be6badc94a960251c6453202/node/impl/full/eth.go#L2048
let sig =
to_eth_signature(msg.signature(), true).context("failed to convert to eth signature")?;

// Recover the original request; this method has better tests.
let tx = to_eth_transaction_request(&msg.message, &chain_id)
let tx = to_eth_typed_transaction(msg.origin_kind, &msg.message, &chain_id)
.context("failed to convert to tx request")?;

let tx = from_eth::to_eth_transaction(tx, sig, hash);
let tx = from_eth::to_eth_transaction_response(&tx, sig)?;

Ok(tx)
}
Expand Down
Loading
Loading