Skip to main content

cast/cmd/tip20/
mine.rs

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
20/// Number of leading zero bytes a TIP-1022 registration hash must have.
21pub(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(&registration_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
38/// Command-specific wording for [`register_virtual_master`].
39pub(crate) struct RegisterMessages {
40    pub(crate) no_signer: &'static str,
41    /// Prefix of the sender-mismatch error, completed with the salt owner and actual sender.
42    pub(crate) mismatch: &'static str,
43    /// Suffix of the `Submitting registerVirtualMaster(..)` status line.
44    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
69/// Validates `master` and returns the registration to submit: `salt` after checking its proof of
70/// work, or a freshly mined salt together with the time it took to find it.
71pub(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
118/// Submits `registerVirtualMaster(salt)` from `master` and returns the transaction hash.
119///
120/// With `quiet`, neither the transaction hash nor the receipt is printed, but the receipt is still
121/// awaited unless `--async` was passed.
122pub(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        // The wallet provider fills the nonce and gas limit; only the fees are filled here.
173        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(&registration_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}