Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cb904da
feat(dpp): shielded state transitions and Orchard bundle types (Medusa)
QuantumExplorer Mar 4, 2026
3616845
chore: add todo!() stubs for shielded state transition types in depen…
QuantumExplorer Mar 4, 2026
03d8a19
Merge branch 'v3.1-dev' into feat/zk-dpp
QuantumExplorer Mar 4, 2026
e7eb917
refactor: address PR review feedback on shielded DPP types
QuantumExplorer Mar 4, 2026
e63453d
docs: correct anchor comments to describe Sinsemilla root, not generi…
QuantumExplorer Mar 4, 2026
88613b7
style: cargo fmt + remove unreachable catch-all patterns in wasm-dpp
QuantumExplorer Mar 4, 2026
9c5e3b5
refactor(dpp): address PR review — flags docs, value_balance u64, sig…
QuantumExplorer Mar 4, 2026
bab0936
refactor(dpp): remove flags field from all shielded transition structs
QuantumExplorer Mar 4, 2026
7016a81
fix(dpp): resolve clippy warnings in shielded builder
QuantumExplorer Mar 4, 2026
4d8cc59
refactor(dpp): rename shielded-bundle-building feature to shielded-tx
QuantumExplorer Mar 5, 2026
ecad334
refactor(dpp): remove flags from OrchardBundleParams
QuantumExplorer Mar 5, 2026
36e95f5
chore(dpp): add TODO to remove user_fee_increase from ShieldFromAsset…
QuantumExplorer Mar 5, 2026
80ffeb0
Merge remote-tracking branch 'origin/v3.1-dev' into feat/zk-dpp
QuantumExplorer Mar 5, 2026
413b4e5
refactor(dpp): apply user_fee_increase trait extraction to shielded t…
QuantumExplorer Mar 5, 2026
114675b
refactor(dpp): split shielded builder into per-function files with tests
QuantumExplorer Mar 5, 2026
aee5147
refactor(dpp): add OrchardProver trait, rename shield value_balance t…
QuantumExplorer Mar 5, 2026
89072a9
docs(dpp): update proving_key doc comments to reference prover param
QuantumExplorer Mar 5, 2026
7d5fd72
fix(drive): resolve CI failures in dpp formatting, drive and drive-abci
QuantumExplorer Mar 5, 2026
babebf0
fix(dpp): use asset lock proof as unique identifier for ShieldFromAss…
QuantumExplorer Mar 5, 2026
fa81bde
refactor(dpp): remove redundant `amount` field from Unshield and Shie…
QuantumExplorer Mar 5, 2026
218981f
refactor(dpp): rename value_balance to unshielding_amount (u64) in Un…
QuantumExplorer Mar 5, 2026
caf604f
refactor(dpp): address PR review for ShieldFromAssetLock
QuantumExplorer Mar 5, 2026
d5940a6
fix(dpp): validate withdrawal/unshield amount is within i64::MAX
QuantumExplorer Mar 5, 2026
5950a33
fix(dpp): validate unshielding_amount <= i64::MAX in structure valida…
QuantumExplorer Mar 5, 2026
189b604
Merge branch 'v3.1-dev' into feat/zk-dpp
QuantumExplorer Mar 5, 2026
1ad16f8
chore(dpp): cargo fmt
QuantumExplorer Mar 5, 2026
cf11390
fix(drive): update PathQuery Display assertion to match grovedb changes
QuantumExplorer Mar 5, 2026
33d5ac4
refactor(dpp): move serde_bytes_64 to serialization module and extrac…
QuantumExplorer Mar 5, 2026
d587ff9
Merge branch 'v3.1-dev' into feat/zk-dpp
QuantumExplorer Mar 5, 2026
dba048f
refactor(dpp): remove Option wrapper from common shielded validation …
QuantumExplorer Mar 5, 2026
14e1fb4
Merge branch 'v3.1-dev' into feat/zk-dpp
QuantumExplorer Mar 6, 2026
a9ad944
refactor(dpp): derive Serialize/Deserialize for asset lock types, rem…
QuantumExplorer Mar 6, 2026
c21b5ff
refactor(dpp): rename shielded-tx feature to shielded-client
QuantumExplorer Mar 6, 2026
ef0a569
chore(dpp): cargo fmt
QuantumExplorer Mar 6, 2026
8633829
chore(dpp): document shielded-client feature
QuantumExplorer Mar 6, 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
494 changes: 482 additions & 12 deletions Cargo.lock

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,35 @@ key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "0bc6592b
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "0bc6592bd41037ffc532d813d4c0828bea7cf882" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "0bc6592bd41037ffc532d813d4c0828bea7cf882" }

# Optimize heavy crypto crates even in dev/test builds so that
# Halo 2 proof generation and verification run at near-release speed.
# Without this, ZK operations are 10-100x slower (debug field arithmetic).
[profile.dev.package.halo2_proofs]
opt-level = 3
[profile.dev.package.halo2_gadgets]
opt-level = 3
[profile.dev.package.halo2_poseidon]
opt-level = 3
[profile.dev.package.orchard]
opt-level = 3
[profile.dev.package.pasta_curves]
opt-level = 3
[profile.dev.package.grovedb-commitment-tree]
opt-level = 3

[profile.test.package.halo2_proofs]
opt-level = 3
[profile.test.package.halo2_gadgets]
opt-level = 3
[profile.test.package.halo2_poseidon]
opt-level = 3
[profile.test.package.orchard]
opt-level = 3
[profile.test.package.pasta_curves]
opt-level = 3
[profile.test.package.grovedb-commitment-tree]
opt-level = 3

[workspace.package]

version = "3.1.0-dev.1"
Expand Down
9 changes: 9 additions & 0 deletions packages/rs-dpp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +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 }

[dev-dependencies]
tokio = { version = "1.40", features = ["full"] }
Expand Down Expand Up @@ -327,5 +328,13 @@ extended-document = [
]
token-reward-explanations = ["dep:chrono-tz"]

# Gates client-side Orchard helpers (address encoding, bundle building, proving).
# Clients that don't need to create shielded transactions can omit this to avoid
# compiling the Orchard/Halo 2 dependency tree. The shielded state transition
# types themselves are always available — only the client tooling is behind this gate.
shielded-client = [
"state-transition-signing",
"dep:grovedb-commitment-tree",
]
factories = []
client = ["factories", "state-transitions"]
4 changes: 4 additions & 0 deletions packages/rs-dpp/src/address_funds/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
pub mod fee_strategy;
#[cfg(feature = "shielded-client")]
mod orchard_address;
mod platform_address;
mod witness;
mod witness_verification_operations;

pub use fee_strategy::*;
#[cfg(feature = "shielded-client")]
pub use orchard_address::*;
pub use platform_address::*;
pub use witness::*;
pub use witness_verification_operations::*;
286 changes: 286 additions & 0 deletions packages/rs-dpp/src/address_funds/orchard_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
use bech32::{Bech32m, Hrp};
use dashcore::Network;

use crate::address_funds::platform_address::{PLATFORM_HRP_MAINNET, PLATFORM_HRP_TESTNET};
use crate::address_funds::PlatformAddress;
use crate::ProtocolError;

/// Size of the Orchard diversifier (11 bytes).
pub const ORCHARD_DIVERSIFIER_SIZE: usize = 11;
/// Size of the Orchard diversified transmission key pk_d (32 bytes, Pallas curve point).
pub const ORCHARD_PKD_SIZE: usize = 32;
/// Total size of a raw Orchard payment address (43 bytes = diversifier + pk_d).
pub const ORCHARD_ADDRESS_SIZE: usize = ORCHARD_DIVERSIFIER_SIZE + ORCHARD_PKD_SIZE;

/// An Orchard shielded payment address.
///
/// Composed of a diversifier (11 bytes) and a diversified transmission key (32 bytes).
/// The diversifier enables a single spending key to derive an unlimited number of
/// unlinkable payment addresses. Only the holder of the corresponding FullViewingKey
/// (or IncomingViewingKey) can link diversified addresses to the same wallet.
///
/// Bech32m encoding uses type byte `0x10`, producing addresses that start with `z`:
/// - Mainnet: `dash1z...`
/// - Testnet: `tdash1z...`
///
/// The raw Orchard address format matches Zcash Orchard (43 bytes), but the
/// string encoding is Dash-specific (no F4Jumble, no Unified Address wrapper).
///
/// Wraps `grovedb_commitment_tree::PaymentAddress`. Use [`From<PaymentAddress>`]
/// to convert from the orchard crate's native type, or [`inner()`](OrchardAddress::inner)
/// / [`into_inner()`](OrchardAddress::into_inner) to access the wrapped address.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrchardAddress(grovedb_commitment_tree::PaymentAddress);

impl OrchardAddress {
/// Type byte for Orchard addresses in bech32m encoding (user-facing).
/// Produces 'z' as the first bech32 character.
pub const ORCHARD_TYPE: u8 = 0x10;

/// Returns the inner [`PaymentAddress`](grovedb_commitment_tree::PaymentAddress).
pub fn inner(&self) -> &grovedb_commitment_tree::PaymentAddress {
&self.0
}

/// Consumes the wrapper and returns the inner `PaymentAddress`.
pub fn into_inner(self) -> grovedb_commitment_tree::PaymentAddress {
self.0
}

/// Creates an OrchardAddress from a 43-byte raw address.
///
/// The first 11 bytes are the diversifier, the next 32 are pk_d.
/// Returns an error if `pk_d` is not a valid Pallas curve point.
pub fn from_raw_bytes(bytes: &[u8; ORCHARD_ADDRESS_SIZE]) -> Result<Self, ProtocolError> {
let addr =
Option::from(grovedb_commitment_tree::PaymentAddress::from_raw_address_bytes(bytes))
.ok_or_else(|| {
ProtocolError::DecodingError(
"OrchardAddress pk_d is not a valid Pallas curve point".to_string(),
)
})?;
Ok(Self(addr))
}

/// Returns the raw 43-byte address (diversifier || pk_d).
pub fn to_raw_bytes(&self) -> [u8; ORCHARD_ADDRESS_SIZE] {
self.0.to_raw_address_bytes()
}

/// Encodes the OrchardAddress as a bech32m string for the specified network.
///
/// Format: `<HRP>1<data-part>`
/// - Data: type_byte (0x10) || diversifier (11 bytes) || pk_d (32 bytes)
/// - Total payload: 44 bytes
/// - Checksum: bech32m (BIP-350)
pub fn to_bech32m_string(&self, network: Network) -> String {
let hrp_str = PlatformAddress::hrp_for_network(network);
let hrp = Hrp::parse(hrp_str).expect("HRP is valid");

let raw = self.to_raw_bytes();
let mut payload = Vec::with_capacity(1 + ORCHARD_ADDRESS_SIZE);
payload.push(Self::ORCHARD_TYPE);
payload.extend_from_slice(&raw);

bech32::encode::<Bech32m>(hrp, &payload).expect("encoding should succeed")
}

/// Decodes a bech32m-encoded Orchard address string.
///
/// # Returns
/// - `Ok((OrchardAddress, Network))` - The decoded address and its network
/// - `Err(ProtocolError)` - If the address is invalid
pub fn from_bech32m_string(s: &str) -> Result<(Self, Network), ProtocolError> {
let (hrp, data) =
bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{}", e)))?;

let hrp_lower = hrp.as_str().to_ascii_lowercase();
let network = match hrp_lower.as_str() {
s if s == PLATFORM_HRP_MAINNET => Network::Dash,
s if s == PLATFORM_HRP_TESTNET => Network::Testnet,
_ => {
return Err(ProtocolError::DecodingError(format!(
"invalid HRP '{}': expected '{}' or '{}'",
hrp, PLATFORM_HRP_MAINNET, PLATFORM_HRP_TESTNET
)))
}
};

// Validate payload: 1 type byte + 11 diversifier + 32 pk_d = 44 bytes
if data.len() != 1 + ORCHARD_ADDRESS_SIZE {
return Err(ProtocolError::DecodingError(format!(
"invalid Orchard address length: expected {} bytes, got {}",
1 + ORCHARD_ADDRESS_SIZE,
data.len()
)));
}

if data[0] != Self::ORCHARD_TYPE {
return Err(ProtocolError::DecodingError(format!(
"invalid Orchard address type byte: expected 0x{:02x}, got 0x{:02x}",
Self::ORCHARD_TYPE,
data[0]
)));
}

let mut raw = [0u8; ORCHARD_ADDRESS_SIZE];
raw.copy_from_slice(&data[1..]);
Self::from_raw_bytes(&raw).map(|addr| (addr, network))
}
}

