Skip to content

silent payments: integrate SPDK scanning and wallet IPC#50

Merged
rustaceanrob merged 3 commits into
kernel-node:masterfrom
pzafonte:silent-payments
May 15, 2026
Merged

silent payments: integrate SPDK scanning and wallet IPC#50
rustaceanrob merged 3 commits into
kernel-node:masterfrom
pzafonte:silent-payments

Conversation

@pzafonte
Copy link
Copy Markdown
Contributor

@pzafonte pzafonte commented May 2, 2026

New PR using SPDK... Integrate BIP-352 silent payment scanning into kernel-node using the silentpayments crate from SPDK.

  • Add crates/wallet as a workspace member with a silentpayments submodule that wraps SPDK's cryptographic primitives
  • Scan each accepted block for payments using kernel undo data to recover prevout scripts
  • Track received UTXOs and balance in an in-memory wallet
  • Expose import_keys, balance, and history over the existing node.sock

Comment thread capnp/wallet.capnp
Comment thread src/bin/node.rs Outdated
Comment thread src/bin/node.rs Outdated
Comment thread src/bin/node.rs Outdated
Comment thread src/bin/node.rs Outdated
Comment thread src/peer.rs Outdated
Comment thread src/bin/node.rs
@yancyribbens
Copy link
Copy Markdown
Contributor

Thanks for adding this. I'm surprised how much code there is still even with including SPDK. Although, I guess that's due to adding supporting wallet code. I'd also prefer that SPDK used rust-bitcoin-coin-selection since it seems more robust and has more supported selection algos, although my opinion is maybe a bit biased :P

@pzafonte pzafonte force-pushed the silent-payments branch from e8ec66d to af29a15 Compare May 4, 2026 19:55
Comment thread crates/wallet/src/silentpayments/scanning.rs Outdated
Comment thread src/bin/node.rs Outdated
Comment thread src/bin/node.rs Outdated
Comment thread src/bin/node.rs Outdated
@pzafonte pzafonte force-pushed the silent-payments branch from af29a15 to 9c9e955 Compare May 6, 2026 22:36
Comment thread crates/wallet/src/silentpayments/scanning.rs Outdated
Comment thread crates/wallet/src/silentpayments/wallet.rs Outdated
Comment thread src/ipc.rs Outdated
@pzafonte pzafonte force-pushed the silent-payments branch from 9c9e955 to 068f3ad Compare May 7, 2026 19:22
@pzafonte
Copy link
Copy Markdown
Contributor Author

pzafonte commented May 7, 2026

Latest commits incorporate the most recent code reviews. Thanks!

@yancyribbens
Copy link
Copy Markdown
Contributor

@pzafonte , im curious what you think about using SPDK now that you've completed PRs both with and without. Did you feel like the SPDK saved you a lot of work and effort vs the first PR you made? Im somewhat skeptical because both PRs seem to be similar in size.

@yancyribbens
Copy link
Copy Markdown
Contributor

Latest commits incorporate the most recent code reviews. Thanks!

btw, this is still in Draft. Move it out of draft if you think it's a merge candidate.

@pzafonte
Copy link
Copy Markdown
Contributor Author

pzafonte commented May 8, 2026

@pzafonte , im curious what you think about using SPDK now that you've completed PRs both with and without. Did you feel like the SPDK saved you a lot of work and effort vs the first PR you made? Im somewhat skeptical because both PRs seem to be similar in size.

This was definitely much easier. W.R.T. size, it looks that way since I pushed up the spdk changes on the previous PR. Comparing just my original code (no test vectors), this PR is roughly ~1/3 the size. Much more maintainable, and we don't have to own the cryptography layer in this repo.

@yancyribbens
Copy link
Copy Markdown
Contributor

and we don't have to own the cryptography layer in this repo.

However, I did notice there is a disclaimer we should keep in mind on the SPDK implementation:

SPDK currently relies on cryptography that is not professionally reviewed. Be mindful of this when using SPDK with real funds.

@pzafonte pzafonte marked this pull request as ready for review May 8, 2026 19:32
@rustaceanrob
Copy link
Copy Markdown
Contributor

SPDK currently relies on cryptography that is not professionally reviewed. Be mindful of this when using SPDK with real funds

