Skip to main content

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