Skip to content
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
11 changes: 9 additions & 2 deletions crates/simulacrum/src/epoch_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@ pub struct EpochState {

impl EpochState {
pub fn new(system_state: SuiSystemState) -> Self {
let protocol_config =
ProtocolConfig::get_for_version(system_state.protocol_version().into(), Chain::Unknown);
Self::new_with_protocol_config(system_state, protocol_config)
}

pub fn new_with_protocol_config(
system_state: SuiSystemState,
protocol_config: ProtocolConfig,
) -> Self {
let epoch_start_state = system_state.into_epoch_start_state();
let committee = epoch_start_state.get_sui_committee();
let protocol_config =
ProtocolConfig::get_for_version(epoch_start_state.protocol_version(), Chain::Unknown);
let registry = prometheus::Registry::new();
let limits_metrics = Arc::new(LimitsMetrics::new(&registry));
let bytecode_verifier_metrics = Arc::new(BytecodeVerifierMetrics::new(&registry));
Expand Down
5 changes: 4 additions & 1 deletion crates/simulacrum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,10 @@ impl<R, S: store::SimulatorStore> Simulacrum<R, S> {
self.execute_transaction(tx.into())
.expect("advancing the epoch cannot fail");

let new_epoch_state = EpochState::new(self.store.get_system_state());
let new_epoch_state = EpochState::new_with_protocol_config(
self.store.get_system_state(),
self.epoch_state.protocol_config().clone(),
);
let end_of_epoch_data = EndOfEpochData {
next_epoch_committee: new_epoch_state.committee().voting_rights.clone(),
next_epoch_protocol_version,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

// This test exercises the deny-list v2 flow when regulated coins are routed through the
// address-balance accumulator APIs. We explicitly enable the accumulator feature so that
// `balance::send_to_account` is callable from PTBs, then confirm the receiver transitions through
// allowed, denied-after-epoch, and re-enabled states.

//# init --accounts A B --addresses test=0x0 --enable-accumulators --simulator

//# publish --sender A
module test::regulated_coin {
use sui::balance;
use sui::coin;

public struct REGULATED_COIN has drop {}

#[allow(deprecated_usage)]
fun init(otw: REGULATED_COIN, ctx: &mut TxContext) {
let (mut treasury_cap, deny_cap, metadata) = coin::create_regulated_currency_v2(
otw,
9,
b"RC",
b"REGULATED_COIN",
b"A new regulated coin",
option::none(),
false,
ctx
);
let coin = coin::mint(&mut treasury_cap, 10000, ctx);
transfer::public_transfer(coin, tx_context::sender(ctx));
transfer::public_transfer(deny_cap, tx_context::sender(ctx));
transfer::public_freeze_object(treasury_cap);
transfer::public_freeze_object(metadata);
}

public fun split_to_balance(
coin: &mut coin::Coin<REGULATED_COIN>,
amount: u64,
): balance::Balance<REGULATED_COIN> {
balance::split(coin::balance_mut(coin), amount)
}
}

// Initial transfer should succeed before any deny-list action is taken.
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));

// Deny account B.
//# run sui::coin::deny_list_v2_add --args object(0x403) object(1,3) @B --type-args test::regulated_coin::REGULATED_COIN --sender A

// Deny entry is not enforced until the next epoch, so the transfer still succeeds.
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));

//# advance-epoch

// After epoch change, the deny list should block the recipient.
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));

// Undeny account B.
//# run sui::coin::deny_list_v2_remove --args object(0x403) object(1,3) @B --type-args test::regulated_coin::REGULATED_COIN --sender A

// Removal only takes effect after the next epoch boundary, so this attempt still fails.
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));

//# advance-epoch

// Once the following epoch begins, transfers to @B succeed again.
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
source: external-crates/move/crates/move-transactional-test-runner/src/framework.rs
---
processed 11 tasks

init:
A: object(0,0), B: object(0,1)

task 1, lines 11-45:
//# publish --sender A
created: object(1,0), object(1,1), object(1,2), object(1,3), object(1,4), object(1,5)
mutated: object(0,0)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
gas summary: computation_cost: 1000000, storage_cost: 19448400, storage_rebate: 0, non_refundable_storage_fee: 0

task 2, lines 46-50:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
// Deny account B.
mutated: object(0,0), object(1,1)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
accumulators_written: fake(2,0)
gas summary: computation_cost: 1000000, storage_cost: 2462400, storage_rebate: 2437776, non_refundable_storage_fee: 24624

