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

Reuse single shared allocation for ABI data #970

Merged
merged 14 commits into from
Apr 22, 2024
2 changes: 1 addition & 1 deletion ethcontract-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Common types for ethcontract-rs runtime and proc macro.
[dependencies]
ethabi = "18.0"
hex = "0.4"
serde = "1.0"
serde= { version = "1.0", features = ["rc"] }
serde_derive = "1.0"
serde_json = "1.0"
thiserror = "1.0"
Expand Down
41 changes: 30 additions & 11 deletions ethcontract-common/src/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
//! artifact models. It also provides tools to load artifacts from different
//! sources, and parse them using different formats.

use crate::contract::{Documentation, Network};
use crate::contract::{Documentation, Interface, Network};
use crate::{Abi, Bytecode, Contract};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc;

pub mod hardhat;
pub mod truffle;
Expand Down Expand Up @@ -151,7 +152,7 @@ pub struct ContractMut<'a>(&'a mut Contract);
impl<'a> ContractMut<'a> {
/// Returns mutable reference to contract's abi.
pub fn abi_mut(&mut self) -> &mut Abi {
&mut self.0.abi
&mut Arc::make_mut(&mut self.0.interface).abi
}

/// Returns mutable reference to contract's bytecode.
Expand Down Expand Up @@ -188,6 +189,18 @@ impl Deref for ContractMut<'_> {
}
}

impl Drop for ContractMut<'_> {
fn drop(&mut self) {
// The ABI might have gotten mutated while this guard was alive.
// Since we compute pre-compute and cache a few values based on the ABI
// as a performance optimization we need to recompute those cached values
// with the new ABI once the user is done updating the mutable contract.
let abi = self.0.interface.abi.clone();
let interface = Interface::from(abi);
*Arc::make_mut(&mut self.0.interface) = interface;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this panic if there is more than one Arc pointer to the data? Asking cuz my Rust is rusty :P

Copy link
Contributor Author

@MartinquaXD MartinquaXD Apr 20, 2024

Choose a reason for hiding this comment

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

No, this function is actually pretty cool. If there are other Arcs this will clone the current content of the Arc and make it point to the new allocation that can now safely be modified. If it's the only Arc it will mutate the content in place. It's slightly wonky because you could clone the original Arc, update the artifact and expect that your cloned Arc would see the updates but this is not the case. I think this is fine, though since fundamentally this behavior was already present before factoring in borrowing rules.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, cool - so it has CoW (copy on write) semantics. Very cool, and the name is a great coincidence 😅.

I think this is fine, though since fundamentally this behavior was already present before factoring in borrowing rules.

I would even argue this is desired to preserve the existing mutation semantics.

}
}

