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            Command, DeserializedPackage, LoadedFunction, PackagePayload, Transaction, Type,
14        },
15    },
16};
17use move_binary_format::{CompiledModule, file_format::Visibility};
18use move_vm_runtime::validation::verification::ast::Package as VerifiedPackage;
19use std::{collections::BTreeMap, sync::Arc};
20use sui_protocol_config::ProtocolConfig;
21use sui_types::{
22    base_types::ObjectID,
23    error::ExecutionErrorTrait,
24    execution_status::{ExecutionErrorKind, PackageUpgradeError},
25};
26use sui_verifier::INIT_FN_NAME;
27
28/// Replace each command's per-call linkage with a single linkage shared by the whole transaction.
29///
30/// Done in two passes:
31///   1. Fold every command's package and type-argument constraints into one `ResolutionTable`,
32///      unifying as we go (an error here means the commands cannot agree on a single set of
33///      package versions).
34///      - Top level functions are pinned `exact`, while their dependencies are
35///        pinned `exact` or `at_least` based on the visibility of the top-level function.
36///        Type-argument packages are always `at_least`.
37///      - Publishes and upgrades introduce their own constraints to the linkage, but only if
38///        they have an `init` function (otherwise they do not contribute to the linkage). See
39///        comments on each of the command arms for details on this.
40///   2. Write the resulting unified linkage back into every `MoveCall`.
41///
42/// Because all calls end up sharing one linkage, every package version selection is consistent
43/// across the transaction.
44pub fn refine_to_single_linkage<E: ExecutionErrorTrait>(
45    txn: &mut Transaction,
46    linkage_analysis: &LinkageAnalyzer,
47    package_store: &VerifiedPackageStore<'_>,
48    protocol_config: &ProtocolConfig,
49) -> Result<(), E> {
50    let mut base_linkage = linkage_analysis
51        .config()
52        .resolution_table_with_native_packages::<E, _>(package_store)?;
53
54    for (i, command) in txn.commands.iter().enumerate() {
55        analyze_command::<E>(command, &mut base_linkage, package_store, protocol_config)
56            .map_err(|e| e.with_command_index(i))?;
57    }
58
59    if protocol_config.enable_order_independent_upgrade_init_linkage() {
60        for (i, command) in txn.commands.iter().enumerate() {
61            let Command::Upgrade(payload, _, current_package_id, _, resolved_linkage) = command
62            else {
63                continue;
64            };
65            analyze_upgrade_command::<E>(
66                payload,
67                current_package_id,
68                resolved_linkage,
69                &mut base_linkage,
70                package_store,
71                protocol_config,
72            )
73            .map_err(|e| e.with_command_index(i))?;
74        }
75    }
76    let resolved_linkage =
77        ExecutableLinkage::new(ResolvedLinkage::from_resolution_table(base_linkage));
78
79    for (i, command) in txn.commands.iter_mut().enumerate() {
80        write_back_linkage::<E>(command, &resolved_linkage).map_err(|e| e.with_command_index(i))?;
81    }
82
83    Ok(())
84}
85
86/// Fold a single command's contribution into the shared `resolution_table` (pass 1). Only commands
87/// that pull packages into the runtime linkage contribute; the rest are no-ops.
88fn analyze_command<E: ExecutionErrorTrait>(
89    command: &Command,
90    resolution_table: &mut ResolutionTable,
91    store: &VerifiedPackageStore<'_>,
92    protocol_config: &ProtocolConfig,
93) -> Result<(), E> {
94    match command {
95        Command::MoveCall(move_call) => {
96            add_call_to_table::<E>(resolution_table, &move_call.function, store)?;
97        }
98        Command::Publish(PackagePayload::Serialized(_), ..) => {
99            invariant_violation!("Unexpected serialized package payload in linkage analysis")
100        }
101        Command::Publish(
102            PackagePayload::Deserialized(DeserializedPackage {
103                deserialized_modules,
104                ..
105            }),
106            _,
107            resolved_linkage,
108        ) => {
109            // A publish only affects the transaction's linkage if the package has an `init`
110            // function: `init` runs as part of the publish, so its dependencies must be resolvable
111            // in this transaction. Without an `init` the freshly published package is not called
112            // and contributes nothing.
113            //
114            // NB: We presuppose here that if there is a function with the name "init" in the
115            // modules being published, then it is the init function for the package.
116            //
117            // If for some reason it is not (i.e., does not conform to `init` function signature
118            // requirements), the entry points verifier will the publish later, and the transaction
119            // as a whole will error.
120            //
121            // `modules` is guaranteed to be non-empty by the `deserialize_modules` function.
122            if deserialized_modules.iter().any(module_has_init) {
123                for resolved in resolved_linkage.linkage.values() {
124                    add_and_unify(resolved, store, resolution_table, VersionConstraint::exact)?;
125                }
126            }
127        }
128        Command::Upgrade(_, _, _, _, _)
129            if protocol_config.enable_order_independent_upgrade_init_linkage() => {}
130        Command::Upgrade(payload, _, current_package_id, _, resolved_linkage) => {
131            analyze_upgrade_command::<E>(
132                payload,
133                current_package_id,
134                resolved_linkage,
135                resolution_table,
136                store,
137                protocol_config,
138            )?;
139        }
140        Command::MakeMoveVec(Some(ty), _) => {
141            add_type_packages::<E>(resolution_table, std::iter::once(ty), store)?;
142        }
143        Command::MakeMoveVec(None, _) => (),
144        Command::TransferObjects(_, _) | Command::SplitCoins(_, _) | Command::MergeCoins(_, _) => {}
145    };
146    Ok(())
147}
148
149/// Analyze the linkage contribution of an upgrade command.
150fn analyze_upgrade_command<E: ExecutionErrorTrait>(
151    payload: &PackagePayload,
152    current_package_id: &ObjectID,
153    resolved_linkage: &ResolvedLinkage,
154    resolution_table: &mut ResolutionTable,
155    store: &VerifiedPackageStore<'_>,
156    protocol_config: &ProtocolConfig,
157) -> Result<(), E> {
158    if !protocol_config.enable_init_on_upgrade() {
159        return Ok(());
160    }
161
162    let current_pkg = get_package(current_package_id, store)?;
163
164    assert_invariant!(
165        protocol_config.enable_unified_linkage(),
166        "Unified linkage must be enabled before init on upgrade is supported"
167    );
168
169    let new_modules = match payload {
170        PackagePayload::Serialized(_) => {
171            invariant_violation!("Unexpected serialized package payload in linkage analysis")
172        }
173        PackagePayload::Deserialized(DeserializedPackage {
174            deserialized_modules,
175            ..
176        }) => deserialized_modules,
177    };
178
179    // Whether each module already present in the current package defines an `init`.
180    let current_module_inits = current_pkg
181        .modules()
182        .iter()
183        .map(|(module_id, module)| {
184            (
185                module_id.name().as_str(),
186                module_has_init(module.compiled_module()),
187            )
188        })
189        .collect::<BTreeMap<_, _>>();
190
191    // reject upgrades where an existing module adds an `init`.
192    reject_existing_module_added_init::<E>(&current_module_inits, new_modules)?;
193
194    // only newly-introduced modules with an `init` contribute to the linkage.
195    if has_new_module_init(&current_module_inits, new_modules) {
196        add_upgrade_init_linkage_to_table::<E>(
197            resolution_table,
198            current_package_id,
199            resolved_linkage,
200            store,
201        )?;
202    }
203
204    Ok(())
205}
206
207/// Reject an upgrade in which a module that already exists in the current package (and did not
208/// previously define an `init`) introduces one.
209fn reject_existing_module_added_init<E: ExecutionErrorTrait>(
210    current_module_inits: &BTreeMap<&str, bool>,
211    new_modules: &[CompiledModule],
212) -> Result<(), E> {
213    for new_module in new_modules {
214        let module_name = new_module
215            .identifier_at(new_module.self_handle().name)
216            .as_str();
217        if current_module_inits.get(module_name) == Some(&false) && module_has_init(new_module) {
218            return Err(<E>::from_kind(ExecutionErrorKind::PackageUpgradeError {
219                upgrade_error: PackageUpgradeError::IncompatibleUpgrade,
220            }));
221        }
222    }
223    Ok(())
224}
225
226/// Return true if the upgrade introduces at least one new module (absent from the current package)
227/// that defines an `init` function. Existing modules never count (rejected by `reject_existing_module_added_init`).
228fn has_new_module_init(
229    current_module_inits: &BTreeMap<&str, bool>,
230    new_modules: &[CompiledModule],
231) -> bool {
232    new_modules.iter().any(|new_module| {
233        let module_name = new_module
234            .identifier_at(new_module.self_handle().name)
235            .as_str();
236        current_module_inits.get(module_name).is_none() && module_has_init(new_module)
237    })
238}
239
240fn module_has_init(module: &CompiledModule) -> bool {
241    module.function_defs().iter().any(|func_def| {
242        let handle = module.function_handle_at(func_def.function);
243        module.identifier_at(handle.name) == INIT_FN_NAME
244    })
245}
246
247/// Add the linkage constraints introduced by an upgrade, there are two cases based on whether the
248/// upgraded package already participates in the transaction-wide (Lumpy) linkage:
249///
250/// - If the upgraded package's original id is not already in the resolution table, the upgrade
251///   is treated like a fresh publish-with-init: every entry of its resolved linkage is added as an
252///   `exact` constraint.
253/// - If the upgraded package's original id is in the resolution table, then for any `(original_id,
254///   version_id)` as defined in the `Upgrade` command either:
255///   a. It is not in the existing Lumpy linkage, and a `original_id -> exact(version_id)` constraint is introduced; or
256///   b. It is in the existing Lumpy linkage, in which case Lumpy[original_id].id must equal `version_id`.
257fn add_upgrade_init_linkage_to_table<E: ExecutionErrorTrait>(
258    resolution_table: &mut ResolutionTable,
259    current_package_id: &ObjectID,
260    resolved_linkage: &ResolvedLinkage,
261    store: &VerifiedPackageStore<'_>,
262) -> Result<(), E> {
263    let current_pkg = get_package(current_package_id, store)?;
264    let pkg_original_id: ObjectID = current_pkg.original_id().into();
265
266    if !resolution_table
267        .resolution_table
268        .contains_key(&pkg_original_id)
269    {
270        for resolved in resolved_linkage.linkage.values() {
271            add_and_unify(resolved, store, resolution_table, VersionConstraint::exact)?;
272        }
273        return Ok(());
274    }
275
276    for (original_id, version_id) in &resolved_linkage.linkage {
277        match resolution_table.resolution_table.get(original_id) {
278            None => {
279                add_and_unify(
280                    version_id,
281                    store,
282                    resolution_table,
283                    VersionConstraint::exact,
284                )?;
285            }
286            Some(existing) if existing.object_id() == *version_id => (),
287            Some(existing) => {
288                return Err(E::new_with_source(
289                    ExecutionErrorKind::InvalidLinkage,
290                    format!(
291                        "upgrade init linkage conflicts with transaction linkage: package \
292                         {original_id} resolves to {} in transaction linkage, but upgrade \
293                         linkage requires {version_id}",
294                        existing.object_id(),
295                    ),
296                ));
297            }
298        }
299    }
300
301    Ok(())
302}
303
304/// Add a `MoveCall`'s target package and type-argument packages to the resolution table.
305///
306/// The called package itself is pinned `exact` (we must run exactly the version being called). Its
307/// dependencies are constrained by the callee's visibility: a public entrypoint is a stable ABI,
308/// so its dependencies may be upgraded (`at_least`); a private/`friend` entrypoint is not, so they
309/// are pinned `exact`. Type-argument packages are always `at_least`, since types resolve upwards
310/// to later versions. This mirrors `LinkageAnalyzer::compute_call_linkage_`.
311fn add_call_to_table<E: ExecutionErrorTrait>(
312    resolution_table: &mut ResolutionTable,
313    function: &LoadedFunction,
314    store: &VerifiedPackageStore<'_>,
315) -> Result<(), E> {
316    let dep_resolution_fn = match function.visibility {
317        Visibility::Public => VersionConstraint::at_least,
318        Visibility::Private | Visibility::Friend => VersionConstraint::exact,
319    };
320    let package: ObjectID = (*function.version_mid.address()).into();
321    add_package::<E>(
322        &package,
323        store,
324        resolution_table,
325        VersionConstraint::exact,
326        dep_resolution_fn,
327    )?;
328    add_type_packages::<E>(resolution_table, function.type_arguments.iter(), store)
329}
330
331/// Resolve every package mentioned by `types`. Types resolve upwards to later versions, so the
332/// package and its deps are both `at_least`.
333fn add_type_packages<'a, E: ExecutionErrorTrait>(
334    resolution_table: &mut ResolutionTable,
335    types: impl IntoIterator<Item = &'a Type>,
336    store: &VerifiedPackageStore<'_>,
337) -> Result<(), E> {
338    for type_defining_id in types.into_iter().flat_map(|ty| ty.all_addresses()) {
339        add_package::<E>(
340            &ObjectID::from(type_defining_id),
341            store,
342            resolution_table,
343            VersionConstraint::at_least,
344            VersionConstraint::at_least,
345        )?;
346    }
347    Ok(())
348}
349
350/// Add a package and its transitive dependencies to the resolution table. The package itself
351/// gets `self_resolution_fn`'s constraint; every transitive dep (per the package's linkage
352/// table) gets `dep_resolution_fn`'s constraint.
353fn add_package<E: ExecutionErrorTrait>(
354    object_id: &ObjectID,
355    store: &VerifiedPackageStore<'_>,
356    resolution_table: &mut ResolutionTable,
357    self_resolution_fn: fn(&Arc<VerifiedPackage>) -> Option<VersionConstraint>,
358    dep_resolution_fn: fn(&Arc<VerifiedPackage>) -> Option<VersionConstraint>,
359) -> Result<(), E> {
360    let pkg = get_package(object_id, store)?;
361    let transitive_deps = resolution_table
362        .config
363        .linkage_table(&pkg)
364        .into_values()
365        .map(ObjectID::from);
366    add_and_unify(object_id, store, resolution_table, self_resolution_fn)?;
367    for dep_id in transitive_deps {
368        add_and_unify(&dep_id, store, resolution_table, dep_resolution_fn)?;
369    }
370    Ok(())
371}
372
373/// Overwrite each `MoveCall`'s per-call linkage with the unified transaction-wide linkage (pass 2).
374/// Only `MoveCall`s carry an executable linkage; the other commands need no write-back.
375fn write_back_linkage<E: ExecutionErrorTrait>(
376    command: &mut Command,
377    ptb_linkage: &ExecutableLinkage,
378) -> Result<(), E> {
379    match command {
380        Command::MoveCall(move_call) => {
381            let previous_linkage = &move_call.function.linkage;
382            // Stronger than the length check above: every package the per-call linkage resolved
383            // must still be present in the per-component linkage. Unification only ever adds
384            // packages (the key set is a union across member calls), so a dropped key signals a
385            // bug in how component constraints were folded together.
386            //
387            // Since `linkage`'s keys are a set, this check also implies that
388            // `previous_linkage.0.linkage.len() <= ptb_linkage.0.linkage.len()`.
389            assert_invariant!(
390                previous_linkage
391                    .0
392                    .linkage
393                    .keys()
394                    .all(|k| ptb_linkage.0.linkage.contains_key(k)),
395                "single linkage drops a package that the per-call linkage of MoveCall had resolved"
396            );
397            debug_assert!(
398                previous_linkage.0.linkage.len() <= ptb_linkage.0.linkage.len(),
399                "single linkage has fewer candidates than the per-call linkage of MoveCall"
400            );
401            move_call.function.linkage = ptb_linkage.clone();
402        }
403        Command::TransferObjects(_, _)
404        | Command::SplitCoins(_, _)
405        | Command::MergeCoins(_, _)
406        | Command::MakeMoveVec(_, _)
407        | Command::Publish(_, _, _)
408        | Command::Upgrade(_, _, _, _, _) => (),
409    };
410    Ok(())
411}