/// Infallible conversion from the orchard crate's `PaymentAddress` to `OrchardAddress`.
impl From<grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
fn from(addr: grovedb_commitment_tree::PaymentAddress) -> Self {
Self(addr)
}
}

/// Infallible conversion from a reference to `PaymentAddress`.
impl From<&grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
fn from(addr: &grovedb_commitment_tree::PaymentAddress) -> Self {
Self(*addr)
}
}

impl std::fmt::Display for OrchardAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let raw = self.to_raw_bytes();
write!(
f,
"Orchard(d={}, pk_d={})",
hex::encode(&raw[..ORCHARD_DIVERSIFIER_SIZE]),
hex::encode(&raw[ORCHARD_DIVERSIFIER_SIZE..])
)
}
}

#[cfg(test)]
mod tests {
use super::*;
use bech32::Hrp;

fn test_orchard_address() -> OrchardAddress {
use grovedb_commitment_tree::{FullViewingKey, Scope, SpendingKey};
let sk = SpendingKey::from_bytes([42u8; 32]).unwrap();
let fvk = FullViewingKey::from(&sk);
let payment_address = fvk.address_at(0u32, Scope::External);
OrchardAddress::from(payment_address)
}

#[test]
fn test_orchard_address_raw_bytes_roundtrip() {
let address = test_orchard_address();
let raw = address.to_raw_bytes();
assert_eq!(raw.len(), 43);

let recovered = OrchardAddress::from_raw_bytes(&raw).unwrap();
assert_eq!(recovered, address);
}

#[test]
fn test_orchard_bech32m_mainnet_roundtrip() {
let address = test_orchard_address();

let encoded = address.to_bech32m_string(Network::Dash);
assert!(
encoded.starts_with("dash1z"),
"Orchard mainnet address should start with 'dash1z', got: {}",
encoded
);

let (decoded, network) =
OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
assert_eq!(decoded, address);
assert_eq!(network, Network::Dash);
}

#[test]
fn test_orchard_bech32m_testnet_roundtrip() {
let address = test_orchard_address();

let encoded = address.to_bech32m_string(Network::Testnet);
assert!(
encoded.starts_with("tdash1z"),
"Orchard testnet address should start with 'tdash1z', got: {}",
encoded
);

let (decoded, network) =
OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
assert_eq!(decoded, address);
assert_eq!(network, Network::Testnet);
}

#[test]
fn test_orchard_bech32m_wrong_type_byte_fails() {
// Manually construct an address with P2PKH type byte (0xb0) but 44-byte payload
let hrp = Hrp::parse("dash").unwrap();
let mut payload = vec![PlatformAddress::P2PKH_TYPE]; // Wrong type byte
payload.extend_from_slice(&[0u8; 43]);
let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();

let result = OrchardAddress::from_bech32m_string(&encoded);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid Orchard address type byte"));
}

#[test]
fn test_orchard_bech32m_wrong_length_fails() {
// Too short (only 20 bytes instead of 43)
let hrp = Hrp::parse("dash").unwrap();
let mut payload = vec![OrchardAddress::ORCHARD_TYPE];
payload.extend_from_slice(&[0u8; 20]);
let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();

let result = OrchardAddress::from_bech32m_string(&encoded);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid Orchard address length"));
}

#[test]
fn test_orchard_and_platform_addresses_are_distinguishable() {
let p2pkh = PlatformAddress::P2pkh([0xAB; 20]);
let p2sh = PlatformAddress::P2sh([0xAB; 20]);
let orchard = test_orchard_address();

let p2pkh_enc = p2pkh.to_bech32m_string(Network::Dash);
let p2sh_enc = p2sh.to_bech32m_string(Network::Dash);
let orchard_enc = orchard.to_bech32m_string(Network::Dash);

// All three start with "dash1" but have different type-byte characters
assert!(p2pkh_enc.starts_with("dash1k"), "P2PKH: {}", p2pkh_enc);
assert!(p2sh_enc.starts_with("dash1s"), "P2SH: {}", p2sh_enc);
assert!(
orchard_enc.starts_with("dash1z"),
"Orchard: {}",
orchard_enc
);

// Cross-decoding should fail
assert!(PlatformAddress::from_bech32m_string(&orchard_enc).is_err());
assert!(OrchardAddress::from_bech32m_string(&p2pkh_enc).is_err());
}

#[test]
fn test_orchard_address_from_raw_bytes_invalid_pk_d() {
// All zeros for pk_d is not a valid Pallas curve point
let mut raw = [0u8; 43];
raw[0] = 0x01; // non-zero diversifier
assert!(OrchardAddress::from_raw_bytes(&raw).is_err());
}

#[test]
fn test_orchard_address_display() {
let address = test_orchard_address();
let display = format!("{}", address);
assert!(display.starts_with("Orchard(d="));
assert!(display.contains("pk_d="));
}
}
2 changes: 1 addition & 1 deletion packages/rs-dpp/src/address_funds/platform_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,7 @@ mod tests {
assert_eq!(p2pkh.to_bytes()[0], 0x00);
assert_eq!(p2sh.to_bytes()[0], 0x01);

// Bech32m encoding uses 0xb0/0x80 (verified by successful roundtrip)
// Bech32m encoding uses 0xb0/0xb8 (verified by successful roundtrip)
let p2pkh_encoded = p2pkh.to_bech32m_string(Network::Dash);
let p2sh_encoded = p2sh.to_bech32m_string(Network::Dash);

Expand Down
1 change: 1 addition & 0 deletions packages/rs-dpp/src/asset_lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub type PastAssetLockStateTransitionHashes = Vec<Vec<u8>>;

/// An enumeration of the possible states when querying platform to get the stored state of an outpoint
/// representing if the asset lock was already used or not.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum StoredAssetLockInfo {
/// The asset lock was fully consumed in the past
FullyConsumed,
Expand Down
13 changes: 12 additions & 1 deletion packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@ mod v0;

pub use v0::{AssetLockValueGettersV0, AssetLockValueSettersV0};

#[derive(Debug, Clone, Encode, Decode, PlatformSerialize, PlatformDeserialize, From, PartialEq)]
#[derive(
Debug,
Clone,
Encode,
Decode,
PlatformSerialize,
PlatformDeserialize,
From,
PartialEq,
serde::Serialize,
serde::Deserialize,
)]
#[platform_serialize(unversioned)]
pub enum AssetLockValue {
V0(AssetLockValueV0),
Expand Down
Loading
Loading