diff --git a/packages/rs-dapi/src/services/platform_service/shielded_proof_failure_budget.rs b/packages/rs-dapi/src/services/platform_service/shielded_proof_failure_budget.rs index 375b76c7c66..4af0056d787 100644 --- a/packages/rs-dapi/src/services/platform_service/shielded_proof_failure_budget.rs +++ b/packages/rs-dapi/src/services/platform_service/shielded_proof_failure_budget.rs @@ -20,6 +20,11 @@ use std::time::{Duration, Instant}; use dpp::serialization::PlatformDeserializableUntrusted; use dpp::state_transition::StateTransition; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; +use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; use dpp::state_transition::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; use dpp::state_transition::identity_top_up_from_shielded_pool_transition::IdentityTopUpFromShieldedPoolTransition; use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; @@ -27,7 +32,11 @@ use dpp::state_transition::shield_from_identity_transition::ShieldFromIdentityTr use dpp::state_transition::shield_transition::ShieldTransition; use dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition; use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; use dpp::state_transition::unshield_transition::UnshieldTransition; +use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use dpp::version::PlatformVersion; use tonic::Request; @@ -110,7 +119,10 @@ fn last_forwarded_address(header: &str) -> Option { /// Orchard actions whose proof Drive verifies before admitting the transition, /// or 0 for bytes without any (including bytes that do not decode, which Drive -/// refuses before any proof work). +/// refuses before any proof work). The same count Drive's CheckTx proof +/// admission takes (`shielded_proof_action_count` in drive-abci). Every kind +/// and version is named, so a new one has to say whether it carries actions +/// before this compiles. pub(super) fn orchard_action_count(state_transition_bytes: &[u8]) -> usize { let Ok(state_transition) = StateTransition::deserialize_from_bytes_untrusted(state_transition_bytes) @@ -124,6 +136,9 @@ pub(super) fn orchard_action_count(state_transition_bytes: &[u8]) -> usize { StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(v0)) => { v0.actions.len() } + StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(v1)) => { + v1.actions.len() + } StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0(v0)) => { v0.actions.len() } @@ -136,7 +151,51 @@ pub(super) fn orchard_action_count(state_transition_bytes: &[u8]) -> usize { StateTransition::IdentityTopUpFromShieldedPool( IdentityTopUpFromShieldedPoolTransition::V0(v0), ) => v0.actions.len(), - _ => 0, + // A token pool transition carries two bundles, one against the token's + // pool and one paying the fee from the credit shielded pool; Drive + // verifies both. + StateTransition::TokenShieldedTransferWithShieldedFee( + TokenShieldedTransferWithShieldedFeeTransition::V0(v0), + ) => v0.token_actions.len() + v0.fee_actions.len(), + StateTransition::TokenUnshieldWithShieldedFee( + TokenUnshieldWithShieldedFeeTransition::V0(v0), + ) => v0.token_actions.len() + v0.fee_actions.len(), + StateTransition::TokenPurchaseFromShieldedPool( + TokenPurchaseFromShieldedPoolTransition::V0(v0), + ) => v0.token_actions.len() + v0.fee_actions.len(), + // A batch carries a bundle for each token transition against a token's + // shielded pool and each document whose token price is paid from one. + StateTransition::Batch(batch) => batch + .transitions_iter() + .map(|transition| match transition { + BatchedTransitionRef::Token(token_transition) => token_transition + .shielded_pool_actions() + .map_or(0, |actions| actions.len()), + BatchedTransitionRef::Document(document_transition) => document_transition + .base() + .token_payment_info_ref() + .as_ref() + .and_then(|info| info.shielded_payment()) + .map_or(0, |payment| payment.actions.len()), + }) + .sum(), + StateTransition::DataContractCreate(_) + | StateTransition::DataContractUpdate(_) + | StateTransition::IdentityCreate(_) + | StateTransition::IdentityTopUp(_) + | StateTransition::IdentityCreditWithdrawal(_) + | StateTransition::IdentityUpdate(_) + | StateTransition::IdentityCreditTransfer(_) + | StateTransition::MasternodeVote(_) + | StateTransition::IdentityCreditTransferToAddresses(_) + | StateTransition::IdentityCreateFromAddresses(_) + | StateTransition::IdentityTopUpFromAddresses(_) + | StateTransition::AddressFundsTransfer(_) + | StateTransition::AddressFundingFromAssetLock(_) + | StateTransition::AddressCreditWithdrawal(_) + | StateTransition::IdentityKeyLimitsUpdate(_) + | StateTransition::ContractUserModeration(_) + | StateTransition::ContractFeeClaim(_) => 0, } } @@ -325,9 +384,33 @@ mod tests { use dpp::consensus::codes::ErrorWithCode; use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; use dpp::consensus::state::state_error::StateError; + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; + use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use dpp::platform_value::{BinaryData, Identifier}; use dpp::serialization::PlatformSerializable; use dpp::shielded::SerializedAction; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_base_transition::v1::DocumentBaseTransitionV1; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_burn_transition::TokenBurnTransitionV0; + use dpp::state_transition::batch_transition::token_shielded_transfer_transition::TokenShieldedTransferTransitionV0; + use dpp::state_transition::batch_transition::{ + BatchTransition, BatchTransitionV1, DocumentDeleteTransition, TokenBurnTransition, + TokenShieldedTransferTransition, + }; + use dpp::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; use dpp::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0; + use dpp::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; + use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; + use dpp::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; + use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; + use dpp::tokens::token_payment_info::TokenPaymentInfo; + use dpp::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment}; use std::net::Ipv6Addr; const SOURCE: SourceKey = SourceKey::V4(Ipv4Addr::new(203, 0, 113, 7)); @@ -585,4 +668,200 @@ mod tests { assert_eq!(orchard_action_count(&bytes), 3); assert_eq!(orchard_action_count(&[0xff, 0x00]), 0); } + + fn actions(count: usize) -> Vec { + vec![ + SerializedAction { + nullifier: [1; 32], + rk: [2; 32], + cmx: [3; 32], + encrypted_note: vec![4; 216], + cv_net: [5; 32], + spend_auth_sig: [6; 64], + }; + count + ] + } + + /// A token pool transition carrying a 3-action token bundle and a 2-action + /// fee bundle is charged 5 actions when its proof fails. That is more than + /// the 4-action allowance, so after one failure the source waits until all + /// 5 have drained. + fn assert_both_bundles_metered(transition: StateTransition) { + let actions = orchard_action_count(&transition.serialize_to_bytes().unwrap()); + assert_eq!(actions, 5, "the token bundle and the fee bundle both count"); + + let budget = budget(); + let now = Instant::now(); + broadcast(&budget, SOURCE, actions, true, now).expect("a clean source is admitted"); + assert_eq!( + broadcast(&budget, SOURCE, actions, true, now), + Err(ACTION_DRAIN_INTERVAL * 5), + "a second failing broadcast waits until the first has drained" + ); + } + + #[test] + fn should_meter_a_token_shielded_transfer_with_shielded_fee_and_refuse_a_failing_source() { + assert_both_bundles_metered(StateTransition::TokenShieldedTransferWithShieldedFee( + TokenShieldedTransferWithShieldedFeeTransition::V0( + TokenShieldedTransferWithShieldedFeeTransitionV0 { + data_contract_id: Identifier::from([9; 32]), + token_contract_position: 0, + token_id: Identifier::from([8; 32]), + token_actions: actions(3), + token_anchor: [7; 32], + token_proof: vec![0; 100], + token_binding_signature: [0; 64], + fee_actions: actions(2), + fee_anchor: [7; 32], + fee_proof: vec![0; 100], + fee_binding_signature: [0; 64], + credit_amount: 1, + }, + ), + )); + } + + #[test] + fn should_meter_a_token_unshield_with_shielded_fee_and_refuse_a_failing_source() { + assert_both_bundles_metered(StateTransition::TokenUnshieldWithShieldedFee( + TokenUnshieldWithShieldedFeeTransition::V0(TokenUnshieldWithShieldedFeeTransitionV0 { + data_contract_id: Identifier::from([9; 32]), + token_contract_position: 0, + token_id: Identifier::from([8; 32]), + recipient_id: Identifier::from([5; 32]), + amount: 1, + token_actions: actions(3), + token_anchor: [7; 32], + token_proof: vec![0; 100], + token_binding_signature: [0; 64], + fee_actions: actions(2), + fee_anchor: [7; 32], + fee_proof: vec![0; 100], + fee_binding_signature: [0; 64], + credit_amount: 1, + }), + )); + } + + #[test] + fn should_meter_a_token_purchase_from_shielded_pool_and_refuse_a_failing_source() { + assert_both_bundles_metered(StateTransition::TokenPurchaseFromShieldedPool( + TokenPurchaseFromShieldedPoolTransition::V0( + TokenPurchaseFromShieldedPoolTransitionV0 { + data_contract_id: Identifier::from([9; 32]), + token_contract_position: 0, + token_id: Identifier::from([8; 32]), + token_count: 1, + total_agreed_price: 1, + token_actions: actions(3), + token_anchor: [7; 32], + token_proof: vec![0; 100], + token_binding_signature: [0; 64], + fee_actions: actions(2), + fee_anchor: [7; 32], + fee_proof: vec![0; 100], + fee_binding_signature: [0; 64], + credit_amount: 1, + }, + ), + )); + } + + /// Version 1 is the only shield from an asset lock protocol version 14 + /// admits. + #[test] + fn should_count_the_actions_of_a_shield_from_asset_lock_v1() { + let shield = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1( + ShieldFromAssetLockTransitionV1 { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof::new(1, [1; 36])), + actions: actions(2), + value_balance: 1, + anchor: [7; 32], + proof: vec![0; 100], + binding_signature: [0; 64], + surplus_output: None, + signature: BinaryData::new(vec![0; 65]), + }, + )); + assert_eq!( + orchard_action_count(&shield.serialize_to_bytes().unwrap()), + 2 + ); + } + + #[test] + fn should_count_the_pool_bundles_of_a_batch() { + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::from([9; 32]), + token_id: Identifier::from([8; 32]), + using_group_info: None, + }); + let burn = BatchedTransition::Token(TokenTransition::Burn(TokenBurnTransition::V0( + TokenBurnTransitionV0 { + base: token_base.clone(), + burn_amount: 1, + public_note: None, + }, + ))); + let shielded_transfer = BatchedTransition::Token(TokenTransition::ShieldedTransfer( + TokenShieldedTransferTransition::V0(TokenShieldedTransferTransitionV0 { + base: token_base, + actions: actions(2), + anchor: [7; 32], + proof: vec![0; 100], + binding_signature: [0; 64], + }), + )); + let paid_from_pool = BatchedTransition::Document(DocumentTransition::Delete( + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: DocumentBaseTransition::V1(DocumentBaseTransitionV1 { + id: Identifier::from([4; 32]), + identity_contract_nonce: 1, + document_type_name: "note".to_string(), + data_contract_id: Identifier::from([9; 32]), + token_payment_info: Some(TokenPaymentInfo::V1(TokenPaymentInfoV1 { + payment_token_contract_id: None, + token_contract_position: 0, + minimum_token_cost: None, + maximum_token_cost: Some(1), + gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, + shielded_payment: Box::new(TokenShieldedPayment { + amount: 1, + actions: actions(3), + anchor: [7; 32], + proof: vec![0; 100], + binding_signature: [0; 64], + }), + })), + }), + }), + )); + let count = |transitions| { + let batch = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::from([3; 32]), + transitions, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + })); + orchard_action_count(&batch.serialize_to_bytes().unwrap()) + }; + + assert_eq!( + count(vec![burn.clone()]), + 0, + "a plain token burn is not metered" + ); + assert_eq!(count(vec![shielded_transfer.clone()]), 2); + assert_eq!(count(vec![paid_from_pool.clone()]), 3); + assert_eq!( + count(vec![shielded_transfer, burn, paid_from_pool]), + 5, + "every pool bundle in the batch counts" + ); + } }