task 3, lines 51-53:
//# run sui::coin::deny_list_v2_add --args object(0x403) object(1,3) @B --type-args test::regulated_coin::REGULATED_COIN --sender A
events: Event { package_id: sui, transaction_module: Identifier("coin"), sender: A, type_: StructTag { address: sui, module: Identifier("deny_list"), name: Identifier("PerTypeConfigCreated"), type_params: [] }, contents: [0, 0, 0, 0, 0, 0, 0, 0, 96, 50, 56, 99, 52, 55, 102, 51, 99, 102, 53, 98, 102, 101, 48, 52, 48, 53, 56, 56, 50, 51, 50, 48, 52, 97, 48, 51, 100, 52, 50, 53, 102, 50, 98, 56, 53, 51, 54, 51, 98, 100, 56, 56, 55, 50, 57, 97, 53, 97, 98, 51, 99, 54, 101, 56, 48, 56, 50, 51, 57, 48, 102, 49, 54, 58, 58, 114, 101, 103, 117, 108, 97, 116, 101, 100, 95, 99, 111, 105, 110, 58, 58, 82, 69, 71, 85, 76, 65, 84, 69, 68, 95, 67, 79, 73, 78, 193, 207, 223, 168, 132, 36, 189, 200, 116, 185, 218, 79, 67, 102, 54, 92, 118, 213, 35, 125, 7, 100, 179, 88, 231, 35, 71, 101, 29, 228, 152, 133] }
created: object(3,0), object(3,1), object(3,2)
mutated: 0x0000000000000000000000000000000000000000000000000000000000000403, object(0,0), object(1,3)
gas summary: computation_cost: 1000000, storage_cost: 12190400, storage_rebate: 2746260, non_refundable_storage_fee: 27740

task 4, lines 54-56:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
mutated: object(0,0), object(1,1)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
accumulators_written: fake(2,0)
gas summary: computation_cost: 1000000, storage_cost: 2462400, storage_rebate: 2437776, non_refundable_storage_fee: 24624

task 5, lines 58-60:
//# advance-epoch
Epoch advanced: 1

task 6, lines 61-65:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
// Undeny account B.
Error: Transaction Effects Status: Address B is denied for coin test::regulated_coin::REGULATED_COIN
Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: AddressDeniedForCoin { address: B, coin_type: "test::regulated_coin::REGULATED_COIN" }, source: None, command: None } }

task 7, lines 66-68:
//# run sui::coin::deny_list_v2_remove --args object(0x403) object(1,3) @B --type-args test::regulated_coin::REGULATED_COIN --sender A
mutated: 0x0000000000000000000000000000000000000000000000000000000000000403, object(0,0), object(1,3), object(3,1)
gas summary: computation_cost: 1000000, storage_cost: 6862800, storage_rebate: 6794172, non_refundable_storage_fee: 68628

task 8, lines 69-71:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
Error: Transaction Effects Status: Address B is denied for coin test::regulated_coin::REGULATED_COIN
Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: AddressDeniedForCoin { address: B, coin_type: "test::regulated_coin::REGULATED_COIN" }, source: None, command: None } }

task 9, lines 73-75:
//# advance-epoch
Epoch advanced: 2

task 10, lines 76-78:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
mutated: object(0,0), object(1,1)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
accumulators_written: fake(2,0)
gas summary: computation_cost: 1000000, storage_cost: 2462400, storage_rebate: 2437776, non_refundable_storage_fee: 24624
77 changes: 77 additions & 0 deletions ...nsactional-tests/tests/deny_list_v2/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
source: external-crates/move/crates/move-transactional-test-runner/src/framework.rs
---
processed 11 tasks

init:
A: object(0,0), B: object(0,1)

task 1, lines 11-45:
//# publish --sender A
created: object(1,0), object(1,1), object(1,2), object(1,3), object(1,4), object(1,5)
mutated: object(0,0)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
gas summary: computation_cost: 1000000, storage_cost: 19448400, storage_rebate: 0, non_refundable_storage_fee: 0

task 2, lines 46-50:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
// Deny account B.
mutated: object(0,0), object(1,1)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
accumulators_written: fake(2,0)
gas summary: computation_cost: 1000000, storage_cost: 2462400, storage_rebate: 2437776, non_refundable_storage_fee: 24624

