From ae6ab7fdca39661565ab04c1a702c5fc201520a5 Mon Sep 17 00:00:00 2001 From: Todd Nowacki Date: Mon, 27 Feb 2023 15:53:17 -0700 Subject: [PATCH 1/3] [10/x][programmable transactions] Add migration and builder - Added a programmable transaction builder - APIs are there mostly for migration --- Cargo.lock | 1 + crates/sui-core/src/authority.rs | 2 +- crates/sui-types/Cargo.toml | 1 + crates/sui-types/src/lib.rs | 1 + crates/sui-types/src/messages.rs | 31 ++- .../src/programmable_transaction_builder.rs | 244 ++++++++++++++++++ 6 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 crates/sui-types/src/programmable_transaction_builder.rs diff --git a/Cargo.lock b/Cargo.lock index 2e1e54056b06d..06ff86bdb4b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9429,6 +9429,7 @@ dependencies = [ "enum_dispatch", "eyre", "fastcrypto", + "indexmap", "itertools", "move-binary-format", "move-bytecode-utils", diff --git a/crates/sui-core/src/authority.rs b/crates/sui-core/src/authority.rs index 2745a0c07dfa9..4cc976897f8fd 100644 --- a/crates/sui-core/src/authority.rs +++ b/crates/sui-core/src/authority.rs @@ -1158,7 +1158,7 @@ impl AuthorityState { cert.data() .intent_message .value - .move_calls() + .legacy_move_calls() .iter() .map(|mc| (mc.package, mc.module.clone(), mc.function.clone())), changes, diff --git a/crates/sui-types/Cargo.toml b/crates/sui-types/Cargo.toml index 986b7e0a6ea85..b569f82f55472 100644 --- a/crates/sui-types/Cargo.toml +++ b/crates/sui-types/Cargo.toml @@ -36,6 +36,7 @@ roaring = "0.10.1" enum_dispatch = "^0.3" eyre = "0.6.8" multiaddr = "0.17.0" +indexmap = "1.9.2" move-binary-format.workspace = true move-bytecode-utils.workspace = true diff --git a/crates/sui-types/src/lib.rs b/crates/sui-types/src/lib.rs index c2ae7fbae6b96..79e19b8dac83d 100644 --- a/crates/sui-types/src/lib.rs +++ b/crates/sui-types/src/lib.rs @@ -45,6 +45,7 @@ pub mod messages_checkpoint; pub mod move_package; pub mod multisig; pub mod object; +pub mod programmable_transaction_builder; pub mod query; pub mod quorum_driver_types; pub mod signature; diff --git a/crates/sui-types/src/messages.rs b/crates/sui-types/src/messages.rs index 1682c7650f1e9..29e5429e2bd77 100644 --- a/crates/sui-types/src/messages.rs +++ b/crates/sui-types/src/messages.rs @@ -794,6 +794,26 @@ impl ProgrammableTransaction { } Ok(()) } + + fn shared_input_objects(&self) -> impl Iterator + '_ { + self.inputs + .iter() + .filter_map(|arg| match arg { + CallArg::Pure(_) | CallArg::Object(ObjectArg::ImmOrOwnedObject(_)) => None, + CallArg::Object(ObjectArg::SharedObject { + id, + initial_shared_version, + mutable, + }) => Some(vec![SharedInputObject { + id: *id, + initial_shared_version: *initial_shared_version, + mutable: *mutable, + }]), + // Not supported in programmable transactions + CallArg::ObjVec(_) => None, + }) + .flatten() + } } impl Display for Argument { @@ -900,7 +920,10 @@ impl SingleTransactionKind { Self::Call(_) | Self::ChangeEpoch(_) | Self::ConsensusCommitPrologue(_) => { Either::Left(self.all_move_call_shared_input_objects()) } - _ => Either::Right(iter::empty()), + Self::ProgrammableTransaction(pt) => { + Either::Right(Either::Left(pt.shared_input_objects())) + } + _ => Either::Right(Either::Right(iter::empty())), } } @@ -961,7 +984,7 @@ impl SingleTransactionKind { } } - pub fn move_call(&self) -> Option<&MoveCall> { + pub fn legacy_move_call(&self) -> Option<&MoveCall> { match &self { Self::Call(call @ MoveCall { .. }) => Some(call), _ => None, @@ -1593,10 +1616,10 @@ impl TransactionData { self.kind.shared_input_objects() } - pub fn move_calls(&self) -> Vec<&MoveCall> { + pub fn legacy_move_calls(&self) -> Vec<&MoveCall> { self.kind .single_transactions() - .flat_map(|s| s.move_call()) + .flat_map(|s| s.legacy_move_call()) .collect() } diff --git a/crates/sui-types/src/programmable_transaction_builder.rs b/crates/sui-types/src/programmable_transaction_builder.rs new file mode 100644 index 0000000000000..9db8665636c0c --- /dev/null +++ b/crates/sui-types/src/programmable_transaction_builder.rs @@ -0,0 +1,244 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Utility for generating programmable transactions, either by specifying a command or for +//! migrating legacy transactions + +use indexmap::IndexSet; +use move_core_types::{identifier::Identifier, language_storage::TypeTag}; +use serde::Serialize; + +use crate::{ + base_types::{ObjectID, ObjectRef, SuiAddress}, + messages::{ + Argument, CallArg, Command, MoveCall, MoveModulePublish, ObjectArg, Pay, PayAllSui, PaySui, + ProgrammableMoveCall, ProgrammableTransaction, SingleTransactionKind, TransactionData, + TransactionKind, TransferObject, TransferSui, + }, +}; + +pub fn migrate_transaction_data(mut m: TransactionData) -> anyhow::Result { + let mut builder = ProgrammableTransactionBuilder::new(); + match m.kind { + TransactionKind::Single(SingleTransactionKind::PaySui(PaySui { + coins: _coins, + recipients, + amounts, + })) => { + builder.pay_sui(recipients, amounts)?; + anyhow::bail!("blocked by gas smashing") + } + TransactionKind::Single(SingleTransactionKind::PayAllSui(PayAllSui { + coins: _coins, + recipient, + })) => { + builder.pay_all_sui(recipient); + anyhow::bail!("blocked by gas smashing") + } + TransactionKind::Single(t) => builder.single_transaction(t)?, + TransactionKind::Batch(ts) => { + for t in ts { + builder.single_transaction(t)? + } + } + }; + let pt = builder.finish(); + m.kind = TransactionKind::Single(SingleTransactionKind::ProgrammableTransaction(pt)); + Ok(m) +} + +#[derive(Default)] +pub struct ProgrammableTransactionBuilder { + inputs: IndexSet, + commands: Vec, +} + +impl ProgrammableTransactionBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn finish(self) -> ProgrammableTransaction { + let Self { inputs, commands } = self; + let inputs = inputs.into_iter().collect(); + ProgrammableTransaction { inputs, commands } + } + + pub fn pure(&mut self, value: T) -> anyhow::Result { + Ok(self.input(CallArg::Pure(bcs::to_bytes(&value).map_err(|e| { + anyhow::anyhow!("Unable to serialize pure argument. Got error: {e}") + })?))) + } + + pub fn obj(&mut self, obj_arg: ObjectArg) -> Argument { + self.input(CallArg::Object(obj_arg)) + } + + pub fn input(&mut self, call_arg: CallArg) -> Argument { + assert!(!matches!(call_arg, CallArg::ObjVec(_))); + Argument::Input(self.inputs.insert_full(call_arg).0 as u16) + } + + pub fn command(&mut self, command: Command) -> Argument { + let i = self.commands.len(); + self.commands.push(command); + Argument::Result(i as u16) + } + + pub fn single_transaction(&mut self, t: SingleTransactionKind) -> anyhow::Result<()> { + match t { + SingleTransactionKind::ProgrammableTransaction(_) => anyhow::bail!( + "ProgrammableTransaction are not supported in ProgrammableTransactionBuilder" + ), + SingleTransactionKind::TransferObject(TransferObject { + recipient, + object_ref, + }) => self.transfer_object(recipient, object_ref), + SingleTransactionKind::Publish(MoveModulePublish { modules }) => self.publish(modules), + SingleTransactionKind::Call(MoveCall { + package, + module, + function, + type_arguments, + arguments, + }) => self.move_call(package, module, function, type_arguments, arguments)?, + SingleTransactionKind::TransferSui(TransferSui { recipient, amount }) => { + self.transfer_sui(recipient, amount) + } + SingleTransactionKind::Pay(Pay { + coins, + recipients, + amounts, + }) => self.pay(coins, recipients, amounts)?, + SingleTransactionKind::PaySui(_) | SingleTransactionKind::PayAllSui(_) => { + anyhow::bail!( + "PaySui and PayAllSui cannot be migrated as a single transaction kind, \ + only as a full transaction" + ) + } + SingleTransactionKind::ChangeEpoch(_) + | SingleTransactionKind::Genesis(_) + | SingleTransactionKind::ConsensusCommitPrologue(_) => anyhow::bail!( + "System transactions are not expressed with programmable transactions" + ), + }; + Ok(()) + } + + /// Will fail to generate if given an empty ObjVec + pub fn move_call( + &mut self, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + call_args: Vec, + ) -> anyhow::Result<()> { + let mut arguments = vec![]; + for call_arg in call_args { + let arg = match call_arg { + call_arg @ (CallArg::Pure(_) | CallArg::Object(_)) => self.input(call_arg), + CallArg::ObjVec(objs) if objs.is_empty() => { + anyhow::bail!( + "Empty ObjVec is not supported in programmable transactions \ + without a type annotation" + ) + } + CallArg::ObjVec(objs) => { + let make_vec_args = objs.into_iter().map(|obj| self.obj(obj)).collect(); + self.command(Command::MakeMoveVec(None, make_vec_args)) + } + }; + arguments.push(arg) + } + self.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module, + function, + type_arguments, + arguments, + }))); + Ok(()) + } + + pub fn publish(&mut self, modules: Vec>) { + self.commands.push(Command::Publish(modules)) + } + + pub fn transfer_object(&mut self, recipient: SuiAddress, object_ref: ObjectRef) { + let rec_arg = self.pure(recipient).unwrap(); + let obj_arg = self.obj(ObjectArg::ImmOrOwnedObject(object_ref)); + self.commands + .push(Command::TransferObjects(vec![obj_arg], rec_arg)); + } + + pub fn transfer_sui(&mut self, recipient: SuiAddress, amount: Option) { + let rec_arg = self.pure(recipient).unwrap(); + let coin_arg = if let Some(amount) = amount { + let amt_arg = self.pure(amount).unwrap(); + self.command(Command::SplitCoin(Argument::GasCoin, amt_arg)) + } else { + Argument::GasCoin + }; + self.command(Command::TransferObjects(vec![coin_arg], rec_arg)); + } + + pub fn pay_all_sui(&mut self, recipient: SuiAddress) { + let rec_arg = self.pure(recipient).unwrap(); + self.command(Command::TransferObjects(vec![Argument::GasCoin], rec_arg)); + } + + /// Will fail to generate if recipients and amounts do not have the same lengths + pub fn pay_sui( + &mut self, + recipients: Vec, + amounts: Vec, + ) -> anyhow::Result<()> { + self.pay_impl(recipients, amounts, Argument::GasCoin) + } + + /// Will fail to generate if recipients and amounts do not have the same lengths. + /// Or if coins is empty + pub fn pay( + &mut self, + coins: Vec, + recipients: Vec, + amounts: Vec, + ) -> anyhow::Result<()> { + let mut coins = coins.into_iter(); + let Some(coin) = coins.next() + else { + anyhow::bail!("coins vector is empty"); + }; + let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin)); + let merge_args: Vec<_> = coins + .map(|c| self.obj(ObjectArg::ImmOrOwnedObject(c))) + .collect(); + if !merge_args.is_empty() { + self.command(Command::MergeCoins(coin_arg, merge_args)); + } + self.pay_impl(recipients, amounts, coin_arg) + } + + fn pay_impl( + &mut self, + recipients: Vec, + amounts: Vec, + coin: Argument, + ) -> anyhow::Result<()> { + if recipients.len() != amounts.len() { + anyhow::bail!( + "Recipients and amounts mismatch. Got {} recipients but {} amounts", + recipients.len(), + amounts.len() + ) + } + for (recipient, amount) in recipients.into_iter().zip(amounts) { + let rec_arg = self.pure(recipient).unwrap(); + let amt_arg = self.pure(amount).unwrap(); + let coin_arg = self.command(Command::SplitCoin(coin, amt_arg)); + self.command(Command::TransferObjects(vec![coin_arg], rec_arg)); + } + Ok(()) + } +} From 29902319085b19c8bb3e11371108ed155048cb8f Mon Sep 17 00:00:00 2001 From: Todd Nowacki Date: Thu, 2 Mar 2023 11:39:57 -0700 Subject: [PATCH 2/3] Fixed ObjVec usage to be more general --- crates/sui-types/src/messages.rs | 8 ++- .../src/programmable_transaction_builder.rs | 53 ++++++++++--------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/crates/sui-types/src/messages.rs b/crates/sui-types/src/messages.rs index 29e5429e2bd77..32ac2389f8890 100644 --- a/crates/sui-types/src/messages.rs +++ b/crates/sui-types/src/messages.rs @@ -809,8 +809,12 @@ impl ProgrammableTransaction { initial_shared_version: *initial_shared_version, mutable: *mutable, }]), - // Not supported in programmable transactions - CallArg::ObjVec(_) => None, + CallArg::ObjVec(_) => { + panic!( + "not supported in programmable transactions, \ + should be unreachable if the input checker was run" + ) + } }) .flatten() } diff --git a/crates/sui-types/src/programmable_transaction_builder.rs b/crates/sui-types/src/programmable_transaction_builder.rs index 9db8665636c0c..eaa4e9eb4f204 100644 --- a/crates/sui-types/src/programmable_transaction_builder.rs +++ b/crates/sui-types/src/programmable_transaction_builder.rs @@ -4,6 +4,7 @@ //! Utility for generating programmable transactions, either by specifying a command or for //! migrating legacy transactions +use anyhow::Context; use indexmap::IndexSet; use move_core_types::{identifier::Identifier, language_storage::TypeTag}; use serde::Serialize; @@ -65,18 +66,35 @@ impl ProgrammableTransactionBuilder { } pub fn pure(&mut self, value: T) -> anyhow::Result { - Ok(self.input(CallArg::Pure(bcs::to_bytes(&value).map_err(|e| { - anyhow::anyhow!("Unable to serialize pure argument. Got error: {e}") - })?))) + Ok(self + .input(CallArg::Pure( + bcs::to_bytes(&value).context("Searlizing pure argument.")?, + )) + .unwrap()) } pub fn obj(&mut self, obj_arg: ObjectArg) -> Argument { - self.input(CallArg::Object(obj_arg)) + self.input(CallArg::Object(obj_arg)).unwrap() } - pub fn input(&mut self, call_arg: CallArg) -> Argument { - assert!(!matches!(call_arg, CallArg::ObjVec(_))); - Argument::Input(self.inputs.insert_full(call_arg).0 as u16) + pub fn input(&mut self, call_arg: CallArg) -> anyhow::Result { + match call_arg { + call_arg @ (CallArg::Pure(_) | CallArg::Object(_)) => { + Ok(Argument::Input(self.inputs.insert_full(call_arg).0 as u16)) + } + CallArg::ObjVec(objs) if objs.is_empty() => { + anyhow::bail!( + "Empty ObjVec is not supported in programmable transactions \ + without a type annotation" + ) + } + CallArg::ObjVec(objs) => Ok(self.make_obj_vec(objs)), + } + } + + pub fn make_obj_vec(&mut self, objs: impl IntoIterator) -> Argument { + let make_vec_args = objs.into_iter().map(|obj| self.obj(obj)).collect(); + self.command(Command::MakeMoveVec(None, make_vec_args)) } pub fn command(&mut self, command: Command) -> Argument { @@ -134,23 +152,10 @@ impl ProgrammableTransactionBuilder { type_arguments: Vec, call_args: Vec, ) -> anyhow::Result<()> { - let mut arguments = vec![]; - for call_arg in call_args { - let arg = match call_arg { - call_arg @ (CallArg::Pure(_) | CallArg::Object(_)) => self.input(call_arg), - CallArg::ObjVec(objs) if objs.is_empty() => { - anyhow::bail!( - "Empty ObjVec is not supported in programmable transactions \ - without a type annotation" - ) - } - CallArg::ObjVec(objs) => { - let make_vec_args = objs.into_iter().map(|obj| self.obj(obj)).collect(); - self.command(Command::MakeMoveVec(None, make_vec_args)) - } - }; - arguments.push(arg) - } + let arguments = call_args + .into_iter() + .map(|a| self.input(a)) + .collect::>()?; self.command(Command::MoveCall(Box::new(ProgrammableMoveCall { package, module, From 806b218bc4d226e929a35c601bc0fd3131c5ec38 Mon Sep 17 00:00:00 2001 From: Todd Nowacki Date: Thu, 2 Mar 2023 12:08:36 -0700 Subject: [PATCH 3/3] fixup! Fixed ObjVec usage to be more general --- crates/sui-types/src/messages.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/sui-types/src/messages.rs b/crates/sui-types/src/messages.rs index 32ac2389f8890..641fd86938537 100644 --- a/crates/sui-types/src/messages.rs +++ b/crates/sui-types/src/messages.rs @@ -988,6 +988,7 @@ impl SingleTransactionKind { } } + /// Actively being replaced by programmable transactions pub fn legacy_move_call(&self) -> Option<&MoveCall> { match &self { Self::Call(call @ MoveCall { .. }) => Some(call), @@ -1620,6 +1621,7 @@ impl TransactionData { self.kind.shared_input_objects() } + /// Actively being replaced by programmable transactions pub fn legacy_move_calls(&self) -> Vec<&MoveCall> { self.kind .single_transactions()