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