Skip to content
Merged
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
19 changes: 15 additions & 4 deletions book/src/evo-sdk/dashpay-contact-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ The current DashPay contract schema requires the system field
`encryptedPublicKey` is exactly 96 bytes:

- 16 bytes: AES-CBC initialization vector
- 80 bytes: AES-CBC ciphertext for the sender's 78-byte serialized contact xpub
- 80 bytes: AES-CBC ciphertext for the sender's 69-byte compact contact xpub

The compact xpub is the parent fingerprint (4 bytes), chain code (32 bytes)
and public key (33 bytes), without the version, depth and child number of a
full 78-byte BIP32 serialization. Receivers, including the reference mobile
wallets and the Rust, Swift and Kotlin SDKs, refuse any other plaintext
length. Both 69 and 78 bytes pad to the same 80-byte ciphertext, so the
contract cannot catch the mistake: a request that encrypts the full 78 bytes
is stored on chain, and the recipient's wallet then drops it.

The sender derives the contact xpub from the sender identity, recipient
identity, account, and address index. The sender then encrypts that xpub with an
Expand Down Expand Up @@ -99,14 +107,17 @@ function deriveSharedKey({
return crypto.createHash('sha256').update(Buffer.concat([compressedPrefix, x])).digest();
}

function serializedXpubPayload(xpub: string): Buffer {
function compactXpubPayload(xpub: string): Buffer {
const payload = dashcore.encoding.Base58Check.decode(xpub);

if (payload.length !== 78) {
throw new Error(`Invalid DashPay contact xpub length: ${payload.length}`);
}

return payload;
// BIP32 layout: version(4) depth(1) parentFingerprint(4) childNumber(4)
// chainCode(32) publicKey(33). DIP-15 encrypts only the parent
// fingerprint, chain code and public key: 69 bytes.
return Buffer.concat([payload.subarray(5, 9), payload.subarray(13, 78)]);
}

function encryptContactXpub({
Expand All @@ -122,7 +133,7 @@ function encryptContactXpub({
privateKeyWif: senderEncryptionPrivateKeyWif,
publicKeyBytes: recipientDecryptionPublicKeyBytes,
});
const payload = serializedXpubPayload(contactXpub);
const payload = compactXpubPayload(contactXpub);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
const encrypted = Buffer.concat([
Expand Down
3 changes: 2 additions & 1 deletion packages/rs-platform-encryption/src/contact_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ pub fn decrypt_enc_to_user_id(key: &[u8; 32], ciphertext: &[u8; 32]) -> [u8; 32]
out
}

/// Encrypt a `contactInfo.privateData` plaintext (CBOR bytes) as
/// Encrypt a `contactInfo.privateData` plaintext (the DIP-15 var-int
/// encoding, built by the wallet's `encode_private_data`) as
/// `IV(16) ‖ AES-256-CBC(plaintext)` — the same prepended-IV layout
/// `encryptedPublicKey` uses (DIP-15 doesn't pin the layout for this
/// field; we adopt the same convention).
Expand Down
3 changes: 1 addition & 2 deletions packages/rs-platform-wallet/examples/shielded_sync_paloma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,7 @@ async fn main() {
hex::encode(platform_wallet.wallet_id())
);

// --- 4. Bind shielded account 0 with the raw seed (mirrors the
// iOS `bindShieldedRawSeed` path; same FFI shape). ---
// --- 4. Bind shielded account 0 with the raw seed. ---
let coordinator = manager
.shielded_coordinator()
.await
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub struct AssetLockManager<B: TransactionBroadcaster + ?Sized> {
pub(super) wallet_manager: Arc<RwLock<WalletManager<PlatformWalletInfo>>>,
/// Identifies which wallet within the manager this manager operates on.
pub(super) wallet_id: WalletId,
/// Notified on InstantLock / ChainLock events by SpvEventForwarder.
/// Notified on InstantLock / ChainLock events by `LockNotifyHandler`.
/// Used by `wait_for_proof()` and `wait_for_chain_lock()`.
pub(super) lock_notify: Arc<Notify>,
/// Transaction broadcaster — pluggable so the same `AssetLockManager`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
///
/// Wait for an asset lock proof by checking transaction context state.
///
/// Wakes on `lock_notify` (fired by `SpvEventForwarder` on InstantLock /
/// Wakes on `lock_notify` (fired by `LockNotifyHandler` on InstantLock /
/// ChainLock events) and re-checks the transaction record context.
///
/// Returns a properly-constructed `AssetLockProof` on success, or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2682,7 +2682,7 @@ mod tests {
/// rebuild that fails at input selection is direct proof the funding
/// reservation is still held.
signer: crate::test_support::WalletSigner,
/// The handle `SpvEventForwarder` fires on IS/ChainLock events, so
/// The handle `LockNotifyHandler` fires on IS/ChainLock events, so
/// a test can wake an in-flight proof wait the way the live wallet
/// does.
lock_notify: Arc<Notify>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ pub struct ContactInfoPrivateData {
pub alias_name: Option<String>,
/// Free-form note.
pub note: Option<String>,
/// Whether the contact is hidden / ignored (DIP-15 `displayHidden` — the
/// hide flag, also the cross-device ignore signal).
/// Whether the contact is hidden (DIP-15 `displayHidden`). This hides an
/// established contact; it is not the ignore feature, which is local to
/// each device and covers senders who are not contacts.
pub display_hidden: bool,
/// Accepted rotated account-references of an established contact (DIP-15
/// `acceptedAccounts`). Empty until multi-account is populated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1885,33 +1885,24 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
out
}

/// Build the two DashPay accounts for one established contact,
/// applying the transient/permanent failure policy.
/// Queue the two DashPay account builds for one established contact.
///
/// Order:
/// 1. Register the `DashpayReceivingFunds` account — derivable from our
/// own seed, no decryption needed. This is what makes *incoming*
/// contact payments visible to SPV; restore-from-seed leaves it
/// unbuilt, so the sweep rebuilds it for every established contact.
/// 2. Fetch the counterparty identity and **validate** the request's
/// key indices via [`validate_contact_request`] BEFORE any ECDH —
/// an attacker-crafted index pointing at an AUTHENTICATION key would
/// otherwise derive a wrong shared secret and poison the account.
/// 3. Register the `DashpayExternalAccount` (decrypt + ECDH).
/// The recurring sweep runs without a signer, so it cannot derive the
/// receiving (friendship) xpub or the ECDH secret itself. It enqueues
/// both ops for the signer-backed drain
/// ([`Self::drain_pending_contact_crypto_verified`]), which registers the
/// `DashpayReceivingFunds` account, fetches the counterparty, validates
/// the request's key indices before any ECDH, and registers the
/// `DashpayExternalAccount`. Transient failures stay queued for the next
/// drain; permanent ones mark the contact `payment_channel_broken`.
///
/// Failure policy:
/// - **Transient** (identity fetch / network): logged, left for the
/// next sweep to retry. The broken flag stays clear.
/// - **Permanent** (validation failure, decrypt/decode failure): the
/// contact is marked `payment_channel_broken` so subsequent sweeps
/// skip it until a superseding request arrives.
/// Identities that are not ours to build (no `identity_index`) are
/// skipped and logged. Enqueueing is idempotent per (owner, contact,
/// kind), so calling this every sweep is a no-op until the drain clears
/// the entries.
///
/// Watch-only / seedless wallets (no `identity_index`) are skipped and
/// logged — the watch-only ECDH path (host-side signing hook) lands
/// later.
///
/// Called **after** the sync write guard is dropped: the register
/// functions re-acquire the non-reentrant wallet-manager lock.
/// Called **after** the sync write guard is dropped: this re-acquires
/// the non-reentrant wallet-manager lock.
async fn build_contact_accounts(
&self,
identity_id: &Identifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//! is an identity-as-actor operation, so it lives here next to the rest of
//! the identity-lifecycle and DashPay surface (same precedent as the
//! DashPay merge described in the parent `mod.rs`). Token *bookkeeping*
//! (watch / sync / balance) stays on [`crate::wallet::tokens::TokenWallet`]
//! (watch / sync / balance) lives on
//! [`IdentitySyncManager`](crate::manager::identity_sync::IdentitySyncManager)
//! because it's wallet-scoped, not identity-scoped.

mod burn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,10 @@ pub struct DashPayState {
/// on-chain ciphertext + public-key indices; each entry still carries its
/// `owner_identity_id` (== this identity) as the drain's routing key and the
/// SQLite key column. In-memory only for the live session: the queue is
/// persisted to the changeset (SQLite backend), but cold-load restore is
/// blocked upstream, so a re-imported wallet re-syncs from scratch and the
/// sweep re-enqueues what it needs. Deliberately NOT captured by
/// persisted to the changeset (SQLite backend), but `load()` does not
/// restore it (nothing blocks that any more; see the storage crate's
/// `pending_contact_crypto` reader), so after a restart the sweep
/// re-enqueues what it needs. Deliberately NOT captured by
/// `IdentityEntry::from_managed` — persistence rides the flat changeset
/// delta, not a per-identity snapshot.
/// See [`PendingContactCrypto`](crate::changeset::PendingContactCrypto).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use rayon::prelude::*;
use platform_wallet::wallet::shielded::keys::OrchardKeySet;

const ENCRYPTED_NOTE_WIRE_LEN: usize = 216;
const SEED_BENCH: [u8; 32] = [0x73; 32]; // matches SEED_A in drive-abci's seeder
const SEED_BENCH: [u8; 32] = [0x73; 32]; // same seed as SEED_A in examples/shielded_sync.rs

/// Generate `count` filler `ShieldedEncryptedNote`s with random bytes
/// matching the on-chain wire layout. Deterministic given `rng_seed`.
Expand Down
Loading