iota_source_validation/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::{HashMap, HashSet};
6
7use futures::future;
8use iota_move_build::CompiledPackage;
9use iota_sdk::{
10    apis::ReadApi,
11    error::Error as SdkError,
12    rpc_types::{IotaObjectDataOptions, IotaRawData, IotaRawMovePackage},
13};
14use iota_types::base_types::ObjectID;
15use move_binary_format::CompiledModule;
16use move_compiler::compiled_unit::NamedCompiledModule;
17use move_core_types::account_address::AccountAddress;
18use move_symbol_pool::Symbol;
19use toolchain::units_for_toolchain;
20
21use crate::error::{AggregateError, Error};
22
23pub mod error;
24mod toolchain;
25
26#[cfg(test)]
27mod tests;
28
29/// Details of what to verify
30pub enum ValidationMode {
31    /// Validate only the dependencies
32    Deps,
33
34    /// Validate the root package, and its linkage.
35    Root {
36        /// Additionally validate the dependencies, and make sure the runtime
37        /// and storage IDs in dependency source code matches the root
38        /// package's on-chain linkage table.
39        deps: bool,
40
41        /// Look for the root package on-chain at the specified address, rather
42        /// than the address in its manifest.
43        at: Option<AccountAddress>,
44    },
45}
46
47pub struct BytecodeSourceVerifier<'a> {
48    rpc_client: &'a ReadApi,
49}
50
51/// Map package addresses and module names to package names and bytecode.
52type LocalModules = HashMap<(AccountAddress, Symbol), (Symbol, CompiledModule)>;
53
54#[derive(Default)]
55struct OnChainRepresentation {
56    /// Storage IDs from the root package's on-chain linkage table. This will
57    /// only be present if root package verification was requested, in which
58    /// case the keys from this mapping must match the source package's
59    /// dependencies.
60    on_chain_dependencies: Option<HashSet<AccountAddress>>,
61
62    /// Map package addresses and module names to bytecode (package names are
63    /// gone in the on-chain representation).
64    modules: HashMap<(AccountAddress, Symbol), CompiledModule>,
65}
66
67impl ValidationMode {
68    /// Only verify that source dependencies match their on-chain versions.
69    pub fn deps() -> Self {
70        Self::Deps
71    }
72
73    /// Only verify that the root package matches its on-chain version (requires
74    /// that the root package is published with its address available in the
75    /// manifest).
76    pub fn root() -> Self {
77        Self::Root {
78            deps: false,
79            at: None,
80        }
81    }
82
83    /// Only verify that the root package matches its on-chain version, but
84    /// override the location to look for the root package to `address`.
85    pub fn root_at(address: AccountAddress) -> Self {
86        Self::Root {
87            deps: false,
88            at: Some(address),
89        }
90    }
91
92    /// Verify both the root package and its dependencies (requires that the
93    /// root package is published with its address available in the
94    /// manifest).
95    pub fn root_and_deps() -> Self {
96        Self::Root {
97            deps: true,
98            at: None,
99        }
100    }
101
102    /// Verify both the root package and its dependencies, but override the
103    /// location to look for the root package to `address`.
104    pub fn root_and_deps_at(address: AccountAddress) -> Self {
105        Self::Root {
106            deps: true,
107            at: Some(address),
108        }
109    }
110
111    /// Should we verify dependencies?
112    fn verify_deps(&self) -> bool {
113        matches!(self, Self::Deps | Self::Root { deps: true, .. })
114    }
115
116    /// If the root package needs to be verified, what address should it be
117    /// fetched from?
118    fn root_address(&self, package: &CompiledPackage) -> Result<Option<AccountAddress>, Error> {
119        match self {
120            Self::Root { at: Some(addr), .. } => Ok(Some(*addr)),
121            Self::Root { at: None, .. } => Ok(Some(*package.published_at.clone()?)),
122            Self::Deps => Ok(None),
123        }
124    }
125
126    /// All the on-chain addresses that we need to fetch to build on-chain
127    /// addresses.
128    fn on_chain_addresses(&self, package: &CompiledPackage) -> Result<Vec<AccountAddress>, Error> {
129        let mut addrs = vec![];
130
131        if let Some(addr) = self.root_address(package)? {
132            addrs.push(addr);
133        }
134
135        if self.verify_deps() {
136            addrs.extend(dependency_addresses(package));
137        }
138
139        Ok(addrs)
140    }
141
142    /// On-chain representation of the package and dependencies compiled to
143    /// `package`, including linkage information.
144    async fn on_chain(
145        &self,
146        package: &CompiledPackage,
147        verifier: &BytecodeSourceVerifier<'_>,
148    ) -> Result<OnChainRepresentation, AggregateError> {
149        let mut on_chain = OnChainRepresentation::default();
150        let mut errs: Vec<Error> = vec![];
151
152        let root = self.root_address(package)?;
153        let addrs = self.on_chain_addresses(package)?;
154
155        let resps =
156            future::join_all(addrs.iter().copied().map(|a| verifier.pkg_for_address(a))).await;
157
158        for (storage_id, pkg) in addrs.into_iter().zip(resps) {
159            let IotaRawMovePackage {
160                module_map,
161                linkage_table,
162                ..
163            } = pkg?;
164
165            let mut modules = module_map
166                .into_iter()
167                .map(|(name, bytes)| {
168                    let Ok(module) = CompiledModule::deserialize_with_defaults(&bytes) else {
169                        return Err(Error::OnChainDependencyDeserializationError {
170                            address: storage_id,
171                            module: name.into(),
172                        });
173                    };
174
175                    Ok::<_, Error>((Symbol::from(name), module))
176                })
177                .peekable();
178
179            let runtime_id = match modules.peek() {
180                Some(Ok((_, module))) => *module.self_id().address(),
181
182                Some(Err(_)) => {
183                    // SAFETY: The error type does not implement `Clone` so we need to take the
184                    // error by value. We do that by calling `next` to take the value we just
185                    // peeked, which we know is an error type.
186                    errs.push(modules.next().unwrap().unwrap_err());
187                    continue;
188                }
189
190                None => {
191                    errs.push(Error::EmptyOnChainPackage(storage_id));
192                    continue;
193                }
194            };
195
196            for module in modules {
197                match module {
198                    Ok((name, module)) => {
199                        on_chain.modules.insert((runtime_id, name), module);
200                    }
201
202                    Err(e) => {
203                        errs.push(e);
204                        continue;
205                    }
206                }
207            }
208
209            if root.is_some_and(|r| r == storage_id) {
210                on_chain.on_chain_dependencies = Some(HashSet::from_iter(
211                    linkage_table.into_values().map(|info| *info.upgraded_id),
212                ));
213            }
214        }
215
216        Ok(on_chain)
217    }
218
219    /// Local representation of the modules in `package`. If the validation mode
220    /// requires verifying dependencies, then the dependencies' modules are
221    /// also included in the output.
222    ///
223    /// For the purposes of this function, a module is considered a dependency
224    /// if it is from a different source package, and that source package
225    /// has already been published. Conversely, a module that is from a
226    /// different source package, but that has not been published is
227    /// considered part of the root package.
228    ///
229    /// If the validation mode requires verifying the root package at a specific
230    /// address, then the modules from the root package will be expected at
231    /// address `0x0` and this address will be substituted with the
232    /// specified address.
233    fn local(&self, package: &CompiledPackage) -> Result<LocalModules, Error> {
234        let iota_package = package;
235        let package = &package.package;
236        let root_package = package.compiled_package_info.package_name;
237        let mut map = LocalModules::new();
238
239        if self.verify_deps() {
240            let deps_compiled_units =
241                units_for_toolchain(&package.deps_compiled_units).map_err(|e| {
242                    Error::CannotCheckLocalModules {
243                        package: package.compiled_package_info.package_name,
244                        message: e.to_string(),
245                    }
246                })?;
247
248            for (package, local_unit) in deps_compiled_units {
249                let m = &local_unit.unit;
250                let module = m.name;
251                let address = m.address.into_inner();
252
253                // Skip modules with on 0x0 because they are treated as part of the root
254                // package, even if they are a source dependency.
255                if address == AccountAddress::ZERO {
256                    continue;
257                }
258
259                map.insert((address, module), (package, m.module.clone()));
260            }
261
262            // Include bytecode dependencies.
263            for (package, module) in iota_package.bytecode_deps.iter() {
264                let address = *module.address();
265                if address == AccountAddress::ZERO {
266                    continue;
267                }
268                map.insert(
269                    (address, Symbol::from(module.name().as_str())),
270                    (*package, module.clone()),
271                );
272            }
273        }
274
275        let Self::Root { at, .. } = self else {
276            return Ok(map);
277        };
278
279        // Potentially rebuild according to the toolchain that the package was
280        // originally built with.
281        let root_compiled_units = units_for_toolchain(
282            &package
283                .root_compiled_units
284                .iter()
285                .map(|u| ("root".into(), u.clone()))
286                .collect(),
287        )
288        .map_err(|e| Error::CannotCheckLocalModules {
289            package: package.compiled_package_info.package_name,
290            message: e.to_string(),
291        })?;
292
293        // Add the root modules, potentially remapping 0x0 if we have been supplied an
294        // address to substitute with.
295        for (_, local_unit) in root_compiled_units {
296            let m = &local_unit.unit;
297            let module = m.name;
298            let address = m.address.into_inner();
299
300            let (address, compiled_module) = if let Some(root_address) = at {
301                (*root_address, substitute_root_address(m, *root_address)?)
302            } else if address == AccountAddress::ZERO {
303                return Err(Error::InvalidModuleFailure {
304                    name: module.to_string(),
305                    message: "Can't verify unpublished source".to_string(),
306                });
307            } else {
308                (address, m.module.clone())
309            };
310
311            map.insert((address, module), (root_package, compiled_module));
312        }
313
314        // If we have a root address to substitute, we need to find unpublished
315        // dependencies that would have gone into the root package as well.
316        if let Some(root_address) = at {
317            for (package, local_unit) in &package.deps_compiled_units {
318                let m = &local_unit.unit;
319                let module = m.name;
320                let address = m.address.into_inner();
321
322                if address != AccountAddress::ZERO {
323                    continue;
324                }
325
326                map.insert(
327                    (*root_address, module),
328                    (*package, substitute_root_address(m, *root_address)?),
329                );
330            }
331        }
332
333        Ok(map)
334    }
335}
336
337impl<'a> BytecodeSourceVerifier<'a> {
338    pub fn new(rpc_client: &'a ReadApi) -> Self {
339        BytecodeSourceVerifier { rpc_client }
340    }
341
342    /// Verify that the `compiled_package` matches its on-chain representation.
343    ///
344    /// See [`ValidationMode`] for more details on what is verified.
345    pub async fn verify(
346        &self,
347        package: &CompiledPackage,
348        mode: ValidationMode,
349    ) -> Result<(), AggregateError> {
350        if matches!(
351            mode,
352            ValidationMode::Root {
353                at: Some(AccountAddress::ZERO),
354                ..
355            }
356        ) {
357            return Err(Error::ZeroOnChainAddressSpecifiedFailure.into());
358        }
359
360        let local = mode.local(package)?;
361        let mut chain = mode.on_chain(package, self).await?;
362        let mut errs = vec![];
363
364        // Check that the transitive dependencies listed on chain match the dependencies
365        // listed in source code. Ignore 0x0 because this signifies an
366        // unpublished dependency.
367        if let Some(on_chain_deps) = &mut chain.on_chain_dependencies {
368            for dependency_id in dependency_addresses(package) {
369                if dependency_id != AccountAddress::ZERO && !on_chain_deps.remove(&dependency_id) {
370                    errs.push(Error::MissingDependencyInLinkageTable(dependency_id));
371                }
372            }
373        }
374
375        for on_chain_dep_id in chain.on_chain_dependencies.take().into_iter().flatten() {
376            errs.push(Error::MissingDependencyInSourcePackage(on_chain_dep_id));
377        }
378
379        // Check that the contents of bytecode matches between modules.
380        for ((address, module), (package, local_module)) in local {
381            let Some(on_chain_module) = chain.modules.remove(&(address, module)) else {
382                errs.push(Error::OnChainDependencyNotFound { package, module });
383                continue;
384            };
385
386            if local_module != on_chain_module {
387                errs.push(Error::ModuleBytecodeMismatch {
388                    address,
389                    package,
390                    module,
391                })
392            }
393        }
394
395        for (address, module) in chain.modules.into_keys() {
396            errs.push(Error::LocalDependencyNotFound { address, module });
397        }
398
399        if !errs.is_empty() {
400            return Err(AggregateError(errs));
401        }
402
403        Ok(())
404    }
405
406    async fn pkg_for_address(&self, addr: AccountAddress) -> Result<IotaRawMovePackage, Error> {
407        // Move packages are specified with an AccountAddress, but are
408        // fetched from a iota network via iota_getObject, which takes an object ID
409        let obj_id = ObjectID::from(addr);
410
411        // fetch the IOTA object at the address specified for the package in the local
412        // resolution table if future packages with a large set of dependency
413        // packages prove too slow to verify, batched object fetching should be
414        // added to the ReadApi & used here
415        let obj_read = self
416            .rpc_client
417            .get_object_with_options(obj_id, IotaObjectDataOptions::new().with_bcs())
418            .await
419            .map_err(Error::DependencyObjectReadFailure)?;
420
421        let obj = obj_read
422            .into_object()
423            .map_err(Error::IotaObjectRefFailure)?
424            .bcs
425            .ok_or_else(|| {
426                Error::DependencyObjectReadFailure(SdkError::Data(
427                    "Bcs field is not found".to_string(),
428                ))
429            })?;
430
431        match obj {
432            IotaRawData::Package(pkg) => Ok(pkg),
433            IotaRawData::MoveObject(move_obj) => Err(Error::ObjectFoundWhenPackageExpected(
434                Box::new((obj_id, move_obj)),
435            )),
436        }
437    }
438}
439
440fn substitute_root_address(
441    named_module: &NamedCompiledModule,
442    root: AccountAddress,
443) -> Result<CompiledModule, Error> {
444    let mut module = named_module.module.clone();
445    let address_idx = module.self_handle().address;
446
447    let Some(addr) = module.address_identifiers.get_mut(address_idx.0 as usize) else {
448        return Err(Error::InvalidModuleFailure {
449            name: named_module.name.to_string(),
450            message: "Self address field missing".into(),
451        });
452    };
453
454    if *addr != AccountAddress::ZERO {
455        return Err(Error::InvalidModuleFailure {
456            name: named_module.name.to_string(),
457            message: "Self address already populated".to_string(),
458        });
459    }
460
461    *addr = root;
462    Ok(module)
463}
464
465/// The on-chain addresses for a source package's dependencies
466fn dependency_addresses(package: &CompiledPackage) -> impl Iterator<Item = AccountAddress> + '_ {
467    package.dependency_ids.published.values().map(|id| **id)
468}