This will likely be delegated to secp once the C library has merged the API for silent payments.

Comment thread src/bin/cli.rs Outdated
Comment thread src/bin/cli.rs Outdated
Comment thread crates/wallet/src/silentpayments/wallet.rs Outdated
Comment thread src/bin/node.rs
shutdown_tx: mpsc::Sender<()>,
tip_state: &Arc<Mutex<TipState>>,
wallet_state: WalletState,
chainman_holder: Arc<std::sync::OnceLock<Arc<ChainstateManager>>>,
Copy link
Copy Markdown
Contributor

@rustaceanrob rustaceanrob May 9, 2026

Choose a reason for hiding this comment

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

I had to remind myself of what OnceLock was. I'm not sure this is a great way to proceed. Have we tried using a std::sync::mpsc::channel? We can spawn a new thread that waits for blocks to scan when this callback is hit. So instead of scan_kernel_block here, we send the block over the channel to a consuming thread.

Copy link
Copy Markdown
Contributor Author

@pzafonte pzafonte May 10, 2026

Choose a reason for hiding this comment

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

I tried using the channel, and replaced OnceLock with a consumer thread that looks up the entry via chainman.get_block_tree_entry(hash) after the callback returns. A round trip test showed get_block_tree_entry returns None for just-connected blocks, even from a separate thread, so the entry isn't available at this stage. read_spent_outputs(entry) has to happen synchronously where the entry is alive. Since the callback is registered before ChainstateManager exists, we need some kind of slot for chainman or if the kernel callback delivered spent outputs alongside the block that might work, I think. Open to alternatives if you can think of one, but OnceLock seems structurally required given the existing state of things.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As in we call send with the block and entry within with_block_connected_validation and the undo data is not present on the receiving thread? That seems strange if it is available within the callback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unless I'm missing something, to get the undo data you call chainman.read_spent_outputs(entry), and the receiving thread doesn't have a reference to chainman.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The receiving thread can clone the Arc cheaply once it starts up. I will push up a branch as a demo for what I'm thinking tomorrow. Other review can be addressed in the meantime.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So far I think you're right. Brutal. The self-referential nature of this architecture bothers me a lot but I think this is the only way to proceed. Confusing why the method returns None when it should return Some, at least at first glance.

Comment thread src/bin/node.rs Outdated
}
};

let btc_block = kernel_block_to_bitcoin_block(kernel_block);
Copy link
Copy Markdown
Contributor

@rustaceanrob rustaceanrob May 10, 2026

Choose a reason for hiding this comment

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

I think this is leaking some wallet logic to the node module, which we would like to keep as tidy as possible. It seems like we can refactor scan_block of the wallet to take kernel::Block and kernel spent outputs to move the entirety of the scanning logic to the wallet module.

Comment thread src/bin/node.rs
Comment thread Cargo.toml Outdated
@rustaceanrob
Copy link
Copy Markdown
Contributor

@pzafonte do you mind if we pair program the rest, i.e. I force-push this branch? You'll still get commit attribution, just eager to move this along while I have the time.

@pzafonte
Copy link
Copy Markdown
Contributor Author

@pzafonte do you mind if we pair program the rest, i.e. I force-push this branch? You'll still get commit attribution, just eager to move this along while I have the time.

I don't mind that, I just pushed up the latest changes that address the existing review.

}

#[derive(Debug, Clone)]
pub struct OwnedUtxo {
Copy link
Copy Markdown
Contributor

@rustaceanrob rustaceanrob May 11, 2026

Choose a reason for hiding this comment

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

For each coin we will need the following:

  • ScriptPubKey
  • OutPoint
  • A scalar representing the "tweak", should be returned at some point by scan_transaction
  • Amount
  • Label for change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only the receiving side of silent payments is being added here, right? Isn't this struct something that should be provided by the SPDK framework?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fwiw, SPDK exposes the following for Receiver:

/// A struct representing a silent payment recipient.
///
/// It can be used to scan for transaction outputs belonging to us by using the [`scan_transaction`](Receiver::scan_transaction) function.
/// It optionally supports labels, which it manages internally.
/// Labels can be added with [`add_label`](Receiver::add_label).
#[derive(Debug, Clone, PartialEq)]
pub struct Receiver {
    version: u8,
    scan_pubkey: PublicKey,
    spend_pubkey: PublicKey,
    change_label: Label, // To be able to tell which label is the change
    labels: BiMap<Label, PublicKey>,
    pub network: Network,
}

Comment thread src/ipc.rs Outdated
};
use secp256k1::{Scalar, SecretKey, XOnlyPublicKey};

