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(forge, cast): add cast --with_local_artifacts/forge selectors cache to trace with local artifacts #7359

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

byteshijinn
Copy link

@byteshijinn byteshijinn commented Mar 10, 2024

Motivation

Closes #8949
Closes #8581
Supply a fast way to add all project contracts functions and events to signatures makes them decode correctly.

Solution

This PR introduces such a possibility using a cli argument --generate-local-signatures for cast run.

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

supportive, though we can solve this a bit differently

lmk if anything's unclear

Comment on lines 86 to 80

#[arg(long, short, alias = "gs")]
pub generate_local_signatures: bool,
Copy link
Member

Choose a reason for hiding this comment

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

This needs docs+explainer

let _ = fs::write_json_file(&path, &cached_signatures);
}
}

handle_traces(result, &config, chain, self.label, self.debug).await?;
Copy link
Member

Choose a reason for hiding this comment

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

perhaps a better solution than doing the preprocess step would be to provide the local signatures here:

so they can be merged with this here

.with_signature_identifier(SignaturesIdentifier::new(
Config::foundry_cache_dir(),
config.offline,
)?)

Copy link
Author

Choose a reason for hiding this comment

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

I implemented it in run.rs to make it optional, and handle_traces as a public method I wanted to change it as little as possible until I understood all others crates package if refered to it.

Copy link
Author

Choose a reason for hiding this comment

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

In fact, it makes more sense for this function to be executed at forge compile time, and it apears in cast run as a shortcut entry to avoid typing the command twice.

Comment on lines 23 to 30
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Serialize, Deserialize)]
struct CachedSignatures {
events: BTreeMap<String, String>,
functions: BTreeMap<String, String>,
}
Copy link
Member

Choose a reason for hiding this comment

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

we already have this type here

#[derive(Debug, Default, Serialize, Deserialize)]
struct CachedSignatures {
events: BTreeMap<String, String>,
functions: BTreeMap<String, String>,
}

we should prevent duplicating this and instead find a way to reuse the existing type

we can make the type pub instead and also make this a helper function:

let identifier = if let Some(cache_path) = cache_path {
let path = cache_path.join("signatures");
trace!(target: "evm::traces", ?path, "reading signature cache");
let cached = if path.is_file() {
fs::read_json_file(&path)
.map_err(|err| warn!(target: "evm::traces", ?path, ?err, "failed to read cache file"))
.unwrap_or_default()
} else {
if let Err(err) = std::fs::create_dir_all(cache_path) {
warn!(target: "evm::traces", "could not create signatures cache dir: {:?}", err);
}
CachedSignatures::default()
};

Copy link
Author

Choose a reason for hiding this comment

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

I defined it again because I find it does not exported. For maintenance reasons, I think you're right. At first I was just using it for myself so I didn't think about it that much, I'll spend some time refactoring the code next.

@byteshijinn byteshijinn force-pushed the master branch 2 times, most recently from 2a08d1c to 4f0ee53 Compare March 17, 2024 14:18
@byteshijinn
Copy link
Author

I updated my code. please check again @mattsse

@byteshijinn byteshijinn changed the title add RunArgs generate_local_signatures to enable trace with local contracts functions and events feat(forge, cast): add RunArgs generate_local_signatures to enable trace with local contracts functions and events Apr 20, 2024
@byteshijinn byteshijinn requested a review from Evalir as a code owner June 30, 2024 12:00
Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

sorry this one slipped through,
some nits.

generally supportive, wdyt @DaniPopes @klkvr

@@ -414,3 +414,25 @@ pub async fn print_traces(result: &mut TraceResult, decoder: &CallTraceDecoder)
println!("Gas used: {}", result.gas_used);
Ok(())
}

pub fn generate_local_signatures(output: &ProjectCompileOutput, cache_path: PathBuf) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

this needs docs and should be renamed to cache_local_signatures

Copy link
Author

Choose a reason for hiding this comment

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

ok i'll fix it later

@@ -220,6 +227,19 @@ impl RunArgs {
}
};

