Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a597479
fix(drive): conserve credits in shield, debit only the shielded amount
QuantumExplorer Jun 4, 2026
063decf
fix(drive): make shield reallocation fee-aware + multi-input conserva…
QuantumExplorer Jun 5, 2026
c4a4055
Merge branch 'v3.1-dev' into claude/fix-shield-credit-conservation
QuantumExplorer Jun 6, 2026
efe3292
fix(drive): satisfy clippy doc_lazy_continuation in shield reallocati…
QuantumExplorer Jun 7, 2026
35588c6
refactor(drive): address shield reallocation review nitpicks
QuantumExplorer Jun 7, 2026
062c47a
docs(drive): correct shield reallocation comments + cover the anti-mi…
QuantumExplorer Jun 7, 2026
5b0ff35
feat(drive): charge the shielded minimum fee on Shield and ShieldFrom…
QuantumExplorer Jun 7, 2026
43d6a4c
fix(swift): mark withOptionalSurplusOutput nonisolated
QuantumExplorer Jun 7, 2026
0a23a4e
refactor(drive): address shielded-fee review nitpicks
QuantumExplorer Jun 7, 2026
1f12528
fix(wasm-dpp): map the new ShieldedImplicitFeeCapExceededError to JS
QuantumExplorer Jun 7, 2026
f8a18a9
fix(dpp): append BasicError variant at the tail + harden surplus_outp…
QuantumExplorer Jun 7, 2026
1fd1bbb
fix(wasm-dpp2): enforce 21-byte length in PlatformAddress fromBytes/f…
QuantumExplorer Jun 7, 2026
b0025d2
docs(book): document shielded entry-transition fees
QuantumExplorer Jun 7, 2026
172c48a
refactor(drive): charge the transparent Shield a metered + ZK-compute…
QuantumExplorer Jun 7, 2026
0d5bdaf
fix(drive): price the Core withdrawal document into the ShieldedWithd…
QuantumExplorer Jun 7, 2026
beec036
refactor(drive): collapse the unshipped shield_from_asset_lock conver…
QuantumExplorer Jun 7, 2026
438de4f
test(drive): pin shielded fee invariants (transfer action count, pool…
QuantumExplorer Jun 7, 2026
b8edb01
docs(book): update the shielded fee chapter for the metered Shield + …
QuantumExplorer Jun 7, 2026
933edcf
refactor(dpp): rename compute_shielded_compute_fee → compute_shielded…
QuantumExplorer Jun 7, 2026
23e0c01
fix(drive): advertise the full Shield fee in CheckTx gas_wanted
QuantumExplorer Jun 7, 2026
8ab9cee
refactor(drive): simplify shield reallocation balance bookkeeping
QuantumExplorer Jun 7, 2026
09cf77f
refactor(drive): drop unused block_info from ShieldFromAssetLock tran…
QuantumExplorer Jun 7, 2026
792d1a5
fix(drive): price the output-address write into the Unshield fee
QuantumExplorer Jun 8, 2026
282c838
fix(drive): size the Unshield address fee to its storage bytes (222, …
QuantumExplorer Jun 8, 2026
086b91d
fix(dpp): validate Shield fee-strategy indices structurally
QuantumExplorer Jun 8, 2026
78310f1
feat(drive): cover the ShieldFromAssetLock surplus_output credit in t…
QuantumExplorer Jun 8, 2026
cfd6ea1
test(drive): block-level conservation test for ShieldFromAssetLock + …
QuantumExplorer Jun 8, 2026
238f2b4
fix(wasm-dpp2): make proof-result address-info fromJSON round-trip it…
QuantumExplorer Jun 8, 2026
5c0d27d
fix(wasm-dpp2): preserve credit values in proof-result fromJSON round…
QuantumExplorer Jun 8, 2026
89efcbb
Merge branch 'v3.1-dev' into claude/fix-shield-credit-conservation
QuantumExplorer Jun 8, 2026
63c31f7
fix(drive): verify the ShieldFromAssetLock surplus proof with one str…
QuantumExplorer Jun 8, 2026
c2f0ed6
docs(drive): scrub ephemeral review-finding references from shielded …
QuantumExplorer Jun 8, 2026
788f515
fix(drive-abci): fail closed if a PaidFromAddressInputs fee is under-…
QuantumExplorer Jun 8, 2026
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
30 changes: 29 additions & 1 deletion packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::address_funds::OrchardAddress;
use crate::address_funds::{OrchardAddress, PlatformAddress};
use crate::prelude::AssetLockProof;
use crate::state_transition::shield_from_asset_lock_transition::methods::ShieldFromAssetLockTransitionMethodsV0;
use crate::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition;
Expand All @@ -20,6 +20,9 @@ use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver
/// - `asset_lock_private_key` - Private key for the asset lock (signs the transition)
/// - `prover` - Orchard prover (holds the Halo 2 proving key)
/// - `memo` - 36-byte structured memo for the recipient (4-byte type tag + 32-byte payload)
/// - `surplus_output` - Optional platform address that receives the asset-lock surplus
/// (`asset_lock_value − shield_amount − fee`); when `None`, the surplus is added to the fee
/// pools, capped at `shielded_implicit_fee_cap`
/// - `platform_version` - Protocol version
#[allow(clippy::too_many_arguments)]
pub fn build_shield_from_asset_lock_transition<P: OrchardProver>(
Expand All @@ -29,6 +32,7 @@ pub fn build_shield_from_asset_lock_transition<P: OrchardProver>(
asset_lock_private_key: &[u8],
prover: &P,
memo: [u8; 36],
surplus_output: Option<PlatformAddress>,
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError> {
let bundle = build_output_only_bundle(recipient, shield_amount, memo, prover)?;
Expand All @@ -54,6 +58,7 @@ pub fn build_shield_from_asset_lock_transition<P: OrchardProver>(
sb.anchor,
sb.proof,
sb.binding_signature,
surplus_output,
platform_version,
)
}
Expand All @@ -72,6 +77,9 @@ pub fn build_shield_from_asset_lock_transition<P: OrchardProver>(
/// - `asset_lock_signer` - External signer that produces the outer ECDSA signature
/// - `prover` - Orchard prover (holds the Halo 2 proving key)
/// - `memo` - 36-byte structured memo for the recipient (4-byte type tag + 32-byte payload)
/// - `surplus_output` - Optional platform address that receives the asset-lock surplus
/// (`asset_lock_value − shield_amount − fee`); when `None`, the surplus is added to the fee
/// pools, capped at `shielded_implicit_fee_cap`
/// - `platform_version` - Protocol version
#[cfg(feature = "core_key_wallet")]
#[allow(clippy::too_many_arguments)]
Expand All @@ -83,6 +91,7 @@ pub async fn build_shield_from_asset_lock_transition_with_signer<P, AS>(
asset_lock_signer: &AS,
prover: &P,
memo: [u8; 36],
surplus_output: Option<PlatformAddress>,
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
Expand Down Expand Up @@ -113,6 +122,7 @@ where
sb.anchor,
sb.proof,
sb.binding_signature,
surplus_output,
platform_version,
)
.await
Expand Down Expand Up @@ -151,6 +161,24 @@ mod tests {
assert_eq!(abs_balance, amount);
}

/// Consensus prices the shielded fee from the on-wire `actions.len()`, and the wallet reserves
/// the fee for exactly 2 actions (Orchard's `MIN_ACTIONS`). A single-output, spends-disabled
/// bundle must therefore serialize to exactly 2 on-wire actions. If a future Orchard or builder
/// change alters that padding, the hardcoded wallet reservation would diverge from what consensus
/// charges (a valid client tx would be rejected); this test fails loudly if that invariant breaks.
#[test]
fn test_output_only_bundle_serializes_to_min_actions() {
let recipient = test_orchard_address();
let bundle = build_output_only_bundle(&recipient, 50_000u64, [0u8; 36], &TestProver)
.expect("bundle should build");
let sb = serialize_authorized_bundle(&bundle);
assert_eq!(
sb.actions.len(),
2,
"single-output shield bundle must pad to exactly 2 on-wire actions"
);
}

// -------------------------------------------------------------
// Arithmetic edge cases on the value_balance conversion branch
// (the `checked_neg().and_then(u64::try_from)` chain).
Expand Down
1 change: 1 addition & 0 deletions packages/rs-dpp/src/state_transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2918,6 +2918,7 @@ mod tests {
anchor: [0u8; 32],
proof: vec![],
binding_signature: [0u8; 64],
surplus_output: None,
signature: BinaryData::new(vec![0x55; 65]),
},
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ mod v0;

pub use v0::*;

#[cfg(feature = "state-transition-signing")]
use crate::address_funds::PlatformAddress;
#[cfg(feature = "state-transition-signing")]
use crate::prelude::AssetLockProof;
#[cfg(feature = "state-transition-signing")]
Expand All @@ -27,6 +29,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransition {
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
surplus_output: Option<PlatformAddress>,
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError> {
match platform_version
Expand All @@ -43,6 +46,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransition {
anchor,
proof,
binding_signature,
surplus_output,
platform_version,
),
version => Err(ProtocolError::UnknownVersionMismatch {
Expand All @@ -64,6 +68,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransition {
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
surplus_output: Option<PlatformAddress>,
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
Expand All @@ -85,6 +90,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransition {
anchor,
proof,
binding_signature,
surplus_output,
platform_version,
)
.await
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#[cfg(feature = "state-transition-signing")]
use crate::address_funds::PlatformAddress;
#[cfg(feature = "state-transition-signing")]
use crate::prelude::AssetLockProof;
#[cfg(feature = "state-transition-signing")]
use crate::shielded::SerializedAction;
Expand All @@ -19,6 +21,7 @@ pub trait ShieldFromAssetLockTransitionMethodsV0 {
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
surplus_output: Option<PlatformAddress>,
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>;

Expand All @@ -44,6 +47,7 @@ pub trait ShieldFromAssetLockTransitionMethodsV0 {
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
surplus_output: Option<PlatformAddress>,
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ async fn try_from_asset_lock_with_bundle_and_signer_produces_recoverable_compact
anchor,
proof,
binding_signature,
None,
PlatformVersion::latest(),
)
.await
Expand Down Expand Up @@ -150,6 +151,7 @@ async fn try_from_asset_lock_with_bundle_and_signer_via_outer_dispatcher() {
[0u8; 32],
vec![],
[0u8; 64],
None,
PlatformVersion::latest(),
)
.await
Expand Down Expand Up @@ -188,6 +190,7 @@ async fn outer_dispatcher_rejects_unknown_serialization_version() {
[0u8; 32],
vec![],
[0u8; 64],
None,
&bad_version,
)
.await
Expand Down Expand Up @@ -229,6 +232,7 @@ async fn build_shield_from_asset_lock_transition_with_signer_end_to_end() {
&signer,
&TestProver,
[0u8; 36],
None,
PlatformVersion::latest(),
)
.await
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ impl StateTransitionEstimatedFeeValidation for ShieldFromAssetLockTransition {
&self,
platform_version: &PlatformVersion,
) -> Result<Credits, ProtocolError> {
// This value (the asset-lock base cost, `albc`) is now folded into the ShieldFromAssetLock
// pool fee on the consensus path, so use `checked_mul` to match the rest of the
// fully-checked shielded-fee arithmetic (overflow is unreachable with the current versioned
// constant, but must never silently wrap).
let asset_lock_base_cost = platform_version
.dpp
.state_transitions
.identities
.asset_locks
.required_asset_lock_duff_balance_for_processing_start_for_address_funding
* CREDITS_PER_DUFF;
.checked_mul(CREDITS_PER_DUFF)
.ok_or(ProtocolError::Overflow(
"asset_lock_base_cost credits conversion overflow",
))?;
Ok(asset_lock_base_cost)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod types;
pub(super) mod v0_methods;
mod version;

use crate::address_funds::PlatformAddress;
use crate::identity::state_transition::asset_lock_proof::AssetLockProof;
use crate::shielded::SerializedAction;
use crate::ProtocolError;
Expand Down Expand Up @@ -45,6 +46,14 @@ pub struct ShieldFromAssetLockTransitionV0 {
pub proof: Vec<u8>,
/// RedPallas binding signature
pub binding_signature: [u8; 64],
/// Optional platform-address output that receives the asset-lock surplus
/// (`asset_lock_value − value_balance − fee`, where `fee = compute_minimum_shielded_fee
/// + asset_lock_base_cost`). When `None`, the surplus is instead added to the fee pools,
/// but only up to `shielded_implicit_fee_cap` (otherwise the transition is rejected so a
/// client cannot accidentally forfeit a large remainder). Placed before `signature` so it
/// is covered by the signable bytes — the ECDSA signature commits to the surplus
/// destination, which therefore cannot be redirected without invalidating the transition.
pub surplus_output: Option<PlatformAddress>,
/// ECDSA signature over the signable bytes (excluded from sig hash)
#[platform_signable(exclude_from_sig_hash)]
pub signature: BinaryData,
Expand Down Expand Up @@ -90,6 +99,7 @@ mod tests {
anchor: [7u8; 32],
proof: vec![8u8; 100],
binding_signature: [9u8; 64],
surplus_output: None,
signature: BinaryData::new(vec![10u8; 65]),
};

Expand All @@ -114,6 +124,7 @@ mod tests {
anchor: [7u8; 32],
proof: vec![8u8; 100],
binding_signature: [9u8; 64],
surplus_output: None,
signature: BinaryData::new(vec![10u8; 65]),
}
}
Expand Down Expand Up @@ -188,4 +199,14 @@ mod tests {
use crate::state_transition::FeatureVersioned;
assert_eq!(make_v0().feature_version(), 0);
}

#[test]
fn test_round_trip_with_surplus_output() {
// The optional surplus_output field must serialize/deserialize identically. It sits before
// the sig-excluded `signature` field, so it is part of the signable bytes (the asset-lock
// ECDSA signature commits to it — a missing/desynced field would change the wire layout).
let mut transition = make_v0();
transition.surplus_output = Some(crate::address_funds::PlatformAddress::P2pkh([0x44; 20]));
test_round_trip(transition);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ mod tests {
anchor: [7u8; 32],
proof: vec![8u8; 100],
binding_signature: [9u8; 64],
surplus_output: None,
signature: BinaryData::new(vec![10u8; 65]),
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#[cfg(feature = "state-transition-signing")]
use crate::address_funds::PlatformAddress;
#[cfg(feature = "state-transition-signing")]
use crate::prelude::AssetLockProof;
#[cfg(feature = "state-transition-signing")]
use crate::serialization::Signable;
Expand All @@ -23,6 +25,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransitionV0
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
surplus_output: Option<PlatformAddress>,
_platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError> {
// Create the unsigned transition
Expand All @@ -33,6 +36,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransitionV0
anchor,
proof,
binding_signature,
surplus_output,
signature: Default::default(),
};

Expand All @@ -57,6 +61,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransitionV0
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
surplus_output: Option<PlatformAddress>,
_platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
Expand All @@ -73,6 +78,7 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransitionV0
anchor,
proof,
binding_signature,
surplus_output,
signature: Default::default(),
};

Expand Down
Loading
Loading