Skip to content

Commit 03b0910

Browse files
authored
Spv proof (#1207)
* feat(unit_tests): add begin of unit test for spv proof * feat(utxo): continue test * feat(unit_tests): very wip * feat(utxo): merkle tree verification now work; need full refactoring * feat(spv): start spv validation module * feat(spv): add spv proof * feat(spv): add merkle proof unit test for a single element * feat(spv): add complex merkle proof inclusion unit test * feat(spv): fix some cargo warnings * feat(spv): simplify error check for validate_spv_proof - native client should not return error on validate_spv_proof since there is no verification * feat(spv): complete the first step of the spv proof validation * feat(spv): complete the unit test for spv proof validation in utxo module * feat(spv_validation): add vin and vout check for spv proof validation * feat(spv_validation): match exact error from the spv - will customize later * feat(utxo): start utxo block header storage + sync with dev * feat(utxo): continue block header storage interface * feat(utxo): revert sled - will switch to sqllite * feat(utxo): start utxo sql block header storage trait implementation * feat(sql): implement partially add_block_headers_to_storage and the unit test * feat(sql): add get_block header functions * feat(sql): add extra function for block insertion * feat(lib_bitcoin): add raw header type * feat(spv): Integrate raw block header into spv validation * feat(spv): remove old comments * feat(spv): Adding raw header into utxo_common spv validation * feat(spv): make slp great again * feat(spv): returning more concrete error types * feat(toolchain): update to `nightly-2022-02-01` to fix osx compilation - rust-lang/rust#93414 * feat(cargo): fix warnings * feat(fmt): rust fmt * feat(utxo_block_header): add a box dyn for block header storage to utxo common fields * Revert "feat(utxo_block_header): add a box dyn for block header storage to utxo common fields" This reverts commit 8914b76. * feat(utxo): add empty utxo_indexedb_block_header_storage.rs * feat(utxo): start an empty loop for downloading headers * feat(indexed_db_block_header): do not use todo to prevent crash at runtime * feat(block_header_loop): add block header loop in arc builder * feat(utxo): add a function to retrieve a storage from the ctx for block header * feat(utxo): start logic of block header downloading, very WIP * feat(header_validation): add partial header validation + params for enabling * feat(header_validation): add unexpected difficulty change check * feat(header_validation): add unit test for validate headers * feat(header_validation): document the validate_headers function * feat(header_validation): use appropriate error for validate_headers * feat(storage): rework storage to make it persistent * feat(storage): use the block header storage as optional and fix unit tests * feat(storage): make the conf into coins settings * feat(storage): implement header from storage or rpc + within validation * feat(improvements): remove non-used function * feat(storage): add to storage after validation in retrieve header from storage * feat(fix_review): first fixes batch * feat(fix_review): sql fixes + rename `from_address` to `sender_address` * feat(fix_review): simplify the way to get height, use into_iter + find * feat(fix_review): add a get_tx_height function + remove more marketcoinops in the arc builder * feat(fix_review): simplify download_loop with a `try_loop` macro * feat(fix_review): improve get_tx_height * feat(fix_review): continue review improvements, move retrieve_last_headers to electrum * feat(fix_review): remove dead code comment * feat(spv): next batch of fixes * feat(review): remove utxo_wrapper_block_header_storage.rs * feat(review): next batche of fixes * feat(review): refactor block_header_from_storage_or_rpc * feat(wasm): use explicitly instant wasm bindgen * feat(spv): fix wasm tests * feat(spv): fix unit test compilation * feat(eth): ignore polygon unit test (unstable) * feat(unit tests): increase to 6 seconds for the docker unit test * feat(utxo): simplify block_headers_storage, add error * feat(review): batch of review fixes * feat(review): another batch of review fixes * feat(eth_test): remove the loop * feat(eth_test): remove the loop * feat(spv_proof): review fixes * feat(spv_proof): review fixes * feat(spv): fix wrong copy paste description
1 parent 1967ac2 commit 03b0910

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1866
-214
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ num-rational = { version = "0.2", features = ["serde", "bigint", "bigint-std"] }
8888
num-traits = "0.2"
8989
rpc = { path = "mm2src/mm2_bitcoin/rpc" }
9090
rpc_task = { path = "mm2src/rpc_task" }
91-
parking_lot = { version = "0.11", features = ["nightly"] }
91+
parking_lot = { version = "0.12.0", features = ["nightly"] }
9292
parity-util-mem = "0.9"
9393
# AP: portfolio RPCs are not documented and not used as of now
9494
# so the crate is disabled to speed up the entire removal of C code
@@ -110,6 +110,7 @@ ser_error = { path = "mm2src/derives/ser_error" }
110110
ser_error_derive = { path = "mm2src/derives/ser_error_derive" }
111111
serialization = { path = "mm2src/mm2_bitcoin/serialization" }
112112
serialization_derive = { path = "mm2src/mm2_bitcoin/serialization_derive" }
113+
spv_validation = { path = "mm2src/mm2_bitcoin/spv_validation" }
113114
sp-runtime-interface = { version = "3.0.0", default-features = false, features = ["disable_target_static_assertions"] }
114115
sp-trie = { version = "3.0", default-features = false }
115116

@@ -124,6 +125,7 @@ wasm-bindgen = { version = "0.2.50", features = ["serde-serialize", "nightly"] }
124125
wasm-bindgen-futures = { version = "0.4.1" }
125126
wasm-bindgen-test = { version = "0.3.1" }
126127
web-sys = { version = "0.3.55", features = ["console"] }
128+
instant = {version = "0.1.12", features = ["wasm-bindgen"]}
127129

128130
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
129131
dirs = { version = "1" }

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ The current state can be considered as very early alpha.
1515
1. (Optional) OSX: run `LIBRARY_PATH=/usr/local/opt/openssl/lib`
1616
1. Run
1717
```
18-
rustup install nightly-2021-12-16
19-
rustup default nightly-2021-12-16
18+
rustup install nightly-2022-02-01
19+
rustup default nightly-2022-02-01
2020
rustup component add rustfmt-preview
2121
```
2222
1. Run `cargo build` (or `cargo build -vv` to get verbose build output).

mm2src/coins/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ doctest = false
1313

1414
[dependencies]
1515
async-std = { version = "1.5", features = ["unstable"] }
16-
async-trait = "0.1"
16+
async-trait = "0.1.52"
1717
base64 = "0.10.0"
1818
bigdecimal = { version = "0.1.0", features = ["serde"] }
1919
bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] }
@@ -52,7 +52,7 @@ lightning-invoice = "0.12.0"
5252
metrics = "0.12"
5353
mocktopus = "0.7.0"
5454
num-traits = "0.2"
55-
parking_lot = { version = "0.11", features = ["nightly"] }
55+
parking_lot = { version = "0.12.0", features = ["nightly"] }
5656
primitives = { path = "../mm2_bitcoin/primitives" }
5757
prost = "0.8"
5858
rand = { version = "0.7", features = ["std", "small_rng"] }
@@ -69,6 +69,7 @@ serde_derive = "1.0"
6969
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
7070
serialization = { path = "../mm2_bitcoin/serialization" }
7171
serialization_derive = { path = "../mm2_bitcoin/serialization_derive" }
72+
spv_validation = { path = "../mm2_bitcoin/spv_validation" }
7273
sha2 = "0.8"
7374
sha3 = "0.8"
7475
utxo_signer = { path = "utxo_signer" }

mm2src/coins/eth/eth_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,7 @@ fn polygon_check_if_my_payment_sent() {
12101210
let request = json!({
12111211
"method": "enable",
12121212
"coin": "MATIC",
1213-
"urls": ["https://polygon-rpc.com"],
1213+
"urls": ["https://polygon-mainnet.g.alchemy.com/v2/9YYl6iMLmXXLoflMPHnMTC4Dcm2L2tFH"],
12141214
"swap_contract_address": "0x9130b257d37a52e52f21054c4da3450c72f595ce",
12151215
});
12161216

mm2src/coins/lightning_persister/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ bitcoin = "0.27.1"
1515
common = { path = "../../common" }
1616
lightning = "0.0.104"
1717
libc = "0.2"
18-
parking_lot = { version = "0.11", features = ["nightly"] }
18+
parking_lot = { version = "0.12.0", features = ["nightly"] }
1919
secp256k1 = { version = "0.20" }
2020
serde_json = "1.0"
2121

mm2src/coins/lp_coins.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ pub struct ValidatePaymentInput {
248248
pub secret_hash: Vec<u8>,
249249
pub amount: BigDecimal,
250250
pub swap_contract_address: Option<BytesJson>,
251+
pub confirmations: u64,
251252
}
252253

253254
/// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets).

mm2src/coins/qrc20/qrc20_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ fn test_validate_maker_payment() {
131131
secret_hash: vec![1; 20],
132132
amount: correct_amount.clone(),
133133
swap_contract_address: coin.swap_contract_address(),
134+
confirmations: 1,
134135
};
135136

136137
coin.validate_maker_payment(input.clone()).wait().unwrap();
@@ -851,6 +852,7 @@ fn test_validate_maker_payment_malicious() {
851852
secret_hash,
852853
amount,
853854
swap_contract_address: coin.swap_contract_address(),
855+
confirmations: 1,
854856
};
855857
let error = coin
856858
.validate_maker_payment(input)

mm2src/coins/sql_tx_history_storage.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ use common::{async_blocking, PagingOptionsEnum};
77
use db_common::sqlite::rusqlite::types::Type;
88
use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, ToSql, NO_PARAMS};
99
use db_common::sqlite::sql_builder::SqlBuilder;
10-
use db_common::sqlite::{offset_by_id, validate_table_name};
10+
use db_common::sqlite::{offset_by_id, string_from_row, validate_table_name, CHECK_TABLE_EXISTS_SQL};
1111
use rpc::v1::types::Bytes as BytesJson;
1212
use serde_json::{self as json};
1313
use std::convert::TryInto;
1414
use std::sync::{Arc, Mutex};
1515

16-
const CHECK_TABLE_EXISTS_SQL: &str = "SELECT name FROM sqlite_master WHERE type='table' AND name=?1;";
17-
1816
fn tx_history_table(ticker: &str) -> String { ticker.to_owned() + "_tx_history" }
1917

2018
fn tx_cache_table(ticker: &str) -> String { ticker.to_owned() + "_tx_cache" }
@@ -194,17 +192,9 @@ where
194192
P::Item: ToSql,
195193
F: FnOnce(&Row<'_>) -> Result<T, SqlError>,
196194
{
197-
let maybe_result = conn.query_row(query, params, map_fn);
198-
if let Err(SqlError::QueryReturnedNoRows) = maybe_result {
199-
return Ok(None);
200-
}
201-
202-
let result = maybe_result?;
203-
Ok(Some(result))
195+
db_common::sqlite::query_single_row(conn, query, params, map_fn).map_err(MmError::new)
204196
}
205197

206-
fn string_from_row(row: &Row<'_>) -> Result<String, SqlError> { row.get(0) }
207-
208198
fn tx_details_from_row(row: &Row<'_>) -> Result<TransactionDetails, SqlError> {
209199
let json_string: String = row.get(0)?;
210200
json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e)))

mm2src/coins/utxo.rs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@ mod bchd_pb;
2929
pub mod qtum;
3030
pub mod rpc_clients;
3131
pub mod slp;
32+
pub mod utxo_block_header_storage;
3233
pub mod utxo_builder;
3334
pub mod utxo_common;
3435
pub mod utxo_standard;
3536
pub mod utxo_withdraw;
3637

37-
#[cfg(not(target_arch = "wasm32"))] pub mod tx_cache;
38-
3938
use async_trait::async_trait;
4039
use bigdecimal::BigDecimal;
4140
use bitcoin::network::constants::Network as BitcoinNetwork;
@@ -69,6 +68,7 @@ use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as
6968
use script::{Builder, Script, SignatureVersion, TransactionInputSigner};
7069
use serde_json::{self as json, Value as Json};
7170
use serialization::{serialize, serialize_with_flags, SERIALIZE_TRANSACTION_WITNESS};
71+
use spv_validation::types::SPVError;
7272
use std::array::TryFromSliceError;
7373
use std::collections::{HashMap, HashSet};
7474
use std::convert::TryInto;
@@ -95,6 +95,13 @@ use super::{BalanceError, BalanceFut, BalanceResult, CoinsContext, DerivationMet
9595
use crate::coin_balance::{EnableCoinScanPolicy, HDAddressBalanceScanner};
9696
use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDWalletCoinOps, HDWalletOps, InvalidBip44ChainError};
9797
use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult};
98+
use crate::utxo::utxo_block_header_storage::BlockHeaderStorageError;
99+
use utxo_block_header_storage::BlockHeaderStorage;
100+
#[cfg(not(target_arch = "wasm32"))] pub mod tx_cache;
101+
#[cfg(target_arch = "wasm32")]
102+
pub mod utxo_indexedb_block_header_storage;
103+
#[cfg(not(target_arch = "wasm32"))]
104+
pub mod utxo_sql_block_header_storage;
98105

99106
#[cfg(test)] pub mod utxo_tests;
100107
#[cfg(target_arch = "wasm32")] pub mod utxo_wasm_tests;
@@ -510,6 +517,7 @@ pub struct UtxoCoinFields {
510517
pub history_sync_state: Mutex<HistorySyncState>,
511518
/// Path to the TX cache directory
512519
pub tx_cache_directory: Option<PathBuf>,
520+
pub block_headers_storage: Option<BlockHeaderStorage>,
513521
/// The cache of recently send transactions used to track the spent UTXOs and replace them with new outputs
514522
/// The daemon needs some time to update the listunspent list for address which makes it return already spent UTXOs
515523
/// This cache helps to prevent UTXO reuse in such cases
@@ -545,6 +553,56 @@ impl From<UnsupportedAddr> for WithdrawError {
545553
fn from(e: UnsupportedAddr) -> Self { WithdrawError::InvalidAddress(e.to_string()) }
546554
}
547555

556+
#[derive(Debug)]
557+
pub enum GetTxHeightError {
558+
HeightNotFound,
559+
}
560+
561+
impl From<GetTxHeightError> for SPVError {
562+
fn from(e: GetTxHeightError) -> Self {
563+
match e {
564+
GetTxHeightError::HeightNotFound => SPVError::InvalidHeight,
565+
}
566+
}
567+
}
568+
569+
#[derive(Debug)]
570+
pub enum GetBlockHeaderError {
571+
StorageError(BlockHeaderStorageError),
572+
RpcError(JsonRpcError),
573+
SerializationError(serialization::Error),
574+
InvalidResponse(String),
575+
SPVError(SPVError),
576+
NativeNotSupported(String),
577+
Internal(String),
578+
}
579+
580+
impl From<JsonRpcError> for GetBlockHeaderError {
581+
fn from(err: JsonRpcError) -> Self { GetBlockHeaderError::RpcError(err) }
582+
}
583+
584+
impl From<UtxoRpcError> for GetBlockHeaderError {
585+
fn from(e: UtxoRpcError) -> Self {
586+
match e {
587+
UtxoRpcError::Transport(e) | UtxoRpcError::ResponseParseError(e) => GetBlockHeaderError::RpcError(e),
588+
UtxoRpcError::InvalidResponse(e) => GetBlockHeaderError::InvalidResponse(e),
589+
UtxoRpcError::Internal(e) => GetBlockHeaderError::Internal(e),
590+
}
591+
}
592+
}
593+
594+
impl From<SPVError> for GetBlockHeaderError {
595+
fn from(e: SPVError) -> Self { GetBlockHeaderError::SPVError(e) }
596+
}
597+
598+
impl From<serialization::Error> for GetBlockHeaderError {
599+
fn from(err: serialization::Error) -> Self { GetBlockHeaderError::SerializationError(err) }
600+
}
601+
602+
impl From<BlockHeaderStorageError> for GetBlockHeaderError {
603+
fn from(err: BlockHeaderStorageError) -> Self { GetBlockHeaderError::StorageError(err) }
604+
}
605+
548606
impl UtxoCoinFields {
549607
pub fn transaction_preimage(&self) -> TransactionInputSigner {
550608
let lock_time = if self.conf.ticker == "KMD" {
@@ -1031,6 +1089,14 @@ pub struct UtxoMergeParams {
10311089
pub max_merge_at_once: usize,
10321090
}
10331091

1092+
#[derive(Clone, Debug, Deserialize, Serialize)]
1093+
pub struct UtxoBlockHeaderVerificationParams {
1094+
pub difficulty_check: bool,
1095+
pub constant_difficulty: bool,
1096+
pub blocks_limit_to_check: NonZeroU64,
1097+
pub check_every: f64,
1098+
}
1099+
10341100
#[derive(Clone, Debug, Deserialize, Serialize)]
10351101
pub struct UtxoActivationParams {
10361102
pub mode: UtxoRpcMode,
@@ -1053,6 +1119,7 @@ pub enum UtxoFromLegacyReqErr {
10531119
UnexpectedMethod,
10541120
InvalidElectrumServers(json::Error),
10551121
InvalidMergeParams(json::Error),
1122+
InvalidBlockHeaderVerificationParams(json::Error),
10561123
InvalidRequiredConfs(json::Error),
10571124
InvalidRequiresNota(json::Error),
10581125
InvalidAddressFormat(json::Error),

mm2src/coins/utxo/qtum_delegation.rs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,13 @@ impl QtumCoin {
124124
let delegation_output = self.remove_delegation_output(QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)?;
125125
let outputs = vec![delegation_output];
126126
let my_address = self.my_address().map_to_mm(DelegationError::InternalError)?;
127-
Ok(self
128-
.generate_delegation_transaction(
129-
outputs,
130-
my_address,
131-
QRC20_GAS_LIMIT_DEFAULT,
132-
TransactionType::RemoveDelegation,
133-
)
134-
.await?)
127+
self.generate_delegation_transaction(
128+
outputs,
129+
my_address,
130+
QRC20_GAS_LIMIT_DEFAULT,
131+
TransactionType::RemoveDelegation,
132+
)
133+
.await
135134
}
136135

137136
async fn am_i_currently_staking(&self) -> Result<Option<String>, MmError<StakingInfosError>> {
@@ -252,14 +251,13 @@ impl QtumCoin {
252251

253252
let outputs = vec![delegation_output];
254253
let my_address = self.my_address().map_to_mm(DelegationError::InternalError)?;
255-
Ok(self
256-
.generate_delegation_transaction(
257-
outputs,
258-
my_address,
259-
QRC20_GAS_LIMIT_DELEGATION,
260-
TransactionType::StakingDelegation,
261-
)
262-
.await?)
254+
self.generate_delegation_transaction(
255+
outputs,
256+
my_address,
257+
QRC20_GAS_LIMIT_DELEGATION,
258+
TransactionType::StakingDelegation,
259+
)
260+
.await
263261
}
264262

265263
async fn generate_delegation_transaction(

mm2src/coins/utxo/rpc_clients.rs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ impl Into<BlockHeaderNonce> for ElectrumNonce {
10251025

10261026
#[derive(Debug, Deserialize)]
10271027
pub struct ElectrumBlockHeadersRes {
1028-
count: u64,
1028+
pub count: u64,
10291029
pub hex: BytesJson,
10301030
#[allow(dead_code)]
10311031
max: u64,
@@ -1044,8 +1044,8 @@ pub struct ElectrumBlockHeaderV12 {
10441044
}
10451045

10461046
impl ElectrumBlockHeaderV12 {
1047-
pub fn hash(&self) -> H256Json {
1048-
let block_header = BlockHeader {
1047+
fn as_block_header(&self) -> BlockHeader {
1048+
BlockHeader {
10491049
version: self.version as u32,
10501050
previous_header_hash: self.prev_block_hash.into(),
10511051
merkle_root_hash: self.merkle_root.into(),
@@ -1064,7 +1064,17 @@ impl ElectrumBlockHeaderV12 {
10641064
n_height: None,
10651065
n_nonce_u64: None,
10661066
mix_hash: None,
1067-
};
1067+
}
1068+
}
1069+
1070+
pub fn as_hex(&self) -> String {
1071+
let block_header = self.as_block_header();
1072+
let serialized = serialize(&block_header);
1073+
hex::encode(serialized)
1074+
}
1075+
1076+
pub fn hash(&self) -> H256Json {
1077+
let block_header = self.as_block_header();
10681078
BlockHeader::hash(&block_header).into()
10691079
}
10701080
}
@@ -1658,6 +1668,49 @@ impl ElectrumClient {
16581668
rpc_func!(self, "blockchain.block.headers", start_height, count)
16591669
}
16601670

1671+
pub fn retrieve_last_headers(
1672+
&self,
1673+
blocks_limit_to_check: NonZeroU64,
1674+
block_height: u64,
1675+
) -> UtxoRpcFut<(HashMap<u64, BlockHeader>, Vec<BlockHeader>)> {
1676+
let (from, count) = {
1677+
let from = if block_height < blocks_limit_to_check.get() {
1678+
0
1679+
} else {
1680+
block_height - blocks_limit_to_check.get()
1681+
};
1682+
(from, blocks_limit_to_check)
1683+
};
1684+
Box::new(
1685+
self.blockchain_block_headers(from, count)
1686+
.map_to_mm_fut(UtxoRpcError::from)
1687+
.and_then(move |headers| {
1688+
let (block_registry, block_headers) = {
1689+
if headers.count == 0 {
1690+
return MmError::err(UtxoRpcError::Internal("No headers available".to_string()));
1691+
}
1692+
let len = CompactInteger::from(headers.count);
1693+
let mut serialized = serialize(&len).take();
1694+
serialized.extend(headers.hex.0.into_iter());
1695+
let mut reader = Reader::new_with_coin_variant(serialized.as_slice(), CoinVariant::Standard);
1696+
let maybe_block_headers = reader.read_list::<BlockHeader>();
1697+
let block_headers = match maybe_block_headers {
1698+
Ok(headers) => headers,
1699+
Err(e) => return MmError::err(UtxoRpcError::InvalidResponse(format!("{:?}", e))),
1700+
};
1701+
let mut block_registry: HashMap<u64, BlockHeader> = HashMap::new();
1702+
let mut starting_height = from;
1703+
for block_header in &block_headers {
1704+
block_registry.insert(starting_height, block_header.clone());
1705+
starting_height += 1;
1706+
}
1707+
(block_registry, block_headers)
1708+
};
1709+
Ok((block_registry, block_headers))
1710+
}),
1711+
)
1712+
}
1713+
16611714
/// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get-merkle
16621715
pub fn blockchain_transaction_get_merkle(&self, txid: H256Json, height: u64) -> RpcRes<TxMerkleBranch> {
16631716
rpc_func!(self, "blockchain.transaction.get_merkle", txid, height)

0 commit comments

Comments
 (0)