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