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