task 3, lines 51-53:
//# run sui::coin::deny_list_v2_add --args object(0x403) object(1,3) @B --type-args test::regulated_coin::REGULATED_COIN --sender A
events: Event { package_id: sui, transaction_module: Identifier("coin"), sender: A, type_: StructTag { address: sui, module: Identifier("deny_list"), name: Identifier("PerTypeConfigCreated"), type_params: [] }, contents: [0, 0, 0, 0, 0, 0, 0, 0, 96, 50, 56, 99, 52, 55, 102, 51, 99, 102, 53, 98, 102, 101, 48, 52, 48, 53, 56, 56, 50, 51, 50, 48, 52, 97, 48, 51, 100, 52, 50, 53, 102, 50, 98, 56, 53, 51, 54, 51, 98, 100, 56, 56, 55, 50, 57, 97, 53, 97, 98, 51, 99, 54, 101, 56, 48, 56, 50, 51, 57, 48, 102, 49, 54, 58, 58, 114, 101, 103, 117, 108, 97, 116, 101, 100, 95, 99, 111, 105, 110, 58, 58, 82, 69, 71, 85, 76, 65, 84, 69, 68, 95, 67, 79, 73, 78, 193, 207, 223, 168, 132, 36, 189, 200, 116, 185, 218, 79, 67, 102, 54, 92, 118, 213, 35, 125, 7, 100, 179, 88, 231, 35, 71, 101, 29, 228, 152, 133] }
created: object(3,0), object(3,1), object(3,2)
mutated: 0x0000000000000000000000000000000000000000000000000000000000000403, object(0,0), object(1,3)
gas summary: computation_cost: 1000000, storage_cost: 12190400, storage_rebate: 2746260, non_refundable_storage_fee: 27740

task 4, lines 54-56:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
mutated: object(0,0), object(1,1)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
accumulators_written: fake(2,0)
gas summary: computation_cost: 1000000, storage_cost: 2462400, storage_rebate: 2437776, non_refundable_storage_fee: 24624

task 5, lines 58-60:
//# advance-epoch
Epoch advanced: 1

task 6, lines 61-65:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
// Undeny account B.
Error: Transaction Effects Status: Address B is denied for coin test::regulated_coin::REGULATED_COIN
Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: AddressDeniedForCoin { address: B, coin_type: "test::regulated_coin::REGULATED_COIN" }, source: None, command: None } }

task 7, lines 66-68:
//# run sui::coin::deny_list_v2_remove --args object(0x403) object(1,3) @B --type-args test::regulated_coin::REGULATED_COIN --sender A
mutated: 0x0000000000000000000000000000000000000000000000000000000000000403, object(0,0), object(1,3), object(3,1)
gas summary: computation_cost: 1000000, storage_cost: 6862800, storage_rebate: 6794172, non_refundable_storage_fee: 68628

task 8, lines 69-71:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
Error: Transaction Effects Status: Address B is denied for coin test::regulated_coin::REGULATED_COIN
Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: AddressDeniedForCoin { address: B, coin_type: "test::regulated_coin::REGULATED_COIN" }, source: None, command: None } }

task 9, lines 73-75:
//# advance-epoch
Epoch advanced: 2

task 10, lines 76-78:
//# programmable --sender A --inputs object(1,1) 1 @B
//> 0: test::regulated_coin::split_to_balance(Input(0), Input(1));
//> 1: sui::balance::send_to_account<test::regulated_coin::REGULATED_COIN>(Result(0), Input(2));
mutated: object(0,0), object(1,1)
unchanged_shared: 0x0000000000000000000000000000000000000000000000000000000000000403
accumulators_written: fake(2,0)
gas summary: computation_cost: 1000000, storage_cost: 2462400, storage_rebate: 2437776, non_refundable_storage_fee: 24624
11 changes: 11 additions & 0 deletions crates/sui-types/src/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ impl Balance {
}
}

/// If the given type is `Balance<T>`, return `Some(T)`.
pub fn maybe_get_balance_type_param(ty: &TypeTag) -> Option<TypeTag> {
if let TypeTag::Struct(struct_tag) = ty {
if Self::is_balance(struct_tag) {
assert_eq!(struct_tag.type_params.len(), 1);
return Some(struct_tag.type_params[0].clone());
}
}
None
}

