1use crate::{
2 cmd::send::cast_send_raw,
3 tempo,
4 tx::{CastTxSender, SendTxOpts, TxParams, apply_poll_interval, fill_transaction_gas_fees},
5};
6use alloy_network::EthereumWallet;
7use alloy_primitives::{Address, B256, keccak256};
8use alloy_signer::Signer;
9use eyre::Result;
10use foundry_cli::utils::get_chain;
11use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder};
12use foundry_evm::hardfork::TempoHardfork;
13use rand::{RngCore, SeedableRng, rngs::StdRng};
14use std::time::{Duration, Instant};
15use tempo_alloy::{
16 TempoNetwork,
17 contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry},
18};
19use tempo_primitives::{MasterId, TempoAddressExt, UserTag};
20
21pub(crate) const POW_BYTES: usize = 4;
23
24pub(crate) struct Output {
25 pub(crate) salt: B256,
26 pub(crate) registration_hash: B256,
27 pub(crate) master_id: MasterId,
28 pub(crate) zero_tag_virtual_address: Address,
29}
30
31impl Output {
32 fn new(salt: B256, registration_hash: B256) -> Self {
33 let master_id = MasterId::from_slice(®istration_hash[4..8]);
34 let zero_tag_virtual_address = Address::new_virtual(master_id, UserTag::ZERO);
35 Self { salt, registration_hash, master_id, zero_tag_virtual_address }
36 }
37}
38
39pub(crate) struct RegisterMessages {
41 pub(crate) no_signer: &'static str,
42 pub(crate) mismatch: &'static str,
44 pub(crate) submitting: &'static str,
46}
47
48pub(super) fn run(
49 master: Address,
50 salt: Option<B256>,
51 threads: Option<usize>,
52 seed: Option<B256>,
53 no_random: bool,
54) -> Result<Output> {
55 let (output, elapsed) = find_salt(master, salt, threads, seed, no_random, false, "master")?;
56 let header = elapsed.map(|elapsed| format!("Found salt in {elapsed:?}\n")).unwrap_or_default();
57 sh_println!(
58 r#"{header}Salt: {}
59Registration hash: {}
60Master ID: {}
61Zero-tag address: {}"#,
62 output.salt,
63 output.registration_hash,
64 output.master_id,
65 output.zero_tag_virtual_address,
66 )?;
67 Ok(output)
68}
69
70pub(crate) fn find_salt(
73 master: Address,
74 salt: Option<B256>,
75 threads: Option<usize>,
76 seed: Option<B256>,
77 no_random: bool,
78 quiet: bool,
79 label: &str,
80) -> Result<(Output, Option<Duration>)> {
81 if !master.is_valid_master() {
82 eyre::bail!(
83 "invalid {label} address {master}; see https://docs.tempo.xyz/protocol/tips/tip-1022"
84 );
85 }
86
87 if let Some(salt) = salt {
88 let output = derive(master, salt);
89 if !has_pow(&output.registration_hash, POW_BYTES) {
90 eyre::bail!(
91 "provided salt does not satisfy TIP-1022 proof of work: {}",
92 output.registration_hash
93 );
94 }
95 return Ok((output, None));
96 }
97
98 let n_threads = match threads {
99 Some(n) if n > 0 => n,
100 _ => std::thread::available_parallelism().map_or(1, |n| n.get()),
101 };
102 let mut salt = B256::ZERO;
103 if !no_random {
104 let mut rng = match seed {
105 Some(seed) => StdRng::from_seed(seed.0),
106 None => StdRng::from_os_rng(),
107 };
108 rng.fill_bytes(&mut salt[..]);
109 }
110
111 if !quiet {
112 sh_status!("Mining TIP-1022 salt for {master} with {n_threads} threads...")?;
113 }
114 let timer = Instant::now();
115 let output = mine(master, salt, n_threads, POW_BYTES)?;
116 Ok((output, Some(timer.elapsed())))
117}
118
119pub(crate) async fn register_virtual_master(
124 master: Address,
125 salt: B256,
126 send_tx: SendTxOpts,
127 mut tx_opts: TxParams,
128 quiet: bool,
129 msgs: &RegisterMessages,
130) -> Result<B256> {
131 let (config, provider) = tempo::tempo_provider(&send_tx.eth.rpc)?;
132 tempo::ensure_tempo_precompile_active(
133 &provider,
134 TempoHardfork::T3,
135 ADDRESS_REGISTRY_ADDRESS,
136 "virtual master registration requires a Tempo T3-capable AddressRegistry RPC",
137 )
138 .await?;
139 apply_poll_interval(&provider, send_tx.poll_interval);
140 let chain = get_chain(config.chain, &provider).await?;
141 tempo::ensure_session_not_browser(&tx_opts.tempo, send_tx.browser.browser)?;
142 let (signer, access_key) =
143 tempo::resolve_session_or_wallet_signer(&tx_opts.tempo, &send_tx.eth.wallet, chain.id())
144 .await?;
145 let sender = match (&access_key, &signer) {
146 (Some(wallet), _) => wallet.account(),
147 (None, Some(signer)) => signer.address(),
148 (None, None) => eyre::bail!("{}", msgs.no_signer),
149 };
150 if sender != master {
151 eyre::bail!(
152 "{} {master}, but the configured signer would register as {sender}",
153 msgs.mismatch
154 );
155 }
156
157 let mut tx = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider)
158 .registerVirtualMaster(salt)
159 .into_transaction_request();
160 tempo::print_expires(tx_opts.tempo.resolve_expires())?;
161 tx_opts.apply::<TempoNetwork>(&mut tx, chain.is_legacy());
162 sh_status!("Submitting registerVirtualMaster({salt}){}", msgs.submitting)?;
163
164 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
165 let fee_provider = (!config.eth_rpc_curl).then_some(&provider);
166 let (tx_hash, receipt) = if let Some(access_key) = &access_key {
167 let prepared = tempo::fill_access_key_transaction(
168 &provider,
169 &mut tx,
170 access_key,
171 chain,
172 config.eip1559_fee_estimate,
173 )
174 .await?;
175 tempo::resolve_and_print_fee_token(fee_provider, &mut tx, Some(sender)).await?;
176 let raw_tx = tx.sign_with_tempo_wallet(&prepared).await?;
177 cast_send_raw(&provider, &raw_tx, send_tx.sync).await?
178 } else {
179 let wallet = EthereumWallet::from(signer.expect("sender was derived from the signer"));
181 let signer_provider =
182 ProviderBuilder::<TempoNetwork>::from_config(&config)?.build_with_wallet(wallet)?;
183 fill_transaction_gas_fees(
184 &signer_provider,
185 &mut tx,
186 chain.is_legacy(),
187 false,
188 config.eip1559_fee_estimate,
189 )
190 .await?;
191 tempo::resolve_and_print_fee_token(fee_provider, &mut tx, Some(sender)).await?;
192 let cast = CastTxSender::new(&signer_provider);
193 if send_tx.sync {
194 let (tx_hash, receipt) = cast.send_sync(tx).await?;
195 (tx_hash, Some(receipt))
196 } else {
197 (*cast.send(tx).await?.inner().tx_hash(), None)
198 }
199 };
200
201 let cast = CastTxSender::new(&provider);
202 match receipt {
203 Some(receipt) if !quiet => sh_println!("{receipt}")?,
204 Some(_) => {}
205 None if quiet => {
206 if !send_tx.cast_async {
207 let hash = format!("{tx_hash:#x}");
208 cast.receipt(hash, None, send_tx.confirmations, Some(timeout), false).await?;
209 }
210 }
211 None => {
212 cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout)
213 .await?
214 }
215 }
216 Ok(tx_hash)
217}
218
219pub(crate) fn mine(
220 master: Address,
221 salt: B256,
222 n_threads: usize,
223 pow_bytes: usize,
224) -> Result<Output> {
225 let mut packed = [0u8; 52];
226 packed[..20].copy_from_slice(master.as_slice());
227
228 crate::cmd::miner::mine_salt(salt, n_threads, move |salt| {
229 packed[20..].copy_from_slice(salt.as_slice());
230 let registration_hash = keccak256(packed);
231 has_pow(®istration_hash, pow_bytes).then(|| Output::new(salt, registration_hash))
232 })
233 .ok_or_else(|| eyre::eyre!("virtual master mining failed: all threads panicked"))
234}
235
236pub(crate) fn derive(master: Address, salt: B256) -> Output {
237 let mut packed = [0u8; 52];
238 packed[..20].copy_from_slice(master.as_slice());
239 packed[20..].copy_from_slice(salt.as_slice());
240 Output::new(salt, keccak256(packed))
241}
242
243pub(crate) fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool {
244 registration_hash[..pow_bytes].iter().all(|byte| *byte == 0)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use alloy_primitives::{address, b256};
251
252 #[test]
253 fn derives_master_id_and_zero_tag_address() {
254 let master = address!("0x1234567890123456789012345678901234567890");
255 let salt = B256::with_last_byte(1);
256 let output = derive(master, salt);
257
258 assert_eq!(
259 output.registration_hash,
260 b256!("0x661db5481211842e0330ea3e4cf0b4e7e5abd2314161ce16e9a99e7460480f21"),
261 );
262 assert_eq!(output.master_id, MasterId::from([0x12, 0x11, 0x84, 0x2e]));
263 assert_eq!(
264 output.zero_tag_virtual_address,
265 address!("0x1211842efdfdfdfdfdfdfdfdfdfd000000000000"),
266 );
267 }
268
269 #[test]
270 fn mines_pow_with_reduced_difficulty() -> Result<()> {
271 let master = address!("0x1234567890123456789012345678901234567890");
272 let output = mine(master, B256::ZERO, 1, 1)?;
273
274 assert_eq!(
275 output.salt,
276 b256!("0x000000000000000000000000000000000000000000000000f301000000000000"),
277 );
278 assert_eq!(output.registration_hash[0], 0);
279 assert_eq!(output.master_id, MasterId::from_slice(&output.registration_hash[4..8]));
280 Ok(())
281 }
282
283 #[test]
284 fn has_pow_checks_leading_zero_bytes() {
285 let mut hash = B256::ZERO;
286 assert!(has_pow(&hash, 4));
287 assert!(has_pow(&hash, 0));
288
289 hash[3] = 1;
290 assert!(!has_pow(&hash, 4));
291 assert!(has_pow(&hash, 3));
292 assert!(has_pow(&hash, 0));
293 }
294}