Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Resolve "Reverted report validator transactions get added in block #10580" #10602

75 changes: 75 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions ethcore/call-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ edition = "2018"

[dependencies]
types = { path = "../types", package = "common-types" }
ethabi = "6.0"
ethereum-types = "0.6.0"
bytes = { version = "0.1", package = "parity-bytes" }
24 changes: 24 additions & 0 deletions ethcore/call-contract/src/call_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,36 @@

use bytes::Bytes;
use ethereum_types::Address;
use ethabi::{decode, ParamType, Token};
use types::ids::BlockId;

const ERROR_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0];

/// Provides `call_contract` method
pub trait CallContract {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have considerations regarding the entire trait, extracting revert messages really calls for introducing a proper error type here (at least a two-value enum).

I think it would also be way better to introduce an Options parameter (with a builder) instead of introducing another method:

pub enum CallError {
  Reverted(String),
  Other(String),
}

#[derive(derive_builder::Builder, Default, Clone)]
pub CallOptions {
  contract_address: Address,
  sender: Address,
  data: Bytes,
  value: U256,
  gas: U256,
  gas_price: U256,
}

impl CallOptions {
  /// Convenience method for creating the most common use case.
  pub fn new(contract: Address, data: Bytes) -> Self {
    Self::default().contract_address(contract).data(data)
  }
}

pub trait CallContract {
  /// Executes a transient call to a contract at given `BlockId`.
  /// 
  /// The constructed transaction must be executed on top of state at block `id`,
  /// any changes introduce by the call must be discarded.
  /// Returns:
  /// - A return data from the contract if the call was successful,
  /// - A `CallError::Reverted(msg)` error in case the call was reverted with an exception and message.
  /// - A `CallError::Other(msg)` in case the call did not succeed for other reasons.
  fn call_contract(&self, id: BlockId, call_options: CallOptions) -> Result<Bytes, CallError>;
}

/// Like `call`, but with various defaults. Designed to be used for calling contracts.
/// Returns an error in case of vm exception. Tries to decode Solidity revert message if there is a revert exception.
fn call_contract(&self, id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String>;

/// Like `call`, but with various defaults. Designed to be used for calling contracts with specified sender.
/// Returns an error in case of vm exception. Tries to decode Solidity revert message if there is a revert exception.
fn call_contract_from_sender(&self, block_id: BlockId, address: Address, sender: Address, data: Bytes) -> Result<Bytes, String>;

/// Try to decode Solidity revert string
fn try_decode_solidity_revert_msg(&self, data: &Bytes) -> Option<String> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't suit the trait at all:

  1. You don't use self
  2. It's should never be overwritten by the implementers.

This should be moved to a separate function that is use in the implementation.

let mut result = None;
if data.len() > 4 {
let (fn_selector, enc_string) = data.split_at(4);
// Error(string) selector. Details: https://solidity.readthedocs.io/en/v0.5.8/control-structures.html#error-handling-assert-require-revert-and-exceptions
if fn_selector == ERROR_SELECTOR {
result = decode(&[ParamType::String], enc_string)
.as_ref()
.map(|d| if let Token::String(str) = &d[0] { Some(str.as_str().to_string()) } else { None })
.unwrap_or_else(|_| None);
}
}
result
}
}

/// Provides information on a blockchain service and it's registry
Expand Down
2 changes: 2 additions & 0 deletions ethcore/call-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
//! This crate exposes traits required to call contracts at particular block.
//! All utilities that depend on on-chain data should use those traits to access it.

extern crate ethabi;

pub mod call_contract;

