Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,23 @@ 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;
use dpp::state_transition::shield_from_identity_transition::ShieldFromIdentityTransition;
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;

Expand Down Expand Up @@ -110,7 +119,10 @@ fn last_forwarded_address(header: &str) -> Option<IpAddr> {

/// 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)
Expand All @@ -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()
}
Expand All @@ -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(),
Comment on lines +157 to +181

@thepastaclaw thepastaclaw Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏭️ Deferred (maintainer decision; not blocking; tracked in #5676) at 704e7fe3; see the replies below.

🟡 Suggestion: Share structural proof-action counting through DPP

These additions extend a second implementation of the structural traversal in Drive-ABCI's shielded_proof_action_count (processor/traits/shielded_proof.rs:163–234). The implementations agree today, and the exhaustive DAPI match catches new StateTransition variants, but it cannot catch changed proof-bearing fields or counting behavior within an existing variant. That leaves the same maintenance mechanism that produced this bypass. The book's lowest-capable-layer guidance points to DPP for this pure-data traversal, consistent with the existing shared shielded_pool_actions() accessor. Use the already-linked #5676 to track a shared StateTransition accessor consumed by both counters; keep decoding, source reservations, clamping, and settlement in DAPI. This is a non-blocking consolidation follow-up, not a defect in the counts introduced here.

source: glm-5.3-flash (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general, architecture-layering, rust-quality, security-auditor)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. It is consensus-neutral too: Drive only calls shielded_proof_action_count from CheckTx admission. Leaving it out of this PR because it touches dpp and drive-abci on a patch branch, and #5799 (v5.1-dev) adds type 29 to both counters. Planned as a follow-up on v5.1-dev after #5799: one exhaustive DPP accessor that both DAPI and Drive call (Drive's copy still ends in _ => 0). Tracking under #5676.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed the counter’s callers are confined to CheckTx admission; the shared exhaustive DPP accessor is a reasonable follow-up on v5.1-dev after #5799, tracked under #5676, rather than expanding this patch.

Tracked in #5676. Deferred on a maintainer's call: this finding no longer blocks this pull request.

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,
}
}

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<SerializedAction> {
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"
);
}
}
Loading