fix: create verifiable Standard JSON for projects with external files#36
Conversation
```
$ cargo test --test project --all-features can_create_standard_json_input_with_external_file
Finished test [unoptimized + debuginfo] target(s) in 0.10s
Running tests/project.rs (target/debug/deps/project-bc070fde513cf057)
running 1 test
test can_create_standard_json_input_with_external_file ... FAILED
failures:
---- can_create_standard_json_input_with_external_file stdout ----
thread 'can_create_standard_json_input_with_external_file' panicked at tests/project.rs:1649:5:
assertion failed: `(left == right)`
Diff < left / right > :
[
"src/Counter.sol",
"../remapped/Parent.sol",
< "/private/var/folders/1m/9vppwqks3pz4btz9gnczfmgh0000gn/T/.tmpSWn2KX/remapped/Child.sol",
> "../remapped/Child.sol",
]
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
can_create_standard_json_input_with_external_file
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 52 filtered out; finished in 1.38s
error: test failed, to rerun pass `--test project`
```
| let verif_dir = utils::canonicalize(dir.path()).unwrap().join("verif"); | ||
| let remapped_dir = utils::canonicalize(dir.path()).unwrap().join("remapped"); |
There was a problem hiding this comment.
After my previous PR is merged, this canonicalization can be removed. It is necessary on this branch because the symlink-resolving canonicalization is still in effect, and the temporary directory on macOS is a symlink.
mattsse
left a comment
There was a problem hiding this comment.
amazing, tysm!
as mentioned in the other PR, I'd like to see a foundry companion PR with patched crate dep as a sanity check before we merge this
|
Thank you for your review! I've created a companion PR in foundry with this patch. Also, I have confirmed that the tests in foundry passed successfully. |
|
The workflow test for this PR failed due to a failure in nextest installation. As this issue is unrelated to the changes made, re-running the test should resolve it. https://github.com/foundry-rs/compilers/actions/runs/7331166894/job/19979490471 |
src/lib.rs
Outdated
| for (i, (base_component, path_component)) in | ||
| base.components().zip(path.components()).enumerate() | ||
| { | ||
| if base_component == path_component { | ||
| new_path.pop_front(); | ||
| } else { | ||
| let mut parent_dirs = | ||
| vec![std::path::Component::ParentDir; base.components().count() - i]; | ||
| parent_dirs.extend(new_path); | ||
| new_path = parent_dirs.into(); | ||
|
|
||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
I think we can solve this a bit differently
don't zip the two iterators but create the two iters separately and then iterate over the path
and continue as long as base and path components match, once they diverge collect the remaining into a PathBuf
this way we don't need the vecdeque
There was a problem hiding this comment.
Hmm, I'm not sure I understand your suggestion.
If we compare the components of base and path at the same positions, it seems similar to using zip.
Could you please elaborate on the type of implementation you have in mind?
There was a problem hiding this comment.
something like
let mut components = Vec::new(); // perhaps we can even directly push components to a PathBuf?
let mut base_iter = base.components();
let mut path_iter = path.components();
while let Some(path) = path_iter.next() {
if Some(path) != base_iter.next() {
components.push(path);
components.extend(path_iter);
}
}
There was a problem hiding this comment.
Thank you for your explanation. I now understand your point. I've made the change and pushed it.
src/lib.rs
Outdated
| // The returned path from this function usually starts either with a normal component (e.g., `src`) | ||
| // or a parent directory component (i.e., `..`), which is based on the base directory. Additionally, | ||
| // this function converts the path into a UTF-8 string and replaces all separators with forward | ||
| // slashes (`/`). | ||
| // | ||
| // The rebasing process is as follows: | ||
| // | ||
| // 1. Remove the leading components from the path that match the base components. | ||
| // 2. Prepend `..` components to the path, equal in number to the remaining base components. |
There was a problem hiding this comment.
can we add an example here that illustrates this, otherwise this is a bit hard to understand.
There was a problem hiding this comment.
I've updated the comment, adding examples to make it easier to understand.
mattsse
left a comment
There was a problem hiding this comment.
awesome job!
I'll merge both and upstream to foundry after a new release
|
Amazing work. Doesn't this PR also address issue foundry-rs/foundry#3507? |
|
@PaulRBerg I haven't looked into it deeply, but it seems like a different matter from this PR and #35, as they don't involve |
Currently, Foundry projects containing Solidity files outside the project root directory face contract verification failures on block explorers. This issue occurs from the Standard JSON including unusable source paths for external files, represented as full absolute paths in their host file systems.
This PR addresses the issue by improving the path conversion process. For files not located under the project root directory, relative parent directory paths (
..) are used, enabling the compiler to find the files within the json.Steps to reproduce the issue are detailed in the linked issue above. Additionally, a test case representing that scenario has been added.
With this change, the json created in the reproduction scenario will appear as follows:
{ "language": "Solidity", "sources": { "src/Counter.sol": { "content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\n\nimport \"@remapped/Parent.sol\";\n\ncontract Counter {\n uint256 public number;\n\n function setNumber(uint256 newNumber) public {\n number = newNumber;\n }\n\n function increment() public {\n number++;\n }\n}\n" }, "../remapped/Parent.sol": { "content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\nimport \"./Child.sol\";\n" }, "../remapped/Child.sol": { "content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\n" } }, "settings": { "remappings": [ "@remapped/=../remapped/", "ds-test/=lib/forge-std/lib/ds-test/src/", "forge-std/=lib/forge-std/src/" ], "optimizer": { "enabled": true, "runs": 200 }, "metadata": { "useLiteralContent": false, "bytecodeHash": "ipfs", "appendCBOR": true }, "outputSelection": { "*": { "": [ "ast" ], "*": [ "abi", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers", "metadata" ] } }, "evmVersion": "paris", "libraries": {} } }The source path is now aligned with the project root.
I have successfully deployed and verified the contract on Etherscan using this change.
forge create --rpc-url "wss://ethereum-holesky.publicnode.com" --verify --verifier-url "https://api-holesky.etherscan.io/api" --etherscan-api-key "..." --private-key "..." src/Counter.sol:Counterhttps://holesky.etherscan.io/address/0xe08c332706185521fc8bc2b224f67adf814b1880#code