1use 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
29pub enum ValidationMode {
31 Deps,
33
34 Root {
36 deps: bool,
40
41 at: Option<AccountAddress>,
44 },
45}
46
47pub struct BytecodeSourceVerifier<'a> {
48 rpc_client: &'a ReadApi,
49}
50
51type LocalModules = HashMap<(AccountAddress, Symbol), (Symbol, CompiledModule)>;
53
54#[derive(Default)]
55struct OnChainRepresentation {
56 on_chain_dependencies: Option<HashSet<AccountAddress>>,
61
62 modules: HashMap<(AccountAddress, Symbol), CompiledModule>,
65}
66
67impl ValidationMode {
68 pub fn deps() -> Self {
70 Self::Deps
71 }
72
73 pub fn root() -> Self {
77 Self::Root {
78 deps: false,
79 at: None,
80 }
81 }
82
83 pub fn root_at(address: AccountAddress) -> Self {
86 Self::Root {
87 deps: false,
88 at: Some(address),
89 }
90 }
91
92 pub fn root_and_deps() -> Self {
96 Self::Root {
97 deps: true,
98 at: None,
99 }
100 }
101
102 pub fn root_and_deps_at(address: AccountAddress) -> Self {
105 Self::Root {
106 deps: true,
107 at: Some(address),
108 }
109 }
110
111 fn verify_deps(&self) -> bool {
113 matches!(self, Self::Deps | Self::Root { deps: true, .. })
114 }
115
116 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 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 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 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 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 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 if address == AccountAddress::ZERO {
262 continue;
263 }
264
265 map.insert((address, module), (package, m.module.clone()));
266 }
267
268 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 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 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 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 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 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 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 let obj_id = ObjectID::from(addr);
416
417 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
471fn dependency_addresses(package: &CompiledPackage) -> impl Iterator<Item = AccountAddress> + '_ {
473 package.dependency_ids.published.values().map(|id| **id)
474}