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
2 changes: 1 addition & 1 deletion book/src/data-model/contract-moderation.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ The pots are not under the contract. The per-block total credits check (`calcula

| Stage | Check | Error |
|---|---|---|
| Transform (state, paid) | the contract exists | `DataContractNotPresentError`, unpaid |
| Transform (state, paid) | the contract exists | `DataContractNotPresentError` (10400) |
| | the signer is a recipient of the pot: the owner for the owner pot, a member of the team for the moderators pot | 41113 |
| | the pot was not paid out in this epoch yet | 41111 |
| | every recipient gets at least a credit | 41112 |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod v0;
mod v1;
mod v2;

use crate::error::execution::ExecutionError;
use crate::error::Error;
Expand Down Expand Up @@ -40,6 +41,9 @@ impl<C> Platform<C> {
/// which contains the logic for version `0`.
/// - If the version is `1`, it calls `perform_events_on_first_block_of_protocol_change_v1`, which runs
/// the same transitions and then refreshes the cached definitions of the contracts they rewrote.
/// - If the version is `2`, it calls `perform_events_on_first_block_of_protocol_change_v2`, which
/// empties the contract cache, so no read is billed at a fee cached under the old fee
/// schedule, and then runs v1.
/// - If no version is specified (`None`), the function does nothing and returns `Ok(())`.
/// - If a different version is specified, it returns an error indicating an unknown version mismatch.
///
Expand Down Expand Up @@ -71,10 +75,17 @@ impl<C> Platform<C> {
previous_protocol_version,
platform_version,
),
Some(2) => self.perform_events_on_first_block_of_protocol_change_v2(
platform_state,
block_info,
transaction,
previous_protocol_version,
platform_version,
),
None => Ok(()),
Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch {
method: "perform_events_on_first_block_of_protocol_change".to_string(),
known_versions: vec![0, 1],
known_versions: vec![0, 1, 2],
received: version,
})),
}
Expand All @@ -88,7 +99,9 @@ mod tests {
use crate::test::helpers::setup::TestPlatformBuilder;
use dpp::block::block_info::BlockInfo;
use dpp::block::epoch::Epoch;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contracts::SystemDataContract;
use dpp::tests::fixtures::get_data_contract_fixture;

#[test]
fn test_perform_events_when_version_method_is_none() {
Expand Down Expand Up @@ -166,13 +179,92 @@ mod tests {
received,
})) => {
assert_eq!(method, "perform_events_on_first_block_of_protocol_change");
assert_eq!(known_versions, vec![0, 1]);
assert_eq!(known_versions, vec![0, 1, 2]);
assert_eq!(received, 255);
}
_ => panic!("expected UnknownVersionMismatch error"),
}
}

#[test]
fn should_empty_the_contract_cache_on_the_first_block_of_protocol_version_14() {
let previous_version = PlatformVersion::get(13).expect("protocol 13");
let platform_version = PlatformVersion::latest();
let platform = TestPlatformBuilder::new()
.with_initial_protocol_version(13)
.build_with_mock_rpc()
.set_genesis_state();
let platform_state = platform.state.load();
let block_info = BlockInfo {
time_ms: 1_000_000,
height: 100,
core_height: 100,
epoch: Epoch::new(1).expect("epoch"),
};

// A user contract a node read while protocol version 13 was active, caching the fee of
// that read under version 13's fee schedule.
let contract = get_data_contract_fixture(None, 0, previous_version.protocol_version)
.data_contract_owned();
let contract_id = contract.id().to_buffer();
platform
.drive
.apply_contract(
&contract,
BlockInfo::default(),
true,
None,
None,
previous_version,
)
.expect("expected to apply the contract");
platform
.drive
.get_contract_with_fetch_info_and_fee(
contract_id,
Some(&Epoch::new(0).expect("epoch")),
true,
None,
previous_version,
)
.expect("expected to read the contract");
let contracts = &platform.drive.cache.data_contracts;
let is_cached_with_its_fee = || {
contracts
.get(contract_id, false)
.is_some_and(|cached| cached.has_fee_for_tests())
};
assert!(is_cached_with_its_fee());

let run_the_events = |platform_version: &PlatformVersion| {
contracts.clear_block_cache();
let transaction = platform.drive.grove.start_transaction();
platform
.perform_events_on_first_block_of_protocol_change(
&platform_state,
&block_info,
&transaction,
previous_version.protocol_version,
platform_version,
)
.expect("expected the protocol change events to run");
};

// The events of v1 leave it there, so a hit would keep billing the old fee.
let mut with_the_events_of_v1 = platform_version.clone();
with_the_events_of_v1
.drive_abci
.methods
.protocol_upgrade
.perform_events_on_first_block_of_protocol_change = Some(1);
run_the_events(&with_the_events_of_v1);
assert!(is_cached_with_its_fee());

run_the_events(platform_version);
assert!(contracts.get(contract_id, false).is_none());
assert!(contracts.get(contract_id, true).is_none());
}

#[test]
fn should_rollback_and_retry_app_connect_registration_through_the_upgrade_dispatcher() {
let previous_version = PlatformVersion::get(13).expect("protocol 13");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::error::Error;
use crate::platform_types::platform::Platform;
use crate::platform_types::platform_state::PlatformState;
use dpp::block::block_info::BlockInfo;
use dpp::version::PlatformVersion;
use dpp::version::ProtocolVersion;
use drive::grovedb::Transaction;

impl<C> Platform<C> {
/// Empties the contract cache, then runs the protocol change events of v1.
///
/// A cached contract can carry the fee of the read that cached it, calculated under the fee
/// schedule of the protocol version it was read in, and a cache hit bills that fee again. A
/// protocol change is the only time the schedule can change, and nothing else empties the
/// cache, so without this a node that stayed up across the change would keep billing reads
/// at the old schedule while a node that restarted bills them at the new one.
///
/// The cache is emptied first because v1 then seeds the block cache with the system
/// contracts the events may have rewritten. `clear` keeps the record of what this block
/// rewrote, so a transactional read of a rewritten contract still never falls back to a copy
/// a query puts into the global cache.
pub(super) fn perform_events_on_first_block_of_protocol_change_v2(
&self,
platform_state: &PlatformState,
block_info: &BlockInfo,
transaction: &Transaction,
previous_protocol_version: ProtocolVersion,
platform_version: &PlatformVersion,
) -> Result<(), Error> {
self.drive.cache.data_contracts.clear();

self.perform_events_on_first_block_of_protocol_change_v1(
platform_state,
block_info,
transaction,
previous_protocol_version,
platform_version,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::error::execution::ExecutionError;
use crate::error::Error;
use crate::execution::types::execution_operation::ValidationOperation;
use crate::execution::types::state_transition_execution_context::{
Expand Down Expand Up @@ -39,9 +40,9 @@ pub(in crate::execution::validation::state_transition::state_transitions::contra
impl ContractFeeClaimStateTransitionStateValidationV0 for ContractFeeClaimTransition {
/// Reads the contract and the pot and settles the payout: the signer is a recipient of the
/// pot, the pot was not claimed in this epoch yet, and it holds enough to pay every
/// recipient something. Every refusal after the contract is found is paid for by bumping
/// the signer's contract nonce, and a refused claim leaves the pot's last claim epoch
/// alone.
/// recipient something. Every refusal, a contract that does not exist included, is paid for
/// by bumping the signer's contract nonce, and a refused claim leaves the pot's last claim
/// epoch alone.
///
/// The action carries what each recipient is paid, so Drive pays the pot out without
/// reading it again, and the mempool, which transforms without a state validation stage,
Expand All @@ -58,24 +59,25 @@ impl ContractFeeClaimStateTransitionStateValidationV0 for ContractFeeClaimTransi
let claimant_id = self.owner_id();
let pot = self.pot();

let Some(contract_fetch_info) = platform
.drive
.get_contract_with_fetch_info_and_fee(
let (contract_fetch_fee, maybe_contract_fetch_info) =
platform.drive.get_contract_with_fetch_info_and_fee(
contract_id.to_buffer(),
Some(&block_info.epoch),
false,
tx,
platform_version,
)?
.1
else {
return Ok(ConsensusValidationResult::new_with_error(
DataContractNotPresentError::new(contract_id).into(),
));
};
if let Some(fee) = contract_fetch_info.fee.clone() {
execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee));
}
)?;
// The read is billed from the fee this call returns, whether the contract was pulled
// from disk, was in the cache or does not exist. The fee a cached fetch info carries is
// only there when that entry was built with an epoch, which differs from node to node,
// so billing it would make the fee, and the app hash, depend on the cache.
let contract_fetch_fee =
contract_fetch_fee.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution(
"fee must exist for the contract fetch of a contract fee claim transition",
)))?;
execution_context.add_operation(ValidationOperation::PrecalculatedOperation(
contract_fetch_fee,
));

let bump_action = || {
StateTransitionAction::BumpIdentityDataContractNonceAction(
Expand All @@ -91,6 +93,12 @@ impl ContractFeeClaimStateTransitionStateValidationV0 for ContractFeeClaimTransi
))
};

// Paid like every other refusal: the signer is authenticated and the lookup happened,
// as for a contract update of a contract that does not exist.
let Some(contract_fetch_info) = maybe_contract_fetch_info else {
return refuse(DataContractNotPresentError::new(contract_id).into());
};

// Only who a payout of the pot goes to may claim it: the contract owner for the owner
// pot, a member of the moderation team for the moderators pot. A contract that
// declares no moderation has no team, so nobody claims its moderators pot.
Expand Down
Loading
Loading