Skip to content

Commit

Permalink
Add a special system transaction that updates epoch on-chain (MystenL…
Browse files Browse the repository at this point in the history
…abs#2223)

* Add a special system transaction type

* Test that the raw tx is not accepted
  • Loading branch information
lxfind authored Jun 1, 2022
1 parent 5f66e7c commit 48098f0
Show file tree
Hide file tree
Showing 17 changed files with 536 additions and 288 deletions.
11 changes: 10 additions & 1 deletion crates/sui-core/src/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ impl AuthorityState {
&self,
transaction: Transaction,
) -> Result<TransactionInfoResponse, SuiError> {
// Validators should never sign an external system transaction.
fp_ensure!(
!transaction.data.kind.is_system_tx(),
SuiError::InvalidSystemTransaction
);

let transaction_digest = *transaction.digest();
// Ensure an idempotent answer.
if self.database.transaction_exists(&transaction_digest)? {
Expand Down Expand Up @@ -433,9 +439,12 @@ impl AuthorityState {
.map(|(_, obj)| obj.compute_object_reference())
.sorted()
.collect();
if !shared_object_refs.is_empty() {
if !shared_object_refs.is_empty() && !certificate.data.kind.is_system_tx() {
// If the transaction contains shared objects, we need to ensure they have been scheduled
// for processing by the consensus protocol.
// There is no need to go through consensus for system transactions that can
// only be executed at a time when consensus is turned off.
// TODO: Add some assert here to make sure consensus is indeed off with is_system_tx.
self.check_shared_locks(&transaction_digest, &shared_object_refs)
.await?;
}
Expand Down
25 changes: 17 additions & 8 deletions crates/sui-core/src/authority/temporary_store.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
use move_core_types::account_address::AccountAddress;
use sui_types::{event::Event, gas::SuiGasStatus};
use sui_types::{event::Event, gas::SuiGasStatus, object::Owner};

use super::*;

Expand Down Expand Up @@ -100,10 +100,12 @@ impl<S> AuthorityTemporaryStore<S> {
/// For every object from active_inputs (i.e. all mutable objects), if they are not
/// mutated during the transaction execution, force mutating them by incrementing the
/// sequence number. This is required to achieve safety.
/// We skip the last object, which is always the gas object, because gas object will be
/// updated after this.
pub fn ensure_active_inputs_mutated(&mut self) {
for (id, _seq, _) in self.active_inputs[..self.active_inputs.len() - 1].iter() {
/// We skip the gas object, because gas object will be updated separately.
pub fn ensure_active_inputs_mutated(&mut self, gas_object_id: &ObjectID) {
for (id, _seq, _) in &self.active_inputs {
if id == gas_object_id {
continue;
}
if !self.written.contains_key(id) && !self.deleted.contains_key(id) {
let mut object = self.objects[id].clone();
// Active input object must be Move object.
Expand Down Expand Up @@ -184,9 +186,16 @@ impl<S> AuthorityTemporaryStore<S> {
transaction_digest: &TransactionDigest,
transaction_dependencies: Vec<TransactionDigest>,
status: ExecutionStatus,
gas_object_id: &ObjectID,
gas_object_ref: ObjectRef,
) -> TransactionEffects {
let (gas_reference, gas_object) = &self.written[gas_object_id];
// In the case of special transactions that don't require a gas object,
// we don't really care about the effects to gas, just use the input for it.
let updated_gas_object_info = if gas_object_ref.0 == ObjectID::ZERO {
(gas_object_ref, Owner::AddressOwner(SuiAddress::default()))
} else {
let (gas_reference, gas_object) = &self.written[&gas_object_ref.0];
(*gas_reference, gas_object.owner)
};
TransactionEffects {
status,
shared_objects: shared_object_refs,
Expand Down Expand Up @@ -233,7 +242,7 @@ impl<S> AuthorityTemporaryStore<S> {
}
})
.collect(),
gas_object: (*gas_reference, gas_object.owner),
gas_object: updated_gas_object_info,
events: self.events.clone(),
dependencies: transaction_dependencies,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/sui-core/src/authority_aggregator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,7 @@ where
Err(SuiError::ErrorWhileRequestingCertificate)
}

/// Find the highest sequence number that is known to a quorum of authorities.
/// Find the higgest sequence number that is known to a quorum of authorities.
/// NOTE: This is only reliable in the synchronous model, with a sufficient timeout value.
#[cfg(test)]
async fn get_latest_sequence_number(&self, object_id: ObjectID) -> SequenceNumber {
Expand Down
99 changes: 62 additions & 37 deletions crates/sui-core/src/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ use sui_types::{
event::{Event, TransferType},
gas::{self, SuiGasStatus},
messages::{
ExecutionStatus, MoveCall, MoveModulePublish, SingleTransactionKind, TransactionData,
TransactionEffects, TransferCoin,
CallArg, ChangeEpoch, ExecutionStatus, MoveCall, MoveModulePublish, SingleTransactionKind,
TransactionData, TransactionEffects, TransferCoin,
},
object::Object,
storage::{BackingPackageStore, Storage},
sui_system_state::{ADVANCE_EPOCH_FUNCTION_NAME, SUI_SYSTEM_MODULE_NAME},
SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_STATE_OBJECT_ID,
};
use tracing::{debug, instrument, trace};

Expand All @@ -36,11 +38,11 @@ pub fn execute_transaction_to_effects<S: BackingPackageStore>(
) -> SuiResult<TransactionEffects> {
let mut tx_ctx = TxContext::new(&transaction_data.signer(), &transaction_digest, epoch);

let gas_object_id = transaction_data.gas_payment_object_ref().0;
let gas_object_ref = *transaction_data.gas_payment_object_ref();
let status = execute_transaction(
temporary_store,
transaction_data,
gas_object_id,
gas_object_ref.0,
&mut tx_ctx,
move_vm,
native_functions,
Expand All @@ -63,7 +65,7 @@ pub fn execute_transaction_to_effects<S: BackingPackageStore>(
&transaction_digest,
transaction_dependencies.into_iter().collect(),
status,
&gas_object_id,
gas_object_ref,
);
Ok(effects)
}
Expand Down Expand Up @@ -93,13 +95,6 @@ fn execute_transaction<S: BackingPackageStore>(
native_functions: &NativeFunctionTable,
mut gas_status: SuiGasStatus,
) -> ExecutionStatus {
let mut 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();
trace!(?gas_object_id, "Obtained gas object");

// We must charge object read gas inside here during transaction execution, because if this fails
// we must still ensure an effect is committed and all objects versions incremented.
let mut result = charge_gas_for_object_read(temporary_store, &mut gas_status);
Expand Down Expand Up @@ -146,6 +141,30 @@ fn execute_transaction<S: BackingPackageStore>(
tx_ctx,
&mut gas_status,
),
SingleTransactionKind::ChangeEpoch(ChangeEpoch {
epoch,
storage_charge,
computation_charge,
}) => {
let module_id =
ModuleId::new(SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_MODULE_NAME.to_owned());
let function = ADVANCE_EPOCH_FUNCTION_NAME.to_owned();
adapter::execute(
move_vm,
temporary_store,
module_id,
&function,
vec![],
vec![
CallArg::SharedObject(SUI_SYSTEM_STATE_OBJECT_ID),
CallArg::Pure(bcs::to_bytes(&epoch).unwrap()),
CallArg::Pure(bcs::to_bytes(&storage_charge).unwrap()),
CallArg::Pure(bcs::to_bytes(&computation_charge).unwrap()),
],
&mut gas_status,
tx_ctx,
)
}
};
if result.is_err() {
break;
Expand All @@ -160,35 +179,41 @@ fn execute_transaction<S: BackingPackageStore>(
// Make sure every mutable object's version number is incremented.
// This needs to happen before `charge_gas_for_storage_changes` so that it
// can charge gas for all mutated objects properly.
temporary_store.ensure_active_inputs_mutated();
if let Err(err) =
temporary_store.charge_gas_for_storage_changes(&mut gas_status, &mut gas_object)
{
// If `result` is already `Err`, we basically have two errors at the same time.
// Users should be generally more interested in the actual execution error, so we
// let that shadow the out of gas error. Also in this case, we don't need to reset
// the `temporary_store` because `charge_gas_for_storage_changes` won't mutate
// `temporary_store` if gas charge failed.
//
// If `result` is `Ok`, now we failed when charging gas, we have to reset
// the `temporary_store` to eliminate all effects caused by the execution,
// and re-ensure all mutable objects' versions are incremented.
if result.is_ok() {
temporary_store.reset();
temporary_store.ensure_active_inputs_mutated();
result = Err(err);
temporary_store.ensure_active_inputs_mutated(&gas_object_id);
if !gas_status.is_unmetered() {
let mut 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();
trace!(?gas_object_id, "Obtained gas object");
if let Err(err) =
temporary_store.charge_gas_for_storage_changes(&mut gas_status, &mut gas_object)
{
// If `result` is already `Err`, we basically have two errors at the same time.
// Users should be generally more interested in the actual execution error, so we
// let that shadow the out of gas error. Also in this case, we don't need to reset
// the `temporary_store` because `charge_gas_for_storage_changes` won't mutate
// `temporary_store` if gas charge failed.
//
// If `result` is `Ok`, now we failed when charging gas, we have to reset
// the `temporary_store` to eliminate all effects caused by the execution,
// and re-ensure all mutable objects' versions are incremented.
if result.is_ok() {
temporary_store.reset();
temporary_store.ensure_active_inputs_mutated(&gas_object_id);
result = Err(err);
}
}
let cost_summary = gas_status.summary(result.is_ok());
let gas_used = cost_summary.gas_used();
let gas_rebate = cost_summary.storage_rebate;
gas::deduct_gas(&mut gas_object, gas_used, gas_rebate);
trace!(gas_used, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
temporary_store.write_object(gas_object);
}

let cost_summary = gas_status.summary(result.is_ok());
let gas_used = cost_summary.gas_used();
let gas_rebate = cost_summary.storage_rebate;
gas::deduct_gas(&mut gas_object, gas_used, gas_rebate);
trace!(gas_used, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
temporary_store.write_object(gas_object);

// TODO: Return cost_summary so that the detailed summary exists in TransactionEffects for
// gas and rebate distribution.
match result {
Ok(()) => ExecutionStatus::Success {
gas_cost: cost_summary,
Expand Down
21 changes: 21 additions & 0 deletions crates/sui-core/src/gateway_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use serde_json::Value;
use sui_types::base_types::{
ObjectDigest, ObjectID, ObjectInfo, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest,
};
use sui_types::committee::EpochId;
use sui_types::crypto::{AuthorityQuorumSignInfo, Signature};
use sui_types::error::SuiError;
use sui_types::event::Event;
Expand Down Expand Up @@ -756,6 +757,8 @@ pub enum SuiTransactionKind {
Publish(SuiMovePackage),
/// Call a function in a published Move module
Call(SuiMoveCall),
/// A system transaction that will update epoch information on-chain.
ChangeEpoch(SuiChangeEpoch),
// .. more transaction types go here
}

Expand Down Expand Up @@ -789,6 +792,12 @@ impl Display for SuiTransactionKind {
writeln!(writer, "Arguments : {:?}", c.arguments)?;
write!(writer, "Type Arguments : {:?}", c.type_arguments)?;
}
Self::ChangeEpoch(e) => {
writeln!(writer, "Transaction Kind: Epoch Change")?;
writeln!(writer, "New epoch ID: {}", e.epoch)?;
writeln!(writer, "Storage gas reward: {}", e.storage_charge)?;
writeln!(writer, "Computation gas reward: {}", e.computation_charge)?;
}
}
write!(f, "{}", writer)
}
Expand Down Expand Up @@ -823,6 +832,11 @@ impl TryFrom<SingleTransactionKind> for SuiTransactionKind {
})
.collect::<Result<Vec<_>, _>>()?,
}),
SingleTransactionKind::ChangeEpoch(e) => Self::ChangeEpoch(SuiChangeEpoch {
epoch: e.epoch,
storage_charge: e.storage_charge,
computation_charge: e.computation_charge,
}),
})
}
}
Expand All @@ -839,6 +853,13 @@ pub struct SuiMoveCall {
pub arguments: Vec<SuiJsonValue>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SuiChangeEpoch {
pub epoch: EpochId,
pub storage_charge: u64,
pub computation_charge: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename = "CertifiedTransaction", rename_all = "camelCase")]
pub struct SuiCertifiedTransaction {
Expand Down
43 changes: 19 additions & 24 deletions crates/sui-core/src/transaction_input_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ pub async fn check_transaction_input<const A: bool, const B: bool, S, T>(
where
S: Eq + Serialize + for<'de> Deserialize<'de>,
{
let (gas_object, mut gas_status) = check_gas(
let mut gas_status = check_gas(
store,
transaction.gas_payment_object_ref().0,
transaction.data.gas_budget,
transaction.data.kind.is_system_tx(),
)
.await?;

let objects_by_kind = check_locks(store, &transaction.data, gas_object).await?;
let objects_by_kind = check_locks(store, &transaction.data).await?;

if transaction.contains_shared_object() {
shared_obj_metric.inc();
Expand All @@ -55,40 +56,35 @@ async fn check_gas<const A: bool, const B: bool, S>(
store: &SuiDataStore<A, B, S>,
gas_payment_id: ObjectID,
gas_budget: u64,
) -> SuiResult<(Object, SuiGasStatus<'static>)>
is_system_tx: bool,
) -> SuiResult<SuiGasStatus<'static>>
where
S: Eq + Serialize + for<'de> Deserialize<'de>,
{
let gas_object = store.get_object(&gas_payment_id)?;
let gas_object = gas_object.ok_or(SuiError::ObjectNotFound {
object_id: gas_payment_id,
})?;
gas::check_gas_balance(&gas_object, gas_budget)?;
// TODO: Pass in real computation gas unit price and storage gas unit price.
let gas_status = gas::start_gas_metering(gas_budget, 1, 1)?;
Ok((gas_object, gas_status))
if is_system_tx {
Ok(SuiGasStatus::new_unmetered())
} else {
let gas_object = store.get_object(&gas_payment_id)?;
let gas_object = gas_object.ok_or(SuiError::ObjectNotFound {
object_id: gas_payment_id,
})?;
gas::check_gas_balance(&gas_object, gas_budget)?;
// TODO: Pass in real computation gas unit price and storage gas unit price.
let gas_status = gas::start_gas_metering(gas_budget, 1, 1)?;
Ok(gas_status)
}
}

#[instrument(level = "trace", skip_all, fields(num_objects = input_objects.len()))]
async fn fetch_objects<const A: bool, const B: bool, S>(
store: &SuiDataStore<A, B, S>,
input_objects: &[InputObjectKind],
gas_object_opt: Option<Object>,
) -> Result<Vec<Option<Object>>, SuiError>
where
S: Eq + Serialize + for<'de> Deserialize<'de>,
{
let ids: Vec<_> = input_objects.iter().map(|kind| kind.object_id()).collect();
if let Some(gas_object) = gas_object_opt {
// If we already have the gas object, avoid fetching it again.
// Skip the last one since it's the gas object.
debug_assert_eq!(gas_object.id(), ids[ids.len() - 1]);
let mut result = store.get_objects(&ids[..ids.len() - 1])?;
result.push(Some(gas_object));
Ok(result)
} else {
store.get_objects(&ids[..])
}
store.get_objects(&ids[..])
}

/// Check all the objects used in the transaction against the database, and ensure
Expand All @@ -97,14 +93,13 @@ where
async fn check_locks<const A: bool, const B: bool, S>(
store: &SuiDataStore<A, B, S>,
transaction: &TransactionData,
gas_object: Object,
) -> Result<Vec<(InputObjectKind, Object)>, SuiError>
where
S: Eq + Serialize + for<'de> Deserialize<'de>,
{
let input_objects = transaction.input_objects()?;
// These IDs act as authenticators that can own other objects.
let objects = fetch_objects(store, &input_objects, Some(gas_object)).await?;
let objects = fetch_objects(store, &input_objects).await?;

// Constructing the list of objects that could be used to authenticate other
// objects. Any mutable object (either shared or owned) can be used to
Expand Down
Loading

0 comments on commit 48098f0

Please sign in to comment.