// Re-export
Expand Down
1 change: 1 addition & 0 deletions ethcore/private-tx/src/key_server_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ mod tests {

impl CallContract for DummyRegistryClient {
fn call_contract(&self, _id: BlockId, _address: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) }
fn call_contract_from_sender(&self, _block_id: BlockId, _address: Address, _sender: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) }
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion ethcore/res/ethereum/tests
Submodule tests updated 10615 files
46 changes: 37 additions & 9 deletions ethcore/src/client/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use std::time::{Instant, Duration};
use blockchain::{BlockReceipts, BlockChain, BlockChainDB, BlockProvider, TreeRoute, ImportRoute, TransactionAddress, ExtrasInsert, BlockNumberKey};
use bytes::Bytes;
use call_contract::{CallContract, RegistryInfo};
use ethabi::{Token};
use ethcore_miner::pool::VerifiedTransaction;
use ethereum_types::{H256, H264, Address, U256};
use evm::Schedule;
Expand All @@ -42,7 +43,7 @@ use types::filter::Filter;
use types::log_entry::LocalizedLogEntry;
use types::receipt::{Receipt, LocalizedReceipt};
use types::{BlockNumber, header::{Header, ExtendedHeader}};
use vm::{EnvInfo, LastHashes};
use vm::{EnvInfo, LastHashes, Error as VmError};

use block::{LockedBlock, Drain, ClosedBlock, OpenBlock, enact_verified, SealedBlock};
use client::ancient_import::AncientVerifier;
Expand Down Expand Up @@ -1223,15 +1224,19 @@ impl Client {
// transaction for calling contracts from services like engine.
// from the null sender, with 50M gas.
fn contract_call_tx(&self, block_id: BlockId, address: Address, data: Bytes) -> SignedTransaction {
let from = Address::zero();
self.contract_call_tx_from_sender(block_id, address, Address::zero(), data)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since it's repeated so many times it should be just default implementation, but instead best to have a look at the CallOptions pattern above.

}

// transaction for calling contracts with specified sender
fn contract_call_tx_from_sender(&self, block_id: BlockId, address: Address, sender: Address, data: Bytes) -> SignedTransaction {
transaction::Transaction {
nonce: self.nonce(&from, block_id).unwrap_or_else(|| self.engine.account_start_nonce(0)),
nonce: self.nonce(&sender, block_id).unwrap_or_else(|| self.engine.account_start_nonce(0)),
action: Action::Call(address),
gas: U256::from(50_000_000),
gas_price: U256::default(),
value: U256::default(),
data: data,
}.fake_sign(from)
data,
}.fake_sign(sender)
}