#[cfg(test)]
mod test {
use super::*;
Expand All @@ -204,26 +217,32 @@ mod test {

assert_eq!(artifact.len(), 0);

let insert_res = artifact.insert(make_contract("C1"));
{
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 those scopes are needed due to us needed to update a modified contract. (see impl Drop for ContractMut<'_>.

let insert_res = artifact.insert(make_contract("C1"));

assert_eq!(insert_res.inserted_contract.name, "C1");
assert!(insert_res.old_contract.is_none());
assert_eq!(insert_res.inserted_contract.name, "C1");
assert!(insert_res.old_contract.is_none());
}

assert_eq!(artifact.len(), 1);
assert!(artifact.contains("C1"));

let insert_res = artifact.insert(make_contract("C2"));
{
let insert_res = artifact.insert(make_contract("C2"));

assert_eq!(insert_res.inserted_contract.name, "C2");
assert!(insert_res.old_contract.is_none());
assert_eq!(insert_res.inserted_contract.name, "C2");
assert!(insert_res.old_contract.is_none());
}

assert_eq!(artifact.len(), 2);
assert!(artifact.contains("C2"));

let insert_res = artifact.insert(make_contract("C1"));
{
let insert_res = artifact.insert(make_contract("C1"));

assert_eq!(insert_res.inserted_contract.name, "C1");
assert!(insert_res.old_contract.is_some());
assert_eq!(insert_res.inserted_contract.name, "C1");
assert!(insert_res.old_contract.is_some());
}

assert_eq!(artifact.len(), 2);
}
Expand Down
17 changes: 9 additions & 8 deletions ethcontract-common/src/artifact/hardhat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,15 +426,16 @@ impl HardHatLoader {
address: Address,
transaction_hash: Option<TransactionHash>,
) -> Result<(), ArtifactError> {
let mut contract = match artifact.get_mut(&contract.name) {
Some(existing_contract) => {
if existing_contract.abi != contract.abi {
return Err(ArtifactError::AbiMismatch(contract.name));
}

existing_contract
let contract_guard = artifact.get_mut(&contract.name);
let mut contract = if let Some(existing_contract) = contract_guard {
if existing_contract.interface != contract.interface {
return Err(ArtifactError::AbiMismatch(contract.name));
}
None => artifact.insert(contract).inserted_contract,

existing_contract
} else {
drop(contract_guard);
MartinquaXD marked this conversation as resolved.
Show resolved Hide resolved
artifact.insert(contract).inserted_contract
};

let deployment_information = transaction_hash.map(DeploymentInformation::TransactionHash);
Expand Down
2 changes: 1 addition & 1 deletion ethcontract-common/src/artifact/truffle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ impl TruffleLoader {
let mut contract: Contract = loader(source)?;

if let Some(name) = &self.name {
contract.name = name.clone();
contract.name.clone_from(name);
}

Ok(contract)
Expand Down
81 changes: 77 additions & 4 deletions ethcontract-common/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
//! Module for reading and examining data produced by truffle.

use crate::abiext::FunctionExt;
use crate::hash::H32;
use crate::Abi;
use crate::{bytecode::Bytecode, DeploymentInformation};
use ethabi::ethereum_types::H256;
use serde::Deserializer;
use serde::Serializer;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::hash::Hash;
use std::sync::Arc;
use web3::types::Address;

/// Represents a contract data.
Expand All @@ -13,8 +20,9 @@ pub struct Contract {
/// The contract name. Unnamed contracts have an empty string as their name.
#[serde(rename = "contractName")]
pub name: String,
/// The contract ABI
pub abi: Abi,
/// The contract interface.
#[serde(rename = "abi")]
pub interface: Arc<Interface>,
/// The contract deployment bytecode.
pub bytecode: Bytecode,
/// The contract's expected deployed bytecode.
Expand All @@ -28,6 +36,71 @@ pub struct Contract {
pub userdoc: Documentation,
}

/// Struct representing publicly accessible interface of a smart contract.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Interface {
/// The contract ABI
pub abi: Abi,
/// A mapping from method signature to a name-index pair for accessing
/// functions in the contract ABI. This is used to avoid allocation when
/// searching for matching functions by signature.
pub methods: HashMap<H32, (String, usize)>,
/// A mapping from event signature to a name-index pair for resolving
/// events in the contract ABI.
pub events: HashMap<H256, (String, usize)>,
}

impl<'de> Deserialize<'de> for Interface {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let abi = Abi::deserialize(deserializer)?;
Ok(abi.into())
}
}

impl Serialize for Interface {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.abi.serialize(serializer)
}
}

impl From<Abi> for Interface {
fn from(abi: Abi) -> Self {
Self {
methods: create_mapping(&abi.functions, |function| function.selector()),
events: create_mapping(&abi.events, |event| event.signature()),
abi,
}
}
}

/// Utility function for creating a mapping between a unique signature and a
/// name-index pair for accessing contract ABI items.
fn create_mapping<T, S, F>(
elements: &BTreeMap<String, Vec<T>>,
signature: F,
) -> HashMap<S, (String, usize)>
where
S: Hash + Eq + Ord,
F: Fn(&T) -> S,
{
let signature = &signature;
elements
.iter()
.flat_map(|(name, sub_elements)| {
sub_elements
.iter()
.enumerate()
.map(move |(index, element)| (signature(element), (name.to_owned(), index)))
})
.collect()
}

impl Contract {
/// Creates an empty contract instance.
pub fn empty() -> Self {
Expand All @@ -38,7 +111,7 @@ impl Contract {
pub fn with_name(name: impl Into<String>) -> Self {
Contract {
name: name.into(),
abi: Default::default(),
interface: Default::default(),
bytecode: Default::default(),
deployed_bytecode: Default::default(),
networks: HashMap::new(),
Expand Down
6 changes: 3 additions & 3 deletions ethcontract-generate/src/generate/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub(crate) fn expand(cx: &Context) -> TokenStream {
lazy_static! {
pub static ref CONTRACT: Contract = {
#[allow(unused_mut)]
let mut contract = TruffleLoader::new()
let mut contract: Contract = TruffleLoader::new()
.load_contract_from_str(#contract_json)
.expect("valid contract JSON");
#( #deployments )*
Expand Down Expand Up @@ -146,8 +146,8 @@ pub(crate) fn expand(cx: &Context) -> TokenStream {

let transport = DynTransport::new(web3.transport().clone());
let web3 = Web3::new(transport);
let abi = Self::raw_contract().abi.clone();
let instance = Instance::with_deployment_info(web3, abi, address, deployment_information);
let interface = Self::raw_contract().interface.clone();
let instance = Instance::with_deployment_info(web3, interface, address, deployment_information);

Contract::from_raw(instance)
}
Expand Down
8 changes: 6 additions & 2 deletions ethcontract-generate/src/generate/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub(crate) fn expand(cx: &Context) -> Result<TokenStream> {

fn expand_deployed(cx: &Context) -> TokenStream {
if cx.contract.networks.is_empty() && cx.networks.is_empty() {
if cx.contract.name == "DeployedContract" {
println!("{:?}", cx.contract);
println!("{:?}", cx.networks);
}
return quote! {};
}

Expand Down Expand Up @@ -80,7 +84,7 @@ fn expand_deploy(cx: &Context) -> Result<TokenStream> {
// can't seem to get truffle to output it
let doc = util::expand_doc("Generated by `ethcontract`");

let (input, arg) = match cx.contract.abi.constructor() {
let (input, arg) = match cx.contract.interface.abi.constructor() {
Some(constructor) => (
methods::expand_inputs(&constructor.inputs)?,
methods::expand_inputs_call_arg(&constructor.inputs),
Expand Down Expand Up @@ -190,7 +194,7 @@ fn expand_deploy(cx: &Context) -> Result<TokenStream> {
}

fn abi(_: &Self::Context) -> &self::ethcontract::common::Abi {
&Self::raw_contract().abi
&Self::raw_contract().interface.abi
}

fn from_deployment(
Expand Down
Loading
Loading