Skip to content

Commit

Permalink
Add a new transaction type that can transfer SUI coin and pay gas wit…
Browse files Browse the repository at this point in the history
…h it (MystenLabs#2403)

* Add TransferSui transaction type

* Add tests

* autogen

* Address feedback
  • Loading branch information
lxfind authored Jun 8, 2022
1 parent 85adec1 commit 82d83e2
Show file tree
Hide file tree
Showing 19 changed files with 1,252 additions and 793 deletions.
2 changes: 1 addition & 1 deletion crates/sui-adapter/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ fn process_successful_execution<
obj.data
.try_as_move_mut()
.expect("We previously checked that mutable ref inputs are Move objects")
.update_contents(new_contents);
.update_contents_and_increment_version(new_contents);
state_view.write_object(obj);
}
let tx_digest = ctx.digest();
Expand Down
7 changes: 1 addition & 6 deletions crates/sui-config/src/genesis_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,7 @@ impl GenesisConfig {
}

for (object_id, value) in preload_objects_map {
let new_object = Object::with_id_owner_gas_coin_object_for_testing(
object_id,
sui_types::base_types::SequenceNumber::new(),
address,
value,
);
let new_object = Object::with_id_owner_gas_for_testing(object_id, address, value);
preload_objects.push(new_object);
}
}
Expand Down
87 changes: 81 additions & 6 deletions crates/sui-core/src/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ use move_core_types::language_storage::ModuleId;
use move_vm_runtime::{move_vm::MoveVM, native_functions::NativeFunctionTable};
use sui_adapter::adapter;
use sui_types::committee::EpochId;
use sui_types::gas_coin::GasCoin;
use sui_types::object::{MoveObject, Owner, OBJECT_START_VERSION};
use sui_types::{
base_types::{ObjectID, ObjectRef, SuiAddress, TransactionDigest, TxContext},
error::SuiResult,
event::{Event, TransferType},
gas::{self, SuiGasStatus},
messages::{
CallArg, ChangeEpoch, ExecutionStatus, MoveCall, MoveModulePublish, SingleTransactionKind,
TransactionData, TransactionEffects, TransferCoin,
TransactionData, TransactionEffects, TransferCoin, TransferSui,
},
object::Object,
storage::{BackingPackageStore, Storage},
Expand Down Expand Up @@ -113,7 +115,15 @@ fn execute_transaction<S: BackingPackageStore>(
.get(&object_ref.0)
.unwrap()
.clone();
transfer(temporary_store, object, recipient)
transfer_coin(temporary_store, object, recipient)
}
SingleTransactionKind::TransferSui(TransferSui { recipient, amount }) => {
let gas_object = temporary_store
.objects()
.get(&gas_object_id)
.expect("We constructed the object map so it should always have the gas object id")
.clone();
transfer_sui(temporary_store, gas_object, recipient, amount, tx_ctx)
}
SingleTransactionKind::Call(MoveCall {
package,
Expand Down Expand Up @@ -181,9 +191,11 @@ fn execute_transaction<S: BackingPackageStore>(
// can charge gas for all mutated objects properly.
temporary_store.ensure_active_inputs_mutated(&gas_object_id);
if !gas_status.is_unmetered() {
// We must call `read_object` instead of getting it from `temporary_store.objects`
// because a `TransferSui` transaction may have already mutated the gas object and put
// it in `temporary_store.written`.
let mut gas_object = temporary_store
.objects()
.get(&gas_object_id)
.read_object(&gas_object_id)
.expect("We constructed the object map so it should always have the gas object id")
.clone();
trace!(?gas_object_id, "Obtained gas object");
Expand Down Expand Up @@ -222,12 +234,12 @@ fn execute_transaction<S: BackingPackageStore>(
}
}

fn transfer<S>(
fn transfer_coin<S>(
temporary_store: &mut AuthorityTemporaryStore<S>,
mut object: Object,
recipient: SuiAddress,
) -> SuiResult {
object.transfer(recipient)?;
object.transfer_and_increment_version(recipient)?;
temporary_store.log_event(Event::TransferObject {
object_id: object.id(),
version: object.version(),
Expand All @@ -237,3 +249,66 @@ fn transfer<S>(
temporary_store.write_object(object);
Ok(())
}

/// Transfer the gas object (which is a SUI coin object) with an optional `amount`.
/// If `amount` is specified, the gas object remains in the original owner, but a new SUI coin
/// is created with `amount` balance and is transferred to `recipient`;
/// if `amount` is not specified, the entire object will be transferred to `recipient`.
/// `tx_ctx` is needed to create new object ID for the split coin.
/// We make sure that the gas object's version is not incremented after this function call, because
/// when we charge gas later, its version will be officially incremented.
fn transfer_sui<S>(
temporary_store: &mut AuthorityTemporaryStore<S>,
mut object: Object,
recipient: SuiAddress,
amount: Option<u64>,
tx_ctx: &mut TxContext,
) -> SuiResult {
#[cfg(debug_assertions)]
let version = object.version();

if let Some(amount) = amount {
// Deduct the amount from the gas coin and update it.
let mut gas_coin = GasCoin::try_from(&object)?;
gas_coin.0.balance.withdraw(amount)?;
let move_object = object
.data
.try_as_move_mut()
.expect("Gas object must be Move object");
// We do not update the version number yet because gas charge will update it latter.
move_object.update_contents_without_version_change(
bcs::to_bytes(&gas_coin).expect("Serializing gas coin can never fail"),
);

// Creat a new gas coin with the amount.
let new_object = Object::new_move(
MoveObject::new(
GasCoin::type_(),
bcs::to_bytes(&GasCoin::new(
tx_ctx.fresh_id(),
OBJECT_START_VERSION,
amount,
))
.expect("Serializing gas object cannot fail"),
),
Owner::AddressOwner(recipient),
tx_ctx.digest(),
);
temporary_store.write_object(new_object);

// This is necessary for the temporary store to know this new object is not unwrapped.
let newly_generated_ids = tx_ctx.recreate_all_ids();
temporary_store.set_create_object_ids(newly_generated_ids);
} else {
// If amount is not specified, we simply transfer the entire coin object.
// We don't want to increment the version number yet because latter gas charge will do it.
object.transfer_without_version_change(recipient)?;
}

// TODO: Emit a new event type for this.

debug_assert_eq!(object.version(), version);
temporary_store.write_object(object);

Ok(())
}
24 changes: 23 additions & 1 deletion crates/sui-core/src/gateway_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,8 @@ pub enum SuiTransactionKind {
Publish(SuiMovePackage),
/// Call a function in a published Move module
Call(SuiMoveCall),
/// Initiate a SUI coin transfer between addresses
TransferSui(SuiTransferSui),
/// A system transaction that will update epoch information on-chain.
ChangeEpoch(SuiChangeEpoch),
// .. more transaction types go here
Expand All @@ -830,7 +832,7 @@ impl Display for SuiTransactionKind {
let mut writer = String::new();
match &self {
Self::TransferCoin(t) => {
writeln!(writer, "Transaction Kind : Transfer")?;
writeln!(writer, "Transaction Kind : Transfer Coin")?;
writeln!(writer, "Recipient : {}", t.recipient)?;
writeln!(writer, "Object ID : {}", t.object_ref.object_id)?;
writeln!(writer, "Version : {:?}", t.object_ref.version)?;
Expand All @@ -840,6 +842,15 @@ impl Display for SuiTransactionKind {
Base64::encode(t.object_ref.digest)
)?;
}
Self::TransferSui(t) => {
writeln!(writer, "Transaction Kind : Transfer SUI")?;
writeln!(writer, "Recipient : {}", t.recipient)?;
if let Some(amount) = t.amount {
writeln!(writer, "Amount: {}", amount)?;
} else {
writeln!(writer, "Amount: Full Balance")?;
}
}
Self::Publish(_p) => {
write!(writer, "Transaction Kind : Publish")?;
}
Expand Down Expand Up @@ -875,6 +886,10 @@ impl TryFrom<SingleTransactionKind> for SuiTransactionKind {
recipient: t.recipient,
object_ref: t.object_ref.into(),
}),
SingleTransactionKind::TransferSui(t) => Self::TransferSui(SuiTransferSui {
recipient: t.recipient,
amount: t.amount,
}),
SingleTransactionKind::Publish(p) => Self::Publish(p.try_into()?),
SingleTransactionKind::Call(c) => Self::Call(SuiMoveCall {
package: c.package.into(),
Expand Down Expand Up @@ -1187,6 +1202,13 @@ pub struct SuiTransferCoin {
pub object_ref: SuiObjectRef,
}

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename = "TransferSui", rename_all = "camelCase")]
pub struct SuiTransferSui {
pub recipient: SuiAddress,
pub amount: Option<u64>,
}

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename = "InputObjectKind")]
pub enum SuiInputObjectKind {
Expand Down
121 changes: 116 additions & 5 deletions crates/sui-core/src/unit_tests/authority_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,9 +460,8 @@ async fn test_publish_module_no_dependencies_ok() {
let (sender, sender_key) = get_key_pair();
let gas_payment_object_id = ObjectID::random();
let gas_balance = MAX_GAS;
let gas_seq = SequenceNumber::new();
let gas_payment_object =
Object::with_id_owner_gas_for_testing(gas_payment_object_id, gas_seq, sender, gas_balance);
Object::with_id_owner_gas_for_testing(gas_payment_object_id, sender, gas_balance);
let gas_payment_object_ref = gas_payment_object.compute_object_reference();
let authority = init_state_with_objects(vec![gas_payment_object]).await;

Expand Down Expand Up @@ -696,7 +695,7 @@ async fn test_handle_confirmation_transaction_bad_sequence_number() {
let o = sender_object.data.try_as_move_mut().unwrap();
let old_contents = o.contents().to_vec();
// update object contents, which will increment the sequence number
o.update_contents(old_contents);
o.update_contents_and_increment_version(old_contents);
authority_state.insert_genesis_object(sender_object).await;
}

Expand Down Expand Up @@ -1293,6 +1292,110 @@ async fn test_change_epoch_transaction() {
assert_eq!(sui_system_object.epoch, 1);
}

#[tokio::test]
async fn test_transfer_sui_no_amount() {
let (sender, sender_key) = get_key_pair();
let recipient = dbg_addr(2);
let gas_object_id = ObjectID::random();
let gas_object = Object::with_id_owner_for_testing(gas_object_id, sender);
let init_balance = sui_types::gas::get_gas_balance(&gas_object).unwrap();
let authority_state = init_state_with_objects(vec![gas_object.clone()]).await;

let tx_data = TransactionData::new_transfer_sui(
recipient,
sender,
None,
gas_object.compute_object_reference(),
MAX_GAS,
);
let signature = Signature::new(&tx_data, &sender_key);
let transaction = Transaction::new(tx_data, signature);

// Make sure transaction handling works as usual.
authority_state
.handle_transaction(transaction.clone())
.await
.unwrap();

let certificate = init_certified_transaction(transaction, &authority_state);
let response = authority_state
.handle_confirmation_transaction(ConfirmationTransaction { certificate })
.await
.unwrap();
let effects = response.signed_effects.unwrap().effects;
// Check that the transaction was successful, and the gas object is the only mutated object,
// and got transferred. Also check on its version and new balance.
assert!(effects.status.is_ok());
assert!(effects.mutated_excluding_gas().next().is_none());
assert_eq!(effects.gas_object.0 .1, SequenceNumber::new().increment());
assert_eq!(effects.gas_object.1, Owner::AddressOwner(recipient));
let new_balance = sui_types::gas::get_gas_balance(
&authority_state
.get_object(&gas_object_id)
.await
.unwrap()
.unwrap(),
)
.unwrap();
assert_eq!(
new_balance as i64 + effects.status.gas_cost_summary().net_gas_usage(),
init_balance as i64
);
}

#[tokio::test]
async fn test_transfer_sui_with_amount() {
let (sender, sender_key) = get_key_pair();
let recipient = dbg_addr(2);
let gas_object_id = ObjectID::random();
let gas_object = Object::with_id_owner_for_testing(gas_object_id, sender);
let init_balance = sui_types::gas::get_gas_balance(&gas_object).unwrap();
let authority_state = init_state_with_objects(vec![gas_object.clone()]).await;

let tx_data = TransactionData::new_transfer_sui(
recipient,
sender,
Some(500),
gas_object.compute_object_reference(),
MAX_GAS,
);
let signature = Signature::new(&tx_data, &sender_key);
let transaction = Transaction::new(tx_data, signature);

let certificate = init_certified_transaction(transaction, &authority_state);
let response = authority_state
.handle_confirmation_transaction(ConfirmationTransaction { certificate })
.await
.unwrap();
let effects = response.signed_effects.unwrap().effects;
// Check that the transaction was successful, the gas object remains in the original owner,
// and an amount is split out and send to the recipient.
assert!(effects.status.is_ok());
assert!(effects.mutated_excluding_gas().next().is_none());
assert_eq!(effects.created.len(), 1);
assert_eq!(effects.created[0].1, Owner::AddressOwner(recipient));
let new_gas = authority_state
.get_object(&effects.created[0].0 .0)
.await
.unwrap()
.unwrap();
assert_eq!(sui_types::gas::get_gas_balance(&new_gas).unwrap(), 500);
assert_eq!(effects.gas_object.0 .1, SequenceNumber::new().increment());
assert_eq!(effects.gas_object.1, Owner::AddressOwner(sender));
let new_balance = sui_types::gas::get_gas_balance(
&authority_state
.get_object(&gas_object_id)
.await
.unwrap()
.unwrap(),
)
.unwrap();
assert_eq!(
new_balance as i64 + effects.status.gas_cost_summary().net_gas_usage() + 500,
init_balance as i64
);
}

// helpers

#[cfg(test)]
Expand Down Expand Up @@ -1380,14 +1483,22 @@ fn init_certified_transfer_transaction(
) -> CertifiedTransaction {
let transfer_transaction =
init_transfer_transaction(sender, secret, recipient, object_ref, gas_object_ref);
init_certified_transaction(transfer_transaction, authority_state)
}

#[cfg(test)]
fn init_certified_transaction(
transaction: Transaction,
authority_state: &AuthorityState,
) -> CertifiedTransaction {
let vote = SignedTransaction::new(
0,
transfer_transaction.clone(),
transaction.clone(),
authority_state.name,
&*authority_state.secret,
);
let committee = authority_state.committee.load();
let mut builder = SignatureAggregator::try_new(transfer_transaction, &committee).unwrap();
let mut builder = SignatureAggregator::try_new(transaction, &committee).unwrap();
builder
.append(vote.auth_sign_info.authority, vote.auth_sign_info.signature)
.unwrap()
Expand Down
1 change: 0 additions & 1 deletion crates/sui-core/src/unit_tests/batch_transaction_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ async fn test_batch_insufficient_gas_balance() -> anyhow::Result<()> {
let gas_object_id = ObjectID::random();
let gas_object = Object::with_id_owner_gas_for_testing(
gas_object_id,
SequenceNumber::new(),
sender,
49999, // We need 50000
);
Expand Down
7 changes: 1 addition & 6 deletions crates/sui-core/src/unit_tests/gas_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,7 @@ async fn execute_transfer(gas_balance: u64, gas_budget: u64, run_confirm: bool)
let recipient = dbg_addr(2);
let authority_state = init_state_with_ids(vec![(sender, object_id)]).await;
let gas_object_id = ObjectID::random();
let gas_object = Object::with_id_owner_gas_coin_object_for_testing(
gas_object_id,
SequenceNumber::new(),
sender,
gas_balance,
);
let gas_object = Object::with_id_owner_gas_for_testing(gas_object_id, sender, gas_balance);
let gas_object_ref = gas_object.compute_object_reference();
authority_state.insert_genesis_object(gas_object).await;
let object = authority_state
Expand Down
Loading

0 comments on commit 82d83e2

Please sign in to comment.