concordium_smart_contract_engine/
utils.rs

1//! Various utilities for testing and extraction of schemas and build
2//! information.
3
4use crate::{
5    constants::MAX_LOG_SIZE,
6    v1::{host, trie, EmittedDebugStatement, InstanceState},
7    ExecResult,
8};
9use anyhow::{anyhow, bail, ensure, Context};
10pub use concordium_contracts_common::WasmVersion;
11use concordium_contracts_common::{
12    self as concordium_std, from_bytes, hashes, schema, ContractAddress, Cursor, Deserial, Seek,
13    SeekFrom, Serial,
14};
15use concordium_std::{AccountAddress, Address, HashMap, OwnedEntrypointName, Read, Write};
16use concordium_wasm::{
17    artifact::{Artifact, ArtifactNamedImport, RunnableCode, TryFromImport},
18    machine::{self, NoInterrupt, Value},
19    parse::{parse_custom, parse_skeleton, Skeleton},
20    types::{ExportDescription, Module, Name},
21    utils,
22    validate::{self, ValidationConfig},
23};
24use rand::RngCore;
25use sha2::Digest;
26use std::{collections::BTreeMap, default::Default};
27use thiserror::Error;
28
29/// A host which traps for any function call.
30pub struct TrapHost;
31
32impl<I> machine::Host<I> for TrapHost {
33    type Interrupt = NoInterrupt;
34
35    fn tick_initial_memory(&mut self, _num_pages: u32) -> machine::RunResult<()> { Ok(()) }
36
37    fn call(
38        &mut self,
39        _f: &I,
40        _memory: &mut [u8],
41        _stack: &mut machine::RuntimeStack,
42    ) -> machine::RunResult<Option<NoInterrupt>> {
43        bail!("TrapHost traps on all host calls.")
44    }
45
46    fn tick_energy(&mut self, _energy: u64) -> machine::RunResult<()> { Ok(()) }
47
48    fn track_call(&mut self) -> machine::RunResult<()> { Ok(()) }
49
50    fn track_return(&mut self) {}
51}
52
53/// A host which traps for any function call apart from `report_error` which it
54/// prints to standard out and `get_random` that calls a random number
55/// generator.
56pub struct TestHost<'a, R, BackingStore> {
57    /// A RNG for randomised testing.
58    rng:                Option<R>,
59    /// A flag set to `true` if the RNG was used.
60    pub rng_used:       bool,
61    /// Debug statements in the order they were emitted.
62    pub debug_events:   Vec<EmittedDebugStatement>,
63    /// In-memory instance state used for state-related host calls.
64    state:              InstanceState<'a, BackingStore>,
65    /// Time in milliseconds at the beginning of the smart contract's block.
66    slot_time:          Option<u64>,
67    /// The address of this smart contract.
68    address:            Option<ContractAddress>,
69    /// The current balance of this smart contract.
70    balance:            Option<u64>,
71    /// The parameters of the smart contract.
72    parameters:         HashMap<u32, Vec<u8>>,
73    /// Events logged by the contract.
74    events:             Vec<Vec<u8>>,
75    /// Account address of the sender.
76    init_origin:        Option<AccountAddress>,
77    /// Invoker of the top-level transaction.
78    receive_invoker:    Option<AccountAddress>,
79    /// Immediate sender of the message.
80    receive_sender:     Option<Address>,
81    /// Owner of the contract.
82    receive_owner:      Option<AccountAddress>,
83    /// The receive entrypoint name.
84    receive_entrypoint: Option<OwnedEntrypointName>,
85}
86
87impl<'a, R: RngCore, BackingStore> TestHost<'a, R, BackingStore> {
88    /// Create a new `TestHost` instance with the given RNG, set the flag to
89    /// unused, no debug events and use the provided instance state for
90    /// state-related host function calls.
91    pub fn new(rng: R, state: InstanceState<'a, BackingStore>) -> Self {
92        TestHost {
93            rng: Some(rng),
94            rng_used: false,
95            debug_events: Vec::new(),
96            state,
97            slot_time: None,
98            address: None,
99            balance: None,
100            parameters: HashMap::default(),
101            events: Vec::new(),
102            init_origin: None,
103            receive_invoker: None,
104            receive_sender: None,
105            receive_owner: None,
106            receive_entrypoint: None,
107        }
108    }
109}
110
111/// Type providing `ValidateImportExport` implementation which only ensure no
112/// duplicate imports. Any module name and item name and type is
113/// considered valid for both import and export.
114pub struct NoDuplicateImport;
115
116impl validate::ValidateImportExport for NoDuplicateImport {
117    /// Simply ensure that there are no duplicates.
118    #[cfg_attr(not(feature = "fuzz-coverage"), inline(always))]
119    fn validate_import_function(
120        &self,
121        duplicate: bool,
122        _mod_name: &Name,
123        _item_name: &Name,
124        _ty: &concordium_wasm::types::FunctionType,
125    ) -> bool {
126        !duplicate
127    }
128
129    #[cfg_attr(not(feature = "fuzz-coverage"), inline(always))]
130    fn validate_export_function(
131        &self,
132        _item_name: &Name,
133        _ty: &concordium_wasm::types::FunctionType,
134    ) -> bool {
135        true
136    }
137}
138
139#[derive(Debug, Clone)]
140/// An auxiliary datatype used by `report_error` to be able to
141/// retain the structured information in case we want to use it later
142/// to insert proper links to the file, or other formatting.
143pub enum ReportError {
144    /// An error reported by `report_error`
145    Reported {
146        filename: String,
147        line:     u32,
148        column:   u32,
149        msg:      String,
150    },
151    /// Some other source of error. We only have the description, and no
152    /// location.
153    Other {
154        msg: String,
155    },
156}
157
158impl std::fmt::Display for ReportError {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            ReportError::Reported {
162                filename,
163                line,
164                column,
165                msg,
166            } => write!(f, "{}, {}:{}:{}", msg, filename, line, column),
167            ReportError::Other {
168                msg,
169            } => msg.fmt(f),
170        }
171    }
172}
173
174/// Extract debug information from the memory and stack. This is used when
175/// handling `report_error` and `debug_print` functions.
176pub(crate) fn extract_debug(
177    memory: &mut [u8],
178    stack: &mut machine::RuntimeStack,
179) -> anyhow::Result<(String, u32, u32, String)> {
180    let column = unsafe { stack.pop_u32() };
181    let line = unsafe { stack.pop_u32() };
182    let filename_length = unsafe { stack.pop_u32() } as usize;
183    let filename_start = unsafe { stack.pop_u32() } as usize;
184    let msg_length = unsafe { stack.pop_u32() } as usize;
185    let msg_start = unsafe { stack.pop_u32() } as usize;
186    ensure!(filename_start + filename_length <= memory.len(), "Illegal memory access.");
187    ensure!(msg_start + msg_length <= memory.len(), "Illegal memory access.");
188    let msg = std::str::from_utf8(&memory[msg_start..msg_start + msg_length])?.to_owned();
189    let filename =
190        std::str::from_utf8(&memory[filename_start..filename_start + filename_length])?.to_owned();
191    Ok((filename, line, column, msg))
192}
193
194#[derive(Error, Debug)]
195enum CallErr {
196    #[error("Unable to read bytes at the given position")]
197    Seek,
198    #[error("No \"{0}\" is set. Make sure to prepare this in the test environment")]
199    Unset(&'static str),
200    #[error("Unable to serialize \"{0}\"")]
201    Serial(&'static str),
202    #[error("Unable to write to given buffer")]
203    Write,
204}
205
206impl<'a, R: RngCore, BackingStore: trie::BackingStoreLoad> machine::Host<ArtifactNamedImport>
207    for TestHost<'a, R, BackingStore>
208{
209    type Interrupt = NoInterrupt;
210
211    fn tick_initial_memory(&mut self, _num_pages: u32) -> machine::RunResult<()> {
212        // The test host does not count energy.
213        Ok(())
214    }
215
216    fn call(
217        &mut self,
218        f: &ArtifactNamedImport,
219        memory: &mut [u8],
220        stack: &mut machine::RuntimeStack,
221    ) -> machine::RunResult<Option<NoInterrupt>> {
222        // We don't track the energy usage in this host, so to reuse code which does, we
223        // provide a really large amount of energy to preventing the case of
224        // running out of energy.
225        let energy = &mut crate::InterpreterEnergy::new(u64::MAX);
226        let state = &mut self.state;
227
228        ensure!(
229            f.get_mod_name() == "concordium",
230            "Unsupported module in host function call: {:?} {:?}",
231            f.get_mod_name(),
232            f.get_item_name()
233        );
234
235        use host::*;
236        match f.get_item_name() {
237            "report_error" => {
238                let (filename, line, column, msg) = extract_debug(memory, stack)?;
239                bail!(ReportError::Reported {
240                    filename,
241                    line,
242                    column,
243                    msg
244                })
245            }
246            "get_random" => {
247                let size = unsafe { stack.pop_u32() } as usize;
248                let dest = unsafe { stack.pop_u32() } as usize;
249                ensure!(dest + size <= memory.len(), "Illegal memory access.");
250                self.rng_used = true;
251                self.rng
252                    .as_mut()
253                    .context("Expected an initialized RNG.")?
254                    .try_fill_bytes(&mut memory[dest..dest + size])?
255            }
256            "debug_print" => {
257                let (filename, line, column, msg) = extract_debug(memory, stack)?;
258                self.debug_events.push(EmittedDebugStatement {
259                    filename,
260                    line,
261                    column,
262                    msg,
263                    remaining_energy: 0.into(), // debug host does not have energy.
264                });
265            }
266            "state_lookup_entry" => state_lookup_entry(memory, stack, energy, state)?,
267            "state_create_entry" => state_create_entry(memory, stack, energy, state)?,
268            "state_delete_entry" => state_delete_entry(memory, stack, energy, state)?,
269            "state_delete_prefix" => state_delete_prefix(memory, stack, energy, state)?,
270            "state_iterate_prefix" => state_iterator(memory, stack, energy, state)?,
271            "state_iterator_next" => state_iterator_next(stack, energy, state)?,
272            "state_iterator_delete" => state_iterator_delete(stack, energy, state)?,
273            "state_iterator_key_size" => state_iterator_key_size(stack, energy, state)?,
274            "state_iterator_key_read" => state_iterator_key_read(memory, stack, energy, state)?,
275            "state_entry_read" => state_entry_read(memory, stack, energy, state)?,
276            "state_entry_write" => state_entry_write(memory, stack, energy, state)?,
277            "state_entry_size" => state_entry_size(stack, energy, state)?,
278            "state_entry_resize" => state_entry_resize(stack, energy, state)?,
279            "set_slot_time" => {
280                let slot_time = unsafe { stack.pop_u64() };
281                self.slot_time = Some(slot_time);
282            }
283            "get_slot_time" => {
284                let slot_time = self.slot_time.ok_or(CallErr::Unset("slot_time"))?;
285                stack.push_value(slot_time);
286            }
287            "set_receive_self_address" => {
288                let addr_ptr = unsafe { stack.pop_u32() };
289
290                let mut cursor = Cursor::new(memory);
291                cursor.seek(SeekFrom::Start(addr_ptr)).map_err(|_| CallErr::Seek)?;
292
293                self.address = Some(ContractAddress::deserial(&mut cursor)?);
294            }
295            "get_receive_self_address" => {
296                let addr_ptr = unsafe { stack.pop_u32() } as usize;
297                self.address
298                    .ok_or(CallErr::Unset("self_address"))?
299                    .serial(&mut &mut memory[addr_ptr..])
300                    .map_err(|_| CallErr::Serial("self_address"))?;
301            }
302            "set_receive_self_balance" => {
303                let balance = unsafe { stack.pop_u64() };
304                self.balance = Some(balance);
305            }
306            "get_receive_self_balance" => {
307                let balance = self.balance.ok_or(CallErr::Unset("self_balance"))?;
308                stack.push_value(balance);
309            }
310            "set_parameter" => {
311                let param_size = unsafe { stack.pop_u32() };
312                let param_ptr = unsafe { stack.pop_u32() };
313                let param_index = unsafe { stack.pop_u32() };
314
315                let mut param = vec![0; param_size as usize];
316
317                let mut cursor = Cursor::new(memory);
318                cursor.seek(SeekFrom::Start(param_ptr)).map_err(|_| CallErr::Seek)?;
319                cursor.read_exact(&mut param)?;
320
321                self.parameters.insert(param_index, param);
322            }
323            "get_parameter_size" => {
324                let param_index = unsafe { stack.pop_u32() };
325
326                if let Some(param) = self.parameters.get(&param_index) {
327                    stack.push_value(param.len() as u64)
328                } else {
329                    stack.push_value(-1i32)
330                }
331            }
332            "get_parameter_section" => {
333                let offset = unsafe { stack.pop_u32() } as usize;
334                let length = unsafe { stack.pop_u32() } as usize;
335                let param_bytes = unsafe { stack.pop_u32() } as usize;
336                let param_index = unsafe { stack.pop_u32() };
337
338                if let Some(param) = self.parameters.get(&param_index) {
339                    let self_param = param.get(offset..length + offset).context(format!(
340                        "Tried to grab {} bytes of parameter[{}], which has length {}",
341                        length,
342                        param_index,
343                        param.len()
344                    ))?;
345
346                    let mut mem = &mut memory[param_bytes..];
347                    let bytes_written: i32 =
348                        mem.write(self_param).map_err(|_| CallErr::Write)?.try_into()?;
349
350                    stack.push_value(bytes_written)
351                } else {
352                    stack.push_value(-1i32)
353                }
354            }
355            "log_event" => {
356                let event_length = unsafe { stack.pop_u32() };
357                let event_start = unsafe { stack.pop_u32() };
358
359                if event_length > MAX_LOG_SIZE {
360                    stack.push_value(-1i32);
361                } else {
362                    let mut cursor = Cursor::new(memory);
363                    cursor.seek(SeekFrom::Start(event_start)).map_err(|_| CallErr::Seek)?;
364
365                    let mut buf = vec![0; event_length as usize];
366                    cursor.read(&mut buf).context("Unable to read provided event")?;
367
368                    self.events.push(buf);
369
370                    stack.push_value(1i32);
371                }
372            }
373            "get_event_size" => {
374                let event_index = unsafe { stack.pop_u32() };
375                let event_opt = self.events.get(event_index as usize);
376
377                if let Some(event) = event_opt {
378                    let event_size: i32 = event.len().try_into()?;
379                    stack.push_value(event_size)
380                } else {
381                    stack.push_value(-1i32);
382                }
383            }
384            "get_event" => {
385                let ret_buf_start = unsafe { stack.pop_u32() } as usize;
386                let event_index = unsafe { stack.pop_u32() };
387                let event_opt = self.events.get(event_index as usize);
388
389                if let Some(event) = event_opt {
390                    let mut mem = &mut memory[ret_buf_start..];
391                    let bytes_written: i32 =
392                        mem.write(event).map_err(|_| CallErr::Write)?.try_into()?;
393
394                    stack.push_value(bytes_written)
395                } else {
396                    stack.push_value(-1i32);
397                }
398            }
399            "set_init_origin" => {
400                let addr_bytes = unsafe { stack.pop_u32() };
401
402                let mut cursor = Cursor::new(memory);
403                cursor.seek(SeekFrom::Start(addr_bytes)).map_err(|_| CallErr::Seek)?;
404
405                self.init_origin = Some(AccountAddress::deserial(&mut cursor)?);
406            }
407            "get_init_origin" => {
408                let ret_buf_start = unsafe { stack.pop_u32() } as usize;
409
410                let mut mem = &mut memory[ret_buf_start..];
411                self.init_origin
412                    .ok_or(CallErr::Unset("init_origin"))?
413                    .serial(&mut mem)
414                    .map_err(|_| CallErr::Serial("init_origin"))?;
415            }
416            "set_receive_invoker" => {
417                let addr_bytes = unsafe { stack.pop_u32() };
418
419                let mut cursor = Cursor::new(memory);
420                cursor.seek(SeekFrom::Start(addr_bytes)).map_err(|_| CallErr::Seek)?;
421
422                self.receive_invoker = Some(AccountAddress::deserial(&mut cursor)?);
423            }
424            "get_receive_invoker" => {
425                let ret_buf_start = unsafe { stack.pop_u32() } as usize;
426
427                let mut mem = &mut memory[ret_buf_start..];
428                self.receive_invoker
429                    .ok_or(CallErr::Unset("receive_invoker"))?
430                    .serial(&mut mem)
431                    .map_err(|_| CallErr::Serial("receive_invoker"))?;
432            }
433            "set_receive_sender" => {
434                let addr_bytes = unsafe { stack.pop_u32() };
435
436                let mut cursor = Cursor::new(memory);
437                cursor.seek(SeekFrom::Start(addr_bytes)).map_err(|_| CallErr::Seek)?;
438
439                self.receive_sender = Some(Address::deserial(&mut cursor)?);
440            }
441            "get_receive_sender" => {
442                let ret_buf_start = unsafe { stack.pop_u32() } as usize;
443
444                let mut mem = &mut memory[ret_buf_start..];
445                self.receive_sender
446                    .ok_or(CallErr::Unset("receive_sender"))?
447                    .serial(&mut mem)
448                    .map_err(|_| CallErr::Serial("receive_sender"))?;
449            }
450            "set_receive_owner" => {
451                let addr_bytes = unsafe { stack.pop_u32() };
452
453                let mut cursor = Cursor::new(memory);
454                cursor.seek(SeekFrom::Start(addr_bytes)).map_err(|_| CallErr::Seek)?;
455
456                self.receive_owner = Some(AccountAddress::deserial(&mut cursor)?);
457            }
458            "get_receive_owner" => {
459                let ret_buf_start = unsafe { stack.pop_u32() } as usize;
460
461                let mut mem = &mut memory[ret_buf_start..];
462                self.receive_owner
463                    .ok_or(CallErr::Unset("receive_owner"))?
464                    .serial(&mut mem)
465                    .map_err(|_| CallErr::Serial("receive_owner"))?;
466            }
467            "set_receive_entrypoint" => {
468                let addr_bytes = unsafe { stack.pop_u32() };
469
470                let mut cursor = Cursor::new(memory);
471                cursor.seek(SeekFrom::Start(addr_bytes)).map_err(|_| CallErr::Seek)?;
472
473                self.receive_entrypoint = Some(OwnedEntrypointName::deserial(&mut cursor)?);
474            }
475            "get_receive_entrypoint_size" => {
476                let size = self
477                    .receive_entrypoint
478                    .as_ref()
479                    .ok_or(CallErr::Unset("receive_entrypoint"))?
480                    .as_entrypoint_name()
481                    .size();
482                stack.push_value(size);
483            }
484            "get_receive_entrypoint" => {
485                let ret_buf_start = unsafe { stack.pop_u32() } as usize;
486
487                let bytes = self
488                    .receive_entrypoint
489                    .clone()
490                    .ok_or(CallErr::Unset("receive_entrypoint"))?
491                    .to_string()
492                    .into_bytes();
493
494                let mut mem = &mut memory[ret_buf_start..];
495                mem.write(&bytes).map_err(|_| CallErr::Write)?;
496            }
497            "verify_ed25519_signature" => {
498                let message_len = unsafe { stack.pop_u32() };
499                let message_ptr = unsafe { stack.pop_u32() };
500                let signature_ptr = unsafe { stack.pop_u32() };
501                let public_key_ptr = unsafe { stack.pop_u32() };
502
503                let mut cursor = Cursor::new(memory);
504
505                cursor.seek(SeekFrom::Start(public_key_ptr)).map_err(|_| CallErr::Seek)?;
506                let mut public_key_bytes = [0; 32];
507                cursor.read(&mut public_key_bytes)?;
508                let z_pk = ed25519_zebra::VerificationKey::try_from(public_key_bytes)?;
509
510                cursor.seek(SeekFrom::Start(signature_ptr)).map_err(|_| CallErr::Seek)?;
511                let mut signature_bytes = [0; 64];
512                cursor.read(&mut signature_bytes)?;
513                let z_sig = ed25519_zebra::Signature::from_bytes(&signature_bytes);
514
515                cursor.seek(SeekFrom::Start(message_ptr)).map_err(|_| CallErr::Seek)?;
516                let mut msg = vec![0; message_len as usize];
517                cursor.read(&mut msg)?;
518
519                let is_verified = z_pk.verify(&z_sig, &msg);
520
521                if is_verified.is_ok() {
522                    stack.push_value(1i32)
523                } else {
524                    stack.push_value(0i32)
525                }
526            }
527            "verify_ecdsa_secp256k1_signature" => {
528                let message_hash_ptr = unsafe { stack.pop_u32() };
529                let signature_ptr = unsafe { stack.pop_u32() };
530                let public_key_ptr = unsafe { stack.pop_u32() };
531
532                let mut cursor = Cursor::new(memory);
533                let secp = secp256k1::Secp256k1::verification_only();
534
535                cursor.seek(SeekFrom::Start(public_key_ptr)).map_err(|_| CallErr::Seek)?;
536                let mut public_key_bytes = [0; 33];
537                cursor.read(&mut public_key_bytes)?;
538                let pk = secp256k1::PublicKey::from_slice(&public_key_bytes)?;
539
540                cursor.seek(SeekFrom::Start(signature_ptr)).map_err(|_| CallErr::Seek)?;
541                let mut signature_bytes = [0; 64];
542                cursor.read(&mut signature_bytes)?;
543                let sig = secp256k1::ecdsa::Signature::from_compact(&signature_bytes)?;
544
545                cursor.seek(SeekFrom::Start(message_hash_ptr)).map_err(|_| CallErr::Seek)?;
546                let mut message_hash_bytes = [0; 32];
547                cursor.read(&mut message_hash_bytes)?;
548                let msg = secp256k1::Message::from_slice(&message_hash_bytes)?;
549
550                let is_verified = secp.verify_ecdsa(&msg, &sig, &pk);
551                if is_verified.is_ok() {
552                    stack.push_value(1i32)
553                } else {
554                    stack.push_value(0i32)
555                }
556            }
557            "hash_sha2_256" => {
558                let output_ptr = unsafe { stack.pop_u32() } as usize;
559                let data_len = unsafe { stack.pop_u32() } as usize;
560                let data_ptr = unsafe { stack.pop_u32() } as usize;
561
562                let mut hasher = sha2::Sha256::default();
563                hasher.update(&memory[data_ptr..data_ptr + data_len]);
564                let data_hash = hasher.finalize();
565
566                let mut mem = &mut memory[output_ptr..];
567                mem.write(&data_hash).map_err(|_| CallErr::Write)?;
568            }
569            "hash_sha3_256" => {
570                let output_ptr = unsafe { stack.pop_u32() } as usize;
571                let data_len = unsafe { stack.pop_u32() } as usize;
572                let data_ptr = unsafe { stack.pop_u32() } as usize;
573
574                let mut hasher = sha3::Sha3_256::default();
575                hasher.update(&memory[data_ptr..data_ptr + data_len]);
576                let data_hash = hasher.finalize();
577
578                let mut mem = &mut memory[output_ptr..];
579                mem.write(&data_hash).map_err(|_| CallErr::Write)?;
580            }
581            "hash_keccak_256" => {
582                let output_ptr = unsafe { stack.pop_u32() } as usize;
583                let data_len = unsafe { stack.pop_u32() } as usize;
584                let data_ptr = unsafe { stack.pop_u32() } as usize;
585
586                let mut hasher = sha3::Keccak256::default();
587                hasher.update(&memory[data_ptr..data_ptr + data_len]);
588                let data_hash = hasher.finalize();
589
590                let mut mem = &mut memory[output_ptr..];
591                mem.write(&data_hash).map_err(|_| CallErr::Write)?;
592            }
593            item_name => {
594                bail!("Unsupported host function call: {:?} {:?}", f.get_mod_name(), item_name)
595            }
596        }
597
598        Ok(None)
599    }
600
601    fn tick_energy(&mut self, _energy: u64) -> machine::RunResult<()> { Ok(()) }
602
603    fn track_call(&mut self) -> machine::RunResult<()> { Ok(()) }
604
605    fn track_return(&mut self) {}
606}
607
608/// Tries to generate a state schema and schemas for parameters of methods of a
609/// V0 contract.
610pub fn generate_contract_schema_v0(
611    module_bytes: &[u8],
612) -> ExecResult<schema::VersionedModuleSchema> {
613    let artifact = utils::instantiate::<ArtifactNamedImport, _>(
614        ValidationConfig::V0,
615        &NoDuplicateImport,
616        module_bytes,
617    )?
618    .artifact;
619
620    let mut contract_schemas = BTreeMap::new();
621
622    for name in artifact.export.keys() {
623        if let Some(contract_name) = name.as_ref().strip_prefix("concordium_schema_state_") {
624            let schema_type = generate_schema_run(&artifact, name.as_ref())?;
625
626            // Get the mutable reference to the contract schema, or make a new empty one if
627            // an entry does not yet exist.
628            let contract_schema = contract_schemas
629                .entry(contract_name.to_owned())
630                .or_insert_with(schema::ContractV0::default);
631
632            contract_schema.state = Some(schema_type);
633        } else if let Some(rest) = name.as_ref().strip_prefix("concordium_schema_function_") {
634            if let Some(contract_name) = rest.strip_prefix("init_") {
635                let schema_type = generate_schema_run(&artifact, name.as_ref())?;
636
637                let contract_schema = contract_schemas
638                    .entry(contract_name.to_owned())
639                    .or_insert_with(schema::ContractV0::default);
640                contract_schema.init = Some(schema_type);
641            } else if rest.contains('.') {
642                let schema_type = generate_schema_run(&artifact, name.as_ref())?;
643
644                // Generates receive-function parameter schema type
645                let split_name: Vec<_> = rest.splitn(2, '.').collect();
646                let contract_name = split_name[0];
647                let function_name = split_name[1];
648
649                let contract_schema = contract_schemas
650                    .entry(contract_name.to_owned())
651                    .or_insert_with(schema::ContractV0::default);
652
653                contract_schema.receive.insert(function_name.to_owned(), schema_type);
654            } else {
655                // do nothing, some other function that is neither init nor
656                // receive.
657            }
658        }
659    }
660
661    Ok(schema::VersionedModuleSchema::V0(schema::ModuleV0 {
662        contracts: contract_schemas,
663    }))
664}
665
666/// Tries to generate schemas for parameters and return values of methods for a
667/// contract with a V1 schema.
668pub fn generate_contract_schema_v1(
669    module_bytes: &[u8],
670) -> ExecResult<schema::VersionedModuleSchema> {
671    let artifact = utils::instantiate::<ArtifactNamedImport, _>(
672        ValidationConfig::V1,
673        &NoDuplicateImport,
674        module_bytes,
675    )?
676    .artifact;
677
678    let mut contract_schemas = BTreeMap::new();
679
680    for name in artifact.export.keys() {
681        if let Some(rest) = name.as_ref().strip_prefix("concordium_schema_function_") {
682            if let Some(contract_name) = rest.strip_prefix("init_") {
683                let function_schema = generate_schema_run(&artifact, name.as_ref())?;
684
685                let contract_schema = contract_schemas
686                    .entry(contract_name.to_owned())
687                    .or_insert_with(schema::ContractV1::default);
688                contract_schema.init = Some(function_schema);
689            } else if rest.contains('.') {
690                let function_schema = generate_schema_run(&artifact, name.as_ref())?;
691
692                // Generates receive-function parameter schema type
693                let split_name: Vec<_> = rest.splitn(2, '.').collect();
694                let contract_name = split_name[0];
695                let function_name = split_name[1];
696
697                let contract_schema = contract_schemas
698                    .entry(contract_name.to_owned())
699                    .or_insert_with(schema::ContractV1::default);
700
701                contract_schema.receive.insert(function_name.to_owned(), function_schema);
702            } else {
703                // do nothing, some other function that is neither init nor
704                // receive.
705            }
706        }
707    }
708
709    Ok(schema::VersionedModuleSchema::V1(schema::ModuleV1 {
710        contracts: contract_schemas,
711    }))
712}
713
714/// Tries to generate schemas for parameters and return values of methods for a
715/// contract with a V2 schema.
716pub fn generate_contract_schema_v2(
717    module_bytes: &[u8],
718) -> ExecResult<schema::VersionedModuleSchema> {
719    let artifact = utils::instantiate::<ArtifactNamedImport, _>(
720        ValidationConfig::V1,
721        &NoDuplicateImport,
722        module_bytes,
723    )?
724    .artifact;
725
726    let mut contract_schemas = BTreeMap::new();
727
728    for name in artifact.export.keys() {
729        if let Some(rest) = name.as_ref().strip_prefix("concordium_schema_function_") {
730            if let Some(contract_name) = rest.strip_prefix("init_") {
731                let function_schema = generate_schema_run(&artifact, name.as_ref())?;
732
733                let contract_schema = contract_schemas
734                    .entry(contract_name.to_owned())
735                    .or_insert_with(schema::ContractV2::default);
736                contract_schema.init = Some(function_schema);
737            } else if rest.contains('.') {
738                let function_schema = generate_schema_run(&artifact, name.as_ref())?;
739
740                // Generates receive-function parameter schema type
741                let split_name: Vec<_> = rest.splitn(2, '.').collect();
742                let contract_name = split_name[0];
743                let function_name = split_name[1];
744
745                let contract_schema = contract_schemas
746                    .entry(contract_name.to_owned())
747                    .or_insert_with(schema::ContractV2::default);
748
749                contract_schema.receive.insert(function_name.to_owned(), function_schema);
750            } else {
751                // do nothing, some other function that is neither init nor
752                // receive.
753            }
754        }
755    }
756
757    Ok(schema::VersionedModuleSchema::V2(schema::ModuleV2 {
758        contracts: contract_schemas,
759    }))
760}
761
762/// Tries to generate schemas for events, parameters, return values, and errors
763/// of methods for a contract with a V3 schema.
764pub fn generate_contract_schema_v3(
765    module_bytes: &[u8],
766) -> ExecResult<schema::VersionedModuleSchema> {
767    let artifact = utils::instantiate::<ArtifactNamedImport, _>(
768        ValidationConfig::V1,
769        &NoDuplicateImport,
770        module_bytes,
771    )?
772    .artifact;
773
774    let mut contract_schemas = BTreeMap::new();
775
776    for name in artifact.export.keys() {
777        if let Some(rest) = name.as_ref().strip_prefix("concordium_event_schema_") {
778            if let Some(contract_name) = rest.strip_prefix("init_") {
779                // Generate event schema
780                let function_schema_event = generate_schema_run(&artifact, name.as_ref())?;
781
782                let contract_schema = contract_schemas
783                    .entry(contract_name.to_owned())
784                    .or_insert_with(schema::ContractV3::default);
785                contract_schema.event = Some(function_schema_event);
786            }
787            // The event schema attached to the init function is globally
788            // available in the smart contract and is applied to all
789            // events logged by receive/init functions. There is no
790            // need to create a separate event schema for receive functions.
791        } else if let Some(rest) = name.as_ref().strip_prefix("concordium_schema_function_") {
792            if let Some(contract_name) = rest.strip_prefix("init_") {
793                // Generate init-function schema
794                let function_schema = generate_schema_run(&artifact, name.as_ref())?;
795
796                let contract_schema = contract_schemas
797                    .entry(contract_name.to_owned())
798                    .or_insert_with(schema::ContractV3::default);
799                contract_schema.init = Some(function_schema);
800            } else if rest.contains('.') {
801                // Generate receive-function schema
802                let function_schema = generate_schema_run(&artifact, name.as_ref())?;
803
804                let split_name: Vec<_> = rest.splitn(2, '.').collect();
805                let contract_name = split_name[0];
806                let function_name = split_name[1];
807
808                let contract_schema = contract_schemas
809                    .entry(contract_name.to_owned())
810                    .or_insert_with(schema::ContractV3::default);
811
812                contract_schema.receive.insert(function_name.to_owned(), function_schema);
813            } else {
814                // do nothing: no event schema and not a schema that was
815                // attached to an init/ receive function
816            }
817        }
818    }
819
820    Ok(schema::VersionedModuleSchema::V3(schema::ModuleV3 {
821        contracts: contract_schemas,
822    }))
823}
824
825/// Runs the given schema function and reads the resulting function schema from
826/// memory, attempting to parse it. If this fails, an error is returned.
827fn generate_schema_run<I: TryFromImport, C: RunnableCode, SchemaType: Deserial>(
828    artifact: &Artifact<I, C>,
829    schema_fn_name: &str,
830) -> ExecResult<SchemaType> {
831    let (ptr, memory) = if let machine::ExecutionOutcome::Success {
832        result: Some(Value::I32(ptr)),
833        memory,
834    } = artifact.run(&mut TrapHost, schema_fn_name, &[])?
835    {
836        (ptr as u32 as usize, memory)
837    } else {
838        bail!("Schema derivation function is malformed.")
839    };
840
841    // First we read an u32 which is the length of the serialized schema
842    ensure!(ptr + 4 <= memory.len(), "Illegal memory access.");
843    let len = u32::deserial(&mut Cursor::new(&memory[ptr..ptr + 4]))
844        .map_err(|_| anyhow!("Cannot read schema length."))?;
845
846    // Read the schema with offset of the u32
847    ensure!(ptr + 4 + len as usize <= memory.len(), "Illegal memory access when reading schema.");
848    let schema_bytes = &memory[ptr + 4..ptr + 4 + len as usize];
849    SchemaType::deserial(&mut Cursor::new(schema_bytes))
850        .map_err(|_| anyhow!("Failed deserialising the schema."))
851}
852
853/// Get the init methods of the module.
854pub fn get_inits(module: &Module) -> Vec<&Name> {
855    let mut out = Vec::new();
856    for export in module.export.exports.iter() {
857        if export.name.as_ref().starts_with("init_") && !export.name.as_ref().contains('.') {
858            if let ExportDescription::Func {
859                ..
860            } = export.description
861            {
862                out.push(&export.name);
863            }
864        }
865    }
866    out
867}
868
869/// Get the receive methods of the module.
870pub fn get_receives(module: &Module) -> Vec<&Name> {
871    let mut out = Vec::new();
872    for export in module.export.exports.iter() {
873        if export.name.as_ref().contains('.') {
874            if let ExportDescription::Func {
875                ..
876            } = export.description
877            {
878                out.push(&export.name);
879            }
880        }
881    }
882    out
883}
884
885/// Get the embedded schema for smart contract modules version 0 if it exists.
886///
887/// First attempt to use the schema in the custom section "concordium-schema"
888/// and if this is not present try to use the custom section
889/// "concordium-schema-v1".
890pub fn get_embedded_schema_v0(bytes: &[u8]) -> ExecResult<schema::VersionedModuleSchema> {
891    let skeleton = parse_skeleton(bytes)?;
892    let mut schema_v1_section = None;
893    let mut schema_versioned_section = None;
894    for ucs in skeleton.custom.iter() {
895        let cs = parse_custom(ucs)?;
896
897        if cs.name.as_ref() == "concordium-schema" && schema_versioned_section.is_none() {
898            schema_versioned_section = Some(cs)
899        } else if cs.name.as_ref() == "concordium-schema-v1" && schema_v1_section.is_none() {
900            schema_v1_section = Some(cs)
901        }
902    }
903
904    if let Some(cs) = schema_versioned_section {
905        let module: schema::VersionedModuleSchema =
906            from_bytes(cs.contents).map_err(|_| anyhow!("Failed parsing schema"))?;
907        Ok(module)
908    } else if let Some(cs) = schema_v1_section {
909        let module = from_bytes(cs.contents).map_err(|_| anyhow!("Failed parsing schema"))?;
910        Ok(schema::VersionedModuleSchema::V0(module))
911    } else {
912        bail!("No schema found in the module")
913    }
914}
915
916/// Get the embedded schema for smart contract modules version 1 if it exists.
917///
918/// First attempt to use the schema in the custom section "concordium-schema"
919/// and if this is not present try to use the custom section
920/// "concordium-schema-v2".
921pub fn get_embedded_schema_v1(bytes: &[u8]) -> ExecResult<schema::VersionedModuleSchema> {
922    let skeleton = parse_skeleton(bytes)?;
923    let mut schema_v2_section = None;
924    let mut schema_versioned_section = None;
925    for ucs in skeleton.custom.iter() {
926        let cs = parse_custom(ucs)?;
927        if cs.name.as_ref() == "concordium-schema" && schema_versioned_section.is_none() {
928            schema_versioned_section = Some(cs)
929        } else if cs.name.as_ref() == "concordium-schema-v2" && schema_v2_section.is_none() {
930            schema_v2_section = Some(cs)
931        }
932    }
933
934    if let Some(cs) = schema_versioned_section {
935        let module: schema::VersionedModuleSchema =
936            from_bytes(cs.contents).map_err(|_| anyhow!("Failed parsing schema"))?;
937        Ok(module)
938    } else if let Some(cs) = schema_v2_section {
939        let module = from_bytes(cs.contents).map_err(|_| anyhow!("Failed parsing schema"))?;
940        Ok(schema::VersionedModuleSchema::V1(module))
941    } else {
942        bail!("No schema found in the module")
943    }
944}
945
946/// The build information that will be embedded as a custom section to
947/// support reproducible builds.
948#[derive(Debug, Clone, concordium_contracts_common::Serialize)]
949pub struct BuildInfo {
950    /// The SHA256 hash of the tar file used to build.
951    /// Note that this is the hash of the **tar** file alone, not of any
952    /// compressed version.
953    pub archive_hash:  hashes::Hash,
954    /// The link to where the source code will be located.
955    pub source_link:   Option<String>,
956    /// The build image that was used.
957    pub image:         String,
958    /// The exact command invocation inside the image that was used to produce
959    /// the contract.
960    pub build_command: Vec<String>,
961}
962
963/// A versioned build information. This is the information that is embedded in a
964/// custom section. Currently there is one version, but the format supports
965/// future evolution.
966///
967/// The value is embedded in a custom section serialized using the smart
968/// contract serialization format
969/// ([`Serial`] trait).
970#[derive(Debug, Clone, concordium_contracts_common::Serialize)]
971pub enum VersionedBuildInfo {
972    V0(BuildInfo),
973}
974
975/// Name of the custom section that contains the build information of the
976/// module.
977pub const BUILD_INFO_SECTION_NAME: &str = "concordium-build-info";
978
979#[derive(Debug, thiserror::Error)]
980pub enum CustomSectionLookupError {
981    #[error("Custom section with a provided name is not present.")]
982    Missing,
983    #[error("Multiple custom sections with the given name are present.")]
984    Multiple,
985    #[error("Parse error: {0}.")]
986    MalformedData(#[from] anyhow::Error),
987}
988
989impl CustomSectionLookupError {
990    /// Returns whether the value is of the [`Missing`](Self::Missing) variant.
991    pub fn is_missing(&self) -> bool { matches!(self, Self::Missing) }
992}
993
994/// Extract the embedded [`VersionedBuildInfo`] from a Wasm module.
995pub fn get_build_info(bytes: &[u8]) -> Result<VersionedBuildInfo, CustomSectionLookupError> {
996    let skeleton = parse_skeleton(bytes)?;
997    get_build_info_from_skeleton(&skeleton)
998}
999
1000/// Extract the embedded [`VersionedBuildInfo`] from a [`Skeleton`].
1001pub fn get_build_info_from_skeleton(
1002    skeleton: &Skeleton,
1003) -> Result<VersionedBuildInfo, CustomSectionLookupError> {
1004    let mut build_context_section = None;
1005    for ucs in skeleton.custom.iter() {
1006        let cs = parse_custom(ucs)?;
1007        if cs.name.as_ref() == BUILD_INFO_SECTION_NAME
1008            && build_context_section.replace(cs).is_some()
1009        {
1010            return Err(CustomSectionLookupError::Multiple);
1011        }
1012    }
1013    let Some(cs) = build_context_section else {
1014        return Err(CustomSectionLookupError::Missing);
1015    };
1016    let info: VersionedBuildInfo = from_bytes(cs.contents).context("Failed parsing build info")?;
1017    Ok(info)
1018}
1019
1020#[cfg(test)]
1021/// Tests for schema parsing functions.
1022mod tests {
1023
1024    #[test]
1025    fn test_schema_embeddings() {
1026        let data =
1027            std::fs::read("../testdata/schemas/cis1-wccd-embedded-schema-v0-unversioned.wasm")
1028                .expect("Could not read file.");
1029        if let Err(e) = super::get_embedded_schema_v0(&data) {
1030            panic!("Failed to parse unversioned v0 module schema: {}", e);
1031        }
1032
1033        let data =
1034            std::fs::read("../testdata/schemas/cis2-wccd-embedded-schema-v1-unversioned.wasm.v1")
1035                .expect("Could not read file.");
1036        if let Err(e) = super::get_embedded_schema_v1(&data[8..]) {
1037            panic!("Failed to parse unversioned v1 module schema: {}", e);
1038        }
1039
1040        let data =
1041            std::fs::read("../testdata/schemas/cis1-wccd-embedded-schema-v0-versioned.wasm.v0")
1042                .expect("Could not read file.");
1043        if let Err(e) = super::get_embedded_schema_v0(&data[8..]) {
1044            panic!("Failed to parse versioned v0 module schema: {}", e);
1045        }
1046
1047        let data =
1048            std::fs::read("../testdata/schemas/cis2-wccd-embedded-schema-v1-versioned.wasm.v1")
1049                .expect("Could not read file.");
1050        if let Err(e) = super::get_embedded_schema_v1(&data[8..]) {
1051            panic!("Failed to parse versioned v1 module schema: {}", e);
1052        }
1053    }
1054}