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

fix(forge): sensitive broadcast logs #4892

Merged
merged 8 commits into from
May 18, 2023
118 changes: 103 additions & 15 deletions cli/src/cmd/forge/script/sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ pub struct ScriptSequence {
pub receipts: Vec<TransactionReceipt>,
pub libraries: Vec<String>,
pub pending: Vec<TxHash>,
#[serde(skip)]
pub path: PathBuf,
#[serde(skip)]
pub sensitive_path: PathBuf,
pub returns: HashMap<String, NestedValue>,
pub timestamp: u64,
pub chain: u64,
Expand All @@ -45,6 +48,30 @@ pub struct ScriptSequence {
pub commit: Option<String>,
}

/// Sensitive values from the transactions in a script sequence
#[derive(Deserialize, Serialize, Clone, Default)]
pub struct SensitiveTransactionMetadata {
pub rpc: Option<String>,
}

/// Sensitive info from the script sequence which is saved into the cache folder
#[derive(Deserialize, Serialize, Clone, Default)]
pub struct SensitiveScriptSequence {
pub transactions: VecDeque<SensitiveTransactionMetadata>,
}

impl From<&mut ScriptSequence> for SensitiveScriptSequence {
fn from(sequence: &mut ScriptSequence) -> Self {
SensitiveScriptSequence {
transactions: sequence
.transactions
.iter()
.map(|tx| SensitiveTransactionMetadata { rpc: tx.rpc.clone() })
.collect(),
}
}
}

impl ScriptSequence {
pub fn new(
transactions: VecDeque<TransactionWithMetadata>,
Expand All @@ -57,13 +84,15 @@ impl ScriptSequence {
) -> eyre::Result<Self> {
let chain = config.chain_id.unwrap_or_default().id();

let path = ScriptSequence::get_path(
let (path, sensitive_path) = ScriptSequence::get_paths(
&config.broadcast,
&config.cache_path,
sig,
target,
chain,
broadcasted && !is_multi,
)?;

let commit = get_commit_hash(&config.__root.0);

Ok(ScriptSequence {
Expand All @@ -72,6 +101,7 @@ impl ScriptSequence {
receipts: vec![],
pending: vec![],
path,
sensitive_path,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Wrong system time.")
Expand All @@ -91,16 +121,46 @@ impl ScriptSequence {
chain_id: u64,
broadcasted: bool,
) -> eyre::Result<Self> {
let path = ScriptSequence::get_path(&config.broadcast, sig, target, chain_id, broadcasted)?;
ethers::solc::utils::read_json_file(path)
.wrap_err(format!("Deployment not found for chain `{chain_id}`."))
let (path, sensitive_path) = ScriptSequence::get_paths(
&config.broadcast,
&config.cache_path,
sig,
target,
chain_id,
broadcasted,
)?;

let mut script_sequence: Self = ethers::solc::utils::read_json_file(&path)
.wrap_err(format!("Deployment not found for chain `{chain_id}`."))?;

let sensitive_script_sequence: SensitiveScriptSequence =
ethers::solc::utils::read_json_file(&sensitive_path).wrap_err(format!(
"Deployment's sensitive details not found for chain `{chain_id}`."
))?;

script_sequence
.transactions
.iter_mut()
.enumerate()
.for_each(|(i, tx)| tx.rpc = sensitive_script_sequence.transactions[i].rpc.clone());

script_sequence.path = path;
script_sequence.sensitive_path = sensitive_path;

Ok(script_sequence)
}

/// Saves the transactions as file if it's a standalone deployment.
pub fn save(&mut self) -> eyre::Result<()> {
if !self.multi && !self.transactions.is_empty() {
self.timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();

let sensitive_script_sequence: SensitiveScriptSequence = self.into();

let path = self.path.to_string_lossy();
let sensitive_path = self.sensitive_path.to_string_lossy();

// broadcast folder writes
//../run-latest.json
serde_json::to_writer_pretty(BufWriter::new(fs::create_file(&self.path)?), &self)?;
//../run-[timestamp].json
Expand All @@ -110,7 +170,23 @@ impl ScriptSequence {
)?),
&self,
)?;

// cache folder writes
//../run-latest.json
serde_json::to_writer_pretty(
BufWriter::new(fs::create_file(&self.sensitive_path)?),
&sensitive_script_sequence,
)?;
//../run-[timestamp].json
serde_json::to_writer_pretty(
BufWriter::new(fs::create_file(
sensitive_path.replace("latest.json", &format!("{}.json", self.timestamp)),
)?),
&sensitive_script_sequence,
)?;

shell::println(format!("\nTransactions saved to: {path}\n"))?;
Copy link
Member

Choose a reason for hiding this comment

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

maybe we should print an extra message indicating sensitive info has been written to a separate path?

shell::println(format!("Sensitive values saved to: {sensitive_path}\n"))?;
}

Ok(())
Expand Down Expand Up @@ -147,29 +223,41 @@ impl ScriptSequence {
.collect();
}

/// Saves to ./broadcast/contract_filename/sig[-timestamp].json
pub fn get_path(
out: &Path,
/// Gets paths in the formats
/// ./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json and
/// ./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json
pub fn get_paths(
broadcast: &Path,
cache: &Path,
sig: &str,
target: &ArtifactId,
chain_id: u64,
broadcasted: bool,
) -> eyre::Result<PathBuf> {
let mut out = out.to_path_buf();
) -> eyre::Result<(PathBuf, PathBuf)> {
let mut broadcast = broadcast.to_path_buf();
let mut cache = cache.to_path_buf();
let mut common = PathBuf::new();

let target_fname = target.source.file_name().wrap_err("No filename.")?;
out.push(target_fname);
out.push(chain_id.to_string());
common.push(target_fname);
common.push(chain_id.to_string());
if !broadcasted {
out.push(DRY_RUN_DIR);
common.push(DRY_RUN_DIR);
}

fs::create_dir_all(&out)?;
broadcast.push(common.clone());
cache.push(common);

fs::create_dir_all(&broadcast)?;
fs::create_dir_all(&cache)?;

// TODO: ideally we want the name of the function here if sig is calldata
let filename = sig_to_file_name(sig);

out.push(format!("{filename}-latest.json"));
Ok(out)
broadcast.push(format!("{filename}-latest.json"));
cache.push(format!("{filename}-latest.json"));

Ok((broadcast, cache))
}

/// Given the broadcast log, it matches transactions with receipts, and tries to verify any
Expand Down
1 change: 1 addition & 0 deletions cli/src/cmd/forge/script/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct TransactionWithMetadata {
pub function: Option<String>,
#[serde(default = "default_vec_of_strings")]
pub arguments: Option<Vec<String>>,
#[serde(skip)]
pub rpc: Option<RpcUrl>,
pub transaction: TypedTransaction,
pub additional_contracts: Vec<AdditionalContract>,
Expand Down
98 changes: 59 additions & 39 deletions cli/tests/it/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,50 +686,70 @@ forgetest_async!(fail_broadcast_staticcall, |prj: TestProject, cmd: TestCommand|
.simulate(ScriptOutcome::StaticCallNotAllowed);
});

forgetest_async!(
#[ignore]
check_broadcast_log,
|prj: TestProject, cmd: TestCommand| async move {
let (api, handle) = spawn(NodeConfig::test()).await;
let mut tester = ScriptTester::new_broadcast(cmd, &handle.http_endpoint(), prj.root());

// Prepare CREATE2 Deployer
let addr = Address::from_str("0x4e59b44847b379578588920ca78fbf26c0b4956c").unwrap();
let code = hex::decode("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3").expect("Could not decode create2 deployer init_code").into();
api.anvil_set_code(addr, code).await.unwrap();

tester
.load_private_keys(vec![0])
.await
.add_sig("BroadcastTestLog", "run()")
.slow()
.simulate(ScriptOutcome::OkSimulation)
.broadcast(ScriptOutcome::OkBroadcast)
.assert_nonce_increment(vec![(0, 7)])
.await;
forgetest_async!(check_broadcast_log, |prj: TestProject, cmd: TestCommand| async move {
let (api, handle) = spawn(NodeConfig::test()).await;
let mut tester = ScriptTester::new_broadcast(cmd, &handle.http_endpoint(), prj.root());

// Uncomment to recreate log
// std::fs::copy("broadcast/Broadcast.t.sol/31337/run-latest.json",
// PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../testdata/fixtures/broadcast.log.json"
// ));
// Prepare CREATE2 Deployer
let addr = Address::from_str("0x4e59b44847b379578588920ca78fbf26c0b4956c").unwrap();
let code = hex::decode("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3").expect("Could not decode create2 deployer init_code").into();
api.anvil_set_code(addr, code).await.unwrap();

// Ignore blockhash and timestamp since they can change inbetween runs.
let re = Regex::new(r#"(blockHash.*?blockNumber)|((timestamp":)[0-9]*)"#).unwrap();
tester
.load_private_keys(vec![0])
.await
.add_sig("BroadcastTestSetup", "run()")
.simulate(ScriptOutcome::OkSimulation)
.broadcast(ScriptOutcome::OkBroadcast)
.assert_nonce_increment(vec![(0, 6)])
.await;

let fixtures_log = std::fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../testdata/fixtures/broadcast.log.json"),
)
.unwrap();
let fixtures_log = re.replace_all(&fixtures_log, "");
// Uncomment to recreate the broadcast log
// std::fs::copy(
// "broadcast/Broadcast.t.sol/31337/run-latest.json",
// PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../testdata/fixtures/broadcast.log.json"
// ), );

// Check broadcast logs
// Ignore timestamp, blockHash, blockNumber, cumulativeGasUsed, effectiveGasPrice,
// transactionIndex and logIndex values since they can change inbetween runs
let re = Regex::new(r#"((timestamp":).[0-9]*)|((blockHash":).*)|((blockNumber":).*)|((cumulativeGasUsed":).*)|((effectiveGasPrice":).*)|((transactionIndex":).*)|((logIndex":).*)"#).unwrap();

let fixtures_log = std::fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../testdata/fixtures/broadcast.log.json"),
)
.unwrap();
let fixtures_log = re.replace_all(&fixtures_log, "");

let run_log =
std::fs::read_to_string("broadcast/Broadcast.t.sol/31337/run-latest.json").unwrap();
let run_log = re.replace_all(&run_log, "");

assert!(fixtures_log == run_log);

// Uncomment to recreate the sensitive log
// std::fs::copy(
// "cache/Broadcast.t.sol/31337/run-latest.json",
// PathBuf::from(env!("CARGO_MANIFEST_DIR"))
// .join("../testdata/fixtures/broadcast.sensitive.log.json"),
// );

// Check sensitive logs
// Ignore port number since it can change inbetween runs
let re = Regex::new(r#":[0-9]+"#).unwrap();

let fixtures_log = std::fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../testdata/fixtures/broadcast.sensitive.log.json"),
)
.unwrap();
let fixtures_log = re.replace_all(&fixtures_log, "");

let run_log =
std::fs::read_to_string("broadcast/Broadcast.t.sol/31337/run-latest.json").unwrap();
let run_log = re.replace_all(&run_log, "");
let run_log = std::fs::read_to_string("cache/Broadcast.t.sol/31337/run-latest.json").unwrap();
let run_log = re.replace_all(&run_log, "");

assert!(fixtures_log == run_log);
}
);
assert!(fixtures_log == run_log);
});

forgetest_async!(test_default_sender_balance, |prj: TestProject, cmd: TestCommand| async move {
let (_api, handle) = spawn(NodeConfig::test()).await;
Expand Down
Loading