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