Skip to content

Commit 2352a2f

Browse files
authored
Phase 1: establish the Quantum network adapter (#2)
<!-- Thank you for your Pull Request. Please provide a description above and review the requirements below. Bug fixes and new features should include tests. Contributors guide: https://github.com/foundry-rs/foundry/blob/HEAD/CONTRIBUTING.md The contributors guide includes instructions for running rustfmt and building the documentation. --> <!-- ** Please select "Allow edits from maintainers" in the PR Options ** --> ## Motivation <!-- Explain the context and why you're making that change. What is the problem you're trying to solve? In some cases there is not a problem and this can be thought of as being the motivation for your change. --> ## Solution <!-- Summarize the solution and provide any necessary context needed to understand the code change. --> ## PR Checklist - [ ] Added Tests - [ ] Added Documentation - [ ] Breaking changes
1 parent b543b5f commit 2352a2f

30 files changed

Lines changed: 2927 additions & 650 deletions

File tree

.github/workflows/ci-tempo.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ concurrency:
3434
env:
3535
CARGO_TERM_COLOR: always
3636
RUSTC_WRAPPER: "sccache"
37+
QUANTUM_FOUNDRY_BASE_COMMIT: "f1abb2ca347187bb6dea8c3881ca44ce50aab1e7"
38+
QUANTUM_HARNESS_COMMIT: "8f3612c60f9fa66ea3a09eab99a2e0802f373673"
3739

3840
jobs:
3941
sanity-check:

Cargo.lock

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/anvil/src/eth/api.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3526,6 +3526,9 @@ impl EthApi<FoundryNetwork> {
35263526
FoundryTxEnvelope::Deposit(_) => self.backend.ensure_op_deposits_active(),
35273527
FoundryTxEnvelope::Legacy(_) => Ok(()),
35283528
FoundryTxEnvelope::Tempo(_) => self.backend.ensure_tempo_active(),
3529+
FoundryTxEnvelope::Quantum(_) => Err(BlockchainError::Message(
3530+
"quantum transactions are not supported by Anvil".to_string(),
3531+
)),
35293532
}
35303533
}
35313534
}

crates/anvil/src/eth/backend/executor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ impl ReceiptBuilder for FoundryReceiptBuilder {
7070
unreachable!("deposit receipts are built in commit_transaction")
7171
}
7272
FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt),
73+
FoundryTxType::Quantum => FoundryReceiptEnvelope::Quantum(receipt),
7374
}
7475
}
7576
}

crates/anvil/src/eth/sign.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ impl Signer<foundry_primitives::FoundryNetwork> for DevSigner {
137137
let sig = signer.sign_transaction_sync(&mut t)?;
138138
FoundryTxEnvelope::Tempo(t.into_signed(sig.into()))
139139
}
140+
FoundryTypedTx::Quantum(_) => {
141+
return Err(BlockchainError::Message(
142+
"Quantum transactions must be pre-signed and are not supported by Anvil's dev signer"
143+
.to_string(),
144+
))
145+
}
140146
};
141147
Ok(envelope)
142148
}
@@ -158,5 +164,6 @@ pub fn build_impersonated(typed_tx: FoundryTypedTx) -> FoundryTxEnvelope {
158164
let tempo_sig: TempoSignature = signature.into();
159165
FoundryTxEnvelope::Tempo(tx.into_signed(tempo_sig))
160166
}
167+
FoundryTypedTx::Quantum(tx) => FoundryTxEnvelope::Quantum(tx),
161168
}
162169
}

crates/cast/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ foundry-compilers.workspace = true
2828
foundry-config.workspace = true
2929
foundry-debugger.workspace = true
3030
foundry-evm.workspace = true
31+
foundry-primitives.workspace = true
3132
foundry-wallets.workspace = true
3233
forge-fmt.workspace = true
3334

@@ -55,7 +56,6 @@ alloy-sol-types.workspace = true
5556
alloy-transport.workspace = true
5657
alloy-ens = { workspace = true, features = ["provider"] }
5758
alloy-eips.workspace = true
58-
ml-dsa = "0.1.0-rc.8"
5959
tempo-alloy.workspace = true
6060
tempo-contracts.workspace = true
6161
tempo-primitives.workspace = true

