Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6d4f7e9
perf(dpp): prove a shield-from-asset-lock bundle before its lock proo…
PastaPastaPasta Oct 5, 2026
9ff4a13
refactor(platform-wallet): report an asset lock's outpoint before its…
PastaPastaPasta Oct 5, 2026
0a127ae
perf(platform-wallet): prove the shield bundle while waiting for the …
PastaPastaPasta Oct 5, 2026
b2d588a
docs(platform-wallet): note that a proved shield bundle retains its s…
PastaPastaPasta Oct 5, 2026
d5a49f2
fix(platform-wallet): derive the speculative and resolved shield amou…
PastaPastaPasta Oct 5, 2026
2065571
test(dpp): verify re-assembled asset-lock shield bundles as consensus…
PastaPastaPasta Oct 5, 2026
f7a124d
test(platform-wallet): cut the shield-proof overlap tests to determin…
PastaPastaPasta Oct 8, 2026
5b4ca2d
perf(platform-wallet): read one tracked asset lock's value without cl…
PastaPastaPasta Oct 8, 2026
ab4f5ad
fix(platform-wallet): return a failed funding resolution without wait…
PastaPastaPasta Oct 8, 2026
79d5d04
refactor(dpp): prove asset-lock shield bundles through the shared bui…
PastaPastaPasta Oct 9, 2026
2e39be0
refactor(platform-wallet): overlap the shield proof with a plain spaw…
PastaPastaPasta Oct 9, 2026
33b059f
fix(platform-wallet): never hold a resolved funding for speculative p…
PastaPastaPasta Oct 9, 2026
f734b65
fix(platform-wallet): drop unstarted speculation as soon as funding r…
PastaPastaPasta Oct 9, 2026
36d5312
fix(platform-wallet): price, prove and assemble a shield bundle under…
PastaPastaPasta Oct 9, 2026
7ae79dd
fix(dpp): pin a proved shield bundle to its outpoint at every protoco…
PastaPastaPasta Oct 9, 2026
77e2a7d
fix(platform-wallet): cancel a queued shield proof on every exit
PastaPastaPasta Oct 9, 2026
be97f1e
fix(platform-wallet): recheck the protocol version after signing a sh…
PastaPastaPasta Oct 9, 2026
83a9586
fix(platform-wallet): drop a stale speculative proof before re-proving
PastaPastaPasta Oct 9, 2026
bc58b5a
fix(platform-wallet): size a fresh shield lock under the same protoco…
PastaPastaPasta Oct 29, 2026
b833f35
docs(platform-wallet): record the shield-from-asset-lock prover bound…
PastaPastaPasta Oct 29, 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@

### Changed

- **platform-wallet (breaking):** Core → Shielded funding
(`PlatformWallet::shielded_fund_from_asset_lock`) proves its Orchard bundle
while the asset lock's InstantSend lock is awaited, on Tokio's blocking pool,
and reuses that proof for the ChainLock fallback and retries. The prover is
moved into that task, so the method now requires
`P: OrchardProver + Copy + Send + 'static`: pass `&CachedOrchardProver`
instead of a borrowed, non-`'static` prover.
- **Rust dependencies:** pin rust-dashcore to `dev` at `280b3f2c`, which includes
[rust-dashcore#1175](https://github.com/dashpay/rust-dashcore/pull/1175):
every transaction payload keeps its stored serde variant index, so wallet rows
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 47 additions & 6 deletions packages/rs-dpp/src/shielded/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ pub use token_shielded_transfer::build_token_shielded_transfer_transition;
pub use token_unshield::build_token_unshield_transition;
pub use unshield::build_unshield_transition;

// Prove-then-assemble form of the external-signer `ShieldFromAssetLock` builder, for proving
// while the asset lock's InstantSend lock is still awaited.
#[cfg(feature = "core_key_wallet")]
pub use shield_from_asset_lock::ProvedShieldFromAssetLockBundle;

use grovedb_commitment_tree::{
Anchor, Authorized, Builder, Bundle, BundleType, DashMemo, Flags as OrchardFlags,
FullViewingKey, MerklePath, Note, NoteValue, OutgoingViewingKey, PaymentAddress, ProvingKey,
Expand Down Expand Up @@ -222,6 +227,19 @@ pub(crate) fn build_output_only_bundle<P: OrchardProver>(
extra_sighash_data: &[u8],
prover: &P,
) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
let builder = output_only_bundle_builder(recipient, amount, memo, sender_ovk, dummy_outputs)?;
prove_and_sign_bundle(builder, prover, &[], extra_sighash_data)
}

/// The unproved builder of an output-only bundle: the real output, then `dummy_outputs`
/// zero-value fillers (see [`build_output_only_bundle`]).
fn output_only_bundle_builder(
recipient: &OrchardAddress,
amount: u64,
memo: [u8; 36],
sender_ovk: Option<OutgoingViewingKey>,
dummy_outputs: usize,
) -> Result<Builder<DashMemo>, ProtocolError> {
let payment_address = PaymentAddress::from(recipient);
let anchor = Anchor::empty_tree();
let mut builder = Builder::<DashMemo>::new(
Expand Down Expand Up @@ -252,7 +270,7 @@ pub(crate) fn build_output_only_bundle<P: OrchardProver>(
})?;
}

prove_and_sign_bundle(builder, prover, &[], extra_sighash_data)
Ok(builder)
}

