Skip to main content

sui_adapter_latest/static_programmable_transactions/linkage/
single_linkage.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    data_store::VerifiedPackageStore,
6    static_programmable_transactions::{
7        linkage::{
8            analysis::LinkageAnalyzer,
9            resolution::{ResolutionTable, VersionConstraint, add_and_unify, get_package},
10            resolved_linkage::{ExecutableLinkage, ResolvedLinkage},
11        },
12        loading::ast::{
13            Argument, Command, DeserializedPackage, InputArg, InputType, Inputs, LoadedFunction,
14            PackagePayload, Transaction, Type, module_has_init,
15        },
16    },
17};
18use move_binary_format::file_format::Visibility;
19use move_vm_runtime::validation::verification::ast::Package as VerifiedPackage;
20use std::{
21    collections::{BTreeMap, BTreeSet},
22    sync::Arc,
23};
24use sui_protocol_config::ProtocolConfig;
25use sui_types::{
26    Identifier,
27    base_types::ObjectID,
28    error::ExecutionErrorTrait,
29    execution_status::{ExecutionErrorKind, PackageUpgradeError},
30};
31
32/// Replace each command's per-call linkage with a single linkage shared by the whole transaction.
33///
34/// Done in two passes:
35///   1. Fold every command's package and type-argument constraints into one `ResolutionTable`,
36///      unifying as we go (an error here means the commands cannot agree on a single set of
37///      package versions).
38///      - Top level functions are pinned `exact`, while their dependencies are
39///        pinned `exact` or `at_least` based on the visibility of the top-level function.
40///        Type-argument packages are always `at_least`.
41///      - Publishes and upgrades introduce their own constraints to the linkage, but only if
42///        they have an `init` function (otherwise they do not contribute to the linkage). See
43///        comments on each of the command arms for details on this.
44///   2. Write the resulting unified linkage back into every `MoveCall`.
45///
46/// Because all calls end up sharing one linkage, every package version selection is consistent
47/// across the transaction.
48pub fn refine_to_single_linkage<E: ExecutionErrorTrait>(
49    txn: &mut Transaction,
50    linkage_analysis: &LinkageAnalyzer,
51    package_store: &VerifiedPackageStore<'_>,
52    protocol_config: &ProtocolConfig,
53) -> Result<(), E> {
54    let mut base_linkage = linkage_analysis
55        .config()
56        .resolution_table_with_native_packages::<E, _>(package_store)?;
57
58    for (i, command) in txn.commands.iter().enumerate() {
59        analyze_command::<E>(command, &mut base_linkage, package_store, protocol_config)
60            .map_err(|e| e.with_command_index(i))?;
61        add_used_input_linkage::<E>(
62            command.arguments(),
63            &txn.inputs,
64            &mut base_linkage,
65            package_store,
66            protocol_config,
67        )
68        .map_err(|e| e.with_command_index(i))?;
69    }
70
71    add_withdrawal_compatibility_input_linkage::<E>(
72        &txn.inputs,
73        &mut base_linkage,
74        package_store,
75        protocol_config,
76    )?;
77
78    if protocol_config.enable_order_independent_upgrade_init_linkage() {
79        for (i, command) in txn.commands.iter().enumerate() {
80            let Command::Upgrade(payload, _, current_package_id, _, resolved_linkage) = command
81            else {
82                continue;
83            };
84            analyze_upgrade_command::<E>(
85                payload,
86                current_package_id,
87                resolved_linkage,
88                &mut base_linkage,
89                package_store,
90                protocol_config,
91            )
92            .map_err(|e| e.with_command_index(i))?;
93        }
94    }
95
96    // Constraint-level invariant check, run before `from_resolution_table` erases the
97    // underlying constraints.
98    if protocol_config.harden_linkage_consistency() {
99        for (i, command) in txn.commands.iter().enumerate() {
100            validate_init_linkage_pinning::<E>(command, &base_linkage, package_store)
101                .map_err(|e| e.with_command_index(i))?;
102        }
103    }
104
105    let resolved_linkage =
106        ExecutableLinkage::new(ResolvedLinkage::from_resolution_table(base_linkage));
107
108    // harden_linkage_consistency ==> every package in the unified linkage must resolve to a specific version.
109    assert_invariant!(
110        !protocol_config.harden_linkage_consistency()
111            || resolved_linkage
112                .0
113                .linkage_resolution
114                .iter()
115                .all(|(_, resolution)| { resolution.version.is_some() }),
116        "Unified linkage must resolve every package to a specific version, but found: {:?}",
117        resolved_linkage
118    );
119
120    for (i, command) in txn.commands.iter_mut().enumerate() {
121        write_back_linkage::<E>(command, &resolved_linkage).map_err(|e| e.with_command_index(i))?;
122    }
123
124    txn.unified_linkage = Some(resolved_linkage);
125
126    Ok(())
127}
128
129fn add_used_input_linkage<'a, E: ExecutionErrorTrait>(
130    arguments: impl IntoIterator<Item = &'a Argument>,
131    inputs: &Inputs,
132    resolution_table: &mut ResolutionTable,
133    store: &VerifiedPackageStore<'_>,
134    protocol_config: &ProtocolConfig,
135) -> Result<(), E> {
136    if !protocol_config.harden_linkage_consistency() {
137        return Ok(());
138    }
139
140    for argument in arguments {
141        if let Argument::Input(i) = argument
142            && let Some((_, InputType::Fixed(ty))) = inputs.get(*i as usize)
143        {
144            add_type_packages::<E>(resolution_table, std::iter::once(ty), store)?;
145        }
146    }
147    Ok(())
148}
149
150fn add_withdrawal_compatibility_input_linkage<E: ExecutionErrorTrait>(
151    inputs: &Inputs,
152    resolution_table: &mut ResolutionTable,
153    store: &VerifiedPackageStore<'_>,
154    protocol_config: &ProtocolConfig,
155) -> Result<(), E> {
156    if !protocol_config.harden_linkage_consistency() {
157        return Ok(());
158    }
159
160    add_type_packages::<E>(
161        resolution_table,
162        inputs.iter().filter_map(|(input_arg, input_ty)| {
163            if let InputArg::FundsWithdrawal(withdrawal) = input_arg
164                && withdrawal.from_compatibility_object
165                && let InputType::Fixed(ty) = input_ty
166            {
167                Some(ty)
168            } else {
169                None
170            }
171        }),
172        store,
173    )
174}
175
176/// A publish or upgrade that runs an `init` executes it under the linkage declared by that command
177/// so every dependency the command declares must be pinned `exact`ly to the version it declared in
178/// the larger unified linkage.
179fn validate_init_linkage_pinning<E: ExecutionErrorTrait>(
180    command: &Command,
181    resolution_table: &ResolutionTable,
182    store: &VerifiedPackageStore<'_>,
183) -> Result<(), E> {
184    let validate_package_init_linkage = |declared_linkage: &ResolvedLinkage, err_context| {
185        for (original_id, version_id) in &declared_linkage.linkage {
186            match resolution_table.resolution_table.get(original_id) {
187                Some(VersionConstraint::Exact(_, pinned_id)) if pinned_id == version_id => (),
188                other => {
189                    invariant_violation!(
190                        "{err_context} runs an `init` that requires package {original_id} at \
191                        {version_id}, but the transaction linkage pins it to {other:?}"
192                    )
193                }
194            }
195        }
196        Ok(())
197    };
198
199    match command {
200        Command::Publish(PackagePayload::Deserialized(pkg), _, resolved_linkage) => {
201            if pkg.has_potential_init() {
202                validate_package_init_linkage(resolved_linkage, "publish")
203            } else {
204                Ok(())
205            }
206        }
207        Command::Upgrade(
208            PackagePayload::Deserialized(pkg),
209            _,
210            current_package_id,
211            _,
212            resolved_linkage,
213        ) => {
214            if upgrade_introduces_new_init::<E>(current_package_id, &pkg.modules_with_init, store)?
215            {
216                validate_package_init_linkage(resolved_linkage, "upgrade")
217            } else {
218                Ok(())
219            }
220        }
221        Command::Publish(PackagePayload::Serialized(_), ..) => {
222            invariant_violation!("Unexpected serialized package payload in linkage analysis")
223        }
224        Command::Upgrade(PackagePayload::Serialized(_), ..) => {
225            invariant_violation!("Unexpected serialized package payload in linkage analysis")
226        }
227        Command::MoveCall(_)
228        | Command::MakeMoveVec(_, _)
229        | Command::TransferObjects(_, _)
230        | Command::SplitCoins(_, _)
231        | Command::MergeCoins(_, _) => Ok(()),
232    }
233}
234
235/// Fold a single command's contribution into the shared `resolution_table` (pass 1). Only commands
236/// that pull packages into the runtime linkage contribute; the rest are no-ops.
237fn analyze_command<E: ExecutionErrorTrait>(
238    command: &Command,
239    resolution_table: &mut ResolutionTable,
240    store: &VerifiedPackageStore<'_>,
241    protocol_config: &ProtocolConfig,
242) -> Result<(), E> {
243    match command {
244        Command::MoveCall(move_call) => {
245            add_call_to_table::<E>(resolution_table, &move_call.function, store)?;
246        }
247        Command::Publish(PackagePayload::Serialized(_), ..) => {
248            invariant_violation!("Unexpected serialized package payload in linkage analysis")
249        }
250        Command::Publish(PackagePayload::Deserialized(pkg), _, resolved_linkage) => {
251            // A publish only affects the transaction's linkage if the package has an `init`
252            // function: `init` runs as part of the publish, so its dependencies must be resolvable
253            // in this transaction. Without an `init` the freshly published package is not called
254            // and contributes nothing.
255            //
256            // `modules` is guaranteed to be non-empty by the `deserialize_modules` function.
257            if pkg.has_potential_init() {
258                for resolved in resolved_linkage.linkage.values() {
259                    add_and_unify(resolved, store, resolution_table, VersionConstraint::exact)?;
260                }
261            }
262        }
263        Command::Upgrade(_, _, _, _, _)
264            if protocol_config.enable_order_independent_upgrade_init_linkage() => {}
265        Command::Upgrade(payload, _, current_package_id, _, resolved_linkage) => {
266            analyze_upgrade_command::<E>(
267                payload,
268                current_package_id,
269                resolved_linkage,
270                resolution_table,
271                store,
272                protocol_config,
273            )?;
274        }
275        Command::MakeMoveVec(Some(ty), _) => {
276            add_type_packages::<E>(resolution_table, std::iter::once(ty), store)?;
277        }
278        Command::MakeMoveVec(None, _) => (),
279        Command::TransferObjects(_, _) | Command::SplitCoins(_, _) | Command::MergeCoins(_, _) => {}
280    };
281    Ok(())
282}
283
284/// Analyze the linkage contribution of an upgrade command.
285fn analyze_upgrade_command<E: ExecutionErrorTrait>(
286    payload: &PackagePayload,
287    current_package_id: &ObjectID,
288    resolved_linkage: &ResolvedLinkage,
289    resolution_table: &mut ResolutionTable,
290    store: &VerifiedPackageStore<'_>,
291    protocol_config: &ProtocolConfig,
292) -> Result<(), E> {
293    if !protocol_config.enable_init_on_upgrade() {
294        return Ok(());
295    }
296
297    let current_pkg = get_package(current_package_id, store)?;
298
299    assert_invariant!(
300        protocol_config.enable_unified_linkage(),
301        "Unified linkage must be enabled before init on upgrade is supported"
302    );
303
304    let upgrade_modules_with_init = match payload {
305        PackagePayload::Serialized(_) => {
306            invariant_violation!("Unexpected serialized package payload in linkage analysis")
307        }
308        PackagePayload::Deserialized(DeserializedPackage {
309            modules_with_init, ..
310        }) => modules_with_init,
311    };
312
313    // Whether each module already present in the current package defines an `init`.
314    let current_module_inits = current_pkg
315        .modules()
316        .iter()
317        .map(|(module_id, module)| {
318            (
319                module_id.name().as_str(),
320                module_has_init(module.compiled_module()),
321            )
322        })
323        .collect::<BTreeMap<_, _>>();
324
325    // reject upgrades where an existing module adds an `init`.
326    reject_existing_module_added_init::<E>(&current_module_inits, upgrade_modules_with_init)?;
327
328    // only newly-introduced modules with an `init` contribute to the linkage.
329    if has_new_module_init(
330        current_module_inits.keys().copied().collect(),
331        upgrade_modules_with_init,
332    ) {
333        add_upgrade_init_linkage_to_table::<E>(
334            resolution_table,
335            current_package_id,
336            resolved_linkage,
337            store,
338            protocol_config,
339        )?;
340    }
341
342    Ok(())
343}
344
345/// Reject an upgrade in which a module that already exists in the current package (and did not
346/// previously define an `init`) introduces one. Only the upgraded `init`-defining module names are
347/// looked up in the current package.
348fn reject_existing_module_added_init<E: ExecutionErrorTrait>(
349    current_module_inits: &BTreeMap<&str, bool>,
350    upgrade_modules_with_init: &BTreeSet<Identifier>,
351) -> Result<(), E> {
352    for module_name in upgrade_modules_with_init {
353        if current_module_inits.get(module_name.as_str()) == Some(&false) {
354            return Err(<E>::from_kind(ExecutionErrorKind::PackageUpgradeError {
355                upgrade_error: PackageUpgradeError::IncompatibleUpgrade,
356            }));
357        }
358    }
359    Ok(())
360}
361
362/// Return true if the upgrade introduces at least one new module (absent from the current package)
363/// that defines an `init` function. Existing modules never count (rejected by `reject_existing_module_added_init`).
364fn has_new_module_init(
365    current_module_names: BTreeSet<&str>,
366    upgrade_modules_with_init: &BTreeSet<Identifier>,
367) -> bool {
368    upgrade_modules_with_init
369        .iter()
370        .any(|module_name| !current_module_names.contains(module_name.as_str()))
371}
372
373/// Whether this upgrade introduces a module that is absent from the current package and defines an
374/// `init` -- i.e. whether this upgrade will run an `init`.
375pub(crate) fn upgrade_introduces_new_init<E: ExecutionErrorTrait>(
376    current_package_id: &ObjectID,
377    upgrade_modules_with_init: &BTreeSet<Identifier>,
378    store: &VerifiedPackageStore<'_>,
379) -> Result<bool, E> {
380    let current_pkg = get_package(current_package_id, store)?;
381    Ok(has_new_module_init(
382        current_pkg
383            .modules()
384            .keys()
385            .map(|module_id| module_id.name().as_str())
386            .collect(),
387        upgrade_modules_with_init,
388    ))
389}
390
391/// Add the linkage constraints introduced by an upgrade, there are two cases based on whether the
392/// upgraded package already participates in the transaction-wide (Lumpy) linkage:
393///
394/// - If the upgraded package's original id is not already in the resolution table, the upgrade
395///   is treated like a fresh publish-with-init: every entry of its resolved linkage is added as an
396///   `exact` constraint.
397/// - If the upgraded package's original id is in the resolution table, then for any `(original_id,
398///   version_id)` as defined in the `Upgrade` command either:
399///   a. It is not in the existing Lumpy linkage, and a `original_id -> exact(version_id)` constraint is introduced; or
400///   b. It is in the existing Lumpy linkage, in which case Lumpy[original_id].id must equal `version_id`, and that entry is fixed to `exact(version_id)`.
401fn add_upgrade_init_linkage_to_table<E: ExecutionErrorTrait>(
402    resolution_table: &mut ResolutionTable,
403    current_package_id: &ObjectID,
404    resolved_linkage: &ResolvedLinkage,
405    store: &VerifiedPackageStore<'_>,
406    protocol_config: &ProtocolConfig,
407) -> Result<(), E> {
408    let current_pkg = get_package(current_package_id, store)?;
409    let pkg_original_id: ObjectID = current_pkg.original_id().into();
410
411    if !resolution_table
412        .resolution_table
413        .contains_key(&pkg_original_id)
414    {
415        for resolved in resolved_linkage.linkage.values() {
416            add_and_unify(resolved, store, resolution_table, VersionConstraint::exact)?;
417        }
418        return Ok(());
419    }
420
421    for (original_id, version_id) in &resolved_linkage.linkage {
422        match resolution_table.resolution_table.get(original_id) {
423            None => {
424                add_and_unify(
425                    version_id,
426                    store,
427                    resolution_table,
428                    VersionConstraint::exact,
429                )?;
430            }
431            Some(existing) if existing.object_id() == *version_id => {
432                if protocol_config.harden_linkage_consistency() {
433                    add_and_unify(
434                        version_id,
435                        store,
436                        resolution_table,
437                        VersionConstraint::exact,
438                    )?;
439                }
440            }
441            Some(existing) => {
442                return Err(E::new_with_source(
443                    ExecutionErrorKind::InvalidLinkage,
444                    format!(
445                        "upgrade init linkage conflicts with transaction linkage: package \
446                         {original_id} resolves to {} in transaction linkage, but upgrade \
447                         linkage requires {version_id}",
448                        existing.object_id(),
449                    ),
450                ));
451            }
452        }
453    }
454
455    Ok(())
456}
457
458/// Add a `MoveCall`'s target package and type-argument packages to the resolution table.
459///
460/// The called package itself is pinned `exact` (we must run exactly the version being called). Its
461/// dependencies are constrained by the callee's visibility: a public entrypoint is a stable ABI,
462/// so its dependencies may be upgraded (`at_least`); a private/`friend` entrypoint is not, so they
463/// are pinned `exact`. Type-argument packages are always `at_least`, since types resolve upwards
464/// to later versions. This mirrors `LinkageAnalyzer::compute_call_linkage_`.
465fn add_call_to_table<E: ExecutionErrorTrait>(
466    resolution_table: &mut ResolutionTable,
467    function: &LoadedFunction,
468    store: &VerifiedPackageStore<'_>,
469) -> Result<(), E> {
470    let dep_resolution_fn = match function.visibility {
471        Visibility::Public => VersionConstraint::at_least,
472        Visibility::Private | Visibility::Friend => VersionConstraint::exact,
473    };
474    let package: ObjectID = (*function.version_mid.address()).into();
475    add_package::<E>(
476        &package,
477        store,
478        resolution_table,
479        VersionConstraint::exact,
480        dep_resolution_fn,
481    )?;
482    add_type_packages::<E>(resolution_table, function.type_arguments.iter(), store)
483}
484
485/// Resolve every package mentioned by `types`. Types resolve upwards to later versions, so the
486/// package and its deps are both `at_least`.
487fn add_type_packages<'a, E: ExecutionErrorTrait>(
488    resolution_table: &mut ResolutionTable,
489    types: impl IntoIterator<Item = &'a Type>,
490    store: &VerifiedPackageStore<'_>,
491) -> Result<(), E> {
492    for type_defining_id in types.into_iter().flat_map(|ty| ty.all_addresses()) {
493        add_package::<E>(
494            &ObjectID::from(type_defining_id),
495            store,
496            resolution_table,
497            VersionConstraint::at_least,
498            VersionConstraint::at_least,
499        )?;
500    }
501    Ok(())
502}
503
504/// Add a package and its transitive dependencies to the resolution table. The package itself
505/// gets `self_resolution_fn`'s constraint; every transitive dep (per the package's linkage
506/// table) gets `dep_resolution_fn`'s constraint.
507fn add_package<E: ExecutionErrorTrait>(
508    object_id: &ObjectID,
509    store: &VerifiedPackageStore<'_>,
510    resolution_table: &mut ResolutionTable,
511    self_resolution_fn: fn(&Arc<VerifiedPackage>) -> Option<VersionConstraint>,
512    dep_resolution_fn: fn(&Arc<VerifiedPackage>) -> Option<VersionConstraint>,
513) -> Result<(), E> {
514    let pkg = get_package(object_id, store)?;
515    let transitive_deps = resolution_table
516        .config
517        .linkage_table(&pkg)
518        .into_values()
519        .map(ObjectID::from);
520    add_and_unify(object_id, store, resolution_table, self_resolution_fn)?;
521    for dep_id in transitive_deps {
522        add_and_unify(&dep_id, store, resolution_table, dep_resolution_fn)?;
523    }
524    Ok(())
525}
526
527/// Overwrite each `MoveCall`'s per-call linkage with the unified transaction-wide linkage (pass 2).
528/// Only `MoveCall`s carry an executable linkage; the other commands need no write-back.
529fn write_back_linkage<E: ExecutionErrorTrait>(
530    command: &mut Command,
531    ptb_linkage: &ExecutableLinkage,
532) -> Result<(), E> {
533    match command {
534        Command::MoveCall(move_call) => {
535            let previous_linkage = &move_call.function.linkage;
536            // Stronger than the length check above: every package the per-call linkage resolved
537            // must still be present in the per-component linkage. Unification only ever adds
538            // packages (the key set is a union across member calls), so a dropped key signals a
539            // bug in how component constraints were folded together.
540            //
541            // Since `linkage`'s keys are a set, this check also implies that
542            // `previous_linkage.0.linkage.len() <= ptb_linkage.0.linkage.len()`.
543            assert_invariant!(
544                previous_linkage
545                    .0
546                    .linkage
547                    .keys()
548                    .all(|k| ptb_linkage.0.linkage.contains_key(k)),
549                "single linkage drops a package that the per-call linkage of MoveCall had resolved"
550            );
551            debug_assert!(
552                previous_linkage.0.linkage.len() <= ptb_linkage.0.linkage.len(),
553                "single linkage has fewer candidates than the per-call linkage of MoveCall"
554            );
555            move_call.function.linkage = ptb_linkage.clone();
556        }
557        Command::TransferObjects(_, _)
558        | Command::SplitCoins(_, _)
559        | Command::MergeCoins(_, _)
560        | Command::MakeMoveVec(_, _)
561        | Command::Publish(_, _, _)
562        | Command::Upgrade(_, _, _, _, _) => (),
563    };
564    Ok(())
565}