Skip to content

Commit b316b27

Browse files
committed
move rgb resolvers implementation from rgb-runtime
1 parent 8ab08f3 commit b316b27

File tree

8 files changed

+1160
-12
lines changed

8 files changed

+1160
-12
lines changed

Cargo.lock

Lines changed: 632 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ amplify = "4.7.0"
2626
nonasync = "0.1.0"
2727
ascii-armor = "0.7.2"
2828
baid64 = "0.2.2"
29+
bp-electrum = "0.11.1-alpha.1"
30+
bp-esplora = { version = "0.11.1-alpha.1", default-features = false }
2931
strict_encoding = "2.7.0"
3032
strict_types = "2.7.2"
3133
commit_verify = { version = "0.11.0-beta.9", features = ["stl"] }
@@ -58,6 +60,8 @@ amplify = { workspace = true }
5860
nonasync = { workspace = true }
5961
ascii-armor = { workspace = true }
6062
baid64 = { workspace = true }
63+
bp-electrum = { workspace = true, optional = true }
64+
bp-esplora = { workspace = true, optional = true }
6165
strict_encoding = { workspace = true }
6266
strict_types = { workspace = true }
6367
commit_verify = { workspace = true }
@@ -73,7 +77,11 @@ rand = "0.8.5"
7377