/// Builds a spend+output Orchard bundle.
Expand Down Expand Up @@ -364,6 +382,26 @@ pub(crate) fn prove_and_sign_bundle_with<P: OrchardProver, F>(
signing_keys: &[SpendAuthorizingKey],
extra_sighash_data: F,
) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError>
where
F: FnOnce(&[[u8; 32]]) -> Result<Vec<u8>, ProtocolError>,
{
prove_bundle_with(builder, prover, extra_sighash_data)?(signing_keys)
}

/// The signing step of a proved bundle (see [`prove_bundle_with`]).
pub(crate) type SignBundleFn = dyn Fn(&[SpendAuthorizingKey]) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError>
+ Send
+ Sync;

/// [`prove_and_sign_bundle_with`] up to, not including, the signatures: returns the signing step,
/// which signs a copy of the proved bundle over the sighash bound here, with fresh randomness on
/// every call. The closure holds the bundle's signing secrets (`bsk`, and the padding spends'
/// `dummy_ask` and `alpha`), fresh per bundle and dropped with it.
pub(crate) fn prove_bundle_with<P: OrchardProver, F>(
builder: Builder<DashMemo>,
prover: &P,
extra_sighash_data: F,
) -> Result<Box<SignBundleFn>, ProtocolError>
where
F: FnOnce(&[[u8; 32]]) -> Result<Vec<u8>, ProtocolError>,
{
Expand Down Expand Up @@ -392,11 +430,14 @@ where
ProtocolError::ShieldedBuildError(format!("failed to create proof: {:?}", e))
})?;

proven
.apply_signatures(rng, sighash, signing_keys)
.map_err(|e| {
ProtocolError::ShieldedBuildError(format!("failed to apply signatures: {:?}", e))
})
Ok(Box::new(move |signing_keys: &[SpendAuthorizingKey]| {
proven
.clone()
.apply_signatures(OsRng, sighash, signing_keys)
.map_err(|e| {
ProtocolError::ShieldedBuildError(format!("failed to apply signatures: {:?}", e))
})
}))
}

/// Shared test utilities for builder tests.
Expand Down
214 changes: 195 additions & 19 deletions packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#[cfg(feature = "core_key_wallet")]
use dashcore::OutPoint;

use crate::address_funds::{OrchardAddress, PlatformAddress};
#[cfg(feature = "core_key_wallet")]
use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
use crate::prelude::AssetLockProof;
use crate::shielded::shield_from_asset_lock_extra_sighash_data;
use crate::state_transition::shield_from_asset_lock_transition::methods::ShieldFromAssetLockTransitionMethodsV0;
Expand All @@ -10,6 +15,121 @@ use platform_version::version::PlatformVersion;
use super::{
build_output_only_bundle, serialize_authorized_bundle, OrchardProver, SerializedBundle,
};
#[cfg(feature = "core_key_wallet")]
use super::{output_only_bundle_builder, prove_bundle_with, SignBundleFn};

/// A `ShieldFromAssetLock` Orchard bundle, proved for a locked outpoint but not yet wrapped into
/// a transition. Proved by [`Self::prove`]; wrapped any number of times by
/// [`Self::build_transition_with_signer`].
///
/// The bundle's sighash binds the locked outpoint (see
/// [`shield_from_asset_lock_extra_sighash_data`]), not the proof that presents it, so the Halo 2
/// proof can be made before the InstantSend lock arrives and reused under a ChainLock proof of the
/// same outpoint. Only the outer ECDSA signature covers the asset-lock proof.
///
/// Each assembly re-signs the bundle with fresh randomness, so a resubmission never repeats a
/// rejected transition's hash (Tenderdash caches those and drops repeats). To allow that, the value
/// holds the bundle's signing secrets, with which its holder could re-bind the proof to another
/// asset lock and fund the same note twice: keep it in memory, and drop it once the transition has
/// landed or been abandoned.
#[cfg(feature = "core_key_wallet")]
pub struct ProvedShieldFromAssetLockBundle {
/// Signs the proved bundle (see `prove_bundle_with`). The proved-but-unsigned bundle type is
/// not re-exported by `grovedb-commitment-tree`, so it lives inside this closure.
sign: Box<SignBundleFn>,
/// The outpoint the bundle was proved for. Checked on its own, because at versions that bind
/// nothing the sighash would accept a proof of any outpoint.
asset_lock_out_point: OutPoint,
/// The asset-lock binding the sighash committed to; empty at versions that bind nothing.
extra_sighash_data: Vec<u8>,
}