crates/cast/src/args.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use foundry_common::{
3131
},
3232
shell, stdin,
3333
};
34+
use foundry_primitives::QuantumNetwork;
3435
use op_alloy_network::Optimism;
3536
use std::time::Instant;
3637
use tempo_alloy::TempoNetwork;
@@ -368,6 +369,13 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
368369
.block_raw(block.unwrap_or(BlockId::Number(Latest)), full)
369370
.await?
370371
}
372+
Some(NetworkVariant::Quantum) => {
373+
let provider =
374+
ProviderBuilder::<QuantumNetwork>::from_config(&config)?.build()?;
375+
Cast::new(&provider)
376+
.block_raw(block.unwrap_or(BlockId::Number(Latest)), full)
377+
.await?
378+
}
371379
// Ethereum (default) or no --raw flag
372380
_ => {
373381
let provider =
@@ -585,6 +593,13 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
585593
.transaction(tx_hash, from, nonce, field, is_raw, to_request)
586594
.await?
587595
}
596+
Some(NetworkVariant::Quantum) => {
597+
let provider =
598+
ProviderBuilder::<QuantumNetwork>::from_config(&config)?.build()?;
599+
Cast::new(&provider)
600+
.transaction(tx_hash, from, nonce, field, is_raw, to_request)
601+
.await?
602+
}
588603
// Ethereum (default) or no --raw flag
589604
_ => {
590605
let provider = utils::get_provider(&config)?;
@@ -799,6 +814,9 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
799814
Some(NetworkVariant::Tempo) => {
800815
SimpleCast::decode_raw_transaction::<TempoNetwork>(&tx)?
801816
}
817+
Some(NetworkVariant::Quantum) => {
818+
SimpleCast::decode_raw_transaction::<QuantumNetwork>(&tx)?
819+
}
802820
_ => SimpleCast::decode_raw_transaction::<Ethereum>(&tx)?,
803821
};
804822
sh_println!("{}", serde_json::to_string_pretty(&decoded_tx)?)?;

crates/cast/src/cmd/da_estimate.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ impl DAEstimateArgs {
4444
NetworkVariant::Tempo => Err(eyre::eyre!(
4545
"DA estimation is not supported for Tempo: EIP-4844 blob transactions are not available on this network"
4646
)),
47+
NetworkVariant::Quantum => Err(eyre::eyre!(
48+
"DA estimation is not supported for Quantum: the first-class QuantumNetwork adapter does not implement this path yet"
49+
)),
4750
}
4851
}
4952
}

crates/cast/src/cmd/send.rs

Lines changed: 73 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,25 @@ use std::{path::PathBuf, str::FromStr, time::Duration};
22

33
use alloy_consensus::{SignableTransaction, Signed};
44
use alloy_ens::NameOrAddress;
5-
use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
6-
use alloy_primitives::Address;
5+
use alloy_network::{Ethereum, EthereumWallet, Network};
6+
use alloy_primitives::{Address, hex};
77
use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
88
use alloy_signer::{Signature, Signer};
99
use clap::Parser;
1010
use eyre::{Result, eyre};
1111
use foundry_cli::{opts::TransactionOpts, utils::LoadConfig};
1212
use foundry_common::{
13-
FoundryTransactionBuilder,
13+
FoundryTransactionBuilder, QUANTUM_BOOTSTRAP_SELECTOR, QUANTUM_KEYVAULT_ADDRESS,
14+
derive_primary_pubkey, parse_seed_file, sign_quantum_transaction_request,
1415
fmt::{UIfmt, UIfmtReceiptExt},
1516
provider::ProviderBuilder,
1617
};
18+
use foundry_primitives::QuantumNetwork;
1719
use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
1820
use tempo_alloy::TempoNetwork;
1921

2022
use crate::{
2123
cmd::tip20::iso4217_warning_message,
22-
quantum::{QuantumWriteContractV1, build_phase0_payload, parse_seed_file},
2324
tx::{self, CastTxBuilder, CastTxSender, SendTxOpts},
2425
};
2526
use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, is_iso4217_currency};
@@ -109,38 +110,57 @@ impl SendTxArgs {
109110
}
110111