if self.generate_local_signatures {
let project = config.project()?;
Copy link
Member

Choose a reason for hiding this comment

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

this should print that we're compiling to get the signatures

@@ -220,6 +227,19 @@ impl RunArgs {
}
};

if self.generate_local_signatures {
let project = config.project()?;
let compiler = ProjectCompiler::new().quiet(true);
Copy link
Member

Choose a reason for hiding this comment

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

we don't need to actually compile here, right?
@klkvr is there an easier way to just get the artifacts?

Copy link
Member

@klkvr klkvr Jul 1, 2024

Choose a reason for hiding this comment

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

we need to compile as it might be run on a clean project. though I think we can only request abi here, should speed things up

if there are cached artifacts then compilation would just use them, so that's fine I believe

Copy link
Author

@byteshijinn byteshijinn Jul 3, 2024

Choose a reason for hiding this comment

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

Usually users are more likely to trace their own project files and compile the whole project before deploying. But some one may dont want to run two command to generate signature cache with new file updates (may be an upgradable contract should try different events). If file not updated, compile will be complete fast.

@@ -414,3 +414,25 @@ pub async fn print_traces(result: &mut TraceResult, decoder: &CallTraceDecoder)
println!("Gas used: {}", result.gas_used);
Ok(())
}

pub fn generate_local_signatures(output: &ProjectCompileOutput, cache_path: PathBuf) -> Result<()> {
let mut cached_signatures = CachedSignatures::default();
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't we firstly read the contents of the file here? looks like this always overwrites existing cache

Copy link
Author

@byteshijinn byteshijinn Jul 3, 2024

Choose a reason for hiding this comment

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

I believe that generating function signatures for a single project.sol file during most project debugging is sufficient, so I choose to overwrite the existing file. If you want to merge the original signature cache, you also need to consider the handling logic for 4-byte signature conflicts. Such as same 4-byte signature with different arguments

Copy link
Member

@klkvr klkvr Jul 3, 2024

Choose a reason for hiding this comment

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

I don't think it's correct to override existing cache anyway. That way running cast run ... --generate-local-signatures twice would result in a bunch of duplicated requests to fetch signatures for unknown contracts

Also wondering if we should instead inject local abis into CallTraceDecoder directly? This would allow us to also decode output values. SignatureIdentifier is mostly suited for decoding only selectors of external contracts

Copy link
Author

Choose a reason for hiding this comment

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

@klkvr Thank you for your guidance. While implementing this feature, I was focused on getting it ready for trace call as soon as possible and didn't look closely at the SignatureIdentifier part. I think I need some time to finish modifying the code, test it with examples of 4-byte signature conflicts, and then resubmit.

Copy link
Author

Choose a reason for hiding this comment

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

I kept the cached local signatures and merged the new signatures with them. However, I need to point out the issue you mentioned earlier with the SignatureIdentifier. If a function's 4-byte signature matches one in the local signatures, the SignatureIdentifier will not automatically attempt to fetch a matching function from external sources, even if the parameters do not match.

Copy link
Author

Choose a reason for hiding this comment

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

signature-test.zip
pic1:
20240706171123
pic2:
20240706171703

with test sol files, deploy and call run, then cast run trace will output like the pic1.

vm.startBroadcast(pkey);
Foobar foo1 = new Foobar();
Foobar2 foo2 = new Foobar2();
Foobar3 foo3 = new Foobar3(foo1, foo2);
foo3.run();
vm.stopBroadcast();

and then I remove "0x6a627842":"watch_tg_invmru_89cb189(uint256,address)" in the cache file, then cast run trace will output like the pic2. anyway that is an another question.

@mattsse
Copy link
Member

mattsse commented Jul 9, 2024

how to proceed here @klkvr ?

| NamedChain::ArbitrumTestnet
| NamedChain::ArbitrumGoerli
| NamedChain::ArbitrumSepolia
| NamedChain::Moonbeam
Copy link
Member

@klkvr klkvr Jul 9, 2024

Choose a reason for hiding this comment

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

please remove all the formatting changes

Copy link
Author

Choose a reason for hiding this comment

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

sorry for my mistakes, I will fix it

Comment on lines 428 to 435
let cache_contents = std::fs::read_to_string(path.clone()).unwrap();
match serde_json::from_str::<CachedSignatures>(&cache_contents) {
Ok(existed_signatures) => cached_signatures = existed_signatures,
Err(e) => {
println!("parse cached local signatures file error: {}", e);
}
}
dbg!(&cached_signatures);
Copy link
Member

Choose a reason for hiding this comment

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

this is now duplicated with SignatureIdentifier::new, can we move this logic to CachedSignatures::load(path)

@klkvr
Copy link
Member

klkvr commented Jul 9, 2024

So this can be broken into 2 separate usecases

  1. Have a way to flush selectors from local project contracts into global signature cache (forge build --cache_local_signatures). Current impl lgtm, however, wondering if it makes more sense to be forge selectors cache instead?

  2. Use local ABIs when decoding call trace from cast run. For this usecase I'd prefer a different implementation which would obtain Project, compile it, construct known_contracts and include them in both CallTraceDecoder and TraceIdentifiers similarly to how it's done in forge test:

    let mut identifier = TraceIdentifiers::new().with_local(&known_contracts);

    The motivation is that if we have an entire Project, then we can not only use artifacts for identification of selectors, but also to resolve return values of functions, and identify contracts by their bytecodes. ref tracing: Improve decoding of functions output #6531 which had similar motivation

@byteshijinn byteshijinn force-pushed the master branch 2 times, most recently from 825579e to c575b2d Compare July 10, 2024 16:59
@byteshijinn
Copy link
Author

So this can be broken into 2 separate usecases

  1. Have a way to flush selectors from local project contracts into global signature cache (forge build --cache_local_signatures). Current impl lgtm, however, wondering if it makes more sense to be forge selectors cache instead?

  2. Use local ABIs when decoding call trace from cast run. For this usecase I'd prefer a different implementation which would obtain Project, compile it, construct known_contracts and include them in both CallTraceDecoder and TraceIdentifiers similarly to how it's done in forge test:

    let mut identifier = TraceIdentifiers::new().with_local(&known_contracts);

    The motivation is that if we have an entire Project, then we can not only use artifacts for identification of selectors, but also to resolve return values of functions, and identify contracts by their bytecodes. ref tracing: Improve decoding of functions output #6531 which had similar motivation

For case 1, I believe having helper options in forge build and cast run would make this feature more noticeable. I think adding a command to clear the cache would be useful because SignatureIdentifier merges matched signatures into the cache. My previous tests showed that if a local signature is matched, even if the parameters do not match, it still displays the matched function name.

For case 2, I have considered the solution you mentioned. However, the changes involved are quite extensive and a bit difficult for me, which is why I chose the current method. It allows me to get it working quickly.

@zerosnacks zerosnacks added this to the v1.0.0 milestone Jul 31, 2024
@zerosnacks zerosnacks added C-cast Command: cast C-forge Command: forge A-tracing Area: tracing Cmd-forge-build Command: forge build labels Jul 31, 2024
@mattsse mattsse requested a review from klkvr September 6, 2024 11:51
@grandizzy
Copy link
Collaborator

grandizzy commented Nov 21, 2024

thanks @byteshijinn ! I made following changes and updated the PR:

  • merged with latest master
  • flush selectors from local project contracts into global signature cache through forge selectors cache instead forge build
  • use local ABIs when decoding call trace from cast run and cast send (--with_local_artifacts or --la)

Adding tests is a little bit hard, how I tested locally was to:

  • start anvil
  • deploy contract
  • send tx (custom errors, events)
  • run cast run tx - no traces shown. run cast run tx --la showing decoded traces (contracts identified)
  • run forge selectors cache, then run cast run tx --la showing decoded traces (contracts not identified though)

@klkvr mind to review? thank you!

@grandizzy grandizzy changed the title feat(forge, cast): add RunArgs generate_local_signatures to enable trace with local contracts functions and events feat(forge, cast): add cast --with_local_artifacts/forge selectors cache to trace with local artifacts Nov 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tracing Area: tracing C-cast Command: cast C-forge Command: forge Cmd-forge-build Command: forge build
Projects
Status: No status
5 participants