#[cfg(feature = "core_key_wallet")]
impl ProvedShieldFromAssetLockBundle {
/// Proves the outputs-only bundle of a `ShieldFromAssetLock` that the asset lock at
/// `asset_lock_out_point` will fund: the CPU-heavy half (seconds of Halo 2 proving) of
/// [`build_shield_from_asset_lock_transition_with_signer`], whose parameters it shares. It does
/// no I/O and no signing, so it can run on a blocking thread while the lock's InstantSend lock
/// is awaited.
#[allow(clippy::too_many_arguments)]
pub fn prove<P: OrchardProver>(
recipient: &OrchardAddress,
shield_amount: u64,
asset_lock_out_point: OutPoint,
prover: &P,
memo: [u8; 36],
sender_ovk: Option<grovedb_commitment_tree::OutgoingViewingKey>,
dummy_outputs: usize,
platform_version: &PlatformVersion,
) -> Result<Self, ProtocolError> {
// The binding depends on the locked outpoint alone, never on the proof kind or the
// chain-lock height, so a chain proof of the outpoint stands in for the proof that does
// not exist yet. Assembly re-derives the binding from the real proof.
let lock_placeholder = AssetLockProof::Chain(ChainAssetLockProof {
core_chain_locked_height: 0,
out_point: asset_lock_out_point,
});
let extra_sighash_data =
shield_from_asset_lock_extra_sighash_data(&lock_placeholder, platform_version)?;
let builder =
output_only_bundle_builder(recipient, shield_amount, memo, sender_ovk, dummy_outputs)?;
let bound = extra_sighash_data.clone();
let sign = prove_bundle_with(builder, prover, move |_| Ok(bound))?;
Ok(Self {
sign,
asset_lock_out_point,
extra_sighash_data,
})
}

/// Wraps the proved bundle into a `ShieldFromAssetLock` transition funded by
/// `asset_lock_proof`: signs the bundle afresh and has `asset_lock_signer` produce the outer
/// ECDSA signature. Does not consume the proved bundle, so a rejected transition can be
/// re-assembled, with a new asset-lock proof or the same one, without proving again.
///
/// Errors when `asset_lock_proof` is for another outpoint, even at versions whose sighash
/// binds nothing (one proved bundle around two locks would fund the same note twice), and,
/// rather than produce a transition consensus would reject, when `platform_version` binds
/// differently from the version the bundle was proved at.
pub async fn build_transition_with_signer<AS>(
&self,
asset_lock_proof: AssetLockProof,
asset_lock_proof_path: &::key_wallet::bip32::DerivationPath,
asset_lock_signer: &AS,
surplus_output: Option<PlatformAddress>,
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
AS: ::key_wallet::signer::Signer,
{
if asset_lock_proof.out_point() != Some(self.asset_lock_out_point)
|| shield_from_asset_lock_extra_sighash_data(&asset_lock_proof, platform_version)?
!= self.extra_sighash_data
{
return Err(ProtocolError::ShieldedBuildError(
"shield_from_asset_lock: the asset lock proof does not match the binding the \
bundle was proved with (different outpoint or protocol version)"
.to_string(),
));
}
let sb = serialize_authorized_bundle(&(self.sign)(&[])?);
let value_balance = pool_inflow(&sb)?;

ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer(
asset_lock_proof,
asset_lock_proof_path,
asset_lock_signer,
sb.actions,
value_balance,
sb.anchor,
sb.proof,
sb.binding_signature,
surplus_output,
platform_version,
)
.await
}
}

/// Builds a ShieldFromAssetLock state transition (core asset lock -> shielded pool).
///
Expand Down Expand Up @@ -119,35 +239,34 @@ where
P: OrchardProver,
AS: ::key_wallet::signer::Signer,
{
let (sb, value_balance) = build_bound_bundle(
// The same proof-then-assembly a client proving ahead of the lock proof goes through.
let asset_lock_out_point = asset_lock_proof.out_point().ok_or_else(|| {
ProtocolError::IdentifierError(String::from("No output at a given index"))
})?;
ProvedShieldFromAssetLockBundle::prove(
recipient,
shield_amount,
&asset_lock_proof,
asset_lock_out_point,
prover,
memo,
sender_ovk,
dummy_outputs,
platform_version,
)?;

ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer(
)?
.build_transition_with_signer(
asset_lock_proof,
asset_lock_proof_path,
asset_lock_signer,
sb.actions,
value_balance,
sb.anchor,
sb.proof,
sb.binding_signature,
surplus_output,
platform_version,
)
.await
}

/// Proves the outputs-only bundle of a `ShieldFromAssetLock` funded by `asset_lock_proof` and
/// returns it with the amount it moves into the pool. Both builders go through here, so a client
/// signing with a raw key and one signing through an external signer bind the same preimage.
/// Proves and signs the outputs-only bundle of a `ShieldFromAssetLock` funded by
/// `asset_lock_proof` and returns it with the amount it moves into the pool, for the raw-key
/// builder. It binds the same preimage as [`ProvedShieldFromAssetLockBundle::prove`], which the
/// external-signer builder goes through.
#[allow(clippy::too_many_arguments)]
fn build_bound_bundle<P: OrchardProver>(
recipient: &OrchardAddress,
Expand All @@ -173,19 +292,21 @@ fn build_bound_bundle<P: OrchardProver>(
prover,
)?;
let sb = serialize_authorized_bundle(&bundle);
let value_balance = pool_inflow(&sb)?;
Ok((sb, value_balance))
}

// For output-only bundles, Orchard value_balance is negative (value flowing in).
// Convert to u64 (absolute amount entering the pool).
let value_balance = sb
.value_balance
/// The amount an outputs-only bundle moves into the pool. Its Orchard value_balance is negative
/// (value flowing in); this is the absolute amount, as a `u64`.
fn pool_inflow(sb: &SerializedBundle) -> Result<u64, ProtocolError> {
sb.value_balance
.checked_neg()
.and_then(|v| u64::try_from(v).ok())
.ok_or_else(|| {
ProtocolError::ShieldedBuildError(
"shield_from_asset_lock: bundle value_balance is not negative".to_string(),
)
})?;
Ok((sb, value_balance))
})
}