pub fn withdraw(&mut self, amount: u64) -> Result<(), ExecutionError> {
fp_ensure!(
self.value >= amount,
Expand Down
39 changes: 16 additions & 23 deletions crates/sui-types/src/deny_list_v2.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::base_types::{EpochId, ObjectID, SuiAddress};
use crate::base_types::{EpochId, SuiAddress};
use crate::coin::COIN_MODULE_NAME;
use crate::config::{Config, Setting};
use crate::deny_list_v1::{
input_object_coin_types_for_denylist_check, DENY_LIST_COIN_TYPE_INDEX, DENY_LIST_MODULE,
};
use crate::dynamic_field::{get_dynamic_field_from_store, DOFWrapper};
use crate::error::{ExecutionError, ExecutionErrorKind, UserInputError, UserInputResult};
use crate::gas_coin::GAS;
use crate::id::UID;
use crate::object::Object;
use crate::storage::{DenyListResult, ObjectStore};
use crate::transaction::{CheckedInputObjects, ReceivingObjects};
use crate::{
Expand Down Expand Up @@ -142,36 +142,29 @@ pub fn check_coin_deny_list_v2_during_signing(
/// 2) the deny lists checked
/// 2) the number of regulated coin owners checked.
pub fn check_coin_deny_list_v2_during_execution(
written_objects: &BTreeMap<ObjectID, Object>,
receiving_funds_type_and_owners: BTreeMap<TypeTag, BTreeSet<SuiAddress>>,
cur_epoch: EpochId,
object_store: &dyn ObjectStore,
) -> DenyListResult {
let mut new_coin_owners = BTreeMap::new();
for obj in written_objects.values() {
if obj.is_gas_coin() {
continue;
}
let Some(coin_type) = obj.coin_type_maybe() else {
continue;
};
let Ok(owner) = obj.owner.get_address_owner_address() else {
continue;
};
new_coin_owners
.entry(coin_type.to_canonical_string(false))
.or_insert_with(BTreeSet::new)
.insert(owner);
}
let num_non_gas_coin_owners = new_coin_owners.values().map(|v| v.len() as u64).sum();
let new_regulated_coin_owners = new_coin_owners
let non_gas_coin_owners = receiving_funds_type_and_owners
.into_iter()
.filter_map(|(ty, owners)| {
if GAS::is_gas_type(&ty) {
None
} else {
Some((ty.to_canonical_string(false), owners))
}
})
.collect::<BTreeMap<_, _>>();
let num_non_gas_coin_owners = non_gas_coin_owners.values().map(|v| v.len() as u64).sum();
let regulated_coin_owners = non_gas_coin_owners
.into_iter()
.filter_map(|(coin_type, owners)| {
let deny_list_config = get_per_type_coin_deny_list_v2(&coin_type, object_store)?;
Some((coin_type, (deny_list_config, owners)))
})
.collect::<BTreeMap<_, _>>();
let result =
check_new_regulated_coin_owners(new_regulated_coin_owners, cur_epoch, object_store);
let result = check_new_regulated_coin_owners(regulated_coin_owners, cur_epoch, object_store);
// `num_non_gas_coin_owners` is used to charge for gas. As such we must be extremely careful
// to not use a number that is not consistent across all validators. For example, relying on
// the number of coins with a deny list is _not_ consistent since the deny list is created
Expand Down
14 changes: 9 additions & 5 deletions crates/sui-types/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ mod shared_in_memory_store;
mod write_store;

use crate::base_types::{
ConsensusObjectSequenceKey, FullObjectID, FullObjectRef, TransactionDigest, VersionNumber,
ConsensusObjectSequenceKey, FullObjectID, FullObjectRef, SuiAddress, TransactionDigest,
VersionNumber,
};
use crate::committee::EpochId;
use crate::effects::{TransactionEffects, TransactionEffectsAPI};
Expand All @@ -25,7 +26,7 @@ use crate::{
};
use itertools::Itertools;
use move_binary_format::CompiledModule;
use move_core_types::language_storage::ModuleId;
use move_core_types::language_storage::{ModuleId, TypeTag};
pub use object_store_trait::ObjectStore;
pub use read_store::BalanceInfo;
pub use read_store::BalanceIterator;
Expand Down Expand Up @@ -226,9 +227,12 @@ pub trait Storage {
wrapped_object_containers: BTreeMap<ObjectID, ObjectID>,
);

/// Check coin denylist during execution,
/// and the number of non-gas-coin owners.
fn check_coin_deny_list(&self, written_objects: &BTreeMap<ObjectID, Object>) -> DenyListResult;
/// Given the set of all coin types and owners that are receiving the coins during execution,
/// Check coin denylist v2, and return the number of non-gas-coin owners.
fn check_coin_deny_list(
&self,
receiving_funds_type_and_owners: BTreeMap<TypeTag, BTreeSet<SuiAddress>>,
) -> DenyListResult;

fn record_generated_object_ids(&mut self, generated_ids: BTreeSet<ObjectID>);
}
Expand Down
Loading
Loading