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            // only keep modules that are actually used
249            let deps_compiled_units: Vec<_> = deps_compiled_units
250                .into_iter()
251                .filter(|pkg| iota_package.dependency_ids.published.contains_key(&pkg.0))
252                .collect();
253
254            for (package, local_unit) in deps_compiled_units {
255                let m = &local_unit.unit;
256                let module = m.name;
257                let address = m.address.into_inner();
258
259                // Skip modules with on 0x0 because they are treated as part of the root
260                // package, even if they are a source dependency.
261                if address == AccountAddress::ZERO {
262                    continue;
263                }
264
265                map.insert((address, module), (package, m.module.clone()));
266            }
267
268            // Include bytecode dependencies.
269            for (package, module) in iota_package.bytecode_deps.iter() {
270                let address = *module.address();
271                if address == AccountAddress::ZERO {
272                    continue;
273                }
274                map.insert(
275                    (address, Symbol::from(module.name().as_str())),
276                    (*package, module.clone()),
277                );
278            }
279        }
280
281        let Self::Root { at, .. } = self else {
282            return Ok(map);
283        };
284
285        // Potentially rebuild according to the toolchain that the package was
286        // originally built with.
287        let root_compiled_units = units_for_toolchain(
288            &package
289                .root_compiled_units
290                .iter()
291                .map(|u| ("root".into(), u.clone()))
292                .collect(),
293        )
294        .map_err(|e| Error::CannotCheckLocalModules {
295            package: package.compiled_package_info.package_name,
296            message: e.to_string(),
297        })?;
298
299        // Add the root modules, potentially remapping 0x0 if we have been supplied an
300        // address to substitute with.
301        for (_, local_unit) in root_compiled_units {
302            let m = &local_unit.unit;
303            let module = m.name;
304            let address = m.address.into_inner();
305
306            let (address, compiled_module) = if let Some(root_address) = at {
307                (*root_address, substitute_root_address(m, *root_address)?)
308            } else if address == AccountAddress::ZERO {
309                return Err(Error::InvalidModuleFailure {
310                    name: module.to_string(),
311                    message: "Can't verify unpublished source".to_string(),
312                });
313            } else {
314                (address, m.module.clone())
315            };
316
317            map.insert((address, module), (root_package, compiled_module));
318        }
319
320        // If we have a root address to substitute, we need to find unpublished
321        // dependencies that would have gone into the root package as well.
322        if let Some(root_address) = at {
323            for (package, local_unit) in &package.deps_compiled_units {
324                let m = &local_unit.unit;
325                let module = m.name;
326                let address = m.address.into_inner();
327
328                if address != AccountAddress::ZERO {
329                    continue;
330                }
331
332                map.insert(
333                    (*root_address, module),
334                    (*package, substitute_root_address(m, *root_address)?),
335                );
336            }
337        }
338
339        Ok(map)
340    }
341}
342
343impl<'a> BytecodeSourceVerifier<'a> {
344    pub fn new(rpc_client: &'a ReadApi) -> Self {
345        BytecodeSourceVerifier { rpc_client }
346    }
347
348    /// Verify that the `compiled_package` matches its on-chain representation.
349    ///
350    /// See [`ValidationMode`] for more details on what is verified.
351    pub async fn verify(
352        &self,
353        package: &CompiledPackage,
354        mode: ValidationMode,
355    ) -> Result<(), AggregateError> {
356        if matches!(
357            mode,
358            ValidationMode::Root {
359                at: Some(AccountAddress::ZERO),
360                ..
361            }
362        ) {
363            return Err(Error::ZeroOnChainAddressSpecifiedFailure.into());
364        }
365
366        let local = mode.local(package)?;
367        let mut chain = mode.on_chain(package, self).await?;
368        let mut errs = vec![];
369
370        // Check that the transitive dependencies listed on chain match the dependencies
371        // listed in source code. Ignore 0x0 because this signifies an
372        // unpublished dependency.
373        if let Some(on_chain_deps) = &mut chain.on_chain_dependencies {
374            for dependency_id in dependency_addresses(package) {
375                if dependency_id != AccountAddress::ZERO && !on_chain_deps.remove(&dependency_id) {
376                    errs.push(Error::MissingDependencyInLinkageTable(dependency_id));
377                }
378            }
379        }
380
381        for on_chain_dep_id in chain.on_chain_dependencies.take().into_iter().flatten() {
382            errs.push(Error::MissingDependencyInSourcePackage(on_chain_dep_id));
383        }
384
385        // Check that the contents of bytecode matches between modules.
386        for ((address, module), (package, local_module)) in local {
387            let Some(on_chain_module) = chain.modules.remove(&(address, module)) else {
388                errs.push(Error::OnChainDependencyNotFound { package, module });
389                continue;
390            };
391
392            if local_module != on_chain_module {
393                errs.push(Error::ModuleBytecodeMismatch {
394                    address,
395                    package,
396                    module,
397                })
398            }
399        }
400
401        for (address, module) in chain.modules.into_keys() {
402            errs.push(Error::LocalDependencyNotFound { address, module });
403        }
404
405        if !errs.is_empty() {
406            return Err(AggregateError(errs));
407        }
408
409        Ok(())
410    }
411
412    async fn pkg_for_address(&self, addr: AccountAddress) -> Result<IotaRawMovePackage, Error> {
413        // Move packages are specified with an AccountAddress, but are
414        // fetched from a iota network via iota_getObject, which takes an object ID
415        let obj_id = ObjectID::from(addr);
416
417        // fetch the IOTA object at the address specified for the package in the local
418        // resolution table if future packages with a large set of dependency
419        // packages prove too slow to verify, batched object fetching should be
420        // added to the ReadApi & used here
421        let obj_read = self
422            .rpc_client
423            .get_object_with_options(obj_id, IotaObjectDataOptions::new().with_bcs())
424            .await
425            .map_err(Error::DependencyObjectReadFailure)?;
426
427        let obj = obj_read
428            .into_object()
429            .map_err(Error::IotaObjectRefFailure)?
430            .bcs
431            .ok_or_else(|| {
432                Error::DependencyObjectReadFailure(SdkError::Data(
433                    "Bcs field is not found".to_string(),
434                ))
435            })?;
436
437        match obj {
438            IotaRawData::Package(pkg) => Ok(pkg),
439            IotaRawData::MoveObject(move_obj) => Err(Error::ObjectFoundWhenPackageExpected(
440                Box::new((obj_id, move_obj)),
441            )),
442        }
443    }
444}
445
446fn substitute_root_address(
447    named_module: &NamedCompiledModule,
448    root: AccountAddress,
449) -> Result<CompiledModule, Error> {
450    let mut module = named_module.module.clone();
451    let address_idx = module.self_handle().address;
452
453    let Some(addr) = module.address_identifiers.get_mut(address_idx.0 as usize) else {
454        return Err(Error::InvalidModuleFailure {
455            name: named_module.name.to_string(),
456            message: "Self address field missing".into(),
457        });
458    };
459
460    if *addr != AccountAddress::ZERO {
461        return Err(Error::InvalidModuleFailure {
462            name: named_module.name.to_string(),
463            message: "Self address already populated".to_string(),
464        });
465    }
466
467    *addr = root;
468    Ok(module)
469}
470
471/// The on-chain addresses for a source package's dependencies
472fn dependency_addresses(package: &CompiledPackage) -> impl Iterator<Item = AccountAddress> + '_ {
473    package.dependency_ids.published.values().map(|id| **id)
474}