#[cfg(test)]
Expand Down Expand Up @@ -285,4 +406,59 @@ mod tests {
);
}
}

#[cfg(feature = "core_key_wallet")]
#[test]
fn proved_bundle_signs_afresh_and_verifies_against_a_real_lock_proof_binding() {
// Consensus derives the sighash from the transition's own lock proof, not from what the
// builder stored: verify against that, for a proof `prove` never saw, after each of two
// signings (`signing_tests` checks they share the proof but not the signatures).
use super::ProvedShieldFromAssetLockBundle;
use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
use crate::prelude::AssetLockProof;
use crate::shielded::{
compute_platform_sighash, shield_from_asset_lock_extra_sighash_data,
};
use dashcore::OutPoint;
use grovedb_commitment_tree::{BatchValidator, VerifyingKey};
use platform_version::version::PlatformVersion;
use rand::rngs::OsRng;

let platform_version = PlatformVersion::latest();
let out_point = OutPoint::from([0x55; 36]);
let proved = ProvedShieldFromAssetLockBundle::prove(
&test_orchard_address(),
50_000,
out_point,
&TestProver,
[0u8; 36],
None,
0,
platform_version,
)
.expect("bundle should prove");
let first = (proved.sign)(&[]).expect("bundle should sign");
let second = (proved.sign)(&[]).expect("bundle should sign again");

let lock = AssetLockProof::Chain(ChainAssetLockProof {
core_chain_locked_height: 100,
out_point,
});
let extra =
shield_from_asset_lock_extra_sighash_data(&lock, platform_version).expect("binding");
assert!(!extra.is_empty(), "the latest version binds the asset lock");
let commitment: [u8; 32] = first.commitment().into();
let vk = VerifyingKey::build();
let verifies = |bundle, extra: &[u8]| {
let mut batch = BatchValidator::new();
batch.add_bundle(bundle, compute_platform_sighash(&commitment, extra));
batch.validate(&vk, OsRng)
};
assert!(verifies(&first, &extra));
assert!(verifies(&second, &extra));
assert!(
!verifies(&first, &extra[..1]),
"signatures bind the proving-time sighash"
);
}
}
Loading
Loading