Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10/x][programmable transactions] Add migration and builder #8769

Merged
merged 3 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/sui-core/src/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/sui-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/sui-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 33 additions & 4 deletions crates/sui-types/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,30 @@ impl ProgrammableTransaction {
}
Ok(())
}

fn shared_input_objects(&self) -> impl Iterator<Item = SharedInputObject> + '_ {
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,
}]),
CallArg::ObjVec(_) => {
panic!(
"not supported in programmable transactions, \
should be unreachable if the input checker was run"
)
}
})
.flatten()
}
}

impl Display for Argument {
Expand Down Expand Up @@ -900,7 +924,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())),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should implement all dynamic dispatch like this

}
}

Expand Down Expand Up @@ -961,7 +988,8 @@ impl SingleTransactionKind {
}
}

pub fn move_call(&self) -> Option<&MoveCall> {
/// Actively being replaced by programmable transactions
pub fn legacy_move_call(&self) -> Option<&MoveCall> {
match &self {
Self::Call(call @ MoveCall { .. }) => Some(call),
_ => None,
Expand Down Expand Up @@ -1593,10 +1621,11 @@ impl TransactionData {
self.kind.shared_input_objects()
}

pub fn move_calls(&self) -> Vec<&MoveCall> {
/// Actively being replaced by programmable transactions
pub fn legacy_move_calls(&self) -> Vec<&MoveCall> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a document here explaining what's legacy about these move calls, and why you may not want to use this anymore, after programmable transactions takes over the world (i.e. what goes wrong in that case, if you keep using this function).

self.kind
.single_transactions()
.flat_map(|s| s.move_call())
.flat_map(|s| s.legacy_move_call())
.collect()
}

Expand Down
249 changes: 249 additions & 0 deletions crates/sui-types/src/programmable_transaction_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Copyright (c) Mysten Labs, Inc.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestions where this should live?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's only temporary, I don't think we should worry too hard about this -- but this doesn't seem like a bad place for it since SingleTransactionKind, and ProgrammableTransaction are both in a sibling module.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having said that, you may want to re-use the builder without the migration logic in the SDK? cc @patrickkuo

// SPDX-License-Identifier: Apache-2.0

//! 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;

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<TransactionData> {
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<CallArg>,
commands: Vec<Command>,
}

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<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> {
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)).unwrap()
}

pub fn input(&mut self, call_arg: CallArg) -> anyhow::Result<Argument> {
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<Item = ObjectArg>) -> 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 {
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!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an existing limitation of PaySui and PayAllSui (that they can't exist in a batch) or a new limitation from programmable transactions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You cannot map PaySui into a SingleTransacitonKind, but only TransactionData because the coins need to be in the gas vector with gas smashing

"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<TypeTag>,
call_args: Vec<CallArg>,
) -> anyhow::Result<()> {
let arguments = call_args
.into_iter()
.map(|a| self.input(a))
.collect::<Result<_, _>>()?;
self.command(Command::MoveCall(Box::new(ProgrammableMoveCall {
package,
module,
function,
type_arguments,
arguments,
})));
Ok(())
}

pub fn publish(&mut self, modules: Vec<Vec<u8>>) {
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<u64>) {
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<SuiAddress>,
amounts: Vec<u64>,
) -> 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<ObjectRef>,
recipients: Vec<SuiAddress>,
amounts: Vec<u64>,
) -> anyhow::Result<()> {
let mut coins = coins.into_iter();
let Some(coin) = coins.next()
else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Funky formatting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the RFC formatting for let else

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the RFC formatting for this let-else would be

let Some(coin) = coins.next() else {
    anyhow::bail!("coins vector is empty");
};

According to this section

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I've got to say, the rules as written in the RFC are super ambiguous in general, and I for one welcome our rustfmt overloads, whenever they choose to impose their iron will on let-else. Until then, I don't have a strong opposition to this formatting, it just scanned strangely for me.

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<SuiAddress>,
amounts: Vec<u64>,
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(())
}
}