Skip to content

Commit 9905dc1

Browse files
committed
feat(oracles): integrate frigate ephemeral scanning
1 parent 6f3e345 commit 9905dc1

7 files changed

Lines changed: 528 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/v2/src/main.rs

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use bdk_sp::{
1010
self,
1111
address::NetworkUnchecked,
1212
bip32,
13-
consensus::Decodable,
13+
consensus::{deserialize, Decodable},
14+
hashes::Hash,
1415
hex::{DisplayHex, FromHex},
1516
key::Secp256k1,
1617
script::PushBytesBuf,
@@ -34,6 +35,7 @@ use bdk_sp_oracles::{
3435
TrustedPeer, UnboundedReceiver, Warning,
3536
},
3637
filters::kyoto::{FilterEvent, FilterSubscriber},
38+
frigate::{FrigateClient, History, SubscribeRequest, UnsubscribeRequest, DUMMY_COINBASE},
3739
tweaks::blindbit::{BlindbitSubscriber, TweakEvent},
3840
};
3941
use bdk_sp_wallet::{
@@ -161,6 +163,16 @@ pub enum Commands {
161163
#[clap(long)]
162164
hash: Option<BlockHash>,
163165
},
166+
167+
ScanFrigate {
168+
#[clap(flatten)]
169+
rpc_args: RpcArgs,
170+
#[clap(long)]
171+
height: Option<u32>,
172+
#[clap(long)]
173+
hash: Option<BlockHash>,
174+
},
175+
164176
Create {
165177
/// Network
166178
#[clap(long, short, default_value = "signet")]
@@ -561,6 +573,167 @@ async fn main() -> anyhow::Result<()> {
561573
);
562574
}
563575
}
576+
Commands::ScanFrigate {
577+
rpc_args,
578+
height,
579+
hash,
580+
} => {
581+
// The implementation done here differs from what is mentioned in the section
582+
// https://github.com/sparrowwallet/frigate/tree/master?tab=readme-ov-file#blockchainsilentpaymentssubscribe
583+
// This implementation is doing a one time scanning only. So instead of calling
584+
// `blockchain.scripthash.subscribe` on each script from the wallet, we just subscribe
585+
// and read the scanning result from the stream. On each result received we update the
586+
// wallet state and once scanning progress reaches 1.0 (100%) we stop.
587+
let sync_point = if let (Some(height), Some(hash)) = (height, hash) {
588+
HeaderCheckpoint::new(height, hash)
589+
} else if wallet.birthday.height <= wallet.chain().tip().height() {
590+
let height = wallet.chain().tip().height();
591+
let hash = wallet.chain().tip().hash();
592+
HeaderCheckpoint::new(height, hash)
593+
} else {
594+
let checkpoint = wallet
595+
.chain()
596+
.get(wallet.birthday.height)
597+
.expect("should be something");
598+
let height = checkpoint.height();
599+
let hash = checkpoint.hash();
600+
HeaderCheckpoint::new(height, hash)
601+
};
602+
603+
let mut client = FrigateClient::connect(&rpc_args.url)
604+
.await
605+
.unwrap()
606+
.with_timeout(tokio::time::Duration::from_secs(60));
607+
608+
let labels = wallet
609+
.indexer()
610+
.index()
611+
.num_to_label
612+
.clone()
613+
.into_keys()
614+
.collect::<Vec<u32>>();
615+
let labels = if !labels.is_empty() {
616+
Some(labels)
617+
} else {
618+
None
619+
};
620+
621+
let subscribe_params = SubscribeRequest {
622+
scan_priv_key: *wallet.indexer().scan_sk(),
623+
spend_pub_key: *wallet.indexer().spend_pk(),
624+
start_height: Some(sync_point.height),
625+
labels,
626+
};
627+
628+
// Attempt to subscribe; any timeout will trigger unsubscribe automatically.
629+
match client.subscribe_with_timeout(&subscribe_params).await {
630+
Ok(Some((histories, progress))) => {
631+
tracing::info!(
632+
"Initial subscription result: {} histories, progress {}",
633+
histories.len(),
634+
progress
635+
);
636+
}
637+
Ok(None) => {
638+
tracing::info!("Subscription acknowledged, awaiting notifications");
639+
}
640+
Err(e) => {
641+
tracing::error!("Subscribe failed: {}", e);
642+
return Err(e.into());
643+
}
644+
}
645+
646+
tracing::info!("Starting frigate scanning loop...");
647+
loop {
648+
match client.read_from_stream(4096).await {
649+
Ok(subscribe_result) => {
650+
if subscribe_result["params"].is_object() {
651+
let histories: Vec<History> = serde_json::from_value(
652+
subscribe_result["params"]["history"].clone(),
653+
)?;
654+
let progress = subscribe_result["params"]["progress"]
655+
.as_f64()
656+
.unwrap_or(0.0) as f32;
657+
658+
let mut secrets_by_height: HashMap<u32, HashMap<Txid, PublicKey>> =
659+
HashMap::new();
660+
661+
tracing::debug!("Received history {:#?}", histories);
662+
663+
histories.iter().for_each(|h| {
664+
secrets_by_height
665+
.entry(h.height)
666+
.and_modify(|v| {
667+
v.insert(h.tx_hash, h.tweak_key);
668+
})
669+
.or_insert(HashMap::from([(h.tx_hash, h.tweak_key)]));
670+
});
671+
672+
// Filter when the height is 0, because that would mean mempool transaction
673+
for secret in secrets_by_height.into_iter().filter(|v| v.0 > 0) {
674+
// Since frigate doesn't provide a blockchain.getblock we will mimick that here
675+
// By constructing a block from the block header and the list of transactions
676+
// received from the scan request
677+
let mut raw_blk = client.get_block_header(secret.0).await.unwrap();
678+
raw_blk.push_str("00");
679+
680+
// Push dummy coinbase
681+
let coinbase: Transaction =
682+
deserialize(&Vec::<u8>::from_hex(DUMMY_COINBASE).unwrap())
683+
.unwrap();
684+
let mut block: Block =
685+
deserialize(&Vec::<u8>::from_hex(&raw_blk).unwrap()).unwrap();
686+
687+
let mut blockhash = BlockHash::all_zeros();
688+
689+
let mut txs: Vec<Transaction> = vec![coinbase];
690+
for key in secret.1.keys() {
691+
let tx_result =
692+
client.get_transaction(key.to_string()).await.unwrap();
693+
let tx: Transaction =
694+
deserialize(&Vec::<u8>::from_hex(&tx_result.1).unwrap())
695+
.unwrap();
696+
txs.push(tx);
697+
698+
blockhash = BlockHash::from_str(&tx_result.0).unwrap();
699+
}
700+
701+
block.txdata = txs;
702+
tracing::debug!("Final block {:?}", block);
703+
wallet.apply_block_relevant(&block, secret.1, secret.0);
704+
705+
tracing::debug!("Checkpoint hash {blockhash:?}");
706+
let checkpoint = wallet.chain().tip().insert(BlockId {
707+
height: secret.0,
708+
hash: blockhash,
709+
});
710+
wallet.update_chain(checkpoint);
711+
}
712+
713+
tracing::info!("Progress {progress}");
714+
// Check the progress
715+
if progress >= 1.0 {
716+
tracing::info!("Scanning completed");
717+
break;
718+
}
719+
}
720+
}
721+
Err(e) if e.to_string().contains("timed out") => {
722+
tracing::warn!("read_from_stream timeout, exiting scan");
723+
let unsubscribe_request = UnsubscribeRequest {
724+
scan_privkey: *wallet.indexer().scan_sk(),
725+
spend_pubkey: *wallet.indexer().spend_pk(),
726+
};
727+
let _ = client.unsubscribe(&unsubscribe_request).await;
728+
break;
729+
}
730+
Err(e) => {
731+
tracing::error!("read_from_stream error: {}", e);
732+
return Err(e.into());
733+
}
734+
}
735+
}
736+
}
564737
Commands::Balance => {
565738
fn print_balances<'a>(
566739
title_str: &'a str,

doc/tabconf7/frigate_playbook.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
3+
########################### STAGE 1: setup ####################################
4+
5+
# 1. Install dependencies locally and setup regtest environment
6+
just non_nix_init
7+
# 2. Check bitcoind is running on regtest
8+
just cli getblockchaininfo
9+
# 3. Check bdk-cli wallet was created correctly
10+
just regtest-bdk balance
11+
# 4. Check sp-cli wallet was created correctly
12+
just regtest-sp balance
13+
# 5. Synchronize bdk-cli wallet
14+
just regtest-bdk sync
15+
16+
###################### STAGE 2: fund bdk-cli wallet ###########################
17+
18+
# 6. Get a new address from bdk-cli wallet
19+
REGTEST_ADDRESS=$(just regtest-bdk unused_address | jq -r '.address' | tr -d '\n')
20+
# 7. Mine a few more blocks to fund the wallet
21+
just mine 1 $REGTEST_ADDRESS
22+
# 8. Mine some of them to the internal wallet to confirm the bdk-cli balance
23+
just mine 101
24+
# 9. Synchronize bdk-cli wallet
25+
just regtest-bdk sync
26+
# 10. Check balance
27+
just regtest-bdk balance
28+
29+
################ STAGE 3: create a silent payment output ######################
30+
31+
# 11. Get a silent payment code from sp-cli2 wallet
32+
SP_CODE=$(just regtest-sp code | jq -r '.silent_payment_code' | tr -d '\n')
33+
# 12. Create a transaction spending bdk-cli wallet UTXOs to a the previous silent payment code
34+
RAW_TX=$(just regtest-bdk create_sp_tx --to-sp $SP_CODE:10000 --fee_rate 5 | jq -r '.raw_tx' | tr -d '\n')
35+
TXID=$(just regtest-bdk broadcast --tx $RAW_TX | jq -r '.txid' | tr -d '\n')
36+
# 14. Mine a new block
37+
just mine 1
38+
# 15. Once the new transaction has been mined, synchronize bdk-cli wallet again
39+
just regtest-bdk sync
40+
41+
# ################## STAGE 4: find a silent payment output ######################
42+
43+
# 16. Now synchronize sp-cli2 wallet using frigate ephemeral scanning
44+
FRIGATE_HOST="127.0.0.1:57001"
45+
just regtest-sp scan-frigate --url $FRIGATE_HOST
46+
# 17. Check balance on sp-cli2 wallet
47+
just regtest-sp balance
48+
# 18. Check balance on bdk-cli wallet
49+
just regtest-bdk balance
50+
51+
# At this point we will able to see SP outputs paid to out wallet!

doc/tabconf7/justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ build TAG="1.0.0" VERSION="29.0" RELEASE="29.0": machine
121121
RUN mkdir -p /build/frigate
122122
RUN mkdir -p /frigate
123123
WORKDIR /frigate
124-
RUN git clone --recursive --branch 1.1.0 --depth 1 https://github.com/sparrowwallet/frigate.git .
124+
RUN git clone --recursive --branch 1.3.2 --depth 1 https://github.com/sparrowwallet/frigate.git .
125125
RUN ./gradlew jpackage
126126
RUN cp -r ./build/jpackage/frigate /build/frigate
127127
RUN rm -rf /frigate

oracles/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ redb = "2.4.0"
1313
rayon = "1.11.0"
1414
reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "http2", "charset"], default-features = false }
1515
serde = { version = "1.0.219", features = ["serde_derive"] }
16-
serde_json = "1.0.142"
16+
serde_json = { version = "1.0.142", features = ["raw_value"]}
1717
url = "2.5.4"
1818
tracing = "0.1.41"
19+
jsonrpc = "=0.18.0"
1920

2021
[lints]
2122
workspace = true

0 commit comments

Comments
 (0)