fn do_virtual_call(
Expand Down Expand Up @@ -1461,15 +1466,34 @@ impl RegistryInfo for Client {

impl CallContract for Client {
fn call_contract(&self, block_id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String> {
self.call_contract_from_sender(block_id, address, Address::default(), data)
}

fn call_contract_from_sender(
&self,
block_id: BlockId,
address: Address,
sender: Address,
data: Bytes
) -> Result<Bytes, String> {
let state_pruned = || CallError::StatePruned.to_string();
let state = &mut self.state_at(block_id).ok_or_else(&state_pruned)?;
let header = self.block_header_decoded(block_id).ok_or_else(&state_pruned)?;

let transaction = self.contract_call_tx(block_id, address, data);
let transaction = self.contract_call_tx_from_sender(block_id, address, sender, data);

self.call(&transaction, Default::default(), state, &header)
.map_err(|e| format!("{:?}", e))
.map(|executed| executed.output)
let executed = self.call(&transaction, Default::default(), state, &header).map_err(|e| format!("{:?}", e))?;
if let Some(exception) = executed.exception {
let mut error = format!("{:?}", exception);
if exception == VmError::Reverted {
let output = executed.output.clone();
let msg = self.try_decode_solidity_revert_msg(&output).unwrap_or_else(|| format!("{}", Token::Bytes(output)));
error = format!("{} {}", error, msg);
}
Err(error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

To make this a little easier to follow I suggest:

if let Some(exception) = executed.exception {
  match exception {
    VmError::Reverted => {
      …
      Err(format!("{:?} {}", exception, msg))
  },
  _ => Err(exception.to_string()) // not sure if `From<String>` is impl'd for `VmError`
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

A more general question I have is: why do this error munging here rather than in the Debug impl for VmError? Couldn't we add the decoding attempt there and essentially keep this code to a simple map_err(|e| format!("{:?}", e)) like it was before? Seems like this is spreading out the logic into several places and I'm not sure I understand why it's worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately there is no access from VmError to tx output which we need for decoding error. Alternatively we could put all this error munging under Executed in some function like format_error_and_decode_ouput. What do you think about it @dvdplm ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Unfortunately there is no access from VmError to tx output

Can we change that maybe? Like, VmError::Reverted(tx_output). If not, then I'm not opposed to having a format_error_and_decode_ouput helper like you suggest.

} else {
Ok(executed.output)
}
}
}

Expand Down Expand Up @@ -2203,6 +2227,10 @@ impl BlockChainClient for Client {
self.importer.miner.import_own_transaction(self, signed.into())
}

fn call_contract_as_author(&self, id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String> {
self.call_contract_from_sender(id, address, self.importer.miner.authoring_params().author, data)
}

fn registrar_address(&self) -> Option<Address> {
self.registrar_address.clone()
}
Expand Down
3 changes: 3 additions & 0 deletions ethcore/src/client/test_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ impl BlockInfo for TestBlockChainClient {

impl CallContract for TestBlockChainClient {
fn call_contract(&self, _id: BlockId, _address: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) }
fn call_contract_from_sender(&self, _block_id: BlockId, _address: Address, _sender: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) }
}

impl TransactionInfo for TestBlockChainClient {
Expand Down Expand Up @@ -900,6 +901,8 @@ impl BlockChainClient for TestBlockChainClient {
self.miner.import_own_transaction(self, signed.into())
}

fn call_contract_as_author(&self, _id: BlockId, _address: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) }

fn registrar_address(&self) -> Option<Address> { None }
}

Expand Down
3 changes: 3 additions & 0 deletions ethcore/src/client/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@ pub trait BlockChainClient : Sync + Send + AccountData + BlockChain + CallContra
/// Schedule state-altering transaction to be executed on the next pending block.
fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error>;

/// Call contract as block author
fn call_contract_as_author(&self, id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure I like this addition. Seems like a convenience method, but maybe I'm missing something. I'm hesitant to add new trait methods if we can avoid it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The issue of not using call_contract_from_sender is because we can't get blocks author outside client in transact method of ValidatorContract. By avoiding usage of this method we would need to introduce getter for author or the whole miner instance (which is available but only for tests):

https://github.com/paritytech/parity-ethereum/blob/af932e6806b080f50d1f3d83c0e96348a6e91e84/ethcore/src/client/client.rs#L1001

Honestly I'm not sure what would be the best solution here. @dvdplm any thoughts about that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure either tbh. @tomusdrw?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We already have CallContract trait for calling contracts, we have put a lot of effort trying to make BlockChainClient smaller, so adding any new methods to this trait is a no go.

If validator_set/contract.rs needs to call a contract as author (what's questionable as well), it should get the author address from engine (where it is created or called)


/// Get the address of the registry itself.
fn registrar_address(&self) -> Option<Address>;
}
Expand Down
2 changes: 2 additions & 0 deletions ethcore/src/engines/validator_set/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use machine::{AuxiliaryData, Call, EthereumMachine};
use parking_lot::RwLock;
use types::BlockNumber;
use types::header::Header;
use types::ids::BlockId;

use client::EngineClient;

Expand Down Expand Up @@ -58,6 +59,7 @@ impl ValidatorContract {

match client.as_full_client() {
Some(c) => {
c.call_contract_as_author(BlockId::Latest, self.contract_address, data.clone())?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

So it seems that now we call the contract twice just to improve the logs?
I'm really hesitant about this change - why can't we decode the error code from the receipt of actual transaction? Now you have no guarantees that if the first call passes the transaction will actually succeed.

c.transact_contract(self.contract_address, data)
.map_err(|e| format!("Transaction import error: {}", e))?;
Ok(())
Expand Down