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