numbat_wasm_debug/
blockchain_mock.rs

1use super::mock_error::BlockchainMockError;
2use crate::{
3    contract_map::*, display_util::*, dcdt_transfer_event_log, tx_context::*, SendBalance, TxInput,
4    TxLog, TxOutput, TxPanic,
5};
6use alloc::{boxed::Box, vec::Vec};
7use numbat_wasm::types::Address;
8use num_bigint::BigUint;
9use num_traits::Zero;
10use std::{collections::HashMap, fmt, fmt::Write};
11
12const NUMBAT_REWARD_KEY: &[u8] = b"NUMBATreward";
13const SC_ADDRESS_NUM_LEADING_ZEROS: u8 = 8;
14
15pub type AccountStorage = HashMap<Vec<u8>, Vec<u8>>;
16pub type AccountDcdt = HashMap<Vec<u8>, BigUint>;
17
18pub struct AccountData {
19    pub address: Address,
20    pub nonce: u64,
21    pub balance: BigUint,
22    pub storage: AccountStorage,
23    pub dcdt: AccountDcdt,
24    pub username: Vec<u8>,
25    pub contract_path: Option<Vec<u8>>,
26    pub contract_owner: Option<Address>,
27}
28
29impl fmt::Display for AccountData {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        let mut dcdt_buf = String::new();
32        let mut dcdt_keys: Vec<Vec<u8>> =
33            self.dcdt.clone().iter().map(|(k, _)| k.clone()).collect();
34        dcdt_keys.sort();
35
36        for key in &dcdt_keys {
37            let value = self.dcdt.get(key).unwrap();
38            write!(
39                &mut dcdt_buf,
40                "\n\t\t\t\t{} -> 0x{}",
41                key_hex(key.as_slice()),
42                hex::encode(value.to_bytes_be())
43            )
44            .unwrap();
45        }
46
47        let mut storage_buf = String::new();
48        let mut storage_keys: Vec<Vec<u8>> = self.storage.iter().map(|(k, _)| k.clone()).collect();
49        storage_keys.sort();
50
51        for key in &storage_keys {
52            let value = self.storage.get(key).unwrap();
53            write!(
54                &mut storage_buf,
55                "\n\t\t\t{} -> 0x{}",
56                key_hex(key.as_slice()),
57                hex::encode(value.as_slice())
58            )
59            .unwrap();
60        }
61
62        write!(
63            f,
64            "AccountData {{
65		nonce: {},
66		balance: {},
67		dcdt: [{} ],
68		username: {},
69		storage: [{} ]
70	}}",
71            self.nonce,
72            self.balance,
73            dcdt_buf,
74            String::from_utf8(self.username.clone()).unwrap(),
75            storage_buf
76        )
77    }
78}
79
80#[derive(Clone, Debug)]
81pub struct BlockInfo {
82    pub block_timestamp: u64,
83    pub block_nonce: u64,
84    pub block_round: u64,
85    pub block_epoch: u64,
86    pub block_random_seed: Box<[u8; 48]>,
87}
88
89impl BlockInfo {
90    pub fn new() -> Self {
91        BlockInfo {
92            block_timestamp: 0,
93            block_nonce: 0,
94            block_round: 0,
95            block_epoch: 0,
96            block_random_seed: Box::from([0u8; 48]),
97        }
98    }
99}
100
101impl Default for BlockInfo {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107pub struct BlockchainMock {
108    pub accounts: HashMap<Address, AccountData>,
109    pub new_addresses: HashMap<(Address, u64), Address>,
110    pub previous_block_info: BlockInfo,
111    pub current_block_info: BlockInfo,
112}
113
114impl BlockchainMock {
115    pub fn new() -> Self {
116        BlockchainMock {
117            accounts: HashMap::new(),
118            new_addresses: HashMap::new(),
119            previous_block_info: BlockInfo::new(),
120            current_block_info: BlockInfo::new(),
121        }
122    }
123}
124
125impl Default for BlockchainMock {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131impl BlockchainMock {
132    pub fn add_account(&mut self, acct: AccountData) {
133        self.accounts.insert(acct.address.clone(), acct);
134    }
135
136    pub fn validate_and_add_account(&mut self, acct: AccountData) {
137        self.validate_account(&acct);
138        self.add_account(acct);
139    }
140
141    pub fn print_accounts(&self) {
142        let mut accounts_buf = String::new();
143        for (address, account) in &self.accounts {
144            write!(
145                &mut accounts_buf,
146                "\n\t{} -> {}",
147                address_hex(address),
148                account
149            )
150            .unwrap();
151        }
152        println!("Accounts: {}", &accounts_buf);
153    }
154
155    pub fn put_new_address(
156        &mut self,
157        creator_address: Address,
158        creator_nonce: u64,
159        new_address: Address,
160    ) {
161        self.new_addresses
162            .insert((creator_address, creator_nonce), new_address);
163    }
164
165    fn get_new_address(&self, creator_address: Address, creator_nonce: u64) -> Option<Address> {
166        self.new_addresses
167            .get(&(creator_address, creator_nonce))
168            .cloned()
169    }
170
171    pub fn validate_account(&self, account: &AccountData) {
172        let is_sc = self.is_smart_contract_address(&account.address);
173        let has_code = self.check_account_has_code(account);
174
175        assert!(
176            !is_sc || has_code,
177            "Account has a smart contract address but no code"
178        );
179
180        assert!(
181            is_sc || !has_code,
182            "Account has no smart contract address but has code"
183        );
184    }
185
186    pub fn is_smart_contract_address(&self, address: &Address) -> bool {
187        address
188            .as_bytes()
189            .iter()
190            .take(SC_ADDRESS_NUM_LEADING_ZEROS.into())
191            .all(|item| item == &0u8)
192    }
193
194    pub fn check_account_has_code(&self, account: &AccountData) -> bool {
195        !account
196            .contract_path
197            .as_ref()
198            .unwrap_or(&Vec::<u8>::new())
199            .is_empty()
200    }
201
202    pub fn subtract_tx_payment(
203        &mut self,
204        address: &Address,
205        call_value: &BigUint,
206    ) -> Result<(), BlockchainMockError> {
207        let sender_account = self
208            .accounts
209            .get_mut(address)
210            .unwrap_or_else(|| panic!("Sender account not found"));
211        if &sender_account.balance < call_value {
212            return Err("failed transfer (insufficient funds)".into());
213        }
214        sender_account.balance -= call_value;
215        Ok(())
216    }
217
218    pub fn subtract_tx_gas(&mut self, address: &Address, gas_limit: u64, gas_price: u64) {
219        let sender_account = self
220            .accounts
221            .get_mut(address)
222            .unwrap_or_else(|| panic!("Sender account not found"));
223        let gas_cost = BigUint::from(gas_limit) * BigUint::from(gas_price);
224        assert!(
225            sender_account.balance >= gas_cost,
226            "Not enough balance to pay gas upfront"
227        );
228        sender_account.balance -= &gas_cost;
229    }
230
231    pub fn increase_balance(&mut self, address: &Address, amount: &BigUint) {
232        let account = self
233            .accounts
234            .get_mut(address)
235            .unwrap_or_else(|| panic!("Receiver account not found"));
236        account.balance += amount;
237    }
238
239    pub fn send_balance(
240        &mut self,
241        contract_address: &Address,
242        send_balance_list: &[SendBalance],
243        result_logs: &mut Vec<TxLog>,
244    ) -> Result<(), BlockchainMockError> {
245        for send_balance in send_balance_list {
246            if send_balance.token_identifier.is_empty() {
247                self.subtract_tx_payment(contract_address, &send_balance.amount)?;
248                self.increase_balance(&send_balance.recipient, &send_balance.amount);
249            } else {
250                let dcdt_token_identifier = send_balance.token_identifier.as_slice();
251                self.substract_dcdt_balance(
252                    contract_address,
253                    dcdt_token_identifier,
254                    &send_balance.amount,
255                );
256                self.increase_dcdt_balance(
257                    &send_balance.recipient,
258                    dcdt_token_identifier,
259                    &send_balance.amount,
260                );
261
262                let log = dcdt_transfer_event_log(
263                    contract_address.clone(),
264                    send_balance.recipient.clone(),
265                    dcdt_token_identifier.to_vec(),
266                    &send_balance.amount,
267                );
268                result_logs.insert(0, log); // TODO: it's a hack, should be inserted during execution, not here
269            }
270        }
271        Ok(())
272    }
273
274    pub fn substract_dcdt_balance(
275        &mut self,
276        address: &Address,
277        dcdt_token_identifier: &[u8],
278        value: &BigUint,
279    ) {
280        let sender_account = self
281            .accounts
282            .get_mut(address)
283            .unwrap_or_else(|| panic!("Sender account {} not found", address_hex(address)));
284
285        let dcdt_balance = sender_account
286            .dcdt
287            .get_mut(dcdt_token_identifier)
288            .unwrap_or_else(|| {
289                panic!(
290                    "Account {} has no dcdt tokens with name {}",
291                    address_hex(address),
292                    String::from_utf8(dcdt_token_identifier.to_vec()).unwrap()
293                )
294            });
295
296        assert!(
297            *dcdt_balance >= *value,
298            "Not enough dcdt balance, have {}, need at least {}",
299            dcdt_balance,
300            value
301        );
302
303        *dcdt_balance -= value;
304    }
305
306    pub fn increase_dcdt_balance(
307        &mut self,
308        address: &Address,
309        dcdt_token_identifier: &[u8],
310        value: &BigUint,
311    ) {
312        let account = self
313            .accounts
314            .get_mut(address)
315            .unwrap_or_else(|| panic!("Receiver account not found"));
316
317        if account.dcdt.contains_key(dcdt_token_identifier) {
318            let dcdt_balance = account.dcdt.get_mut(dcdt_token_identifier).unwrap();
319            *dcdt_balance += value;
320        } else {
321            account
322                .dcdt
323                .insert(dcdt_token_identifier.to_vec(), value.clone());
324        }
325    }
326
327    pub fn increase_nonce(&mut self, address: &Address) {
328        let account = self.accounts.get_mut(address).unwrap_or_else(|| {
329            panic!(
330                "Account not found: {}",
331                &std::str::from_utf8(address.as_ref()).unwrap()
332            )
333        });
334        account.nonce += 1;
335    }
336
337    pub fn create_account_after_deploy(
338        &mut self,
339        tx_input: &TxInput,
340        new_storage: HashMap<Vec<u8>, Vec<u8>>,
341        contract_path: Vec<u8>,
342    ) -> Address {
343        let sender = self
344            .accounts
345            .get(&tx_input.from)
346            .unwrap_or_else(|| panic!("Unknown deployer"));
347        let sender_nonce_before_tx = sender.nonce - 1;
348        let new_address = self
349            .get_new_address(tx_input.from.clone(), sender_nonce_before_tx)
350            .unwrap_or_else(|| {
351                panic!("Missing new address. Only explicit new deploy addresses supported")
352            });
353        let mut dcdt = HashMap::<Vec<u8>, BigUint>::new();
354        if !tx_input.dcdt_token_identifier.is_empty() {
355            dcdt.insert(
356                tx_input.dcdt_token_identifier.clone(),
357                tx_input.dcdt_value.clone(),
358            );
359        }
360
361        let old_value = self.accounts.insert(
362            new_address.clone(),
363            AccountData {
364                address: new_address.clone(),
365                nonce: 0,
366                balance: tx_input.call_value.clone(),
367                storage: new_storage,
368                dcdt,
369                username: Vec::new(),
370                contract_path: Some(contract_path),
371                contract_owner: Some(tx_input.from.clone()),
372            },
373        );
374        assert!(
375            old_value.is_none(),
376            "Account already exists at deploy address."
377        );
378
379        new_address
380    }
381
382    pub fn increase_validator_reward(&mut self, address: &Address, amount: &BigUint) {
383        let account = self.accounts.get_mut(address).unwrap_or_else(|| {
384            panic!(
385                "Account not found: {}",
386                &std::str::from_utf8(address.as_ref()).unwrap()
387            )
388        });
389        account.balance += amount;
390        let mut storage_v_rew =
391            if let Some(old_storage_value) = account.storage.get(NUMBAT_REWARD_KEY) {
392                BigUint::from_bytes_be(old_storage_value)
393            } else {
394                BigUint::zero()
395            };
396        storage_v_rew += amount;
397        account
398            .storage
399            .insert(NUMBAT_REWARD_KEY.to_vec(), storage_v_rew.to_bytes_be());
400    }
401
402    pub fn try_set_username(&mut self, address: &Address, username: &[u8]) -> bool {
403        let account = self.accounts.get_mut(address).unwrap_or_else(|| {
404            panic!(
405                "Account not found: {}",
406                &std::str::from_utf8(address.as_ref()).unwrap()
407            )
408        });
409        if account.username.is_empty() {
410            account.username = username.to_vec();
411            true
412        } else {
413            false
414        }
415    }
416}
417
418pub fn execute_tx(
419    tx_context: TxContext,
420    contract_identifier: &[u8],
421    contract_map: &ContractMap<TxContext>,
422) -> TxOutput {
423    let func_name = tx_context.tx_input_box.func_name.clone();
424    let contract_inst = contract_map.new_contract_instance(contract_identifier, tx_context);
425    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
426        let call_successful = contract_inst.call(func_name.as_slice());
427        if !call_successful {
428            std::panic::panic_any(TxPanic {
429                status: 1,
430                message: b"invalid function (not found)".to_vec(),
431            });
432        }
433        let context = contract_inst.into_api();
434        context.into_output()
435    }));
436    match result {
437        Ok(tx_output) => tx_output,
438        Err(panic_any) => panic_result(panic_any),
439    }
440}
441
442fn panic_result(panic_any: Box<dyn std::any::Any + std::marker::Send>) -> TxOutput {
443    if panic_any.downcast_ref::<TxOutput>().is_some() {
444        // async calls panic with the tx output directly
445        // it is not a failure, simply a way to kill the execution
446        return *panic_any.downcast::<TxOutput>().unwrap();
447    }
448
449    if let Some(panic_obj) = panic_any.downcast_ref::<TxPanic>() {
450        return TxOutput::from_panic_obj(panic_obj);
451    }
452
453    if let Some(panic_string) = panic_any.downcast_ref::<String>() {
454        return TxOutput::from_panic_string(panic_string.as_str());
455    }
456
457    TxOutput::from_panic_string("unknown panic")
458}
459
460/// Some data to get copied for the tx.
461/// Would be nice maybe at some point to have a reference to the full blockchain mock in the tx context,
462/// but for now, copying some data is enough.
463#[derive(Clone, Debug)]
464pub struct BlockchainTxInfo {
465    pub previous_block_info: BlockInfo,
466    pub current_block_info: BlockInfo,
467    pub contract_balance: BigUint,
468    pub contract_dcdt: HashMap<Vec<u8>, BigUint>,
469    pub contract_owner: Option<Address>,
470}
471
472impl BlockchainMock {
473    pub fn create_tx_info(&self, contract_address: &Address) -> BlockchainTxInfo {
474        if let Some(contract) = self.accounts.get(contract_address) {
475            BlockchainTxInfo {
476                previous_block_info: self.previous_block_info.clone(),
477                current_block_info: self.current_block_info.clone(),
478                contract_balance: contract.balance.clone(),
479                contract_dcdt: contract.dcdt.clone(),
480                contract_owner: contract.contract_owner.clone(),
481            }
482        } else {
483            BlockchainTxInfo {
484                previous_block_info: self.previous_block_info.clone(),
485                current_block_info: self.current_block_info.clone(),
486                contract_balance: 0u32.into(),
487                contract_dcdt: HashMap::new(),
488                contract_owner: None,
489            }
490        }
491    }
492}