Skip to content

Commit 7fff683

Browse files
committed
feat(oracles): integrate frigate ephemeral scanning
1 parent 2f28d19 commit 7fff683

7 files changed

Lines changed: 529 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")]
@@ -567,6 +579,167 @@ async fn main() -> anyhow::Result<()> {
567579
);
568580
}
569581
}
582+
Commands::ScanFrigate {
583+
rpc_args,
584+
height,
585+
hash,
586+
} => {
587+
// The implementation done here differs from what is mentioned in the section
588+
// https://github.com/sparrowwallet/frigate/tree/master?tab=readme-ov-file#blockchainsilentpaymentssubscribe
589+
// This implementation is doing a one time scanning only. So instead of calling
590+
// `blockchain.scripthash.subscribe` on each script from the wallet, we just subscribe
591+
// and read the scanning result from the stream. On each result received we update the
592+
// wallet state and once scanning progress reaches 1.0 (100%) we stop.
593+
let sync_point = if let (Some(height), Some(hash)) = (height, hash) {
594+
HeaderCheckpoint::new(height, hash)
595+
} else if wallet.birthday.height <= wallet.chain().tip().height() {
596+
let height = wallet.chain().tip().height();
597+
let hash = wallet.chain().tip().hash();
598+
HeaderCheckpoint::new(height, hash)
599+
} else {
600+
let checkpoint = wallet
601+
.chain()
602+
.get(wallet.birthday.height)
603+
.expect("should be something");
604+
let height = checkpoint.height();
605+
let hash = checkpoint.hash();
606+
HeaderCheckpoint::new(height, hash)
607+
};
608+
609+
let mut client = FrigateClient::connect(&rpc_args.url)
610+
.await
611+
.unwrap()
612+
.with_timeout(tokio::time::Duration::from_secs(60));
613+
614+
let labels = wallet
615+
.indexer()
616+
.index()
617+
.num_to_label
618+
.clone()
619+
.into_keys()
620+
.collect::<Vec<u32>>();
621+
let labels = if !labels.is_empty() {
622+
Some(labels)
623+
} else {
624+
None
625+
};
626+
627+
let subscribe_params = SubscribeRequest {
628+
scan_priv_key: *wallet.indexer().scan_sk(),
629+
spend_pub_key: *wallet.indexer().spend_pk(),
630+
start_height: Some(sync_point.height),
631+
labels,
632+
};
633+
634+
// Attempt to subscribe; any timeout will trigger unsubscribe automatically.
635+
match client.subscribe_with_timeout(&subscribe_params).await {
636+
Ok(Some((histories, progress))) => {
637+
tracing::info!(
638+
"Initial subscription result: {} histories, progress {}",
639+
histories.len(),
640+
progress
641+
);
642+
}
643+
Ok(None) => {
644+
tracing::info!("Subscription acknowledged, awaiting notifications");
645+
}
646+
Err(e) => {
647+
tracing::error!("Subscribe failed: {}", e);
648+
return Err(e.into());
649+
}
650+
}
651+
652+
tracing::info!("Starting frigate scanning loop...");
653+
loop {
654+
match client.read_from_stream(4096).await {
655+
Ok(subscribe_result) => {
656+
if subscribe_result["params"].is_object() {
657+
let histories: Vec<History> = serde_json::from_value(
658+
subscribe_result["params"]["history"].clone(),
659+
)?;
660+
let progress = subscribe_result["params"]["progress"]
661+
.as_f64()
662+
.unwrap_or(0.0) as f32;
663+
664+
let mut secrets_by_height: HashMap<u32, HashMap<Txid, PublicKey>> =
665+
HashMap::new();
666+
667+
tracing::debug!("Received history {:#?}", histories);
668+
669+
histories.iter().for_each(|h| {
670+
secrets_by_height
671+
.entry(h.height)
672+
.and_modify(|v| {
673+
v.insert(h.tx_hash, h.tweak_key);
674+
})
675+
.or_insert(HashMap::from([(h.tx_hash, h.tweak_key)]));
676+
});
677+
678+
// Filter when the height is 0, because that would mean mempool transaction
679+
for secret in secrets_by_height.into_iter().filter(|v| v.0 > 0) {
680+
// Since frigate doesn't provide a blockchain.getblock we will mimick that here
681+
// By constructing a block from the block header and the list of transactions
682+
// received from the scan request
683+
let mut raw_blk = client.get_block_header(secret.0).await.unwrap();
684+
raw_blk.push_str("00");
685+
686+
// Push dummy coinbase
687+
let coinbase: Transaction =
688+
deserialize(&Vec::<u8>::from_hex(DUMMY_COINBASE).unwrap())
689+
.unwrap();
690+
let mut block: Block =
691+
deserialize(&Vec::<u8>::from_hex(&raw_blk).unwrap()).unwrap();
692+
693+
let mut blockhash = BlockHash::all_zeros();
694+
695+
let mut txs: Vec<Transaction> = vec![coinbase];
696+
for key in secret.1.keys() {
697+
let tx_result =
698+
client.get_transaction(key.to_string()).await.unwrap();
699+
let tx: Transaction =
700+
deserialize(&Vec::<u8>::from_hex(&tx_result.1).unwrap())
701+
.unwrap();
702+
txs.push(tx);
703+
704+
blockhash = BlockHash::from_str(&tx_result.0).unwrap();
705+
}
706+
707+
block.txdata = txs;
708+
tracing::debug!("Final block {:?}", block);
709+
wallet.apply_block_relevant(&block, secret.1, secret.0);
710+
711+
tracing::debug!("Checkpoint hash {blockhash:?}");
712+
let checkpoint = wallet.chain().tip().insert(BlockId {
713+
height: secret.0,
714+
hash: blockhash,
715+
});
716+
wallet.update_chain(checkpoint);
717+
}
718+
719+
tracing::info!("Progress {progress}");
720+
// Check the progress
721+
if progress >= 1.0 {
722+
tracing::info!("Scanning completed");
723+
break;
724+
}
725+
}
726+
}
727+
Err(e) if e.to_string().contains("timed out") => {
728+
tracing::warn!("read_from_stream timeout, exiting scan");
729+
let unsubscribe_request = UnsubscribeRequest {
730+
scan_privkey: *wallet.indexer().scan_sk(),
731+
spend_pubkey: *wallet.indexer().spend_pk(),
732+
};
733+
let _ = client.unsubscribe(&unsubscribe_request).await;
734+
break;
735+
}
736+
Err(e) => {
737+
tracing::error!("read_from_stream error: {}", e);
738+
return Err(e.into());
739+
}
740+
}
741+
}
742+
}
570743
Commands::Balance => {
571744
fn print_balances<'a>(
572745
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
@@ -123,7 +123,7 @@ build TAG="1.0.0" VERSION="29.0" RELEASE="29.0": machine
123123
RUN mkdir -p /build/frigate
124124
RUN mkdir -p /frigate
125125
WORKDIR /frigate
126-
RUN git clone --recursive --branch 1.1.0 --depth 1 https://github.com/sparrowwallet/frigate.git .
126+
RUN git clone --recursive --branch 1.3.2 --depth 1 https://github.com/sparrowwallet/frigate.git .
127127
RUN ./gradlew jpackage
128128
RUN cp -r ./build/jpackage/frigate /build/frigate
129129
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)