Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
855d593
feat(drive): add shielded pool storage, actions, and verification
QuantumExplorer Mar 6, 2026
2c74098
fix(drive): use RootTree enum instead of magic number 72u8 (#3205)
QuantumExplorer Mar 6, 2026
c93ae0e
fix(drive): guard against max_elements=0 in encrypted notes verificat…
QuantumExplorer Mar 6, 2026
73c058a
fix(drive): replace expect/unwrap with error propagation in productio…
QuantumExplorer Mar 6, 2026
d1c1a61
fix(drive): import nullifier constants instead of duplicating them (#…
QuantumExplorer Mar 6, 2026
357e9b0
fix(drive): replace unchecked integer casts with try_from (#3201)
QuantumExplorer Mar 6, 2026
5218ddf
fix(drive): correct tree types in shielded pool estimated costs (#3200)
QuantumExplorer Mar 6, 2026
743faeb
fix(drive): match min_depth between prove and verify for trunk query …
QuantumExplorer Mar 6, 2026
95f8e04
Merge branch 'v3.1-dev' into feat/zk-drive
QuantumExplorer Mar 6, 2026
77615a6
fix(drive): resolve clippy lints in shielded module
QuantumExplorer Mar 6, 2026
2543341
fix(drive): fix CI failures for shielded module
QuantumExplorer Mar 7, 2026
0432425
fix(drive): fix bugs found in CodeRabbit review of shielded module
QuantumExplorer Mar 7, 2026
7221772
refactor(dpp): replace ProtocolError::Generic with specific error types
QuantumExplorer Mar 8, 2026
7ca50a6
build(drive): update grovedb to latest develop (99a2ab01)
QuantumExplorer Mar 8, 2026
ea9e63c
refactor(drive): move per-block nullifier paths to shielded credit pool
QuantumExplorer Mar 8, 2026
f2b5338
refactor(drive): extract shielded pool ops into versioned functions a…
QuantumExplorer Mar 9, 2026
0b82b6b
refactor(drive): derive notes and anchor from transition in try_from_…
QuantumExplorer Mar 9, 2026
295f777
fix(drive): harden bincode decoding and fix limit==0 edge case in shi…
QuantumExplorer Mar 9, 2026
f483b62
Merge branch 'v3.1-dev' into feat/zk-drive
QuantumExplorer Mar 9, 2026
3fb7801
refactor(drive): address review feedback on shielded nullifiers
QuantumExplorer Mar 9, 2026
a8de751
fix(drive): adapt proof verification for shielded pool tree structure…
QuantumExplorer Mar 9, 2026
4d13397
refactor(drive): upgrade grovedb, fix estimated costs, and clean up d…
QuantumExplorer Mar 9, 2026
0f5acd5
refactor(drive): replace nullifier tuple type aliases with structs
QuantumExplorer Mar 9, 2026
acf830a
Merge remote-tracking branch 'origin/v3.1-dev' into feat/zk-drive
QuantumExplorer Mar 9, 2026
b78d53e
Merge remote-tracking branch 'origin/v3.1-dev' into feat/zk-drive
QuantumExplorer Mar 9, 2026
04c537e
feat(drive): adapt to grovedb SubelementsDeletionBehavior per-op enum…
QuantumExplorer Mar 9, 2026
83ea01f
chore: update grovedb to b191ff27 with DontCheckWithNoCleanup
QuantumExplorer Mar 10, 2026
160749d
fix(drive-abci): update version upgrade test for proper DeleteChildre…
QuantumExplorer Mar 10, 2026
3220e43
Merge remote-tracking branch 'origin/v3.1-dev' into feat/zk-drive
QuantumExplorer Mar 10, 2026
82eddb8
chore: update grovedb to dd99ed1d
QuantumExplorer Mar 10, 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
525 changes: 361 additions & 164 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/rs-dpp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ strum = { version = "0.26", features = ["derive"] }
json-schema-compatibility-validator = { path = '../rs-json-schema-compatibility-validator', optional = true }
once_cell = "1.19.0"
tracing = { version = "0.1.41" }
grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "7ecb8465fad750c7cddd5332adb6f97fcceb498b", optional = true }
grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "dd99ed1db0350e5f39127573808dd172c6bc2346", optional = true }

[dev-dependencies]
tokio = { version = "1.40", features = ["full"] }
Expand Down
56 changes: 32 additions & 24 deletions packages/rs-dpp/src/address_funds/platform_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,10 @@ impl PlatformAddress {
pubkey_hash,
)
.map_err(|e| {
ProtocolError::Generic(format!("P2PKH signature verification failed: {}", e))
ProtocolError::AddressWitnessError(format!(
"P2PKH signature verification failed: {}",
e
))
})?;

Ok(AddressWitnessVerificationOperations::for_p2pkh(
Expand All @@ -325,7 +328,7 @@ impl PlatformAddress {
let script = ScriptBuf::from_bytes(redeem_script.to_vec());
let computed_hash = script.script_hash();
if computed_hash.as_byte_array() != script_hash {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Script hash {} does not match address hash {}",
hex::encode(computed_hash.as_byte_array()),
hex::encode(script_hash)
Expand All @@ -343,7 +346,7 @@ impl PlatformAddress {
.collect();

if valid_signatures.len() < threshold {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Not enough signatures: got {}, need {}",
valid_signatures.len(),
threshold
Expand All @@ -368,11 +371,14 @@ impl PlatformAddress {
valid_signatures[sig_idx].as_slice(),
)
.map_err(|e| {
ProtocolError::Generic(format!("Invalid signature format: {}", e))
ProtocolError::AddressWitnessError(format!(
"Invalid signature format: {}",
e
))
})?;

let pub_key = PublicKey::from_slice(&pubkeys[pubkey_idx]).map_err(|e| {
ProtocolError::Generic(format!("Invalid public key: {}", e))
ProtocolError::AddressWitnessError(format!("Invalid public key: {}", e))
})?;

if secp
Expand All @@ -391,20 +397,22 @@ impl PlatformAddress {
signable_bytes.len(),
))
} else {
Err(ProtocolError::Generic(format!(
Err(ProtocolError::AddressWitnessError(format!(
"Not enough valid signatures: verified {}, need {}",
matched, threshold
)))
}
}
(PlatformAddress::P2pkh(_), AddressWitness::P2sh { .. }) => {
Err(ProtocolError::Generic(
Err(ProtocolError::AddressWitnessError(
"P2PKH address requires P2pkh witness, got P2sh".to_string(),
))
}
(PlatformAddress::P2sh(_), AddressWitness::P2pkh { .. }) => Err(
ProtocolError::Generic("P2SH address requires P2sh witness, got P2pkh".to_string()),
),
(PlatformAddress::P2sh(_), AddressWitness::P2pkh { .. }) => {
Err(ProtocolError::AddressWitnessError(
"P2SH address requires P2sh witness, got P2pkh".to_string(),
))
}
}
}

Expand Down Expand Up @@ -435,28 +443,28 @@ impl PlatformAddress {
if byte >= OP_PUSHNUM_1.to_u8() && byte <= OP_PUSHNUM_16.to_u8() {
(byte - OP_PUSHNUM_1.to_u8() + 1) as usize
} else {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Unsupported P2SH script type: only standard multisig (OP_M ... OP_N OP_CHECKMULTISIG) is supported. \
First opcode was 0x{:02x}, expected OP_1 through OP_16",
byte
)));
}
}
Some(Ok(dashcore::blockdata::script::Instruction::PushBytes(_))) => {
return Err(ProtocolError::Generic(
return Err(ProtocolError::AddressWitnessError(
"Unsupported P2SH script type: only standard multisig is supported. \
Script starts with a data push instead of OP_M threshold."
.to_string(),
))
}
Some(Err(e)) => {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Error parsing P2SH script: {:?}",
e
)))
}
None => {
return Err(ProtocolError::Generic(
return Err(ProtocolError::AddressWitnessError(
"Empty P2SH redeem script".to_string(),
))
}
Expand All @@ -483,7 +491,7 @@ impl PlatformAddress {
// This is OP_N, the total number of keys
let n = (byte - OP_PUSHNUM_1.to_u8() + 1) as usize;
if pubkeys.len() != n {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Multisig script declares {} keys but contains {}",
n,
pubkeys.len()
Expand All @@ -492,24 +500,24 @@ impl PlatformAddress {
break;
} else if op == OP_CHECKMULTISIG || op == OP_CHECKMULTISIGVERIFY {
// Hit CHECKMULTISIG without seeing OP_N - malformed
return Err(ProtocolError::Generic(
return Err(ProtocolError::AddressWitnessError(
"Malformed multisig script: OP_CHECKMULTISIG before OP_N".to_string(),
));
} else {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Unsupported opcode 0x{:02x} in P2SH script. Only standard multisig is supported.",
byte
)));
}
}
Some(Err(e)) => {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Error parsing multisig script: {:?}",
e
)))
}
None => {
return Err(ProtocolError::Generic(
return Err(ProtocolError::AddressWitnessError(
"Incomplete multisig script: unexpected end before OP_N".to_string(),
))
}
Expand All @@ -518,7 +526,7 @@ impl PlatformAddress {

// Validate threshold
if threshold > pubkeys.len() {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::AddressWitnessError(format!(
"Invalid multisig: threshold {} exceeds number of keys {}",
threshold,
pubkeys.len()
Expand All @@ -531,24 +539,24 @@ impl PlatformAddress {
if op == OP_CHECKMULTISIG {
// Standard multisig - verify script is complete
if instructions.next().is_some() {
return Err(ProtocolError::Generic(
return Err(ProtocolError::AddressWitnessError(
"Multisig script has extra data after OP_CHECKMULTISIG".to_string(),
));
}
Ok((threshold, pubkeys))
} else if op == OP_CHECKMULTISIGVERIFY {
Err(ProtocolError::Generic(
Err(ProtocolError::AddressWitnessError(
"OP_CHECKMULTISIGVERIFY is not supported, only OP_CHECKMULTISIG"
.to_string(),
))
} else {
Err(ProtocolError::Generic(format!(
Err(ProtocolError::AddressWitnessError(format!(
"Expected OP_CHECKMULTISIG, got opcode 0x{:02x}",
op.to_u8()
)))
}
}
_ => Err(ProtocolError::Generic(
_ => Err(ProtocolError::AddressWitnessError(
"Invalid multisig script: expected OP_CHECKMULTISIG after OP_N".to_string(),
)),
}
Expand Down
6 changes: 6 additions & 0 deletions packages/rs-dpp/src/errors/protocol_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ pub enum ProtocolError {
#[error("Generic Error: {0}")]
Generic(String),

#[error("Address witness verification error: {0}")]
AddressWitnessError(String),

#[error("Shielded transaction build error: {0}")]
ShieldedBuildError(String),
Comment on lines +143 to +147
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Rust semver, is adding a variant to a public enum without #[non_exhaustive] considered a breaking change for downstream crates?

💡 Result:

Yes. In Rust, adding a new variant to a public enum that is not marked #[non_exhaustive] is considered a SemVer-breaking change for downstream crates, because downstream code can (and often does) exhaustively match on the enum, and a new variant makes those matches fail to compile. [1], [2]

If the enum is #[non_exhaustive], downstream crates are required to include a wildcard arm (or otherwise handle unknown variants), so adding variants is intended to be non-breaking. [1]

Sources:
[1] Rust Cargo Book — SemVer Compatibility
[2] Effective Rust — SemVer discussion of adding enum variants


🏁 Script executed:

cat -n packages/rs-dpp/src/errors/protocol_error.rs | head -160

Repository: dashpay/platform

Length of output: 6248


🏁 Script executed:

rg "AddressWitnessError|ShieldedBuildError" --type rust -B 2 -A 2

Repository: dashpay/platform

Length of output: 22729


This is a breaking expansion of the public ProtocolError surface.

ProtocolError is public and not #[non_exhaustive], so adding AddressWitnessError and ShieldedBuildError breaks downstream exhaustive matches. These variants are returned from public builder and witness-verification functions, so callers that key off Generic specifically will miss them unless this release is treated as breaking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/errors/protocol_error.rs` around lines 143 - 147, The new
variants AddressWitnessError and ShieldedBuildError expand the public
ProtocolError enum and break exhaustive matches; to avoid a breaking change,
mark the ProtocolError enum with #[non_exhaustive] (i.e., add the
#[non_exhaustive] attribute above the enum declaration of ProtocolError) so
downstream code cannot exhaustively match it, and update any public
docs/changelog to note the change; ensure the added attribute is applied where
ProtocolError is defined and re-run cargo build/tests.


#[error("Not supported Error: {0}")]
NotSupported(String),

Expand Down
22 changes: 15 additions & 7 deletions packages/rs-dpp/src/shielded/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ pub(crate) fn build_output_only_bundle<P: OrchardProver>(

builder
.add_output(None, payment_address, NoteValue::from_raw(amount), memo)
.map_err(|e| ProtocolError::Generic(format!("failed to add output: {:?}", e)))?;
.map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;

prove_and_sign_bundle(builder, prover, &[], &[])
}
Expand Down Expand Up @@ -185,7 +185,9 @@ pub(crate) fn build_spend_bundle<P: OrchardProver>(
for spend in spends {
builder
.add_spend(fvk.clone(), spend.note, spend.merkle_path)
.map_err(|e| ProtocolError::Generic(format!("failed to add spend: {:?}", e)))?;
.map_err(|e| {
ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
})?;
}

builder
Expand All @@ -195,7 +197,7 @@ pub(crate) fn build_spend_bundle<P: OrchardProver>(
NoteValue::from_raw(output_amount),
memo,
)
.map_err(|e| ProtocolError::Generic(format!("failed to add output: {:?}", e)))?;
.map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;

prove_and_sign_bundle(
builder,
Expand All @@ -217,19 +219,25 @@ pub(crate) fn prove_and_sign_bundle<P: OrchardProver>(

let (unauthorized, _) = builder
.build::<i64>(&mut rng)
.map_err(|e| ProtocolError::Generic(format!("failed to build bundle: {:?}", e)))?
.ok_or_else(|| ProtocolError::Generic("bundle was empty after build".to_string()))?;
.map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to build bundle: {:?}", e)))?
.ok_or_else(|| {
ProtocolError::ShieldedBuildError("bundle was empty after build".to_string())
})?;

let bundle_commitment: [u8; 32] = unauthorized.commitment().into();
let sighash = compute_platform_sighash(&bundle_commitment, extra_sighash_data);

let proven = unauthorized
.create_proof(prover.proving_key(), &mut rng)
.map_err(|e| ProtocolError::Generic(format!("failed to create proof: {:?}", e)))?;
.map_err(|e| {
ProtocolError::ShieldedBuildError(format!("failed to create proof: {:?}", e))
})?;

proven
.apply_signatures(rng, sighash, signing_keys)
.map_err(|e| ProtocolError::Generic(format!("failed to apply signatures: {:?}", e)))
.map_err(|e| {
ProtocolError::ShieldedBuildError(format!("failed to apply signatures: {:?}", e))
})
}

/// Shared test utilities for builder tests.
Expand Down
2 changes: 1 addition & 1 deletion packages/rs-dpp/src/shielded/builder/shield.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn build_shield_transition<S: Signer<PlatformAddress>, P: OrchardProver>(
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError> {
if fee_strategy.is_empty() {
return Err(ProtocolError::Generic(
return Err(ProtocolError::ShieldedBuildError(
"fee_strategy must have at least one step".to_string(),
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn build_shield_from_asset_lock_transition<P: OrchardProver>(
.checked_neg()
.and_then(|v| u64::try_from(v).ok())
.ok_or_else(|| {
ProtocolError::Generic(
ProtocolError::ShieldedBuildError(
"shield_from_asset_lock: bundle value_balance is not negative".to_string(),
)
})?;
Expand Down
22 changes: 13 additions & 9 deletions packages/rs-dpp/src/shielded/builder/shielded_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ pub fn build_shielded_transfer_transition<P: OrchardProver>(
let min_fee = compute_minimum_shielded_fee(num_actions, platform_version);
let effective_fee = match fee {
Some(f) if f < min_fee => {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::ShieldedBuildError(format!(
"fee {} is below minimum required fee {}",
f, min_fee
)));
}
Some(f) if f > min_fee.saturating_mul(1000) => {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::ShieldedBuildError(format!(
"fee {} exceeds 1000x the minimum fee {}",
f, min_fee
)));
Expand All @@ -70,11 +70,11 @@ pub fn build_shielded_transfer_transition<P: OrchardProver>(
None => min_fee,
};

let required = transfer_amount
.checked_add(effective_fee)
.ok_or_else(|| ProtocolError::Generic("fee + transfer_amount overflows u64".to_string()))?;
let required = transfer_amount.checked_add(effective_fee).ok_or_else(|| {
ProtocolError::ShieldedBuildError("fee + transfer_amount overflows u64".to_string())
})?;
if required > total_spent {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::ShieldedBuildError(format!(
"transfer amount {} + fee {} = {} exceeds total spendable value {}",
transfer_amount, effective_fee, required, total_spent
)));
Expand All @@ -89,7 +89,9 @@ pub fn build_shielded_transfer_transition<P: OrchardProver>(
for spend in spends {
builder
.add_spend(fvk.clone(), spend.note, spend.merkle_path)
.map_err(|e| ProtocolError::Generic(format!("failed to add spend: {:?}", e)))?;
.map_err(|e| {
ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
})?;
}

// Primary output to recipient
Expand All @@ -100,7 +102,7 @@ pub fn build_shielded_transfer_transition<P: OrchardProver>(
NoteValue::from_raw(transfer_amount),
memo,
)
.map_err(|e| ProtocolError::Generic(format!("failed to add output: {:?}", e)))?;
.map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;

// Change output (if any)
if change_amount > 0 {
Expand All @@ -112,7 +114,9 @@ pub fn build_shielded_transfer_transition<P: OrchardProver>(
NoteValue::from_raw(change_amount),
[0u8; 36],
)
.map_err(|e| ProtocolError::Generic(format!("failed to add change output: {:?}", e)))?;
.map_err(|e| {
ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e))
})?;
}

// ShieldedTransfer has no extra_data in sighash
Expand Down
8 changes: 4 additions & 4 deletions packages/rs-dpp/src/shielded/builder/shielded_withdrawal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub fn build_shielded_withdrawal_transition<P: OrchardProver>(
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError> {
if withdrawal_amount > i64::MAX as u64 {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::ShieldedBuildError(format!(
"withdrawal amount {} exceeds maximum allowed value {}",
withdrawal_amount,
i64::MAX as u64
Expand All @@ -65,7 +65,7 @@ pub fn build_shielded_withdrawal_transition<P: OrchardProver>(
let min_fee = compute_minimum_shielded_fee(num_actions, platform_version);
let effective_fee = match fee {
Some(f) if f < min_fee => {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::ShieldedBuildError(format!(
"fee {} is below minimum required fee {}",
f, min_fee
)));
Expand All @@ -77,10 +77,10 @@ pub fn build_shielded_withdrawal_transition<P: OrchardProver>(
let required = withdrawal_amount
.checked_add(effective_fee)
.ok_or_else(|| {
ProtocolError::Generic("fee + withdrawal_amount overflows u64".to_string())
ProtocolError::ShieldedBuildError("fee + withdrawal_amount overflows u64".to_string())
})?;
if required > total_spent {
return Err(ProtocolError::Generic(format!(
return Err(ProtocolError::ShieldedBuildError(format!(
"withdrawal amount {} + fee {} = {} exceeds total spendable value {}",
withdrawal_amount, effective_fee, required, total_spent
)));
Expand Down
Loading
Loading