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 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 if address == AccountAddress::ZERO {
256 continue;
257 }
258
259 map.insert((address, module), (package, m.module.clone()));
260 }
261
262 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 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 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 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 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 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 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 let obj_id = ObjectID::from(addr);
410
411 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
465fn dependency_addresses(package: &CompiledPackage) -> impl Iterator<Item = AccountAddress> + '_ {
467 package.dependency_ids.published.values().map(|id| **id)
468}