7478
[features]
7579
default = []
76-
all = ["fs", "serde"]
80+
all = ["esplora_blocking", "electrum_blocking", "mempool_blocking", "fs", "serde"]
81+
esplora_blocking = ["bp-esplora", "bp-esplora/blocking", "bp-esplora/blocking-https"]
82+
esplora_blocking-wasm = ["bp-esplora", "bp-esplora/blocking-wasm"]
83+
electrum_blocking = ["bp-electrum"]
84+
mempool_blocking = ["esplora_blocking"]
7785
serde = [
7886
"serde_crate",
7987
"chrono/serde",
@@ -99,4 +107,6 @@ wasm-bindgen-test = "0.3"
99107
features = ["all"]
100108

101109
[patch.crates-io]
110+
bp-electrum = { git = "https://github.com/zoedberg/bp-electrum-client", branch = "remove_bpstd" }
111+
bp-esplora = { git = "https://github.com/zoedberg/bp-esplora-client", branch = "remove_bpstd" }
102112
rgb-core = { git = "https://github.com/zoedberg/rgb-core", branch = "0.11.1-2" }

src/indexers/any.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// RGB standard library for working with smart contracts on Bitcoin & Lightning
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Written in 2024 by
6+
// Zoe Faltibà <[email protected]>
7+
// Rewritten in 2024 by
8+
// Dr Maxim Orlovsky <[email protected]>
9+
//
10+
// Copyright (C) 2024 LNP/BP Standards Association. All rights reserved.
11+
//
12+
// Licensed under the Apache License, Version 2.0 (the "License");
13+
// you may not use this file except in compliance with the License.
14+
// You may obtain a copy of the License at
15+
//
16+
// http://www.apache.org/licenses/LICENSE-2.0
17+
//
18+
// Unless required by applicable law or agreed to in writing, software
19+
// distributed under the License is distributed on an "AS IS" BASIS,
20+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21+
// See the License for the specific language governing permissions and
22+
// limitations under the License.
23+
24+
use std::collections::HashMap;
25+
26+
use bp::{Tx, Txid};
27+
use rgbcore::validation::{ResolveWitness, WitnessResolverError};
28+
use rgbcore::vm::WitnessOrd;
29+
use rgbcore::ChainNet;
30+
31+
use crate::containers::Consignment;
32+
33+
// We need to repeat methods of `WitnessResolve` trait here to avoid making
34+
// wrappers around resolver types. TODO: Use wrappers instead
35+
pub trait RgbResolver: Send {
36+
fn check_chain_net(&self, chain_net: ChainNet) -> Result<(), String>;
37+
fn resolve_pub_witness(&self, txid: Txid) -> Result<Option<Tx>, String>;
38+
fn resolve_pub_witness_ord(&self, txid: Txid) -> Result<WitnessOrd, String>;
39+
}
40+
41+
/// Type that contains any of the [`Resolver`] types defined by the library
42+
#[derive(From)]
43+
#[non_exhaustive]
44+
pub struct AnyResolver {
45+
inner: Box<dyn RgbResolver>,
46+
consignment_txes: HashMap<Txid, Tx>,
47+
}
48+
49+
impl AnyResolver {
50+
#[cfg(feature = "electrum_blocking")]
51+
pub fn electrum_blocking(url: &str, config: Option<electrum::Config>) -> Result<Self, String> {
52+
Ok(AnyResolver {
53+
inner: Box::new(
54+
electrum::Client::from_config(url, config.unwrap_or_default())
55+
.map_err(|e| e.to_string())?,
56+
),
57+
consignment_txes: Default::default(),
58+
})
59+
}
60+
61+
#[cfg(feature = "esplora_blocking")]
62+
pub fn esplora_blocking(url: &str, config: Option<esplora::Config>) -> Result<Self, String> {
63+
Ok(AnyResolver {
64+
inner: Box::new(
65+
esplora::BlockingClient::from_config(url, config.unwrap_or_default())
66+
.map_err(|e| e.to_string())?,
67+
),
68+
consignment_txes: Default::default(),
69+
})
70+
}
71+
72+
#[cfg(feature = "mempool_blocking")]
73+
pub fn mempool_blocking(url: &str, config: Option<esplora::Config>) -> Result<Self, String> {
74+
Ok(AnyResolver {
75+
inner: Box::new(super::mempool_blocking::MemPoolClient::new(
76+
url,
77+
config.unwrap_or_default(),
78+
)?),
79+
consignment_txes: Default::default(),
80+
})
81+
}
82+
83+
/// Add to the resolver the TXs found in the consignment bundles. Those TXs
84+
/// will not be resolved by an indexer and will be considered tentative.
85+
/// Use with caution, this could allow accepting a consignment containing TXs that have not
86+
/// been broadcasted.
87+
pub fn add_consignment_txes<const TYPE: bool>(&mut self, consignment: &Consignment<TYPE>) {
88+
self.consignment_txes.extend(
89+
consignment
90+
.bundles
91+
.iter()
92+
.filter_map(|bw| bw.pub_witness.tx().cloned())
93+
.map(|tx| (tx.txid(), tx)),
94+
);
95+
}
96+
}
97+
98+
impl ResolveWitness for AnyResolver {
99+
fn resolve_pub_witness(&self, witness_id: Txid) -> Result<Tx, WitnessResolverError> {
100+
if let Some(tx) = self.consignment_txes.get(&witness_id) {
101+
return Ok(tx.clone());
102+
}
103+
104+
self.inner
105+
.resolve_pub_witness(witness_id)
106+
.map_err(|e| WitnessResolverError::Other(witness_id, e))
107+
.and_then(|r| r.ok_or(WitnessResolverError::Unknown(witness_id)))
108+
}
109+
110+
fn resolve_pub_witness_ord(
111+
&self,
112+
witness_id: Txid,
113+
) -> Result<WitnessOrd, WitnessResolverError> {
114+
if self.consignment_txes.contains_key(&witness_id) {
115+
return Ok(WitnessOrd::Tentative);
116+
}
117+
118+
self.inner
119+
.resolve_pub_witness_ord(witness_id)
120+
.map_err(|e| WitnessResolverError::Other(witness_id, e))
121+
}
122+
123+
fn check_chain_net(&self, chain_net: ChainNet) -> Result<(), WitnessResolverError> {
124+
self.inner
125+
.check_chain_net(chain_net)
126+
.map_err(|_| WitnessResolverError::WrongChainNet)
127+
}
128+
}

src/indexers/electrum_blocking.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// RGB standard library for working with smart contracts on Bitcoin & Lightning
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Written in 2024 by
6+
// Zoe Faltibà <[email protected]>
7+
// Rewritten in 2024 by
8+
// Dr Maxim Orlovsky <[email protected]>
9+
//
10+
// Copyright (C) 2024 LNP/BP Standards Association. All rights reserved.
11+
//
12+
// Licensed under the Apache License, Version 2.0 (the "License");
13+
// you may not use this file except in compliance with the License.
14+
// You may obtain a copy of the License at
15+
//
16+
// http://www.apache.org/licenses/LICENSE-2.0
17+
//
18+
// Unless required by applicable law or agreed to in writing, software
19+
// distributed under the License is distributed on an "AS IS" BASIS,
20+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21+
// See the License for the specific language governing permissions and
22+
// limitations under the License.
23+
24+
use std::iter;
25+
use std::num::NonZeroU32;
26+
27+
use bp::{ConsensusDecode, Tx, Txid};
28+
use electrum::{Client, ElectrumApi, Param};
29+
pub use electrum::{Config, ConfigBuilder, Error, Socks5Config};
30+
use rgbcore::vm::{WitnessOrd, WitnessPos};
31+
use rgbcore::ChainNet;
32+
33+
use super::RgbResolver;
34+
35+
macro_rules! check {
36+
($e:expr) => {
37+
$e.map_err(|e| e.to_string())?
38+
};
39+
}
40+
41+
impl RgbResolver for Client {
42+
fn check_chain_net(&self, chain_net: ChainNet) -> Result<(), String> {
43+
// check the electrum server is for the correct network
44+
let block_hash = check!(self.block_header(0)).block_hash();
45+
if chain_net.genesis_block_hash() != block_hash {
46+
return Err(s!("resolver is for a network different from the wallet's one"));
47+
}
48+
// check the electrum server has the required functionality (verbose
49+
// transactions)
50+
let txid = match chain_net {
51+
ChainNet::BitcoinMainnet => {
52+
"33e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac98799036"
53+
}
54+
ChainNet::BitcoinTestnet3 => {
55+
"5e6560fd518aadbed67ee4a55bdc09f19e619544f5511e9343ebba66d2f62653"
56+
}
57+
ChainNet::BitcoinTestnet4 => {
58+
"7aa0a7ae1e223414cb807e40cd57e667b718e42aaf9306db9102fe28912b7b4e"
59+
}
60+
ChainNet::BitcoinSignet => {
61+
"8153034f45e695453250a8fb7225a5e545144071d8ed7b0d3211efa1f3c92ad8"
62+
}
63+
ChainNet::BitcoinRegtest => {
64+
"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
65+
}
66+
_ => return Err(s!("only bitcoin is supported")),
67+
};
68+
if let Err(e) = self.raw_call("blockchain.transaction.get", vec![
69+
Param::String(txid.to_string()),
70+
Param::Bool(true),
71+
]) {
72+
if !e
73+
.to_string()
74+
.contains("genesis block coinbase is not considered an ordinary transaction")
75+
{
76+
return Err(s!(
77+
"verbose transactions are unsupported by the provided electrum service"
78+
));
79+
}
80+
}
81+
Ok(())
82+
}
83+
84+
fn resolve_pub_witness_ord(&self, txid: Txid) -> Result<WitnessOrd, String> {
85+
// We get the height of the tip of blockchain
86+
let header = check!(self.block_headers_subscribe());
87+
88+
// Now we get and parse transaction information to get the number of
89+
// confirmations
90+
let tx_details = match self.raw_call("blockchain.transaction.get", vec![
91+
Param::String(txid.to_string()),
92+
Param::Bool(true),
93+
]) {
94+
Err(e)
95+
if e.to_string()
96+
.contains("No such mempool or blockchain transaction") =>
97+
{
98+
return Ok(WitnessOrd::Archived);
99+
}
100+
Err(e) => return Err(e.to_string()),
101+
Ok(v) => v,
102+
};
103+
let forward = iter::from_fn(|| self.block_headers_pop().ok().flatten()).count() as isize;
104+
105+
let Some(confirmations) = tx_details.get("confirmations") else {
106+
return Ok(WitnessOrd::Tentative);
107+
};
108+
let confirmations = check!(confirmations
109+
.as_u64()
110+
.and_then(|x| u32::try_from(x).ok())
111+
.ok_or(Error::InvalidResponse(tx_details.clone())));
112+
if confirmations == 0 {
113+
return Ok(WitnessOrd::Tentative);
114+
}
115+
let block_time = check!(tx_details
116+
.get("blocktime")
117+
.and_then(|v| v.as_i64())
118+
.ok_or(Error::InvalidResponse(tx_details.clone())));
119+
120+
let tip_height = u32::try_from(header.height).map_err(|_| s!("impossible height value"))?;
121+
let height: isize = (tip_height - confirmations) as isize;
122+
const SAFETY_MARGIN: isize = 1;
123+
// first check from expected min to max height
124+
let get_merkle_res = (1..=forward + 1)
125+
// we need this under assumption that electrum was lying due to "DB desynchronization"
126+
// since this have a very low probability we do that after everything else
127+
.chain((1..=SAFETY_MARGIN).flat_map(|i| [i + forward + 1, 1 - i]))
128+
.find_map(|offset| self.transaction_get_merkle(&txid, (height + offset) as usize).ok())
129+
.ok_or_else(|| s!("transaction can't be located in the blockchain"))?;
130+
131+
let tx_height = u32::try_from(get_merkle_res.block_height)
132+
.map_err(|_| s!("impossible height value"))?;
133+
134+
let height =
135+
check!(NonZeroU32::new(tx_height).ok_or(Error::InvalidResponse(tx_details.clone())));
136+
let pos = check!(WitnessPos::bitcoin(height, block_time)
137+
.ok_or(Error::InvalidResponse(tx_details.clone())));
138+
139+
Ok(WitnessOrd::Mined(pos))
140+
}
141+
142+
fn resolve_pub_witness(&self, txid: Txid) -> Result<Option<Tx>, String> {
143+
self.transaction_get_raw(&txid)
144+
.map_err(|e| e.to_string())
145+
.and_then(|raw_tx| {
146+
Tx::consensus_deserialize(raw_tx)
147+
.map_err(|e| format!("cannot deserialize raw TX - {e}"))
148+
})
149+
.map(Some)
150+
.or_else(|e| {
151+
if e.contains("No such mempool or blockchain transaction") {
152+
Ok(None)
153+
} else {
154+
Err(e)
155+
}
156+
})
157+
}
158+
}

src/indexers/esplora_blocking.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// RGB standard library for working with smart contracts on Bitcoin & Lightning
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Written in 2019-2023 by
6+
// Dr Maxim Orlovsky <[email protected]>
7+
//
8+
// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved.
9+
//
10+
// Licensed under the Apache License, Version 2.0 (the "License");
11+
// you may not use this file except in compliance with the License.
12+
// You may obtain a copy of the License at
13+
//
14+
// http://www.apache.org/licenses/LICENSE-2.0
15+
//
16+
// Unless required by applicable law or agreed to in writing, software
17+
// distributed under the License is distributed on an "AS IS" BASIS,
18+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
// See the License for the specific language governing permissions and
20+
// limitations under the License.
21+
22+
use std::num::NonZeroU32;
23+
24+
use bp::{Tx, Txid};
25+
use esplora::BlockingClient;
26+
pub use esplora::{Builder, Config, Error};
27+
use rgbcore::vm::{WitnessOrd, WitnessPos};
28+
use rgbcore::ChainNet;
29+
30+
use super::RgbResolver;
31+
32+
impl RgbResolver for BlockingClient {
33+
fn check_chain_net(&self, chain_net: ChainNet) -> Result<(), String> {
34+
// check the esplora server is for the correct network
35+
let block_hash = self.block_hash(0)?;
36+
if chain_net.genesis_block_hash() != block_hash {
37+
return Err(s!("resolver is for a network different from the wallet's one"));
38+
}
39+
Ok(())
40+
}
41+
42+
fn resolve_pub_witness_ord(&self, txid: Txid) -> Result<WitnessOrd, String> {
43+
if self.tx(&txid)?.is_none() {
44+
return Ok(WitnessOrd::Archived);
45+
}
46+
let status = self.tx_status(&txid)?;
47+
let ord = match status
48+
.block_height
49+
.and_then(|h| status.block_time.map(|t| (h, t)))
50+
{
51+
Some((h, t)) => {
52+
let height = NonZeroU32::new(h).ok_or(Error::InvalidServerData)?;
53+
WitnessOrd::Mined(
54+
WitnessPos::bitcoin(height, t as i64).ok_or(Error::InvalidServerData)?,
55+
)
56+
}
57+
None => WitnessOrd::Tentative,
58+
};
59+
Ok(ord)
60+
}
61+
62+
fn resolve_pub_witness(&self, txid: Txid) -> Result<Option<Tx>, String> {
63+
self.tx(&txid).or_else(|e| match e {
64+
Error::TransactionNotFound(_) => Ok(None),
65+
e => Err(e.to_string()),
66+
})
67+
}
68+
}

0 commit comments

Comments
 (0)