111112
async fn run_quantum(self) -> Result<()> {
112-
let Self { to, mut sig, args, data, send_tx, command, unlocked, force: _, tx, path } =
113-
self;
113+
let Self {
114+
to,
115+
mut sig,
116+
args,
117+
data,
118+
send_tx,
119+
command,
120+
unlocked,
121+
force: _,
122+
mut tx,
123+
path,
124+
} = self;
114125

115126
if unlocked {
116-
return Err(eyre!("the Phase 0 Quantum seam does not support --unlocked"));
127+
return Err(eyre!("the Quantum adapter path does not support --unlocked"));
117128
}
118129
if send_tx.browser.browser {
119-
return Err(eyre!("the Phase 0 Quantum seam does not support browser signing"));
130+
return Err(eyre!("the Quantum adapter path does not support browser signing"));
120131
}
121132
if tx.tempo.is_tempo() {
122133
return Err(eyre!("Quantum and Tempo options cannot be combined"));
123134
}
124135
if command.is_some() {
125-
return Err(eyre!("the Phase 0 Quantum seam only supports cast send-style call flows"));
136+
return Err(eyre!("the Quantum adapter path only supports cast send-style call flows"));
126137
}
127138
if path.is_some() {
128-
return Err(eyre!("the Phase 0 Quantum seam does not support blob data"));
139+
return Err(eyre!("the Quantum adapter path does not support blob data"));
129140
}
130141
if let Some(data) = data {
131142
sig = Some(data);
132143
}
133144

134-
let sender = tx.quantum.sender.ok_or_else(|| {
135-
eyre!("--quantum.sender is required for the Phase 0 Quantum seam")
136-
})?;
137-
let seed_path = tx.quantum.primary_seed_file.as_ref().ok_or_else(|| {
138-
eyre!("--quantum.primary-seed-file is required for the Phase 0 Quantum seam")
139-
})?;
145+
let sender = tx
146+
.quantum
147+
.sender
148+
.ok_or_else(|| eyre!("--quantum.sender is required for Quantum writes"))?;
149+
validate_quantum_sender(send_tx.eth.wallet.from, sender)?;
150+
let seed_path =
151+
tx.quantum.primary_seed_file.as_ref().ok_or_else(|| {
152+
eyre!("--quantum.primary-seed-file is required for Quantum writes")
153+
})?;
140154
let primary_seed = parse_seed_file(seed_path)?;
141155

156+
if quantum_send_requests_bootstrap(to.as_ref(), sig.as_deref())
157+
&& tx.quantum.init_primary_pubkey.is_none()
158+
{
159+
tx.quantum.init_primary_pubkey = Some(derive_primary_pubkey(primary_seed));
160+
}
161+
142162
let config = send_tx.eth.load_config()?;
143-
let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
163+
let provider = ProviderBuilder::<QuantumNetwork>::from_config(&config)?.build()?;
144164

145165
if let Some(interval) = send_tx.poll_interval {
146166
provider.client().set_poll_interval(Duration::from_secs(interval));
@@ -154,37 +174,7 @@ impl SendTxArgs {
154174
.await?;
155175

156176
let (tx_request, _) = builder.build(sender).await?;
157-
let kind = TransactionBuilder::kind(&tx_request)
158-
.ok_or_else(|| eyre!("Quantum Phase 0 requires an explicit call destination"))?;
159-
let max_fee_per_gas = TransactionBuilder::max_fee_per_gas(&tx_request)
160-
.ok_or_else(|| eyre!("failed to resolve max fee per gas for Quantum transaction"))?;
161-
let max_priority_fee_per_gas = TransactionBuilder::max_priority_fee_per_gas(&tx_request)
162-
.ok_or_else(|| {
163-
eyre!("failed to resolve max priority fee per gas for Quantum transaction")
164-
})?;
165-
let gas_limit = TransactionBuilder::gas_limit(&tx_request)
166-
.ok_or_else(|| eyre!("failed to resolve gas limit for Quantum transaction"))?;
167-
let nonce = TransactionBuilder::nonce(&tx_request)
168-
.ok_or_else(|| eyre!("failed to resolve nonce for Quantum transaction"))?;
169-
let chain_id = TransactionBuilder::chain_id(&tx_request)
170-
.ok_or_else(|| eyre!("failed to resolve chain ID for Quantum transaction"))?;
171-
172-
let payload = build_phase0_payload(QuantumWriteContractV1 {
173-
sender,
174-
key_id: tx.quantum.resolved_key_id(),
175-
nonce,
176-
chain_id,
177-
max_priority_fee_per_gas,
178-
max_fee_per_gas,
179-
gas_limit,
180-
kind,
181-
value: TransactionBuilder::value(&tx_request).unwrap_or_default(),
182-
input: TransactionBuilder::input(&tx_request).cloned().unwrap_or_default(),
183-
access_list: TransactionBuilder::access_list(&tx_request)
184-
.cloned()
185-
.unwrap_or_default(),
186-
primary_seed,
187-
})?;
177+
let payload = sign_quantum_transaction_request(tx_request, primary_seed)?;
188178

189179
let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
190180
let cast = CastTxSender::new(&provider);
@@ -418,6 +408,41 @@ impl SendTxArgs {
418408
}
419409
}
420410

411+
fn validate_quantum_sender(cli_from: Option<Address>, quantum_sender: Address) -> Result<()> {
412+
if let Some(from) = cli_from
413+
&& from != quantum_sender
414+
{
415+
eyre::bail!(
416+
"--from must match --quantum.sender when using the Quantum adapter path"
417+
)
418+
}
419+
420+
Ok(())
421+
}
422+
423+
fn quantum_send_requests_bootstrap(to: Option<&NameOrAddress>, input: Option<&str>) -> bool {
424+
quantum_destination_is_keyvault(to) && quantum_input_is_bootstrap(input)
425+
}
426+
427+
fn quantum_destination_is_keyvault(to: Option<&NameOrAddress>) -> bool {
428+
match to {
429+
Some(NameOrAddress::Address(addr)) => *addr == QUANTUM_KEYVAULT_ADDRESS,
430+
Some(NameOrAddress::Name(name)) => {
431+
Address::from_str(name).ok() == Some(QUANTUM_KEYVAULT_ADDRESS)
432+
}
433+
None => false,
434+
}
435+
}
436+
437+
fn quantum_input_is_bootstrap(input: Option<&str>) -> bool {
438+
let Some(input) = input else { return false };
439+
input.starts_with("bootstrapKey(")
440+
|| input
441+
.trim()
442+
.trim_start_matches("0x")
443+
.starts_with(&hex::encode(QUANTUM_BOOTSTRAP_SELECTOR))
444+
}
445+
421446
pub(crate) async fn cast_send<N: Network, P: Provider<N>>(
422447
provider: P,
423448
tx: N::TransactionRequest,

0 commit comments

Comments
 (0)