Skip to content
Closed
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
18 changes: 18 additions & 0 deletions book/src/fees/shielded-fees.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,24 @@ storage and processing, plus the `shielded_verification_fee` folded into process
(into the pool), `surplus_amount` (to `surplus_output`, or `0`), and `fee_amount` (to the
fee pools); see [Entry-Transition Fees](#entry-transition-fees-shield-shieldfromassetlock-and-shieldfromidentity).

From protocol version 14, an authenticated `Shield` whose Orchard proof fails
moves no credits into the pool. Its input nonces are consumed and its address
inputs pay the metered failure fee plus `shielded_proof_verification_failure`.
The penalty is capped at the funds reachable by the signed fee strategy after
reserving the estimated base fee. It is charged once as a fixed processing fee,
without the user's fee increase. If those funds cannot cover even the base fee,
the refusal is unpaid and consumes no nonce. Duplicate-nullifier refusals remain
unpaid. CheckTx rejects bad proofs before mempool admission; a directly proposed
funded bad proof is a paid failure that validators can accept.

This policy requires Shield wire format 1, whose format tag is covered by the
address witnesses. Format 0 is accepted only at protocol versions 12 and 13;
after activation it is refused before authentication or proof verification,
without charging fees or consuming input nonces. A pending legacy Shield must
be rebuilt and re-signed against the active version. Changing its format tag
alone invalidates its address witnesses. Historical blocks retain their original
wire format and unpaid proof-failure behavior.

## Cryptographic Binding

The fee is not just a field that the platform trusts. It is cryptographically bound
Expand Down
9 changes: 9 additions & 0 deletions book/src/versioning/feature-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ This is used when a field can accept a *range* of versions -- for example, a
data contract serialization format where the system can read versions 0 through
2 but writes version 2 by default.

A state transition can also retire an older signed format. Shield uses format 0
at protocol versions 12 and 13, and format 1 from version 14: the serialization
bounds select the client format before signing, while
`StateTransition::active_version_range` enforces acceptance at untrusted
decoding. The format tag is part of the address signing bytes. Legacy Shield
bytes therefore cannot authorize the bound proof domain and paid proof-failure
policy after activation; pending transactions must be rebuilt and re-signed.
The historical format and validation generations remain available for replay.

## Version Structs: The Middle of the Tree

Between the top-level `PlatformVersion` and the leaf-level `FeatureVersion`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use dpp::state_transition::identity_create_from_shielded_pool_transition::Identi
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::shield_transition::accessors::ShieldTransitionAccessorsV0;
use dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition;
use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition;
use dpp::state_transition::unshield_transition::UnshieldTransition;
Expand Down Expand Up @@ -118,7 +118,7 @@ pub(super) fn orchard_action_count(state_transition_bytes: &[u8]) -> usize {
return 0;
};
match state_transition {
StateTransition::Shield(ShieldTransition::V0(v0)) => v0.actions.len(),
StateTransition::Shield(shield) => shield.actions().len(),
StateTransition::ShieldedTransfer(ShieldedTransferTransition::V0(v0)) => v0.actions.len(),
StateTransition::Unshield(UnshieldTransition::V0(v0)) => v0.actions.len(),
StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(v0)) => {
Expand Down Expand Up @@ -327,6 +327,9 @@ mod tests {
use dpp::consensus::state::state_error::StateError;
use dpp::serialization::PlatformSerializable;
use dpp::shielded::SerializedAction;
use dpp::state_transition::shield_transition::{
ShieldTransition, v0::ShieldTransitionV0, v1::ShieldTransitionV1,
};
use dpp::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0;
use std::net::Ipv6Addr;

Expand Down Expand Up @@ -585,4 +588,59 @@ mod tests {
assert_eq!(orchard_action_count(&bytes), 3);
assert_eq!(orchard_action_count(&[0xff, 0x00]), 0);
}

#[test]
fn should_meter_failed_shield_broadcasts_in_both_wire_formats() {
let actions = 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],
};
3
];
let legacy = ShieldTransitionV0 {
inputs: Default::default(),
actions,
amount: 1,
anchor: [7; 32],
proof: vec![0; 100],
binding_signature: [0; 64],
fee_strategy: Default::default(),
user_fee_increase: 0,
input_witnesses: vec![],
};
let bound = ShieldTransitionV1 {
inputs: legacy.inputs.clone(),
actions: legacy.actions.clone(),
amount: legacy.amount,
anchor: legacy.anchor,
proof: legacy.proof.clone(),
binding_signature: legacy.binding_signature,
fee_strategy: legacy.fee_strategy.clone(),
user_fee_increase: legacy.user_fee_increase,
input_witnesses: legacy.input_witnesses.clone(),
};
for shield in [ShieldTransition::V0(legacy), ShieldTransition::V1(bound)] {
let bytes = StateTransition::Shield(shield)
.serialize_to_bytes()
.unwrap();
let count = orchard_action_count(&bytes);
assert_eq!(
count, 3,
"both formats reserve their proof work before verification"
);
let budget = budget();
let now = Instant::now();
broadcast(&budget, SOURCE, count, true, now).unwrap();
assert_eq!(
broadcast(&budget, SOURCE, count, true, now),
Err(ACTION_DRAIN_INTERVAL * 2),
"a new wire format cannot bypass the source's failed-proof budget"
);
}
}
}
7 changes: 5 additions & 2 deletions packages/rs-dpp/src/state_transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,8 +1208,11 @@ impl StateTransition {
| StateTransition::AddressFundsTransfer(_)
| StateTransition::AddressFundingFromAssetLock(_)
| StateTransition::AddressCreditWithdrawal(_) => 11..=LATEST_VERSION,
StateTransition::Shield(_)
| StateTransition::ShieldedTransfer(_)
StateTransition::Shield(st) => match st {
ShieldTransition::V0(_) => 12..=13,
ShieldTransition::V1(_) => 14..=LATEST_VERSION,
},
StateTransition::ShieldedTransfer(_)
| StateTransition::Unshield(_)
| StateTransition::ShieldedWithdrawal(_) => 12..=LATEST_VERSION,
// From protocol version 14 the bundle must bind its kind and its asset lock, which
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,72 +10,84 @@ impl ShieldTransitionAccessorsV0 for ShieldTransition {
fn actions(&self) -> &[SerializedAction] {
match self {
ShieldTransition::V0(v0) => &v0.actions,
ShieldTransition::V1(v1) => &v1.actions,
}
}

fn set_actions(&mut self, actions: Vec<SerializedAction>) {
match self {
ShieldTransition::V0(v0) => v0.actions = actions,
ShieldTransition::V1(v1) => v1.actions = actions,
}
}

fn amount(&self) -> u64 {
match self {
ShieldTransition::V0(v0) => v0.amount,
ShieldTransition::V1(v1) => v1.amount,
}
}

fn set_amount(&mut self, amount: u64) {
match self {
ShieldTransition::V0(v0) => v0.amount = amount,
ShieldTransition::V1(v1) => v1.amount = amount,
}
}

fn anchor(&self) -> [u8; 32] {
match self {
ShieldTransition::V0(v0) => v0.anchor,
ShieldTransition::V1(v1) => v1.anchor,
}
}

fn set_anchor(&mut self, anchor: [u8; 32]) {
match self {
ShieldTransition::V0(v0) => v0.anchor = anchor,
ShieldTransition::V1(v1) => v1.anchor = anchor,
}
}

fn proof(&self) -> &[u8] {
match self {
ShieldTransition::V0(v0) => &v0.proof,
ShieldTransition::V1(v1) => &v1.proof,
}
}

fn set_proof(&mut self, proof: Vec<u8>) {
match self {
ShieldTransition::V0(v0) => v0.proof = proof,
ShieldTransition::V1(v1) => v1.proof = proof,
}
}

fn binding_signature(&self) -> [u8; 64] {
match self {
ShieldTransition::V0(v0) => v0.binding_signature,
ShieldTransition::V1(v1) => v1.binding_signature,
}
}

fn set_binding_signature(&mut self, binding_signature: [u8; 64]) {
match self {
ShieldTransition::V0(v0) => v0.binding_signature = binding_signature,
ShieldTransition::V1(v1) => v1.binding_signature = binding_signature,
}
}

fn fee_strategy(&self) -> &AddressFundsFeeStrategy {
match self {
ShieldTransition::V0(v0) => &v0.fee_strategy,
ShieldTransition::V1(v1) => &v1.fee_strategy,
}
}

fn set_fee_strategy(&mut self, fee_strategy: AddressFundsFeeStrategy) {
match self {
ShieldTransition::V0(v0) => v0.fee_strategy = fee_strategy,
ShieldTransition::V1(v1) => v1.fee_strategy = fee_strategy,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use crate::state_transition::shield_transition::ShieldTransition;
#[cfg(feature = "state-transition-signing")]
use crate::{
prelude::{AddressNonce, UserFeeIncrease},
state_transition::{shield_transition::v0::ShieldTransitionV0, StateTransition},
state_transition::{
shield_transition::{v0::ShieldTransitionV0, v1::ShieldTransitionV1},
StateTransition,
},
ProtocolError,
};
#[cfg(feature = "state-transition-signing")]
Expand Down Expand Up @@ -57,9 +60,24 @@ impl ShieldTransitionMethodsV0 for ShieldTransition {
)
.await
}
1 => {
ShieldTransitionV1::try_from_bundle_with_signer(
inputs,
actions,
amount,
anchor,
proof,
binding_signature,
fee_strategy,
signer,
user_fee_increase,
platform_version,
)
.await
}
version => Err(ProtocolError::UnknownVersionMismatch {
method: "ShieldTransition::try_from_bundle_with_signer".to_string(),
known_versions: vec![0],
known_versions: vec![0, 1],
received: version,
}),
}
Expand Down
Loading
Loading