Skip to main content

cast/cmd/tip20/
mine.rs

1use crate::{
2    cmd::{
3        erc20::build_provider_with_signer,
4        send::{cast_send, cast_send_with_tempo_wallet},
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 sender = match &tempo_access_key {
93        Some(wallet) => wallet.account(),
94        None => signer
95            .as_ref()
96            .ok_or_else(|| {
97                eyre::eyre!(
98                    "--register requires a signer or Tempo keychain identity (for example --private-key or --from)"
99                )
100            })?
101            .address(),
102    };
103
104    if sender != master {
105        eyre::bail!(
106            "registration sender mismatch: mined salt is for {master}, but the configured signer would register as {sender}"
107        );
108    }
109
110    let mut tx = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider)
111        .registerVirtualMaster(salt)
112        .into_transaction_request();
113    let expires_at = tx_opts.tempo.resolve_expires();
114    tempo::print_expires(expires_at)?;
115    tx_opts.apply::<TempoNetwork>(&mut tx, chain.is_legacy());
116
117    sh_status!("Submitting registerVirtualMaster({salt}) on Tempo...")?;
118
119    if let Some(ref access_key) = tempo_access_key {
120        let prepared = tempo::fill_access_key_transaction(
121            &provider,
122            &mut tx,
123            access_key,
124            chain,
125            config.eip1559_fee_estimate,
126        )
127        .await?;
128        cast_send_with_tempo_wallet(
129            &provider,
130            tx,
131            &prepared,
132            Some(chain),
133            None,
134            send_tx.cast_async,
135            send_tx.sync,
136            send_tx.confirmations,
137            timeout,
138            !config.eth_rpc_curl,
139        )
140        .await?;
141    } else if let Some(signer) = signer {
142        let provider = build_provider_with_signer::<TempoNetwork>(&send_tx, signer)?;
143        // Fill only the fees; the provider fills nonce and gas limit.
144        fill_transaction_gas_fees(
145            &provider,
146            &mut tx,
147            chain.is_legacy(),
148            false,
149            config.eip1559_fee_estimate,
150        )
151        .await?;
152        cast_send(
153            provider,
154            tx,
155            Some(chain),
156            None,
157            send_tx.cast_async,
158            send_tx.sync,
159            send_tx.confirmations,
160            timeout,
161            !config.eth_rpc_curl,
162        )
163        .await?;
164    } else {
165        eyre::bail!(
166            "--register requires a signer or Tempo keychain identity (for example --private-key or --from)"
167        );
168    }
169
170    Ok(())
171}
172
173pub(crate) fn mine(
174    master: Address,
175    salt: B256,
176    n_threads: usize,
177    pow_bytes: usize,
178) -> Result<Output> {
179    let mut packed = [0u8; 52];
180    packed[..20].copy_from_slice(master.as_slice());
181
182    crate::cmd::miner::mine_salt(salt, n_threads, move |salt| {
183        packed[20..].copy_from_slice(salt.as_slice());
184        let registration_hash = keccak256(packed);
185
186        has_pow(&registration_hash, pow_bytes).then(|| {
187            let master_id = MasterId::from_slice(&registration_hash[4..8]);
188            let zero_tag_virtual_address = Address::new_virtual(master_id, UserTag::ZERO);
189            Output { salt, registration_hash, master_id, zero_tag_virtual_address }
190        })
191    })
192    .ok_or_else(|| eyre::eyre!("virtual master mining failed: all threads panicked"))
193}
194
195pub(crate) fn derive(master: Address, salt: B256) -> Output {
196    let registration_hash = registration_hash(master, salt);
197    let master_id = MasterId::from_slice(&registration_hash[4..8]);
198    let zero_tag_virtual_address = Address::new_virtual(master_id, UserTag::ZERO);
199
200    Output { salt, registration_hash, master_id, zero_tag_virtual_address }
201}
202
203pub(crate) fn registration_hash(master: Address, salt: B256) -> B256 {
204    let mut packed = [0u8; 52];
205    packed[..20].copy_from_slice(master.as_slice());
206    packed[20..].copy_from_slice(salt.as_slice());
207    keccak256(packed)
208}
209
210pub(crate) fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool {
211    registration_hash[..pow_bytes].iter().all(|byte| *byte == 0)
212}
213
214fn print_output(output: &Output, elapsed: Option<Duration>) -> Result<()> {
215    let header = if let Some(elapsed) = elapsed {
216        format!("Found salt in {elapsed:?}\n")
217    } else {
218        String::new()
219    };
220
221    sh_println!(
222        r#"{header}Salt:              {}
223Registration hash: {}
224Master ID:         {}
225Zero-tag address:  {}"#,
226        output.salt,
227        output.registration_hash,
228        output.master_id,
229        output.zero_tag_virtual_address,
230    )?;
231    Ok(())
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use alloy_primitives::{address, b256};
238
239    #[test]
240    fn derives_master_id_and_zero_tag_address() {
241        let master = address!("0x1234567890123456789012345678901234567890");
242        let salt = b256!("0x0000000000000000000000000000000000000000000000000000000000000001");
243        let output = derive(master, salt);
244
245        assert_eq!(
246            output.registration_hash,
247            b256!("0x661db5481211842e0330ea3e4cf0b4e7e5abd2314161ce16e9a99e7460480f21"),
248        );
249        assert_eq!(output.master_id, MasterId::from([0x12, 0x11, 0x84, 0x2e]));
250        assert_eq!(
251            output.zero_tag_virtual_address,
252            address!("0x1211842efdfdfdfdfdfdfdfdfdfd000000000000"),
253        );
254        assert_eq!(output.master_id, MasterId::from_slice(&output.registration_hash[4..8]));
255        assert_eq!(
256            output.zero_tag_virtual_address,
257            Address::new_virtual(output.master_id, UserTag::ZERO),
258        );
259    }
260
261    #[test]
262    fn mines_pow_with_reduced_difficulty() -> Result<()> {
263        let master = address!("0x1234567890123456789012345678901234567890");
264        let output = mine(master, B256::ZERO, 1, 1)?;
265
266        assert_eq!(
267            output.salt,
268            b256!("0x000000000000000000000000000000000000000000000000f301000000000000"),
269        );
270        assert_eq!(output.registration_hash[0], 0);
271        assert_eq!(output.master_id, MasterId::from_slice(&output.registration_hash[4..8]));
272        assert_eq!(
273            output.zero_tag_virtual_address,
274            Address::new_virtual(output.master_id, UserTag::ZERO),
275        );
276        Ok(())
277    }
278
279    #[test]
280    fn has_pow_checks_leading_zero_bytes() {
281        let mut hash = B256::ZERO;
282        assert!(has_pow(&hash, 4));
283        assert!(has_pow(&hash, 0));
284
285        hash[3] = 1;
286        assert!(!has_pow(&hash, 4));
287        assert!(has_pow(&hash, 3));
288        assert!(has_pow(&hash, 0));
289    }
290
291    #[test]
292    fn rejects_invalid_master_addresses() {
293        assert!(!Address::ZERO.is_valid_master());
294        assert!(!address!("0x00000000fdfdfdfdfdfdfdfdfdfd000000000001").is_valid_master());
295        assert!(!address!("0x20c0000000000000000000000000000000000001").is_valid_master());
296    }
297}