pub struct InputData {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SPDK doesn't use bitcoin types so the conversion is unnecessary. That being said I think we can clean up the number of types introduced in this PR.

}

#[derive(Debug, Clone)]
pub struct FoundUtxo {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread src/bin/cli.rs Outdated
@rustaceanrob
Copy link
Copy Markdown
Contributor

rustaceanrob commented May 12, 2026

High level commit structure has good separation of concerns. I brainstormed a bit yesterday about final clean up for this PR if possible, here is my architectural feedback:

The distinction between Wallet and WalletState is not clear to me. I suggest we roll these into a single structure, Wallet. Within the WalletState, we have reference counted fields behind a mutex, but we should instead have a single Arc with a Mutex that locks the high level Wallet struct. This will minimize the heap allocations and make it clear at which points the wallet is being mutated.

It appears to me that we can also roll scan_block and Wallet::process_block into a single call. This makes the scope of where the wallet will be mutated to a single function call that we can evaluate. Having UTXOs be added after the block is "scanned" doesn't make much sense IMO.

We should probably try to avoid converting to a bitcoin::Block at all. We already process undo data directly from Core, it should be possible to check for taproot keys directly from the kernel::Block easily. Removing serialization roundtrips should be a top priority for us.

OwnedUTXO should just be called Coin.

We should also early-return in the callback functions of ContextBuilder if there is no wallet present.

Comment thread src/bin/cli.rs
@rustaceanrob
Copy link
Copy Markdown
Contributor

Looking good. Does SPDK have tooling for generating the address? I'd like to give this a final smoke test by sending some signet coins.

@yancyribbens
Copy link
Copy Markdown
Contributor

Does SPDK have tooling for generating the address?

https://github.com/cygnet3/spdk/blob/master/silentpayments/examples/create_wallet.rs

@pzafonte
Copy link
Copy Markdown
Contributor Author

Looking good. Does SPDK have tooling for generating the address? I'd like to give this a final smoke test by sending some signet coins.

It has silentpayments::SilentPaymentAddress::new() I can write an example for deriving a silent payment address, and then write an end-to-end script to do the smoke test. Is there a better approach?

@rustaceanrob
Copy link
Copy Markdown
Contributor

I would like to develop a more thoughtful approach of how we do testing here, likely taking some inspiration from Floresta. In the meantime, let's add a final method Receive that returns the SP address string with no label.

pzafonte added 3 commits May 14, 2026 12:45
Add a workspace crate implementing BIP-352 silent payment scanning.
Provides Wallet (UTXO tracking, balance, history), scan_transaction
for detecting incoming payments, and build_receiver for constructing
a Receiver from scan/spend keys.
Add scan_kernel_block registered via with_block_connected_validation so
undo data is tied to the block being scanned. OnceLock provides
ChainstateManager to the callback after construction. Expose wallet
state over IPC via a makeWallet sub-client on the existing Server
interface, with Receiver cached on import-keys.
…nce, and history

Add receive generate-keys, import-keys, balance, and history subcommands
under a wallet sub-command group. The IPC-bound subcommands reach the
node via a single node.sock connection, calling makeWallet to obtain the
wallet sub-client capability. generate-keys runs client-side only,
generating keys from OsRng via bitcoin's rand-std feature.
@rustaceanrob
Copy link
Copy Markdown
Contributor

Epic. I tested the flow and was able to receive a payment on signet. I have some follow-ups in mind to clean this up and potentially make it more performant, but this is a good start.

ACK fac0921

@rustaceanrob rustaceanrob merged commit 24ef7bd into kernel-node:master May 15, 2026
1 check passed
@sedited
Copy link
Copy Markdown
Collaborator

sedited commented May 15, 2026

Nice! :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants