Policy and contribution
This commit is contained in:
parent
7df3b4844e
commit
2a7c7d5272
@ -25,7 +25,7 @@ use bitcoin::{Address, Network, OutPoint};
|
|||||||
use magical_bitcoin_wallet::bitcoin;
|
use magical_bitcoin_wallet::bitcoin;
|
||||||
use magical_bitcoin_wallet::sled;
|
use magical_bitcoin_wallet::sled;
|
||||||
use magical_bitcoin_wallet::types::ScriptType;
|
use magical_bitcoin_wallet::types::ScriptType;
|
||||||
use magical_bitcoin_wallet::{Client, ExtendedDescriptor, Wallet};
|
use magical_bitcoin_wallet::{Client, Wallet};
|
||||||
|
|
||||||
fn prepare_home_dir() -> PathBuf {
|
fn prepare_home_dir() -> PathBuf {
|
||||||
let mut dir = PathBuf::new();
|
let mut dir = PathBuf::new();
|
||||||
@ -232,13 +232,8 @@ fn main() {
|
|||||||
Some("testnet") | _ => Network::Testnet,
|
Some("testnet") | _ => Network::Testnet,
|
||||||
};
|
};
|
||||||
|
|
||||||
let descriptor = matches
|
let descriptor = matches.value_of("descriptor").unwrap();
|
||||||
.value_of("descriptor")
|
let change_descriptor = matches.value_of("change_descriptor");
|
||||||
.map(|x| ExtendedDescriptor::from_str(x).unwrap())
|
|
||||||
.unwrap();
|
|
||||||
let change_descriptor = matches
|
|
||||||
.value_of("change_descriptor")
|
|
||||||
.map(|x| ExtendedDescriptor::from_str(x).unwrap());
|
|
||||||
debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
|
debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
|
||||||
|
|
||||||
let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
|
let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
|
||||||
@ -248,7 +243,7 @@ fn main() {
|
|||||||
debug!("database opened successfully");
|
debug!("database opened successfully");
|
||||||
|
|
||||||
let client = Client::new(matches.value_of("server").unwrap()).unwrap();
|
let client = Client::new(matches.value_of("server").unwrap()).unwrap();
|
||||||
let wallet = Wallet::new(descriptor, change_descriptor, network, tree, client);
|
let wallet = Wallet::new(descriptor, change_descriptor, network, tree, client).unwrap();
|
||||||
|
|
||||||
// TODO: print errors in a nice way
|
// TODO: print errors in a nice way
|
||||||
let handle_matches = |matches: ArgMatches<'_>| {
|
let handle_matches = |matches: ArgMatches<'_>| {
|
||||||
|
@ -17,6 +17,7 @@ use crate::types::*;
|
|||||||
// rawtx r<txid> -> tx
|
// rawtx r<txid> -> tx
|
||||||
// transactions t<txid> -> tx details
|
// transactions t<txid> -> tx details
|
||||||
// deriv indexes c{i,e} -> u32
|
// deriv indexes c{i,e} -> u32
|
||||||
|
// descriptor checksum d{i,e} -> vec<u8>
|
||||||
|
|
||||||
enum SledKey<'a> {
|
enum SledKey<'a> {
|
||||||
Path((Option<ScriptType>, Option<&'a DerivationPath>)),
|
Path((Option<ScriptType>, Option<&'a DerivationPath>)),
|
||||||
@ -25,6 +26,7 @@ enum SledKey<'a> {
|
|||||||
RawTx(Option<&'a Txid>),
|
RawTx(Option<&'a Txid>),
|
||||||
Transaction(Option<&'a Txid>),
|
Transaction(Option<&'a Txid>),
|
||||||
LastIndex(ScriptType),
|
LastIndex(ScriptType),
|
||||||
|
DescriptorChecksum(ScriptType),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SledKey<'_> {
|
impl SledKey<'_> {
|
||||||
@ -42,6 +44,7 @@ impl SledKey<'_> {
|
|||||||
SledKey::RawTx(_) => b"r".to_vec(),
|
SledKey::RawTx(_) => b"r".to_vec(),
|
||||||
SledKey::Transaction(_) => b"t".to_vec(),
|
SledKey::Transaction(_) => b"t".to_vec(),
|
||||||
SledKey::LastIndex(st) => [b"c", st.as_ref()].concat(),
|
SledKey::LastIndex(st) => [b"c", st.as_ref()].concat(),
|
||||||
|
SledKey::DescriptorChecksum(st) => [b"d", st.as_ref()].concat(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +238,26 @@ impl BatchOperations for Batch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Database for Tree {
|
impl Database for Tree {
|
||||||
|
fn check_descriptor_checksum<B: AsRef<[u8]>>(
|
||||||
|
&mut self,
|
||||||
|
script_type: ScriptType,
|
||||||
|
bytes: B,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let key = SledKey::DescriptorChecksum(script_type).as_sled_key();
|
||||||
|
|
||||||
|
let prev = self.get(&key)?.map(|x| x.to_vec());
|
||||||
|
if let Some(val) = prev {
|
||||||
|
if val == bytes.as_ref() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::ChecksumMismatch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.insert(&key, bytes.as_ref())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error> {
|
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error> {
|
||||||
let key = SledKey::Path((script_type, None)).as_sled_key();
|
let key = SledKey::Path((script_type, None)).as_sled_key();
|
||||||
self.scan_prefix(key)
|
self.scan_prefix(key)
|
||||||
@ -358,7 +381,7 @@ impl Database for Tree {
|
|||||||
// inserts 0 if not present
|
// inserts 0 if not present
|
||||||
fn increment_last_index(&mut self, script_type: ScriptType) -> Result<u32, Error> {
|
fn increment_last_index(&mut self, script_type: ScriptType) -> Result<u32, Error> {
|
||||||
let key = SledKey::LastIndex(script_type).as_sled_key();
|
let key = SledKey::LastIndex(script_type).as_sled_key();
|
||||||
self.fetch_and_update(key, |prev| {
|
self.update_and_fetch(key, |prev| {
|
||||||
let new = match prev {
|
let new = match prev {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
let array: [u8; 4] = b.try_into().unwrap_or([0; 4]);
|
let array: [u8; 4] = b.try_into().unwrap_or([0; 4]);
|
||||||
@ -366,7 +389,7 @@ impl Database for Tree {
|
|||||||
|
|
||||||
val + 1
|
val + 1
|
||||||
}
|
}
|
||||||
None => 1, // start from 1, we return 0 when the prev value was None
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(new.to_be_bytes().to_vec())
|
Some(new.to_be_bytes().to_vec())
|
||||||
|
@ -40,6 +40,12 @@ pub trait BatchOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait Database: BatchOperations {
|
pub trait Database: BatchOperations {
|
||||||
|
fn check_descriptor_checksum<B: AsRef<[u8]>>(
|
||||||
|
&mut self,
|
||||||
|
script_type: ScriptType,
|
||||||
|
bytes: B,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error>;
|
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error>;
|
||||||
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error>;
|
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error>;
|
||||||
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error>;
|
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error>;
|
||||||
|
64
src/descriptor/checksum.rs
Normal file
64
src/descriptor/checksum.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
use crate::descriptor::Error;
|
||||||
|
|
||||||
|
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||||
|
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
|
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||||
|
let c0 = c >> 35;
|
||||||
|
c = ((c & 0x7ffffffff) << 5) ^ val;
|
||||||
|
if c0 & 1 > 0 {
|
||||||
|
c ^= 0xf5dee51989
|
||||||
|
};
|
||||||
|
if c0 & 2 > 0 {
|
||||||
|
c ^= 0xa9fdca3312
|
||||||
|
};
|
||||||
|
if c0 & 4 > 0 {
|
||||||
|
c ^= 0x1bab10e32d
|
||||||
|
};
|
||||||
|
if c0 & 8 > 0 {
|
||||||
|
c ^= 0x3706b1677a
|
||||||
|
};
|
||||||
|
if c0 & 16 > 0 {
|
||||||
|
c ^= 0x644d626ffd
|
||||||
|
};
|
||||||
|
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_checksum(desc: &str) -> Result<String, Error> {
|
||||||
|
let mut c = 1;
|
||||||
|
let mut cls = 0;
|
||||||
|
let mut clscount = 0;
|
||||||
|
for ch in desc.chars() {
|
||||||
|
let pos = INPUT_CHARSET
|
||||||
|
.find(ch)
|
||||||
|
.ok_or(Error::InvalidDescriptorCharacter(ch))? as u64;
|
||||||
|
c = poly_mod(c, pos & 31);
|
||||||
|
cls = cls * 3 + (pos >> 5);
|
||||||
|
clscount += 1;
|
||||||
|
if clscount == 3 {
|
||||||
|
c = poly_mod(c, cls);
|
||||||
|
cls = 0;
|
||||||
|
clscount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clscount > 0 {
|
||||||
|
c = poly_mod(c, cls);
|
||||||
|
}
|
||||||
|
(0..8).for_each(|_| c = poly_mod(c, 0));
|
||||||
|
c ^= 1;
|
||||||
|
|
||||||
|
let mut chars = Vec::with_capacity(8);
|
||||||
|
for j in 0..8 {
|
||||||
|
chars.push(
|
||||||
|
CHECKSUM_CHARSET
|
||||||
|
.chars()
|
||||||
|
.nth(((c >> (5 * (7 - j))) & 31) as usize)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::from_iter(chars))
|
||||||
|
}
|
@ -6,6 +6,14 @@ pub enum Error {
|
|||||||
MalformedInput,
|
MalformedInput,
|
||||||
KeyParsingError(String),
|
KeyParsingError(String),
|
||||||
|
|
||||||
|
InputIndexDoesntExist,
|
||||||
|
MissingPublicKey,
|
||||||
|
MissingDetails,
|
||||||
|
|
||||||
|
InvalidDescriptorCharacter(char),
|
||||||
|
|
||||||
|
CantDeriveWithMiniscript,
|
||||||
|
|
||||||
BIP32(bitcoin::util::bip32::Error),
|
BIP32(bitcoin::util::bip32::Error),
|
||||||
Base58(bitcoin::util::base58::Error),
|
Base58(bitcoin::util::base58::Error),
|
||||||
PK(bitcoin::util::key::Error),
|
PK(bitcoin::util::key::Error),
|
||||||
|
@ -54,7 +54,13 @@ pub struct DescriptorExtendedKey {
|
|||||||
|
|
||||||
impl DescriptorExtendedKey {
|
impl DescriptorExtendedKey {
|
||||||
pub fn full_path(&self, index: u32) -> DerivationPath {
|
pub fn full_path(&self, index: u32) -> DerivationPath {
|
||||||
let mut final_path: Vec<ChildNumber> = self.path.clone().into();
|
let mut final_path: Vec<ChildNumber> = Vec::new();
|
||||||
|
if let Some(path) = &self.master_derivation {
|
||||||
|
let path_as_vec: Vec<ChildNumber> = path.clone().into();
|
||||||
|
final_path.extend_from_slice(&path_as_vec);
|
||||||
|
}
|
||||||
|
let our_path: Vec<ChildNumber> = self.path.clone().into();
|
||||||
|
final_path.extend_from_slice(&our_path);
|
||||||
let other_path: Vec<ChildNumber> = self.final_index.as_path(index).into();
|
let other_path: Vec<ChildNumber> = self.final_index.as_path(index).into();
|
||||||
final_path.extend_from_slice(&other_path);
|
final_path.extend_from_slice(&other_path);
|
||||||
|
|
||||||
|
@ -4,28 +4,36 @@ use std::convert::{Into, TryFrom};
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bitcoin::blockdata::script::Script;
|
|
||||||
use bitcoin::hashes::{hash160, Hash};
|
use bitcoin::hashes::{hash160, Hash};
|
||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, Fingerprint};
|
use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, Fingerprint};
|
||||||
use bitcoin::{PrivateKey, PublicKey};
|
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
|
||||||
|
use bitcoin::{PrivateKey, PublicKey, Script};
|
||||||
|
|
||||||
pub use miniscript::descriptor::Descriptor;
|
pub use miniscript::{descriptor::Descriptor, Miniscript};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::psbt::utils::PSBTUtils;
|
||||||
|
|
||||||
|
pub mod checksum;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod extended_key;
|
pub mod extended_key;
|
||||||
pub mod policy;
|
pub mod policy;
|
||||||
|
|
||||||
|
pub use self::checksum::get_checksum;
|
||||||
pub use self::error::Error;
|
pub use self::error::Error;
|
||||||
pub use self::extended_key::{DerivationIndex, DescriptorExtendedKey};
|
pub use self::extended_key::{DerivationIndex, DescriptorExtendedKey};
|
||||||
pub use self::policy::{ExtractPolicy, Policy};
|
pub use self::policy::Policy;
|
||||||
|
|
||||||
trait MiniscriptExtractPolicy {
|
trait MiniscriptExtractPolicy {
|
||||||
fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy>;
|
fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ExtractPolicy {
|
||||||
|
fn extract_policy(&self) -> Option<Policy>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Default)]
|
#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Default)]
|
||||||
struct DummyKey();
|
struct DummyKey();
|
||||||
|
|
||||||
@ -78,11 +86,7 @@ where
|
|||||||
|
|
||||||
fn psbt_redeem_script(&self) -> Option<Script> {
|
fn psbt_redeem_script(&self) -> Option<Script> {
|
||||||
match self {
|
match self {
|
||||||
Descriptor::ShWpkh(ref pk) => {
|
Descriptor::ShWpkh(_) => Some(self.witness_script()),
|
||||||
let addr =
|
|
||||||
bitcoin::Address::p2shwpkh(&pk.to_public_key(), bitcoin::Network::Bitcoin);
|
|
||||||
Some(addr.script_pubkey())
|
|
||||||
}
|
|
||||||
Descriptor::ShWsh(ref script) => Some(script.encode().to_v0_p2wsh()),
|
Descriptor::ShWsh(ref script) => Some(script.encode().to_v0_p2wsh()),
|
||||||
Descriptor::Sh(ref script) => Some(script.encode()),
|
Descriptor::Sh(ref script) => Some(script.encode()),
|
||||||
_ => None,
|
_ => None,
|
||||||
@ -105,6 +109,10 @@ trait Key: std::fmt::Debug {
|
|||||||
fn xprv(&self) -> Option<ExtendedPrivKey>;
|
fn xprv(&self) -> Option<ExtendedPrivKey>;
|
||||||
fn full_path(&self, index: u32) -> Option<DerivationPath>;
|
fn full_path(&self, index: u32) -> Option<DerivationPath>;
|
||||||
fn is_fixed(&self) -> bool;
|
fn is_fixed(&self) -> bool;
|
||||||
|
|
||||||
|
fn has_secret(&self) -> bool {
|
||||||
|
self.xprv().is_some() || self.as_secret_key().is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Key for PublicKey {
|
impl Key for PublicKey {
|
||||||
@ -169,7 +177,11 @@ impl Key for PrivateKey {
|
|||||||
|
|
||||||
impl Key for DescriptorExtendedKey {
|
impl Key for DescriptorExtendedKey {
|
||||||
fn fingerprint(&self, secp: &Secp256k1<All>) -> Option<Fingerprint> {
|
fn fingerprint(&self, secp: &Secp256k1<All>) -> Option<Fingerprint> {
|
||||||
Some(self.root_xpub(secp).fingerprint())
|
if let Some(fing) = self.master_fingerprint {
|
||||||
|
Some(fing.clone())
|
||||||
|
} else {
|
||||||
|
Some(self.root_xpub(secp).fingerprint())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_public_key(&self, secp: &Secp256k1<All>, index: Option<u32>) -> Result<PublicKey, Error> {
|
fn as_public_key(&self, secp: &Secp256k1<All>, index: Option<u32>) -> Result<PublicKey, Error> {
|
||||||
@ -255,6 +267,62 @@ impl ExtendedDescriptor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn derive_with_miniscript(
|
||||||
|
&self,
|
||||||
|
miniscript: Miniscript<PublicKey>,
|
||||||
|
) -> Result<DerivedDescriptor, Error> {
|
||||||
|
// TODO: make sure they are "equivalent"
|
||||||
|
match self.internal {
|
||||||
|
Descriptor::Bare(_) => Ok(Descriptor::Bare(miniscript)),
|
||||||
|
Descriptor::Sh(_) => Ok(Descriptor::Sh(miniscript)),
|
||||||
|
Descriptor::Wsh(_) => Ok(Descriptor::Wsh(miniscript)),
|
||||||
|
Descriptor::ShWsh(_) => Ok(Descriptor::ShWsh(miniscript)),
|
||||||
|
_ => Err(Error::CantDeriveWithMiniscript),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn derive_from_psbt_input(
|
||||||
|
&self,
|
||||||
|
psbt: &PSBT,
|
||||||
|
input_index: usize,
|
||||||
|
) -> Result<DerivedDescriptor, Error> {
|
||||||
|
let get_pk_from_partial_sigs = || {
|
||||||
|
// here we need the public key.. since it's a single sig, there are only two
|
||||||
|
// options: we can either find it in the `partial_sigs`, or we can't. if we
|
||||||
|
// can't, it means that we can't even satisfy the input, so we can exit knowing
|
||||||
|
// that we did our best to try to find it.
|
||||||
|
psbt.inputs[input_index]
|
||||||
|
.partial_sigs
|
||||||
|
.keys()
|
||||||
|
.nth(0)
|
||||||
|
.ok_or(Error::MissingPublicKey)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(wit_script) = &psbt.inputs[input_index].witness_script {
|
||||||
|
self.derive_with_miniscript(Miniscript::parse(wit_script)?)
|
||||||
|
} else if let Some(p2sh_script) = &psbt.inputs[input_index].redeem_script {
|
||||||
|
if p2sh_script.is_v0_p2wpkh() {
|
||||||
|
// wrapped p2wpkh
|
||||||
|
get_pk_from_partial_sigs().map(|pk| Descriptor::ShWpkh(*pk))
|
||||||
|
} else {
|
||||||
|
self.derive_with_miniscript(Miniscript::parse(p2sh_script)?)
|
||||||
|
}
|
||||||
|
} else if let Some(utxo) = psbt.get_utxo_for(input_index) {
|
||||||
|
if utxo.script_pubkey.is_p2pkh() {
|
||||||
|
get_pk_from_partial_sigs().map(|pk| Descriptor::Pkh(*pk))
|
||||||
|
} else if utxo.script_pubkey.is_p2pk() {
|
||||||
|
get_pk_from_partial_sigs().map(|pk| Descriptor::Pk(*pk))
|
||||||
|
} else if utxo.script_pubkey.is_v0_p2wpkh() {
|
||||||
|
get_pk_from_partial_sigs().map(|pk| Descriptor::Wpkh(*pk))
|
||||||
|
} else {
|
||||||
|
// try as bare script
|
||||||
|
self.derive_with_miniscript(Miniscript::parse(&utxo.script_pubkey)?)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Error::MissingDetails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn derive(&self, index: u32) -> Result<DerivedDescriptor, Error> {
|
pub fn derive(&self, index: u32) -> Result<DerivedDescriptor, Error> {
|
||||||
let translatefpk = |xpub: &String| {
|
let translatefpk = |xpub: &String| {
|
||||||
self.keys
|
self.keys
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use bitcoin::hashes::*;
|
use bitcoin::hashes::*;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use bitcoin::util::bip32::Fingerprint;
|
use bitcoin::util::bip32::Fingerprint;
|
||||||
|
use bitcoin::util::psbt;
|
||||||
use bitcoin::PublicKey;
|
use bitcoin::PublicKey;
|
||||||
|
|
||||||
use miniscript::{Descriptor, Miniscript, Terminal};
|
use miniscript::{Descriptor, Miniscript, Terminal};
|
||||||
@ -23,6 +24,7 @@ impl PKOrF {
|
|||||||
fn from_key(k: &Box<dyn Key>) -> Self {
|
fn from_key(k: &Box<dyn Key>) -> Self {
|
||||||
let secp = Secp256k1::gen_new();
|
let secp = Secp256k1::gen_new();
|
||||||
|
|
||||||
|
let pubkey = k.as_public_key(&secp, None).unwrap();
|
||||||
if let Some(fing) = k.fingerprint(&secp) {
|
if let Some(fing) = k.fingerprint(&secp) {
|
||||||
PKOrF {
|
PKOrF {
|
||||||
fingerprint: Some(fing),
|
fingerprint: Some(fing),
|
||||||
@ -31,7 +33,7 @@ impl PKOrF {
|
|||||||
} else {
|
} else {
|
||||||
PKOrF {
|
PKOrF {
|
||||||
fingerprint: None,
|
fingerprint: None,
|
||||||
pubkey: Some(k.as_public_key(&secp, None).unwrap()),
|
pubkey: Some(pubkey),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,10 +63,10 @@ pub enum SatisfiableItem {
|
|||||||
hash: hash160::Hash,
|
hash: hash160::Hash,
|
||||||
},
|
},
|
||||||
AbsoluteTimelock {
|
AbsoluteTimelock {
|
||||||
height: u32,
|
value: u32,
|
||||||
},
|
},
|
||||||
RelativeTimelock {
|
RelativeTimelock {
|
||||||
blocks: u32,
|
value: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Complex item
|
// Complex item
|
||||||
@ -88,26 +90,107 @@ impl SatisfiableItem {
|
|||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn satisfy(&self, _input: &psbt::Input) -> Satisfaction {
|
||||||
|
Satisfaction::None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub enum ItemSatisfier {
|
#[serde(tag = "type", rename_all = "UPPERCASE")]
|
||||||
Us,
|
pub enum Satisfaction {
|
||||||
Other(Option<Fingerprint>),
|
Complete {
|
||||||
Timelock(Option<u32>), // remaining blocks. TODO: time-based timelocks
|
#[serde(skip_serializing_if = "PathRequirements::is_null")]
|
||||||
|
condition: PathRequirements,
|
||||||
|
},
|
||||||
|
Partial {
|
||||||
|
m: usize,
|
||||||
|
n: usize,
|
||||||
|
completed: HashSet<usize>,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Satisfaction {
|
||||||
|
fn from_items_threshold(items: HashSet<usize>, threshold: usize) -> Satisfaction {
|
||||||
|
Satisfaction::Partial {
|
||||||
|
m: items.len(),
|
||||||
|
n: threshold,
|
||||||
|
completed: items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> std::ops::Add<&'a Satisfaction> for Satisfaction {
|
||||||
|
type Output = Satisfaction;
|
||||||
|
|
||||||
|
fn add(self, other: &'a Satisfaction) -> Satisfaction {
|
||||||
|
&self + other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> std::ops::Add<&'b Satisfaction> for &'a Satisfaction {
|
||||||
|
type Output = Satisfaction;
|
||||||
|
|
||||||
|
fn add(self, other: &'b Satisfaction) -> Satisfaction {
|
||||||
|
match (self, other) {
|
||||||
|
// complete-complete
|
||||||
|
(
|
||||||
|
Satisfaction::Complete { condition: mut a },
|
||||||
|
Satisfaction::Complete { condition: b },
|
||||||
|
) => {
|
||||||
|
a.merge(&b).unwrap();
|
||||||
|
Satisfaction::Complete { condition: a }
|
||||||
|
}
|
||||||
|
// complete-<any>
|
||||||
|
(Satisfaction::Complete { condition }, _) => Satisfaction::Complete {
|
||||||
|
condition: *condition,
|
||||||
|
},
|
||||||
|
(_, Satisfaction::Complete { condition }) => Satisfaction::Complete {
|
||||||
|
condition: *condition,
|
||||||
|
},
|
||||||
|
|
||||||
|
// none-<any>
|
||||||
|
(Satisfaction::None, any) => any.clone(),
|
||||||
|
(any, Satisfaction::None) => any.clone(),
|
||||||
|
|
||||||
|
// partial-partial
|
||||||
|
(
|
||||||
|
Satisfaction::Partial {
|
||||||
|
m: _,
|
||||||
|
n: a_n,
|
||||||
|
completed: a_items,
|
||||||
|
},
|
||||||
|
Satisfaction::Partial {
|
||||||
|
m: _,
|
||||||
|
n: _,
|
||||||
|
completed: b_items,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let union: HashSet<_> = a_items.union(&b_items).cloned().collect();
|
||||||
|
Satisfaction::Partial {
|
||||||
|
m: union.len(),
|
||||||
|
n: *a_n,
|
||||||
|
completed: union,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct Policy {
|
pub struct Policy {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
item: SatisfiableItem,
|
item: SatisfiableItem,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
satisfaction: Satisfaction,
|
||||||
satisfier: Option<ItemSatisfier>,
|
contribution: Satisfaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, Serialize)]
|
||||||
pub struct PathRequirements {
|
pub struct PathRequirements {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub csv: Option<u32>,
|
pub csv: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub timelock: Option<u32>,
|
pub timelock: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +209,8 @@ impl PathRequirements {
|
|||||||
}?;
|
}?;
|
||||||
|
|
||||||
match (self.timelock, other.timelock) {
|
match (self.timelock, other.timelock) {
|
||||||
|
// TODO: we could actually set the timelock to the highest of the two, but we would
|
||||||
|
// have to first check that they are both in the same "unit" (blocks vs time)
|
||||||
(Some(old), Some(new)) if old != new => Err(PolicyError::DifferentTimelock(old, new)),
|
(Some(old), Some(new)) if old != new => Err(PolicyError::DifferentTimelock(old, new)),
|
||||||
_ => {
|
_ => {
|
||||||
self.timelock = self.timelock.or(other.timelock);
|
self.timelock = self.timelock.or(other.timelock);
|
||||||
@ -154,7 +239,8 @@ impl Policy {
|
|||||||
pub fn new(item: SatisfiableItem) -> Self {
|
pub fn new(item: SatisfiableItem) -> Self {
|
||||||
Policy {
|
Policy {
|
||||||
item,
|
item,
|
||||||
satisfier: None,
|
satisfaction: Satisfaction::None,
|
||||||
|
contribution: Satisfaction::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,13 +248,7 @@ impl Policy {
|
|||||||
match (a, b) {
|
match (a, b) {
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(Some(x), None) | (None, Some(x)) => Some(x),
|
(Some(x), None) | (None, Some(x)) => Some(x),
|
||||||
(Some(a), Some(b)) => Some(
|
(Some(a), Some(b)) => Self::make_thresh(vec![a, b], 2),
|
||||||
SatisfiableItem::Thresh {
|
|
||||||
items: vec![a, b],
|
|
||||||
threshold: 2,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,33 +256,49 @@ impl Policy {
|
|||||||
match (a, b) {
|
match (a, b) {
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(Some(x), None) | (None, Some(x)) => Some(x),
|
(Some(x), None) | (None, Some(x)) => Some(x),
|
||||||
(Some(a), Some(b)) => Some(
|
(Some(a), Some(b)) => Self::make_thresh(vec![a, b], 1),
|
||||||
SatisfiableItem::Thresh {
|
|
||||||
items: vec![a, b],
|
|
||||||
threshold: 1,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_thresh(items: Vec<Policy>, mut threshold: usize) -> Option<Policy> {
|
pub fn make_thresh(items: Vec<Policy>, threshold: usize) -> Option<Policy> {
|
||||||
if threshold == 0 {
|
if threshold == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if threshold > items.len() {
|
|
||||||
threshold = items.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(SatisfiableItem::Thresh { items, threshold }.into())
|
let contribution = items.iter().fold(
|
||||||
|
Satisfaction::Partial {
|
||||||
|
m: 0,
|
||||||
|
n: threshold,
|
||||||
|
completed: HashSet::new(),
|
||||||
|
},
|
||||||
|
|acc, x| acc + &x.contribution,
|
||||||
|
);
|
||||||
|
let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into();
|
||||||
|
policy.contribution = contribution;
|
||||||
|
|
||||||
|
Some(policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_multisig(pubkeys: Vec<Option<&Box<dyn Key>>>, threshold: usize) -> Option<Policy> {
|
fn make_multisig(keys: Vec<Option<&Box<dyn Key>>>, threshold: usize) -> Option<Policy> {
|
||||||
let keys = pubkeys
|
let parsed_keys = keys.iter().map(|k| PKOrF::from_key(k.unwrap())).collect();
|
||||||
.into_iter()
|
let mut policy: Policy = SatisfiableItem::Multisig {
|
||||||
.map(|k| PKOrF::from_key(k.unwrap()))
|
keys: parsed_keys,
|
||||||
|
threshold,
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
let our_keys = keys
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, x)| x.is_some() && x.unwrap().has_secret())
|
||||||
|
.map(|(k, _)| k)
|
||||||
.collect();
|
.collect();
|
||||||
Some(SatisfiableItem::Multisig { keys, threshold }.into())
|
policy.contribution = Satisfaction::from_items_threshold(our_keys, threshold);
|
||||||
|
|
||||||
|
Some(policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn satisfy(&mut self, input: &psbt::Input) {
|
||||||
|
self.satisfaction = self.item.satisfy(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requires_path(&self) -> bool {
|
pub fn requires_path(&self) -> bool {
|
||||||
@ -267,12 +363,12 @@ impl Policy {
|
|||||||
Ok(requirements)
|
Ok(requirements)
|
||||||
}
|
}
|
||||||
_ if !selected.is_empty() => Err(PolicyError::TooManyItemsSelected(index)),
|
_ if !selected.is_empty() => Err(PolicyError::TooManyItemsSelected(index)),
|
||||||
SatisfiableItem::AbsoluteTimelock { height } => Ok(PathRequirements {
|
SatisfiableItem::AbsoluteTimelock { value } => Ok(PathRequirements {
|
||||||
csv: None,
|
csv: None,
|
||||||
timelock: Some(*height),
|
timelock: Some(*value),
|
||||||
}),
|
}),
|
||||||
SatisfiableItem::RelativeTimelock { blocks } => Ok(PathRequirements {
|
SatisfiableItem::RelativeTimelock { value } => Ok(PathRequirements {
|
||||||
csv: Some(*blocks),
|
csv: Some(*value),
|
||||||
timelock: None,
|
timelock: None,
|
||||||
}),
|
}),
|
||||||
_ => Ok(PathRequirements::default()),
|
_ => Ok(PathRequirements::default()),
|
||||||
@ -286,19 +382,27 @@ impl From<SatisfiableItem> for Policy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ExtractPolicy {
|
|
||||||
fn extract_policy(&self) -> Option<Policy>;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signature_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
|
fn signature_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
|
||||||
key.map(|k| SatisfiableItem::Signature(PKOrF::from_key(k)).into())
|
key.map(|k| {
|
||||||
|
let mut policy: Policy = SatisfiableItem::Signature(PKOrF::from_key(k)).into();
|
||||||
|
policy.contribution = if k.has_secret() {
|
||||||
|
Satisfaction::Complete {
|
||||||
|
condition: Default::default(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Satisfaction::None
|
||||||
|
};
|
||||||
|
|
||||||
|
policy
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature_key_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
|
fn signature_key_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
|
||||||
let secp = Secp256k1::gen_new();
|
let secp = Secp256k1::gen_new();
|
||||||
|
|
||||||
key.map(|k| {
|
key.map(|k| {
|
||||||
if let Some(fing) = k.fingerprint(&secp) {
|
let pubkey = k.as_public_key(&secp, None).unwrap();
|
||||||
|
let mut policy: Policy = if let Some(fing) = k.fingerprint(&secp) {
|
||||||
SatisfiableItem::SignatureKey {
|
SatisfiableItem::SignatureKey {
|
||||||
fingerprint: Some(fing),
|
fingerprint: Some(fing),
|
||||||
pubkey_hash: None,
|
pubkey_hash: None,
|
||||||
@ -306,12 +410,19 @@ fn signature_key_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
|
|||||||
} else {
|
} else {
|
||||||
SatisfiableItem::SignatureKey {
|
SatisfiableItem::SignatureKey {
|
||||||
fingerprint: None,
|
fingerprint: None,
|
||||||
pubkey_hash: Some(hash160::Hash::hash(
|
pubkey_hash: Some(hash160::Hash::hash(&pubkey.to_bytes())),
|
||||||
&k.as_public_key(&secp, None).unwrap().to_bytes(),
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.into()
|
.into();
|
||||||
|
policy.contribution = if k.has_secret() {
|
||||||
|
Satisfaction::Complete {
|
||||||
|
condition: Default::default(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Satisfaction::None
|
||||||
|
};
|
||||||
|
|
||||||
|
policy
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,11 +433,27 @@ impl MiniscriptExtractPolicy for Miniscript<String> {
|
|||||||
Terminal::True | Terminal::False => None,
|
Terminal::True | Terminal::False => None,
|
||||||
Terminal::Pk(pubkey) => signature_from_string(lookup_map.get(pubkey)),
|
Terminal::Pk(pubkey) => signature_from_string(lookup_map.get(pubkey)),
|
||||||
Terminal::PkH(pubkey_hash) => signature_key_from_string(lookup_map.get(pubkey_hash)),
|
Terminal::PkH(pubkey_hash) => signature_key_from_string(lookup_map.get(pubkey_hash)),
|
||||||
Terminal::After(height) => {
|
Terminal::After(value) => {
|
||||||
Some(SatisfiableItem::AbsoluteTimelock { height: *height }.into())
|
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
|
||||||
|
policy.contribution = Satisfaction::Complete {
|
||||||
|
condition: PathRequirements {
|
||||||
|
csv: None,
|
||||||
|
timelock: Some(*value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(policy)
|
||||||
}
|
}
|
||||||
Terminal::Older(blocks) => {
|
Terminal::Older(value) => {
|
||||||
Some(SatisfiableItem::RelativeTimelock { blocks: *blocks }.into())
|
let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into();
|
||||||
|
policy.contribution = Satisfaction::Complete {
|
||||||
|
condition: PathRequirements {
|
||||||
|
csv: Some(*value),
|
||||||
|
timelock: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(policy)
|
||||||
}
|
}
|
||||||
Terminal::Sha256(hash) => Some(SatisfiableItem::SHA256Preimage { hash: *hash }.into()),
|
Terminal::Sha256(hash) => Some(SatisfiableItem::SHA256Preimage { hash: *hash }.into()),
|
||||||
Terminal::Hash256(hash) => {
|
Terminal::Hash256(hash) => {
|
||||||
@ -338,6 +465,9 @@ impl MiniscriptExtractPolicy for Miniscript<String> {
|
|||||||
Terminal::Hash160(hash) => {
|
Terminal::Hash160(hash) => {
|
||||||
Some(SatisfiableItem::HASH160Preimage { hash: *hash }.into())
|
Some(SatisfiableItem::HASH160Preimage { hash: *hash }.into())
|
||||||
}
|
}
|
||||||
|
Terminal::ThreshM(k, pks) => {
|
||||||
|
Policy::make_multisig(pks.iter().map(|s| lookup_map.get(s)).collect(), *k)
|
||||||
|
}
|
||||||
// Identities
|
// Identities
|
||||||
Terminal::Alt(inner)
|
Terminal::Alt(inner)
|
||||||
| Terminal::Swap(inner)
|
| Terminal::Swap(inner)
|
||||||
@ -376,9 +506,6 @@ impl MiniscriptExtractPolicy for Miniscript<String> {
|
|||||||
|
|
||||||
Policy::make_thresh(mapped, threshold)
|
Policy::make_thresh(mapped, threshold)
|
||||||
}
|
}
|
||||||
Terminal::ThreshM(k, pks) => {
|
|
||||||
Policy::make_multisig(pks.iter().map(|s| lookup_map.get(s)).collect(), *k)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ pub enum Error {
|
|||||||
UnknownUTXO,
|
UnknownUTXO,
|
||||||
DifferentTransactions,
|
DifferentTransactions,
|
||||||
|
|
||||||
|
ChecksumMismatch,
|
||||||
|
|
||||||
SpendingPolicyRequired,
|
SpendingPolicyRequired,
|
||||||
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
|
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ use crate::descriptor::ExtendedDescriptor;
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::signer::Signer;
|
use crate::signer::Signer;
|
||||||
|
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
pub struct PSBTSatisfier<'a> {
|
pub struct PSBTSatisfier<'a> {
|
||||||
input: &'a psbt::Input,
|
input: &'a psbt::Input,
|
||||||
create_height: Option<u32>,
|
create_height: Option<u32>,
|
||||||
@ -99,7 +101,7 @@ impl<'a> Satisfier<bitcoin::PublicKey> for PSBTSatisfier<'a> {
|
|||||||
|
|
||||||
fn check_after(&self, height: u32) -> bool {
|
fn check_after(&self, height: u32) -> bool {
|
||||||
// TODO: also check if `nLockTime` is right
|
// TODO: also check if `nLockTime` is right
|
||||||
debug!("check_older: {}", height);
|
debug!("check_after: {}", height);
|
||||||
|
|
||||||
self.current_height.unwrap_or(0) > height
|
self.current_height.unwrap_or(0) > height
|
||||||
}
|
}
|
28
src/psbt/utils.rs
Normal file
28
src/psbt/utils.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
|
||||||
|
use bitcoin::TxOut;
|
||||||
|
|
||||||
|
pub trait PSBTUtils {
|
||||||
|
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PSBTUtils for PSBT {
|
||||||
|
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||||
|
let tx = &self.global.unsigned_tx;
|
||||||
|
|
||||||
|
if input_index >= tx.input.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(input) = self.inputs.get(input_index) {
|
||||||
|
if let Some(wit_utxo) = &input.witness_utxo {
|
||||||
|
Some(wit_utxo.clone())
|
||||||
|
} else if let Some(in_tx) = &input.non_witness_utxo {
|
||||||
|
Some(in_tx.output[tx.input[input_index].previous_output.vout as usize].clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ use std::cmp;
|
|||||||
use std::collections::{BTreeMap, HashSet, VecDeque};
|
use std::collections::{BTreeMap, HashSet, VecDeque};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
use std::str::FromStr;
|
||||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use bitcoin::blockdata::opcodes;
|
use bitcoin::blockdata::opcodes;
|
||||||
@ -25,11 +26,9 @@ pub mod utils;
|
|||||||
|
|
||||||
use self::utils::{ChunksIterator, IsDust};
|
use self::utils::{ChunksIterator, IsDust};
|
||||||
use crate::database::{BatchDatabase, BatchOperations};
|
use crate::database::{BatchDatabase, BatchOperations};
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{get_checksum, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, Policy};
|
||||||
DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, Policy,
|
|
||||||
};
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::psbt::{PSBTSatisfier, PSBTSigner};
|
use crate::psbt::{utils::PSBTUtils, PSBTSatisfier, PSBTSigner};
|
||||||
use crate::signer::Signer;
|
use crate::signer::Signer;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ use electrum_client::Client;
|
|||||||
#[cfg(not(any(feature = "electrum", feature = "default")))]
|
#[cfg(not(any(feature = "electrum", feature = "default")))]
|
||||||
use std::marker::PhantomData as Client;
|
use std::marker::PhantomData as Client;
|
||||||
|
|
||||||
// TODO: force descriptor and change_descriptor to have the same policies?
|
|
||||||
pub struct Wallet<S: Read + Write, D: BatchDatabase> {
|
pub struct Wallet<S: Read + Write, D: BatchDatabase> {
|
||||||
descriptor: ExtendedDescriptor,
|
descriptor: ExtendedDescriptor,
|
||||||
change_descriptor: Option<ExtendedDescriptor>,
|
change_descriptor: Option<ExtendedDescriptor>,
|
||||||
@ -58,12 +56,30 @@ where
|
|||||||
D: BatchDatabase,
|
D: BatchDatabase,
|
||||||
{
|
{
|
||||||
pub fn new_offline(
|
pub fn new_offline(
|
||||||
descriptor: ExtendedDescriptor,
|
descriptor: &str,
|
||||||
change_descriptor: Option<ExtendedDescriptor>,
|
change_descriptor: Option<&str>,
|
||||||
network: Network,
|
network: Network,
|
||||||
database: D,
|
mut database: D,
|
||||||
) -> Self {
|
) -> Result<Self, Error> {
|
||||||
Wallet {
|
database.check_descriptor_checksum(
|
||||||
|
ScriptType::External,
|
||||||
|
get_checksum(descriptor)?.as_bytes(),
|
||||||
|
)?;
|
||||||
|
let descriptor = ExtendedDescriptor::from_str(descriptor)?;
|
||||||
|
let change_descriptor = match change_descriptor {
|
||||||
|
Some(desc) => {
|
||||||
|
database.check_descriptor_checksum(
|
||||||
|
ScriptType::Internal,
|
||||||
|
get_checksum(desc)?.as_bytes(),
|
||||||
|
)?;
|
||||||
|
Some(ExtendedDescriptor::from_str(desc)?)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: make sure that both descriptor have the same structure
|
||||||
|
|
||||||
|
Ok(Wallet {
|
||||||
descriptor,
|
descriptor,
|
||||||
change_descriptor,
|
change_descriptor,
|
||||||
network,
|
network,
|
||||||
@ -71,7 +87,7 @@ where
|
|||||||
client: None,
|
client: None,
|
||||||
database: RefCell::new(database),
|
database: RefCell::new(database),
|
||||||
_secp: Secp256k1::gen_new(),
|
_secp: Secp256k1::gen_new(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_new_address(&self) -> Result<Address, Error> {
|
pub fn get_new_address(&self) -> Result<Address, Error> {
|
||||||
@ -116,7 +132,6 @@ where
|
|||||||
utxos: Option<Vec<OutPoint>>,
|
utxos: Option<Vec<OutPoint>>,
|
||||||
unspendable: Option<Vec<OutPoint>>,
|
unspendable: Option<Vec<OutPoint>>,
|
||||||
) -> Result<(PSBT, TransactionDetails), Error> {
|
) -> Result<(PSBT, TransactionDetails), Error> {
|
||||||
// TODO: run before deriving the descriptor
|
|
||||||
let policy = self.descriptor.extract_policy().unwrap();
|
let policy = self.descriptor.extract_policy().unwrap();
|
||||||
if policy.requires_path() && policy_path.is_none() {
|
if policy.requires_path() && policy_path.is_none() {
|
||||||
return Err(Error::SpendingPolicyRequired);
|
return Err(Error::SpendingPolicyRequired);
|
||||||
@ -287,22 +302,14 @@ where
|
|||||||
|
|
||||||
// TODO: define an enum for signing errors
|
// TODO: define an enum for signing errors
|
||||||
pub fn sign(&self, mut psbt: PSBT) -> Result<(PSBT, bool), Error> {
|
pub fn sign(&self, mut psbt: PSBT) -> Result<(PSBT, bool), Error> {
|
||||||
let mut derived_descriptors = BTreeMap::new();
|
|
||||||
|
|
||||||
let tx = &psbt.global.unsigned_tx;
|
let tx = &psbt.global.unsigned_tx;
|
||||||
|
let mut input_utxos = Vec::with_capacity(psbt.inputs.len());
|
||||||
|
for n in 0..psbt.inputs.len() {
|
||||||
|
input_utxos.push(psbt.get_utxo_for(n).clone());
|
||||||
|
}
|
||||||
|
|
||||||
// try to add hd_keypaths if we've already seen the output
|
// try to add hd_keypaths if we've already seen the output
|
||||||
for (n, psbt_input) in psbt.inputs.iter_mut().enumerate() {
|
for (psbt_input, out) in psbt.inputs.iter_mut().zip(input_utxos.iter()) {
|
||||||
let out = match (&psbt_input.witness_utxo, &psbt_input.non_witness_utxo) {
|
|
||||||
(Some(wit_out), _) => Some(wit_out),
|
|
||||||
(_, Some(in_tx))
|
|
||||||
if (tx.input[n].previous_output.vout as usize) < in_tx.output.len() =>
|
|
||||||
{
|
|
||||||
Some(&in_tx.output[tx.input[n].previous_output.vout as usize])
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("searching hd_keypaths for out: {:?}", out);
|
debug!("searching hd_keypaths for out: {:?}", out);
|
||||||
|
|
||||||
if let Some(out) = out {
|
if let Some(out) = out {
|
||||||
@ -325,11 +332,8 @@ where
|
|||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let desc = self.get_descriptor_for(script_type);
|
|
||||||
let derived_descriptor = desc.derive(index)?;
|
|
||||||
derived_descriptors.insert(n, derived_descriptor);
|
|
||||||
|
|
||||||
// merge hd_keypaths
|
// merge hd_keypaths
|
||||||
|
let desc = self.get_descriptor_for(script_type);
|
||||||
let mut hd_keypaths = desc.get_hd_keypaths(index)?;
|
let mut hd_keypaths = desc.get_hd_keypaths(index)?;
|
||||||
psbt_input.hd_keypaths.append(&mut hd_keypaths);
|
psbt_input.hd_keypaths.append(&mut hd_keypaths);
|
||||||
}
|
}
|
||||||
@ -469,7 +473,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// attempt to finalize
|
// attempt to finalize
|
||||||
let finalized = self.finalize_psbt(tx.clone(), &mut psbt, derived_descriptors);
|
let finalized = self.finalize_psbt(tx.clone(), &mut psbt);
|
||||||
|
|
||||||
Ok((psbt, finalized))
|
Ok((psbt, finalized))
|
||||||
}
|
}
|
||||||
@ -626,18 +630,16 @@ where
|
|||||||
Ok((answer, paths, selected_amount, fee_val))
|
Ok((answer, paths, selected_amount, fee_val))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finalize_psbt(
|
fn finalize_psbt(&self, mut tx: Transaction, psbt: &mut PSBT) -> bool {
|
||||||
&self,
|
|
||||||
mut tx: Transaction,
|
|
||||||
psbt: &mut PSBT,
|
|
||||||
derived_descriptors: BTreeMap<usize, DerivedDescriptor>,
|
|
||||||
) -> bool {
|
|
||||||
for (n, input) in tx.input.iter_mut().enumerate() {
|
for (n, input) in tx.input.iter_mut().enumerate() {
|
||||||
debug!("getting descriptor for {}", n);
|
// safe to run only on the descriptor because we assume the change descriptor also has
|
||||||
|
// the same structure
|
||||||
|
let desc = self.descriptor.derive_from_psbt_input(psbt, n);
|
||||||
|
debug!("reconstructed descriptor is {:?}", desc);
|
||||||
|
|
||||||
let desc = match derived_descriptors.get(&n) {
|
let desc = match desc {
|
||||||
None => return false,
|
Err(_) => return false,
|
||||||
Some(desc) => desc,
|
Ok(desc) => desc,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: use height once we sync headers
|
// TODO: use height once we sync headers
|
||||||
@ -669,13 +671,31 @@ where
|
|||||||
D: BatchDatabase,
|
D: BatchDatabase,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
descriptor: ExtendedDescriptor,
|
descriptor: &str,
|
||||||
change_descriptor: Option<ExtendedDescriptor>,
|
change_descriptor: Option<&str>,
|
||||||
network: Network,
|
network: Network,
|
||||||
database: D,
|
mut database: D,
|
||||||
client: Client<S>,
|
client: Client<S>,
|
||||||
) -> Self {
|
) -> Result<Self, Error> {
|
||||||
Wallet {
|
database.check_descriptor_checksum(
|
||||||
|
ScriptType::External,
|
||||||
|
get_checksum(descriptor)?.as_bytes(),
|
||||||
|
)?;
|
||||||
|
let descriptor = ExtendedDescriptor::from_str(descriptor)?;
|
||||||
|
let change_descriptor = match change_descriptor {
|
||||||
|
Some(desc) => {
|
||||||
|
database.check_descriptor_checksum(
|
||||||
|
ScriptType::Internal,
|
||||||
|
get_checksum(desc)?.as_bytes(),
|
||||||
|
)?;
|
||||||
|
Some(ExtendedDescriptor::from_str(desc)?)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: make sure that both descriptor have the same structure
|
||||||
|
|
||||||
|
Ok(Wallet {
|
||||||
descriptor,
|
descriptor,
|
||||||
change_descriptor,
|
change_descriptor,
|
||||||
network,
|
network,
|
||||||
@ -683,7 +703,7 @@ where
|
|||||||
client: Some(RefCell::new(client)),
|
client: Some(RefCell::new(client)),
|
||||||
database: RefCell::new(database),
|
database: RefCell::new(database),
|
||||||
_secp: Secp256k1::gen_new(),
|
_secp: Secp256k1::gen_new(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_previous_output(&self, outpoint: &OutPoint) -> Option<TxOut> {
|
fn get_previous_output(&self, outpoint: &OutPoint) -> Option<TxOut> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user