Browse Source
490: Depend on cfd protocol from `maia` repository r=klochowicz a=klochowicz Cfd protocol got moved into a separate repository. All references of `cfd_protocol` were renamed to `maia`. Patch cargo.toml with a fixed git revision until it gets a public release. Co-authored-by: Mariusz Klochowicz <mariusz@klochowicz.com>rollover-test
bors[bot]
3 years ago
committed by
GitHub
27 changed files with 47 additions and 3191 deletions
@ -1,8 +1,9 @@ |
|||||
[workspace] |
[workspace] |
||||
members = ["cfd_protocol", "daemon", "xtra_productivity"] |
members = ["daemon", "xtra_productivity"] |
||||
resolver = "2" |
resolver = "2" |
||||
|
|
||||
[patch.crates-io] |
[patch.crates-io] |
||||
rocket = { git = "https://github.com/SergioBenitez/Rocket" } # Need to patch rocket dependency of `rocket_basicauth` until there is an official release. |
rocket = { git = "https://github.com/SergioBenitez/Rocket" } # Need to patch rocket dependency of `rocket_basicauth` until there is an official release. |
||||
xtra = { git = "https://github.com/Restioson/xtra" } # Latest master has crucial patches. |
xtra = { git = "https://github.com/Restioson/xtra" } # Latest master has crucial patches. |
||||
secp256k1-zkp = { git = "https://github.com/ElementsProject/rust-secp256k1-zkp" } # Latest master has crucial patches. |
secp256k1-zkp = { git = "https://github.com/ElementsProject/rust-secp256k1-zkp" } # Latest master has crucial patches. |
||||
|
maia = { git = "https://github.com/comit-network/maia", rev = "70fc548da0fe4f34478fb34ec437fa9a434c7ee3" } |
||||
|
@ -1,17 +0,0 @@ |
|||||
[package] |
|
||||
name = "cfd_protocol" |
|
||||
version = "0.1.0" |
|
||||
edition = "2018" |
|
||||
|
|
||||
[dependencies] |
|
||||
anyhow = "1" |
|
||||
bdk = { version = "0.13", default-features = false } |
|
||||
bit-vec = "0.6" |
|
||||
itertools = "0.10" |
|
||||
rand = "0.6" |
|
||||
secp256k1-zkp = { version = "0.4", features = ["bitcoin_hashes", "global-context", "serde"] } |
|
||||
thiserror = "1" |
|
||||
|
|
||||
[dev-dependencies] |
|
||||
bitcoin = { version = "0.27", features = ["rand", "bitcoinconsensus"] } |
|
||||
proptest = { version = "1", default-features = false, features = ["std"] } |
|
@ -1,157 +0,0 @@ |
|||||
use bit_vec::BitVec; |
|
||||
use std::fmt::Display; |
|
||||
use std::num::NonZeroU8; |
|
||||
use std::ops::RangeInclusive; |
|
||||
|
|
||||
mod digit_decomposition; |
|
||||
|
|
||||
/// Maximum supported BTC price in whole USD.
|
|
||||
pub const MAX_PRICE_DEC: u64 = (BASE as u64).pow(MAX_DIGITS as u32) - 1; |
|
||||
|
|
||||
/// Maximum number of binary digits for BTC price in whole USD.
|
|
||||
const MAX_DIGITS: usize = 20; |
|
||||
|
|
||||
const BASE: usize = 2; |
|
||||
|
|
||||
/// Binary representation of a price interval.
|
|
||||
#[derive(Clone, Debug, PartialEq)] |
|
||||
pub struct Digits(BitVec); |
|
||||
|
|
||||
impl Digits { |
|
||||
pub fn new(range: RangeInclusive<u64>) -> Result<Vec<Self>, Error> { |
|
||||
let (start, end) = range.into_inner(); |
|
||||
if start > MAX_PRICE_DEC || end > MAX_PRICE_DEC { |
|
||||
return Err(Error::RangeOverMax); |
|
||||
} |
|
||||
|
|
||||
if start > end { |
|
||||
return Err(Error::DecreasingRange); |
|
||||
} |
|
||||
|
|
||||
let digits = digit_decomposition::group_by_ignoring_digits( |
|
||||
start as usize, |
|
||||
end as usize, |
|
||||
BASE, |
|
||||
MAX_DIGITS, |
|
||||
) |
|
||||
.iter() |
|
||||
.map(|digits| { |
|
||||
let digits = digits.iter().map(|n| *n != 0).collect::<BitVec>(); |
|
||||
Digits(digits) |
|
||||
}) |
|
||||
.collect(); |
|
||||
|
|
||||
Ok(digits) |
|
||||
} |
|
||||
|
|
||||
/// Calculate the range of prices expressed by these digits.
|
|
||||
///
|
|
||||
/// With the resulting range one can assess wether a particular
|
|
||||
/// price corresponds to the described interval.
|
|
||||
pub fn range(&self) -> RangeInclusive<u64> { |
|
||||
let missing_bits = MAX_DIGITS - self.0.len(); |
|
||||
|
|
||||
let mut bits = self.0.clone(); |
|
||||
bits.append(&mut BitVec::from_elem(missing_bits, false)); |
|
||||
let start = bits.as_u64(); |
|
||||
|
|
||||
let mut bits = self.0.clone(); |
|
||||
bits.append(&mut BitVec::from_elem(missing_bits, true)); |
|
||||
let end = bits.as_u64(); |
|
||||
|
|
||||
start..=end |
|
||||
} |
|
||||
|
|
||||
/// Map each bit to its index in the set {0, 1}, starting at 1.
|
|
||||
pub fn to_indices(&self) -> Vec<NonZeroU8> { |
|
||||
self.0 |
|
||||
.iter() |
|
||||
.map(|bit| NonZeroU8::new(if bit { 2u8 } else { 1u8 }).expect("1 and 2 are non-zero")) |
|
||||
.collect() |
|
||||
} |
|
||||
|
|
||||
pub fn len(&self) -> usize { |
|
||||
self.0.len() |
|
||||
} |
|
||||
|
|
||||
pub fn is_empty(&self) -> bool { |
|
||||
self.0.is_empty() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[derive(thiserror::Error, Debug)] |
|
||||
pub enum Error { |
|
||||
#[error("Interval would generate values over maximum price of {MAX_PRICE_DEC}.")] |
|
||||
RangeOverMax, |
|
||||
#[error("Invalid decreasing interval.")] |
|
||||
DecreasingRange, |
|
||||
} |
|
||||
|
|
||||
impl Display for Digits { |
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
||||
self.0 |
|
||||
.iter() |
|
||||
.try_for_each(|digit| write!(f, "{}", digit as u8))?; |
|
||||
|
|
||||
Ok(()) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
trait BitVecExt { |
|
||||
fn as_u64(&self) -> u64; |
|
||||
} |
|
||||
|
|
||||
impl BitVecExt for BitVec { |
|
||||
fn as_u64(&self) -> u64 { |
|
||||
let len = self.len(); |
|
||||
|
|
||||
self.iter().enumerate().fold(0, |acc, (i, x)| { |
|
||||
acc + ((x as u64) * (BASE.pow((len - i - 1) as u32) as u64)) |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[cfg(test)] |
|
||||
mod tests { |
|
||||
use super::*; |
|
||||
|
|
||||
use anyhow::Result; |
|
||||
use proptest::prelude::*; |
|
||||
|
|
||||
#[derive(Debug, Clone)] |
|
||||
struct Interval(RangeInclusive<u64>); |
|
||||
|
|
||||
impl Interval { |
|
||||
fn new(range: RangeInclusive<u64>) -> Self { |
|
||||
Self(range) |
|
||||
} |
|
||||
|
|
||||
fn to_digits(&self) -> Result<Vec<Digits>> { |
|
||||
let digits = Digits::new(self.0.clone())?; |
|
||||
|
|
||||
Ok(digits) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
impl PartialEq<Vec<Digits>> for Interval { |
|
||||
fn eq(&self, other: &Vec<Digits>) -> bool { |
|
||||
let sub_intervals = other.iter().flat_map(|i| i.range()); |
|
||||
sub_intervals.eq(self.0.clone()) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
prop_compose! { |
|
||||
fn interval()(x in 0u64..=MAX_PRICE_DEC, y in 0u64..=MAX_PRICE_DEC) -> Interval { |
|
||||
let (start, end) = if x < y { (x, y) } else { (y, x) }; |
|
||||
|
|
||||
Interval::new(start..=end) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
proptest! { |
|
||||
#[test] |
|
||||
fn interval_equal_to_sum_of_sub_intervals_described_by_digits(interval in interval()) { |
|
||||
prop_assert!(interval == interval.to_digits().unwrap()) |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,459 +0,0 @@ |
|||||
//! Utility functions to decompose numeric outcome values
|
|
||||
//!
|
|
||||
//! This code has been lifted from:
|
|
||||
//! <https://github.com/p2pderivatives/rust-dlc/blob/chore%2Ffactor-out-dlc-trie/dlc-trie/src/digit_decomposition.rs>
|
|
||||
|
|
||||
/// Describes an interval that starts at `prefix || start` and terminates at `prefix || end`.
|
|
||||
struct PrefixInterval { |
|
||||
/// The prefix common to all numbers within the interval.
|
|
||||
prefix: Vec<usize>, |
|
||||
/// The suffix of the first number in the interval.
|
|
||||
start: Vec<usize>, |
|
||||
/// The suffix of the last number in the interval.
|
|
||||
end: Vec<usize>, |
|
||||
} |
|
||||
|
|
||||
/// Decompose a numeric value into digits in the specified base. If the decomposed
|
|
||||
/// value contains less than `nb_digits`, zeroes will be prepended to reach `nb_digits`
|
|
||||
/// size.
|
|
||||
fn decompose_value(mut value: usize, base: usize, nb_digits: usize) -> Vec<usize> { |
|
||||
let mut res = Vec::new(); |
|
||||
|
|
||||
while value > 0 { |
|
||||
res.push(value % base); |
|
||||
value = ((value as f64) / (base as f64)).floor() as usize; |
|
||||
} |
|
||||
|
|
||||
while res.len() < nb_digits { |
|
||||
res.push(0); |
|
||||
} |
|
||||
|
|
||||
assert_eq!(nb_digits, res.len()); |
|
||||
|
|
||||
res.into_iter().rev().collect() |
|
||||
} |
|
||||
|
|
||||
/// Returns the interval [start, end] as a `PrefixInterval`, which will contain
|
|
||||
/// the common prefix to all numbers in the interval as well as the start and end
|
|
||||
/// suffixes decomposed in the specified base, and zero padded to `nb_digits` if
|
|
||||
/// necessary.
|
|
||||
fn separate_prefix(start: usize, end: usize, base: usize, nb_digits: usize) -> PrefixInterval { |
|
||||
let start_digits = decompose_value(start, base, nb_digits); |
|
||||
let end_digits = decompose_value(end, base, nb_digits); |
|
||||
let mut prefix = Vec::new(); |
|
||||
|
|
||||
let mut i = 0; |
|
||||
while i < nb_digits && start_digits[i] == end_digits[i] { |
|
||||
prefix.push(start_digits[i]); |
|
||||
i += 1; |
|
||||
} |
|
||||
let start = start_digits.into_iter().skip(prefix.len()).collect(); |
|
||||
|
|
||||
let end = end_digits.into_iter().skip(prefix.len()).collect(); |
|
||||
|
|
||||
PrefixInterval { prefix, start, end } |
|
||||
} |
|
||||
|
|
||||
/// Removes the trailing digits from `digits` that are equal to `num`.
|
|
||||
fn remove_tail_if_equal(mut digits: Vec<usize>, num: usize) -> Vec<usize> { |
|
||||
let mut i = digits.len(); |
|
||||
while i > 1 && digits[i - 1] == num { |
|
||||
i -= 1; |
|
||||
} |
|
||||
digits.truncate(i); |
|
||||
digits |
|
||||
} |
|
||||
|
|
||||
/// Compute the groupings for the end of the interval.
|
|
||||
fn back_groupings(digits: Vec<usize>, base: usize) -> Vec<Vec<usize>> { |
|
||||
let digits = remove_tail_if_equal(digits, base - 1); |
|
||||
if digits.is_empty() { |
|
||||
return vec![vec![base - 1]]; |
|
||||
} |
|
||||
let mut prefix = vec![digits[0]]; |
|
||||
let mut res: Vec<Vec<usize>> = Vec::new(); |
|
||||
for digit in digits.iter().skip(1) { |
|
||||
let mut last = 0; |
|
||||
let digit = *digit; |
|
||||
while last < digit { |
|
||||
let mut new_res = prefix.clone(); |
|
||||
new_res.push(last); |
|
||||
res.push(new_res); |
|
||||
last += 1; |
|
||||
} |
|
||||
prefix.push(digit); |
|
||||
} |
|
||||
res.push(digits); |
|
||||
res |
|
||||
} |
|
||||
|
|
||||
/// Compute the groupings for the beginning of the interval.
|
|
||||
fn front_groupings(digits: Vec<usize>, base: usize) -> Vec<Vec<usize>> { |
|
||||
let digits = remove_tail_if_equal(digits, 0); |
|
||||
if digits.is_empty() { |
|
||||
return vec![vec![0]]; |
|
||||
} |
|
||||
let mut prefix = digits.clone(); |
|
||||
let mut res: Vec<Vec<usize>> = vec![digits.clone()]; |
|
||||
for digit in digits.into_iter().skip(1).rev() { |
|
||||
prefix.pop(); |
|
||||
let mut last = digit + 1; |
|
||||
while last < base { |
|
||||
let mut new_res = prefix.clone(); |
|
||||
new_res.push(last); |
|
||||
res.push(new_res); |
|
||||
last += 1; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
res |
|
||||
} |
|
||||
|
|
||||
/// Compute the groupings for the middle of the interval.
|
|
||||
fn middle_grouping(first_digit_start: usize, first_digit_end: usize) -> Vec<Vec<usize>> { |
|
||||
let mut res: Vec<Vec<usize>> = Vec::new(); |
|
||||
let mut first_digit_start = first_digit_start + 1; |
|
||||
while first_digit_start < first_digit_end { |
|
||||
res.push(vec![first_digit_start]); |
|
||||
first_digit_start += 1; |
|
||||
} |
|
||||
|
|
||||
res |
|
||||
} |
|
||||
|
|
||||
/// Returns the set of decomposed prefixes that cover the range [start, end].
|
|
||||
pub(crate) fn group_by_ignoring_digits( |
|
||||
start: usize, |
|
||||
end: usize, |
|
||||
base: usize, |
|
||||
num_digits: usize, |
|
||||
) -> Vec<Vec<usize>> { |
|
||||
let prefix_range = separate_prefix(start, end, base, num_digits); |
|
||||
let start_is_all_zeros = prefix_range.start.iter().all(|x| *x == 0); |
|
||||
let end_is_all_max = prefix_range.end.iter().all(|x| *x == base - 1); |
|
||||
|
|
||||
if start == end || start_is_all_zeros && end_is_all_max && !prefix_range.prefix.is_empty() { |
|
||||
return vec![prefix_range.prefix]; |
|
||||
} |
|
||||
let mut res: Vec<Vec<usize>> = Vec::new(); |
|
||||
if prefix_range.prefix.len() == num_digits - 1 { |
|
||||
for i in prefix_range.start[prefix_range.start.len() - 1] |
|
||||
..prefix_range.end[prefix_range.end.len() - 1] + 1 |
|
||||
{ |
|
||||
let mut new_res = prefix_range.prefix.clone(); |
|
||||
new_res.push(i); |
|
||||
res.push(new_res) |
|
||||
} |
|
||||
} else { |
|
||||
let mut front = front_groupings(prefix_range.start.clone(), base); |
|
||||
let mut middle = middle_grouping(prefix_range.start[0], prefix_range.end[0]); |
|
||||
let mut back = back_groupings(prefix_range.end.clone(), base); |
|
||||
res.append(&mut front); |
|
||||
res.append(&mut middle); |
|
||||
res.append(&mut back); |
|
||||
res = res |
|
||||
.into_iter() |
|
||||
.map(|x| { |
|
||||
prefix_range |
|
||||
.prefix |
|
||||
.iter() |
|
||||
.cloned() |
|
||||
.chain(x.into_iter()) |
|
||||
.collect() |
|
||||
}) |
|
||||
.collect(); |
|
||||
} |
|
||||
|
|
||||
res |
|
||||
} |
|
||||
|
|
||||
#[cfg(test)] |
|
||||
mod tests { |
|
||||
struct DecompositionTestCase { |
|
||||
composed: usize, |
|
||||
decomposed: Vec<usize>, |
|
||||
base: usize, |
|
||||
nb_digits: usize, |
|
||||
} |
|
||||
|
|
||||
struct GroupingTestCase { |
|
||||
start_index: usize, |
|
||||
end_index: usize, |
|
||||
base: usize, |
|
||||
nb_digits: usize, |
|
||||
expected: Vec<Vec<usize>>, |
|
||||
} |
|
||||
fn decomposition_test_cases() -> Vec<DecompositionTestCase> { |
|
||||
vec![ |
|
||||
DecompositionTestCase { |
|
||||
composed: 123456789, |
|
||||
decomposed: vec![1, 2, 3, 4, 5, 6, 7, 8, 9], |
|
||||
base: 10, |
|
||||
nb_digits: 9, |
|
||||
}, |
|
||||
DecompositionTestCase { |
|
||||
composed: 4321, |
|
||||
decomposed: vec![1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1], |
|
||||
base: 2, |
|
||||
nb_digits: 13, |
|
||||
}, |
|
||||
DecompositionTestCase { |
|
||||
composed: 0, |
|
||||
decomposed: vec![0, 0, 0, 0], |
|
||||
base: 8, |
|
||||
nb_digits: 4, |
|
||||
}, |
|
||||
DecompositionTestCase { |
|
||||
composed: 2, |
|
||||
decomposed: vec![0, 2], |
|
||||
base: 10, |
|
||||
nb_digits: 2, |
|
||||
}, |
|
||||
DecompositionTestCase { |
|
||||
composed: 1, |
|
||||
decomposed: vec![1], |
|
||||
base: 2, |
|
||||
nb_digits: 1, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
|
|
||||
fn grouping_test_cases() -> Vec<GroupingTestCase> { |
|
||||
vec![ |
|
||||
GroupingTestCase { |
|
||||
start_index: 123, |
|
||||
end_index: 123, |
|
||||
base: 10, |
|
||||
nb_digits: 3, |
|
||||
expected: vec![vec![1, 2, 3]], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 171, |
|
||||
end_index: 210, |
|
||||
base: 16, |
|
||||
nb_digits: 2, |
|
||||
expected: vec![ |
|
||||
vec![10, 11], |
|
||||
vec![10, 12], |
|
||||
vec![10, 13], |
|
||||
vec![10, 14], |
|
||||
vec![10, 15], |
|
||||
vec![11], |
|
||||
vec![12], |
|
||||
vec![13, 0], |
|
||||
vec![13, 1], |
|
||||
vec![13, 2], |
|
||||
], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 73899, |
|
||||
end_index: 73938, |
|
||||
base: 16, |
|
||||
nb_digits: 6, |
|
||||
expected: vec![ |
|
||||
vec![0, 1, 2, 0, 10, 11], |
|
||||
vec![0, 1, 2, 0, 10, 12], |
|
||||
vec![0, 1, 2, 0, 10, 13], |
|
||||
vec![0, 1, 2, 0, 10, 14], |
|
||||
vec![0, 1, 2, 0, 10, 15], |
|
||||
vec![0, 1, 2, 0, 11], |
|
||||
vec![0, 1, 2, 0, 12], |
|
||||
vec![0, 1, 2, 0, 13, 0], |
|
||||
vec![0, 1, 2, 0, 13, 1], |
|
||||
vec![0, 1, 2, 0, 13, 2], |
|
||||
], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 1234, |
|
||||
end_index: 4321, |
|
||||
base: 10, |
|
||||
nb_digits: 4, |
|
||||
expected: vec![ |
|
||||
vec![1, 2, 3, 4], |
|
||||
vec![1, 2, 3, 5], |
|
||||
vec![1, 2, 3, 6], |
|
||||
vec![1, 2, 3, 7], |
|
||||
vec![1, 2, 3, 8], |
|
||||
vec![1, 2, 3, 9], |
|
||||
vec![1, 2, 4], |
|
||||
vec![1, 2, 5], |
|
||||
vec![1, 2, 6], |
|
||||
vec![1, 2, 7], |
|
||||
vec![1, 2, 8], |
|
||||
vec![1, 2, 9], |
|
||||
vec![1, 3], |
|
||||
vec![1, 4], |
|
||||
vec![1, 5], |
|
||||
vec![1, 6], |
|
||||
vec![1, 7], |
|
||||
vec![1, 8], |
|
||||
vec![1, 9], |
|
||||
vec![2], |
|
||||
vec![3], |
|
||||
vec![4, 0], |
|
||||
vec![4, 1], |
|
||||
vec![4, 2], |
|
||||
vec![4, 3, 0], |
|
||||
vec![4, 3, 1], |
|
||||
vec![4, 3, 2, 0], |
|
||||
vec![4, 3, 2, 1], |
|
||||
], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 1201234, |
|
||||
end_index: 1204321, |
|
||||
base: 10, |
|
||||
nb_digits: 8, |
|
||||
expected: vec![ |
|
||||
vec![0, 1, 2, 0, 1, 2, 3, 4], |
|
||||
vec![0, 1, 2, 0, 1, 2, 3, 5], |
|
||||
vec![0, 1, 2, 0, 1, 2, 3, 6], |
|
||||
vec![0, 1, 2, 0, 1, 2, 3, 7], |
|
||||
vec![0, 1, 2, 0, 1, 2, 3, 8], |
|
||||
vec![0, 1, 2, 0, 1, 2, 3, 9], |
|
||||
vec![0, 1, 2, 0, 1, 2, 4], |
|
||||
vec![0, 1, 2, 0, 1, 2, 5], |
|
||||
vec![0, 1, 2, 0, 1, 2, 6], |
|
||||
vec![0, 1, 2, 0, 1, 2, 7], |
|
||||
vec![0, 1, 2, 0, 1, 2, 8], |
|
||||
vec![0, 1, 2, 0, 1, 2, 9], |
|
||||
vec![0, 1, 2, 0, 1, 3], |
|
||||
vec![0, 1, 2, 0, 1, 4], |
|
||||
vec![0, 1, 2, 0, 1, 5], |
|
||||
vec![0, 1, 2, 0, 1, 6], |
|
||||
vec![0, 1, 2, 0, 1, 7], |
|
||||
vec![0, 1, 2, 0, 1, 8], |
|
||||
vec![0, 1, 2, 0, 1, 9], |
|
||||
vec![0, 1, 2, 0, 2], |
|
||||
vec![0, 1, 2, 0, 3], |
|
||||
vec![0, 1, 2, 0, 4, 0], |
|
||||
vec![0, 1, 2, 0, 4, 1], |
|
||||
vec![0, 1, 2, 0, 4, 2], |
|
||||
vec![0, 1, 2, 0, 4, 3, 0], |
|
||||
vec![0, 1, 2, 0, 4, 3, 1], |
|
||||
vec![0, 1, 2, 0, 4, 3, 2, 0], |
|
||||
vec![0, 1, 2, 0, 4, 3, 2, 1], |
|
||||
], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 2200, |
|
||||
end_index: 4999, |
|
||||
base: 10, |
|
||||
nb_digits: 4, |
|
||||
expected: vec![ |
|
||||
vec![2, 2], |
|
||||
vec![2, 3], |
|
||||
vec![2, 4], |
|
||||
vec![2, 5], |
|
||||
vec![2, 6], |
|
||||
vec![2, 7], |
|
||||
vec![2, 8], |
|
||||
vec![2, 9], |
|
||||
vec![3], |
|
||||
vec![4], |
|
||||
], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 0, |
|
||||
end_index: 99, |
|
||||
base: 10, |
|
||||
nb_digits: 2, |
|
||||
expected: vec![ |
|
||||
vec![0], |
|
||||
vec![1], |
|
||||
vec![2], |
|
||||
vec![3], |
|
||||
vec![4], |
|
||||
vec![5], |
|
||||
vec![6], |
|
||||
vec![7], |
|
||||
vec![8], |
|
||||
vec![9], |
|
||||
], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 100, |
|
||||
end_index: 199, |
|
||||
base: 10, |
|
||||
nb_digits: 3, |
|
||||
expected: vec![vec![1]], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 100, |
|
||||
end_index: 200, |
|
||||
base: 10, |
|
||||
nb_digits: 3, |
|
||||
expected: vec![vec![1], vec![2, 0, 0]], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 11, |
|
||||
end_index: 18, |
|
||||
base: 10, |
|
||||
nb_digits: 2, |
|
||||
expected: vec![ |
|
||||
vec![1, 1], |
|
||||
vec![1, 2], |
|
||||
vec![1, 3], |
|
||||
vec![1, 4], |
|
||||
vec![1, 5], |
|
||||
vec![1, 6], |
|
||||
vec![1, 7], |
|
||||
vec![1, 8], |
|
||||
], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 11, |
|
||||
end_index: 23, |
|
||||
base: 2, |
|
||||
nb_digits: 5, |
|
||||
expected: vec![vec![0, 1, 0, 1, 1], vec![0, 1, 1], vec![1, 0]], |
|
||||
}, |
|
||||
GroupingTestCase { |
|
||||
start_index: 5677, |
|
||||
end_index: 8621, |
|
||||
base: 2, |
|
||||
nb_digits: 14, |
|
||||
expected: vec![ |
|
||||
vec![0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1], |
|
||||
vec![0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1], |
|
||||
vec![0, 1, 0, 1, 1, 0, 0, 0, 1, 1], |
|
||||
vec![0, 1, 0, 1, 1, 0, 0, 1], |
|
||||
vec![0, 1, 0, 1, 1, 0, 1], |
|
||||
vec![0, 1, 0, 1, 1, 1], |
|
||||
vec![0, 1, 1], |
|
||||
vec![1, 0, 0, 0, 0, 0], |
|
||||
vec![1, 0, 0, 0, 0, 1, 0], |
|
||||
vec![1, 0, 0, 0, 0, 1, 1, 0, 0], |
|
||||
vec![1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0], |
|
||||
vec![1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0], |
|
||||
vec![1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0], |
|
||||
], |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
|
|
||||
#[test] |
|
||||
fn decompose_value_test() { |
|
||||
for test_case in decomposition_test_cases() { |
|
||||
assert_eq!( |
|
||||
test_case.decomposed, |
|
||||
super::decompose_value(test_case.composed, test_case.base, test_case.nb_digits) |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[test] |
|
||||
fn group_by_ignoring_digits_test() { |
|
||||
for test_case in grouping_test_cases() { |
|
||||
assert_eq!( |
|
||||
test_case.expected, |
|
||||
super::group_by_ignoring_digits( |
|
||||
test_case.start_index, |
|
||||
test_case.end_index, |
|
||||
test_case.base, |
|
||||
test_case.nb_digits |
|
||||
) |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,12 +0,0 @@ |
|||||
mod oracle; |
|
||||
mod protocol; |
|
||||
|
|
||||
pub mod interval; |
|
||||
|
|
||||
pub use protocol::{ |
|
||||
close_transaction, commit_descriptor, compute_adaptor_pk, create_cfd_transactions, |
|
||||
finalize_spend_transaction, generate_payouts, lock_descriptor, punish_transaction, |
|
||||
renew_cfd_transactions, spending_tx_sighash, Announcement, Cets, CfdTransactions, PartyParams, |
|
||||
Payout, PunishParams, TransactionExt, WalletExt, |
|
||||
}; |
|
||||
pub use secp256k1_zkp; |
|
@ -1,40 +0,0 @@ |
|||||
pub use secp256k1_zkp::*; |
|
||||
|
|
||||
use anyhow::Result; |
|
||||
use secp256k1_zkp::schnorrsig; |
|
||||
use std::num::NonZeroU8; |
|
||||
|
|
||||
/// Compute an attestation public key for the given oracle public key,
|
|
||||
/// announcement nonce public key and outcome index.
|
|
||||
pub fn attestation_pk( |
|
||||
oracle_pk: &schnorrsig::PublicKey, |
|
||||
nonce_pk: &schnorrsig::PublicKey, |
|
||||
index: NonZeroU8, |
|
||||
) -> Result<secp256k1_zkp::PublicKey> { |
|
||||
let nonce_pk = schnorr_pubkey_to_pubkey(nonce_pk); |
|
||||
|
|
||||
let mut nonce_pk_sum = nonce_pk; |
|
||||
nonce_pk_sum.mul_assign(SECP256K1, &index_to_bytes(index))?; |
|
||||
|
|
||||
let oracle_pk = schnorr_pubkey_to_pubkey(oracle_pk); |
|
||||
let attestation_pk = oracle_pk.combine(&nonce_pk_sum)?; |
|
||||
|
|
||||
Ok(attestation_pk) |
|
||||
} |
|
||||
|
|
||||
fn schnorr_pubkey_to_pubkey(pk: &schnorrsig::PublicKey) -> secp256k1_zkp::PublicKey { |
|
||||
let mut buf = Vec::<u8>::with_capacity(33); |
|
||||
|
|
||||
buf.push(0x02); // append even byte
|
|
||||
buf.extend(&pk.serialize()); |
|
||||
|
|
||||
secp256k1_zkp::PublicKey::from_slice(&buf).expect("valid key") |
|
||||
} |
|
||||
|
|
||||
fn index_to_bytes(index: NonZeroU8) -> [u8; 32] { |
|
||||
let mut bytes = [0u8; 32]; |
|
||||
|
|
||||
bytes[31] = index.get(); |
|
||||
|
|
||||
bytes |
|
||||
} |
|
@ -1,573 +0,0 @@ |
|||||
pub use transaction_ext::TransactionExt; |
|
||||
pub use transactions::{close_transaction, punish_transaction}; |
|
||||
|
|
||||
use crate::protocol::sighash_ext::SigHashExt; |
|
||||
use crate::protocol::transactions::{ |
|
||||
lock_transaction, CommitTransaction, ContractExecutionTransaction as ContractExecutionTx, |
|
||||
RefundTransaction, |
|
||||
}; |
|
||||
use crate::{interval, oracle}; |
|
||||
use anyhow::{bail, Context, Result}; |
|
||||
use bdk::bitcoin::hashes::hex::ToHex; |
|
||||
use bdk::bitcoin::util::bip143::SigHashCache; |
|
||||
use bdk::bitcoin::util::psbt::PartiallySignedTransaction; |
|
||||
use bdk::bitcoin::{Address, Amount, PublicKey, SigHashType, Transaction, TxOut}; |
|
||||
use bdk::database::BatchDatabase; |
|
||||
use bdk::descriptor::Descriptor; |
|
||||
use bdk::miniscript::descriptor::Wsh; |
|
||||
use bdk::miniscript::DescriptorTrait; |
|
||||
use bdk::wallet::AddressIndex; |
|
||||
use bdk::FeeRate; |
|
||||
use itertools::Itertools; |
|
||||
use secp256k1_zkp::{self, schnorrsig, EcdsaAdaptorSignature, SecretKey, Signature, SECP256K1}; |
|
||||
use std::collections::HashMap; |
|
||||
use std::hash::Hasher; |
|
||||
use std::iter::FromIterator; |
|
||||
use std::num::NonZeroU8; |
|
||||
use std::ops::RangeInclusive; |
|
||||
|
|
||||
mod sighash_ext; |
|
||||
mod transaction_ext; |
|
||||
mod transactions; |
|
||||
mod txin_ext; |
|
||||
|
|
||||
/// Static script to be used to create lock tx
|
|
||||
const DUMMY_2OF2_MULTISIG: &str = |
|
||||
"0020b5aa99ed7e0fa92483eb045ab8b7a59146d4d9f6653f21ba729b4331895a5b46"; |
|
||||
|
|
||||
pub trait WalletExt { |
|
||||
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams>; |
|
||||
} |
|
||||
|
|
||||
impl<B, D> WalletExt for bdk::Wallet<B, D> |
|
||||
where |
|
||||
D: BatchDatabase, |
|
||||
{ |
|
||||
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams> { |
|
||||
let mut builder = self.build_tx(); |
|
||||
builder |
|
||||
.ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic) |
|
||||
.fee_rate(FeeRate::from_sat_per_vb(1.0)) |
|
||||
.add_recipient( |
|
||||
DUMMY_2OF2_MULTISIG.parse().expect("Should be valid script"), |
|
||||
amount.as_sat(), |
|
||||
); |
|
||||
let (lock_psbt, _) = builder.finish()?; |
|
||||
let address = self.get_address(AddressIndex::New)?.address; |
|
||||
Ok(PartyParams { |
|
||||
lock_psbt, |
|
||||
identity_pk, |
|
||||
lock_amount: amount, |
|
||||
address, |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// Build all the transactions and some of the signatures and
|
|
||||
/// encrypted signatures needed to perform the CFD protocol.
|
|
||||
///
|
|
||||
/// # Arguments
|
|
||||
///
|
|
||||
/// * `maker` - The initial parameters of the maker.
|
|
||||
/// * `maker_punish_params` - The punish parameters of the maker.
|
|
||||
/// * `taker` - The initial parameters of the taker.
|
|
||||
/// * `taker_punish_params` - The punish parameters of the taker.
|
|
||||
/// * `oracle_pk` - The public key of the oracle.
|
|
||||
/// * `cet_timelock` - Relative timelock of the CET transaction with respect to the commit
|
|
||||
/// transaction.
|
|
||||
/// * `refund_timelock` - Relative timelock of the refund transaction with respect to the commit
|
|
||||
/// transaction.
|
|
||||
/// * `payouts_per_event` - All the possible ways in which the contract can be settled, according to
|
|
||||
/// the conditions of the bet. The key is the event at which the oracle will attest the price.
|
|
||||
/// * `identity_sk` - The secret key of the caller, used to sign and encsign different transactions.
|
|
||||
pub fn create_cfd_transactions( |
|
||||
(maker, maker_punish_params): (PartyParams, PunishParams), |
|
||||
(taker, taker_punish_params): (PartyParams, PunishParams), |
|
||||
oracle_pk: schnorrsig::PublicKey, |
|
||||
(cet_timelock, refund_timelock): (u32, u32), |
|
||||
payouts_per_event: HashMap<Announcement, Vec<Payout>>, |
|
||||
identity_sk: SecretKey, |
|
||||
) -> Result<CfdTransactions> { |
|
||||
let lock_tx = lock_transaction( |
|
||||
maker.lock_psbt.clone(), |
|
||||
taker.lock_psbt.clone(), |
|
||||
maker.identity_pk, |
|
||||
taker.identity_pk, |
|
||||
maker.lock_amount + taker.lock_amount, |
|
||||
); |
|
||||
|
|
||||
build_cfds( |
|
||||
lock_tx, |
|
||||
( |
|
||||
maker.identity_pk, |
|
||||
maker.lock_amount, |
|
||||
maker.address, |
|
||||
maker_punish_params, |
|
||||
), |
|
||||
( |
|
||||
taker.identity_pk, |
|
||||
taker.lock_amount, |
|
||||
taker.address, |
|
||||
taker_punish_params, |
|
||||
), |
|
||||
oracle_pk, |
|
||||
(cet_timelock, refund_timelock), |
|
||||
payouts_per_event, |
|
||||
identity_sk, |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
pub fn renew_cfd_transactions( |
|
||||
lock_tx: PartiallySignedTransaction, |
|
||||
(maker_pk, maker_lock_amount, maker_address, maker_punish_params): ( |
|
||||
PublicKey, |
|
||||
Amount, |
|
||||
Address, |
|
||||
PunishParams, |
|
||||
), |
|
||||
(taker_pk, taker_lock_amount, taker_address, taker_punish_params): ( |
|
||||
PublicKey, |
|
||||
Amount, |
|
||||
Address, |
|
||||
PunishParams, |
|
||||
), |
|
||||
oracle_pk: schnorrsig::PublicKey, |
|
||||
(cet_timelock, refund_timelock): (u32, u32), |
|
||||
payouts_per_event: HashMap<Announcement, Vec<Payout>>, |
|
||||
identity_sk: SecretKey, |
|
||||
) -> Result<CfdTransactions> { |
|
||||
build_cfds( |
|
||||
lock_tx, |
|
||||
( |
|
||||
maker_pk, |
|
||||
maker_lock_amount, |
|
||||
maker_address, |
|
||||
maker_punish_params, |
|
||||
), |
|
||||
( |
|
||||
taker_pk, |
|
||||
taker_lock_amount, |
|
||||
taker_address, |
|
||||
taker_punish_params, |
|
||||
), |
|
||||
oracle_pk, |
|
||||
(cet_timelock, refund_timelock), |
|
||||
payouts_per_event, |
|
||||
identity_sk, |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
fn build_cfds( |
|
||||
lock_tx: PartiallySignedTransaction, |
|
||||
(maker_pk, maker_lock_amount, maker_address, maker_punish_params): ( |
|
||||
PublicKey, |
|
||||
Amount, |
|
||||
Address, |
|
||||
PunishParams, |
|
||||
), |
|
||||
(taker_pk, taker_lock_amount, taker_address, taker_punish_params): ( |
|
||||
PublicKey, |
|
||||
Amount, |
|
||||
Address, |
|
||||
PunishParams, |
|
||||
), |
|
||||
oracle_pk: schnorrsig::PublicKey, |
|
||||
(cet_timelock, refund_timelock): (u32, u32), |
|
||||
payouts_per_event: HashMap<Announcement, Vec<Payout>>, |
|
||||
identity_sk: SecretKey, |
|
||||
) -> Result<CfdTransactions> { |
|
||||
let commit_tx = CommitTransaction::new( |
|
||||
&lock_tx.global.unsigned_tx, |
|
||||
( |
|
||||
maker_pk, |
|
||||
maker_punish_params.revocation_pk, |
|
||||
maker_punish_params.publish_pk, |
|
||||
), |
|
||||
( |
|
||||
taker_pk, |
|
||||
taker_punish_params.revocation_pk, |
|
||||
taker_punish_params.publish_pk, |
|
||||
), |
|
||||
) |
|
||||
.context("cannot build commit tx")?; |
|
||||
|
|
||||
let identity_pk = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &identity_sk); |
|
||||
let commit_encsig = if identity_pk == maker_pk.key { |
|
||||
commit_tx.encsign(identity_sk, &taker_punish_params.publish_pk) |
|
||||
} else if identity_pk == taker_pk.key { |
|
||||
commit_tx.encsign(identity_sk, &maker_punish_params.publish_pk) |
|
||||
} else { |
|
||||
bail!("identity sk does not belong to taker or maker") |
|
||||
}; |
|
||||
|
|
||||
let refund = { |
|
||||
let tx = RefundTransaction::new( |
|
||||
&commit_tx, |
|
||||
refund_timelock, |
|
||||
&maker_address, |
|
||||
&taker_address, |
|
||||
maker_lock_amount, |
|
||||
taker_lock_amount, |
|
||||
); |
|
||||
|
|
||||
let sighash = tx.sighash().to_message(); |
|
||||
let sig = SECP256K1.sign(&sighash, &identity_sk); |
|
||||
|
|
||||
(tx.into_inner(), sig) |
|
||||
}; |
|
||||
|
|
||||
let cets = payouts_per_event |
|
||||
.into_iter() |
|
||||
.map(|(event, payouts)| { |
|
||||
let cets = payouts |
|
||||
.iter() |
|
||||
.map(|payout| { |
|
||||
let cet = ContractExecutionTx::new( |
|
||||
&commit_tx, |
|
||||
payout.clone(), |
|
||||
&maker_address, |
|
||||
&taker_address, |
|
||||
event.nonce_pks.as_slice(), |
|
||||
cet_timelock, |
|
||||
)?; |
|
||||
|
|
||||
let encsig = cet.encsign(identity_sk, &oracle_pk)?; |
|
||||
|
|
||||
Ok((cet.into_inner(), encsig, payout.digits.clone())) |
|
||||
}) |
|
||||
.collect::<Result<Vec<_>>>() |
|
||||
.context("cannot build and sign all cets")?; |
|
||||
|
|
||||
Ok(Cets { event, cets }) |
|
||||
}) |
|
||||
.collect::<Result<_>>()?; |
|
||||
|
|
||||
Ok(CfdTransactions { |
|
||||
lock: lock_tx, |
|
||||
commit: (commit_tx.into_inner(), commit_encsig), |
|
||||
cets, |
|
||||
refund, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
pub fn lock_descriptor(maker_pk: PublicKey, taker_pk: PublicKey) -> Descriptor<PublicKey> { |
|
||||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; |
|
||||
|
|
||||
let maker_pk = ToHex::to_hex(&maker_pk.key); |
|
||||
let taker_pk = ToHex::to_hex(&taker_pk.key); |
|
||||
|
|
||||
let miniscript = MINISCRIPT_TEMPLATE |
|
||||
.replace("A", &maker_pk) |
|
||||
.replace("B", &taker_pk); |
|
||||
|
|
||||
let miniscript = miniscript.parse().expect("a valid miniscript"); |
|
||||
|
|
||||
Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor")) |
|
||||
} |
|
||||
|
|
||||
pub fn commit_descriptor( |
|
||||
(maker_own_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|
||||
(taker_own_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|
||||
) -> Descriptor<PublicKey> { |
|
||||
let maker_own_pk_hash = maker_own_pk.pubkey_hash().as_hash(); |
|
||||
let maker_own_pk = (&maker_own_pk.key.serialize().to_vec()).to_hex(); |
|
||||
let maker_publish_pk_hash = maker_publish_pk.pubkey_hash().as_hash(); |
|
||||
let maker_rev_pk_hash = maker_rev_pk.pubkey_hash().as_hash(); |
|
||||
|
|
||||
let taker_own_pk_hash = taker_own_pk.pubkey_hash().as_hash(); |
|
||||
let taker_own_pk = (&taker_own_pk.key.serialize().to_vec()).to_hex(); |
|
||||
let taker_publish_pk_hash = taker_publish_pk.pubkey_hash().as_hash(); |
|
||||
let taker_rev_pk_hash = taker_rev_pk.pubkey_hash().as_hash(); |
|
||||
|
|
||||
// raw script:
|
|
||||
// or(and(pk(maker_own_pk),pk(taker_own_pk)),or(and(pk(maker_own_pk),and(pk(taker_publish_pk),
|
|
||||
// pk(taker_rev_pk))),and(pk(taker_own_pk),and(pk(maker_publish_pk),pk(maker_rev_pk)))))
|
|
||||
let full_script = format!("wsh(c:andor(pk({maker_own_pk}),pk_k({taker_own_pk}),or_i(and_v(v:pkh({maker_own_pk_hash}),and_v(v:pkh({taker_publish_pk_hash}),pk_h({taker_rev_pk_hash}))),and_v(v:pkh({taker_own_pk_hash}),and_v(v:pkh({maker_publish_pk_hash}),pk_h({maker_rev_pk_hash}))))))", |
|
||||
maker_own_pk = maker_own_pk, |
|
||||
taker_own_pk = taker_own_pk, |
|
||||
maker_own_pk_hash = maker_own_pk_hash, |
|
||||
taker_own_pk_hash = taker_own_pk_hash, |
|
||||
taker_publish_pk_hash = taker_publish_pk_hash, |
|
||||
taker_rev_pk_hash = taker_rev_pk_hash, |
|
||||
maker_publish_pk_hash = maker_publish_pk_hash, |
|
||||
maker_rev_pk_hash = maker_rev_pk_hash |
|
||||
); |
|
||||
|
|
||||
full_script.parse().expect("a valid miniscript") |
|
||||
} |
|
||||
|
|
||||
pub fn spending_tx_sighash( |
|
||||
spending_tx: &Transaction, |
|
||||
spent_descriptor: &Descriptor<PublicKey>, |
|
||||
spent_amount: Amount, |
|
||||
) -> secp256k1_zkp::Message { |
|
||||
let sighash = SigHashCache::new(spending_tx).signature_hash( |
|
||||
0, |
|
||||
&spent_descriptor.script_code(), |
|
||||
spent_amount.as_sat(), |
|
||||
SigHashType::All, |
|
||||
); |
|
||||
sighash.to_message() |
|
||||
} |
|
||||
|
|
||||
pub fn finalize_spend_transaction( |
|
||||
mut tx: Transaction, |
|
||||
spent_descriptor: &Descriptor<PublicKey>, |
|
||||
(pk_0, sig_0): (PublicKey, Signature), |
|
||||
(pk_1, sig_1): (PublicKey, Signature), |
|
||||
) -> Result<Transaction> { |
|
||||
let satisfier = HashMap::from_iter(vec![ |
|
||||
(pk_0, (sig_0, SigHashType::All)), |
|
||||
(pk_1, (sig_1, SigHashType::All)), |
|
||||
]); |
|
||||
|
|
||||
let input = tx |
|
||||
.input |
|
||||
.iter_mut() |
|
||||
.exactly_one() |
|
||||
.expect("all spend transactions to have one input"); |
|
||||
spent_descriptor.satisfy(input, satisfier)?; |
|
||||
|
|
||||
Ok(tx) |
|
||||
} |
|
||||
|
|
||||
#[derive(Clone)] |
|
||||
pub struct PartyParams { |
|
||||
pub lock_psbt: PartiallySignedTransaction, |
|
||||
pub identity_pk: PublicKey, |
|
||||
pub lock_amount: Amount, |
|
||||
pub address: Address, |
|
||||
} |
|
||||
|
|
||||
#[derive(Debug, Copy, Clone)] |
|
||||
pub struct PunishParams { |
|
||||
pub revocation_pk: PublicKey, |
|
||||
pub publish_pk: PublicKey, |
|
||||
} |
|
||||
|
|
||||
#[derive(Debug, Clone)] |
|
||||
pub struct CfdTransactions { |
|
||||
pub lock: PartiallySignedTransaction, |
|
||||
pub commit: (Transaction, EcdsaAdaptorSignature), |
|
||||
pub cets: Vec<Cets>, |
|
||||
pub refund: (Transaction, Signature), |
|
||||
} |
|
||||
|
|
||||
/// Group of CETs associated with a particular oracle announcement.
|
|
||||
///
|
|
||||
/// All of the adaptor signatures included will be _possibly_ unlocked
|
|
||||
/// by the attestation corresponding to the announcement. In practice,
|
|
||||
/// only one of the adaptor signatures should be unlocked if the
|
|
||||
/// payout intervals are constructed correctly. To check if an adaptor
|
|
||||
/// signature can be unlocked by a price attestation, verify whether
|
|
||||
/// the price attested to lies within its interval.
|
|
||||
#[derive(Debug, Clone)] |
|
||||
pub struct Cets { |
|
||||
pub event: Announcement, |
|
||||
pub cets: Vec<(Transaction, EcdsaAdaptorSignature, interval::Digits)>, |
|
||||
} |
|
||||
|
|
||||
#[derive(Debug, Clone, Eq)] |
|
||||
pub struct Announcement { |
|
||||
pub id: String, |
|
||||
pub nonce_pks: Vec<schnorrsig::PublicKey>, |
|
||||
} |
|
||||
|
|
||||
impl std::hash::Hash for Announcement { |
|
||||
fn hash<H: Hasher>(&self, state: &mut H) { |
|
||||
self.id.hash(state) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
impl PartialEq for Announcement { |
|
||||
fn eq(&self, other: &Self) -> bool { |
|
||||
self.id.eq(&other.id) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[derive(Debug, Clone, PartialEq)] |
|
||||
pub struct Payout { |
|
||||
digits: interval::Digits, |
|
||||
maker_amount: Amount, |
|
||||
taker_amount: Amount, |
|
||||
} |
|
||||
|
|
||||
pub fn generate_payouts( |
|
||||
range: RangeInclusive<u64>, |
|
||||
maker_amount: Amount, |
|
||||
taker_amount: Amount, |
|
||||
) -> Result<Vec<Payout>> { |
|
||||
let digits = interval::Digits::new(range).context("invalid interval")?; |
|
||||
Ok(digits |
|
||||
.into_iter() |
|
||||
.map(|digits| Payout { |
|
||||
digits, |
|
||||
maker_amount, |
|
||||
taker_amount, |
|
||||
}) |
|
||||
.collect()) |
|
||||
} |
|
||||
|
|
||||
impl Payout { |
|
||||
pub fn digits(&self) -> &interval::Digits { |
|
||||
&self.digits |
|
||||
} |
|
||||
|
|
||||
pub fn maker_amount(&self) -> &Amount { |
|
||||
&self.maker_amount |
|
||||
} |
|
||||
|
|
||||
pub fn taker_amount(&self) -> &Amount { |
|
||||
&self.taker_amount |
|
||||
} |
|
||||
|
|
||||
fn into_txouts(self, maker_address: &Address, taker_address: &Address) -> Vec<TxOut> { |
|
||||
let txouts = [ |
|
||||
(self.maker_amount, maker_address), |
|
||||
(self.taker_amount, taker_address), |
|
||||
] |
|
||||
.iter() |
|
||||
.filter_map(|(amount, address)| { |
|
||||
let script_pubkey = address.script_pubkey(); |
|
||||
let dust_limit = script_pubkey.dust_value(); |
|
||||
(amount >= &dust_limit).then(|| TxOut { |
|
||||
value: amount.as_sat(), |
|
||||
script_pubkey, |
|
||||
}) |
|
||||
}) |
|
||||
.collect::<Vec<_>>(); |
|
||||
|
|
||||
txouts |
|
||||
} |
|
||||
|
|
||||
/// Subtracts fee fairly from both outputs
|
|
||||
///
|
|
||||
/// We need to consider a few cases:
|
|
||||
/// - If both amounts are >= DUST, they share the fee equally
|
|
||||
/// - If one amount is < DUST, it set to 0 and the other output needs to cover for the fee.
|
|
||||
fn with_updated_fee( |
|
||||
self, |
|
||||
fee: Amount, |
|
||||
dust_limit_maker: Amount, |
|
||||
dust_limit_taker: Amount, |
|
||||
) -> Result<Self> { |
|
||||
let maker_amount = self.maker_amount; |
|
||||
let taker_amount = self.taker_amount; |
|
||||
|
|
||||
let mut updated = self; |
|
||||
match ( |
|
||||
maker_amount |
|
||||
.checked_sub(fee / 2) |
|
||||
.map(|a| a > dust_limit_maker) |
|
||||
.unwrap_or(false), |
|
||||
taker_amount |
|
||||
.checked_sub(fee / 2) |
|
||||
.map(|a| a > dust_limit_taker) |
|
||||
.unwrap_or(false), |
|
||||
) { |
|
||||
(true, true) => { |
|
||||
updated.maker_amount -= fee / 2; |
|
||||
updated.taker_amount -= fee / 2; |
|
||||
} |
|
||||
(false, true) => { |
|
||||
updated.maker_amount = Amount::ZERO; |
|
||||
updated.taker_amount = taker_amount - (fee + maker_amount); |
|
||||
} |
|
||||
(true, false) => { |
|
||||
updated.maker_amount = maker_amount - (fee + taker_amount); |
|
||||
updated.taker_amount = Amount::ZERO; |
|
||||
} |
|
||||
(false, false) => bail!("Amounts are too small, could not subtract fee."), |
|
||||
} |
|
||||
Ok(updated) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
pub fn compute_adaptor_pk( |
|
||||
oracle_pk: &schnorrsig::PublicKey, |
|
||||
index_nonce_pairs: &[(NonZeroU8, schnorrsig::PublicKey)], |
|
||||
) -> Result<secp256k1_zkp::PublicKey> { |
|
||||
let attestation_pks = index_nonce_pairs |
|
||||
.iter() |
|
||||
.map(|(index, nonce_pk)| oracle::attestation_pk(oracle_pk, nonce_pk, *index)) |
|
||||
.collect::<Result<Vec<_>>>()?; |
|
||||
let adaptor_pk = secp256k1_zkp::PublicKey::combine_keys( |
|
||||
attestation_pks.iter().collect::<Vec<_>>().as_slice(), |
|
||||
)?; |
|
||||
|
|
||||
Ok(adaptor_pk) |
|
||||
} |
|
||||
|
|
||||
#[cfg(test)] |
|
||||
mod tests { |
|
||||
use super::*; |
|
||||
|
|
||||
use bdk::bitcoin::Network; |
|
||||
|
|
||||
// TODO add proptest for this
|
|
||||
|
|
||||
#[test] |
|
||||
fn test_fee_subtraction_bigger_than_dust() { |
|
||||
let key = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af" |
|
||||
.parse() |
|
||||
.unwrap(); |
|
||||
let dummy_address = Address::p2wpkh(&key, Network::Regtest).unwrap(); |
|
||||
let dummy_dust_limit = dummy_address.script_pubkey().dust_value(); |
|
||||
|
|
||||
let orig_maker_amount = 1000; |
|
||||
let orig_taker_amount = 1000; |
|
||||
let payouts = generate_payouts( |
|
||||
0..=10_000, |
|
||||
Amount::from_sat(orig_maker_amount), |
|
||||
Amount::from_sat(orig_taker_amount), |
|
||||
) |
|
||||
.unwrap(); |
|
||||
let fee = 100; |
|
||||
|
|
||||
for payout in payouts { |
|
||||
let updated_payout = payout |
|
||||
.with_updated_fee(Amount::from_sat(fee), dummy_dust_limit, dummy_dust_limit) |
|
||||
.unwrap(); |
|
||||
|
|
||||
assert_eq!( |
|
||||
updated_payout.maker_amount, |
|
||||
Amount::from_sat(orig_maker_amount - fee / 2) |
|
||||
); |
|
||||
assert_eq!( |
|
||||
updated_payout.taker_amount, |
|
||||
Amount::from_sat(orig_taker_amount - fee / 2) |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[test] |
|
||||
fn test_fee_subtraction_smaller_than_dust() { |
|
||||
let key = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af" |
|
||||
.parse() |
|
||||
.unwrap(); |
|
||||
let dummy_address = Address::p2wpkh(&key, Network::Regtest).unwrap(); |
|
||||
let dummy_dust_limit = dummy_address.script_pubkey().dust_value(); |
|
||||
|
|
||||
let orig_maker_amount = dummy_dust_limit.as_sat() - 1; |
|
||||
let orig_taker_amount = 1000; |
|
||||
let payouts = generate_payouts( |
|
||||
0..=10_000, |
|
||||
Amount::from_sat(orig_maker_amount), |
|
||||
Amount::from_sat(orig_taker_amount), |
|
||||
) |
|
||||
.unwrap(); |
|
||||
let fee = 100; |
|
||||
|
|
||||
for payout in payouts { |
|
||||
let updated_payout = payout |
|
||||
.with_updated_fee(Amount::from_sat(fee), dummy_dust_limit, dummy_dust_limit) |
|
||||
.unwrap(); |
|
||||
|
|
||||
assert_eq!(updated_payout.maker_amount, Amount::from_sat(0)); |
|
||||
assert_eq!( |
|
||||
updated_payout.taker_amount, |
|
||||
Amount::from_sat(orig_taker_amount - (fee + orig_maker_amount)) |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,15 +0,0 @@ |
|||||
use bdk::bitcoin::hashes::Hash; |
|
||||
use bdk::bitcoin::SigHash; |
|
||||
|
|
||||
pub(crate) trait SigHashExt { |
|
||||
fn to_message(self) -> secp256k1_zkp::Message; |
|
||||
} |
|
||||
|
|
||||
impl SigHashExt for SigHash { |
|
||||
fn to_message(self) -> secp256k1_zkp::Message { |
|
||||
use secp256k1_zkp::bitcoin_hashes::Hash; |
|
||||
let hash = secp256k1_zkp::bitcoin_hashes::sha256d::Hash::from_inner(*self.as_inner()); |
|
||||
|
|
||||
hash.into() |
|
||||
} |
|
||||
} |
|
@ -1,26 +0,0 @@ |
|||||
use anyhow::{Context, Result}; |
|
||||
use bdk::bitcoin::{OutPoint, Script, Transaction}; |
|
||||
|
|
||||
pub trait TransactionExt { |
|
||||
fn get_virtual_size(&self) -> f64; |
|
||||
fn outpoint(&self, script_pubkey: &Script) -> Result<OutPoint>; |
|
||||
} |
|
||||
|
|
||||
impl TransactionExt for Transaction { |
|
||||
fn get_virtual_size(&self) -> f64 { |
|
||||
self.get_weight() as f64 / 4.0 |
|
||||
} |
|
||||
|
|
||||
fn outpoint(&self, script_pubkey: &Script) -> Result<OutPoint> { |
|
||||
let vout = self |
|
||||
.output |
|
||||
.iter() |
|
||||
.position(|out| &out.script_pubkey == script_pubkey) |
|
||||
.context("script pubkey not found in tx")?; |
|
||||
|
|
||||
Ok(OutPoint { |
|
||||
txid: self.txid(), |
|
||||
vout: vout as u32, |
|
||||
}) |
|
||||
} |
|
||||
} |
|
@ -1,596 +0,0 @@ |
|||||
use crate::protocol::sighash_ext::SigHashExt; |
|
||||
use crate::protocol::transaction_ext::TransactionExt; |
|
||||
use crate::protocol::txin_ext::TxInExt; |
|
||||
use crate::protocol::{ |
|
||||
commit_descriptor, compute_adaptor_pk, lock_descriptor, Payout, DUMMY_2OF2_MULTISIG, |
|
||||
}; |
|
||||
use anyhow::{Context, Result}; |
|
||||
use bdk::bitcoin::util::bip143::SigHashCache; |
|
||||
use bdk::bitcoin::util::psbt::{Global, PartiallySignedTransaction}; |
|
||||
use bdk::bitcoin::{ |
|
||||
Address, Amount, OutPoint, PublicKey, SigHash, SigHashType, Transaction, TxIn, TxOut, |
|
||||
}; |
|
||||
use bdk::descriptor::Descriptor; |
|
||||
use bdk::miniscript::DescriptorTrait; |
|
||||
use itertools::Itertools; |
|
||||
use secp256k1_zkp::{self, schnorrsig, EcdsaAdaptorSignature, SecretKey, SECP256K1}; |
|
||||
use std::collections::HashMap; |
|
||||
use std::iter::FromIterator; |
|
||||
use std::num::NonZeroU8; |
|
||||
|
|
||||
/// In satoshi per vbyte.
|
|
||||
const SATS_PER_VBYTE: f64 = 1.0; |
|
||||
|
|
||||
pub(crate) fn lock_transaction( |
|
||||
maker_psbt: PartiallySignedTransaction, |
|
||||
taker_psbt: PartiallySignedTransaction, |
|
||||
maker_pk: PublicKey, |
|
||||
taker_pk: PublicKey, |
|
||||
amount: Amount, |
|
||||
) -> PartiallySignedTransaction { |
|
||||
let lock_descriptor = lock_descriptor(maker_pk, taker_pk); |
|
||||
|
|
||||
let maker_change = maker_psbt |
|
||||
.global |
|
||||
.unsigned_tx |
|
||||
.output |
|
||||
.into_iter() |
|
||||
.filter(|out| { |
|
||||
out.script_pubkey != DUMMY_2OF2_MULTISIG.parse().expect("To be a valid script") |
|
||||
}) |
|
||||
.collect::<Vec<_>>(); |
|
||||
|
|
||||
let taker_change = taker_psbt |
|
||||
.global |
|
||||
.unsigned_tx |
|
||||
.output |
|
||||
.into_iter() |
|
||||
.filter(|out| { |
|
||||
out.script_pubkey != DUMMY_2OF2_MULTISIG.parse().expect("To be a valid script") |
|
||||
}) |
|
||||
.collect::<Vec<_>>(); |
|
||||
|
|
||||
let lock_output = TxOut { |
|
||||
value: amount.as_sat(), |
|
||||
script_pubkey: lock_descriptor.script_pubkey(), |
|
||||
}; |
|
||||
|
|
||||
let input = vec![ |
|
||||
maker_psbt.global.unsigned_tx.input, |
|
||||
taker_psbt.global.unsigned_tx.input, |
|
||||
] |
|
||||
.concat(); |
|
||||
|
|
||||
let output = std::iter::once(lock_output) |
|
||||
.chain(maker_change) |
|
||||
.chain(taker_change) |
|
||||
.collect(); |
|
||||
|
|
||||
let lock_tx = Transaction { |
|
||||
version: 2, |
|
||||
lock_time: 0, |
|
||||
input, |
|
||||
output, |
|
||||
}; |
|
||||
|
|
||||
PartiallySignedTransaction { |
|
||||
global: Global::from_unsigned_tx(lock_tx).expect("to be unsigned"), |
|
||||
inputs: vec![maker_psbt.inputs, taker_psbt.inputs].concat(), |
|
||||
outputs: vec![maker_psbt.outputs, taker_psbt.outputs].concat(), |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[derive(Debug, Clone)] |
|
||||
pub(crate) struct CommitTransaction { |
|
||||
inner: Transaction, |
|
||||
descriptor: Descriptor<PublicKey>, |
|
||||
amount: Amount, |
|
||||
sighash: SigHash, |
|
||||
lock_descriptor: Descriptor<PublicKey>, |
|
||||
fee: Fee, |
|
||||
} |
|
||||
|
|
||||
impl CommitTransaction { |
|
||||
/// Expected size of signed transaction in virtual bytes, plus a
|
|
||||
/// buffer to account for different signature lengths.
|
|
||||
const SIGNED_VBYTES: f64 = 148.5 + (3.0 * 2.0) / 4.0; |
|
||||
|
|
||||
pub(crate) fn new( |
|
||||
lock_tx: &Transaction, |
|
||||
(maker_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|
||||
(taker_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|
||||
) -> Result<Self> { |
|
||||
let lock_descriptor = lock_descriptor(maker_pk, taker_pk); |
|
||||
let (lock_outpoint, lock_amount) = { |
|
||||
let outpoint = lock_tx |
|
||||
.outpoint(&lock_descriptor.script_pubkey()) |
|
||||
.context("lock script not found in lock tx")?; |
|
||||
let amount = lock_tx.output[outpoint.vout as usize].value; |
|
||||
|
|
||||
(outpoint, amount) |
|
||||
}; |
|
||||
|
|
||||
let lock_input = TxIn { |
|
||||
previous_output: lock_outpoint, |
|
||||
..Default::default() |
|
||||
}; |
|
||||
|
|
||||
let descriptor = commit_descriptor( |
|
||||
(maker_pk, maker_rev_pk, maker_publish_pk), |
|
||||
(taker_pk, taker_rev_pk, taker_publish_pk), |
|
||||
); |
|
||||
|
|
||||
let output = TxOut { |
|
||||
value: lock_amount, |
|
||||
script_pubkey: descriptor.script_pubkey(), |
|
||||
}; |
|
||||
|
|
||||
let mut inner = Transaction { |
|
||||
version: 2, |
|
||||
lock_time: 0, |
|
||||
input: vec![lock_input], |
|
||||
output: vec![output], |
|
||||
}; |
|
||||
let fee = Fee::new(Self::SIGNED_VBYTES); |
|
||||
|
|
||||
let commit_tx_amount = lock_amount - fee.as_u64(); |
|
||||
inner.output[0].value = commit_tx_amount; |
|
||||
|
|
||||
let sighash = SigHashCache::new(&inner).signature_hash( |
|
||||
0, |
|
||||
&lock_descriptor.script_code(), |
|
||||
lock_amount, |
|
||||
SigHashType::All, |
|
||||
); |
|
||||
|
|
||||
Ok(Self { |
|
||||
inner, |
|
||||
descriptor, |
|
||||
lock_descriptor, |
|
||||
amount: Amount::from_sat(commit_tx_amount), |
|
||||
sighash, |
|
||||
fee, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
pub(crate) fn encsign( |
|
||||
&self, |
|
||||
sk: SecretKey, |
|
||||
publish_them_pk: &PublicKey, |
|
||||
) -> EcdsaAdaptorSignature { |
|
||||
EcdsaAdaptorSignature::encrypt( |
|
||||
SECP256K1, |
|
||||
&self.sighash.to_message(), |
|
||||
&sk, |
|
||||
&publish_them_pk.key, |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
pub(crate) fn into_inner(self) -> Transaction { |
|
||||
self.inner |
|
||||
} |
|
||||
|
|
||||
fn outpoint(&self) -> OutPoint { |
|
||||
self.inner |
|
||||
.outpoint(&self.descriptor.script_pubkey()) |
|
||||
.expect("to find commit output in commit tx") |
|
||||
} |
|
||||
|
|
||||
fn amount(&self) -> Amount { |
|
||||
self.amount |
|
||||
} |
|
||||
|
|
||||
fn descriptor(&self) -> Descriptor<PublicKey> { |
|
||||
self.descriptor.clone() |
|
||||
} |
|
||||
|
|
||||
fn fee(&self) -> u64 { |
|
||||
self.fee.as_u64() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[derive(Debug, Clone)] |
|
||||
pub(crate) struct ContractExecutionTransaction { |
|
||||
inner: Transaction, |
|
||||
index_nonce_pairs: Vec<(NonZeroU8, schnorrsig::PublicKey)>, |
|
||||
sighash: SigHash, |
|
||||
commit_descriptor: Descriptor<PublicKey>, |
|
||||
} |
|
||||
|
|
||||
impl ContractExecutionTransaction { |
|
||||
/// Expected size of signed transaction in virtual bytes, plus a
|
|
||||
/// buffer to account for different signature lengths.
|
|
||||
const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; |
|
||||
|
|
||||
pub(crate) fn new( |
|
||||
commit_tx: &CommitTransaction, |
|
||||
payout: Payout, |
|
||||
maker_address: &Address, |
|
||||
taker_address: &Address, |
|
||||
nonce_pks: &[schnorrsig::PublicKey], |
|
||||
relative_timelock_in_blocks: u32, |
|
||||
) -> Result<Self> { |
|
||||
let index_nonce_pairs: Vec<_> = payout |
|
||||
.digits |
|
||||
.to_indices() |
|
||||
.into_iter() |
|
||||
.zip(nonce_pks.iter().cloned()) |
|
||||
.collect(); |
|
||||
|
|
||||
let commit_input = TxIn { |
|
||||
previous_output: commit_tx.outpoint(), |
|
||||
sequence: relative_timelock_in_blocks, |
|
||||
..Default::default() |
|
||||
}; |
|
||||
|
|
||||
let fee = Fee::new(Self::SIGNED_VBYTES).add(commit_tx.fee() as f64); |
|
||||
let output = payout |
|
||||
.with_updated_fee( |
|
||||
Amount::from_sat(fee.as_u64()), |
|
||||
maker_address.script_pubkey().dust_value(), |
|
||||
taker_address.script_pubkey().dust_value(), |
|
||||
)? |
|
||||
.into_txouts(maker_address, taker_address); |
|
||||
|
|
||||
let tx = Transaction { |
|
||||
version: 2, |
|
||||
lock_time: 0, |
|
||||
input: vec![commit_input], |
|
||||
output, |
|
||||
}; |
|
||||
|
|
||||
let sighash = SigHashCache::new(&tx).signature_hash( |
|
||||
0, |
|
||||
&commit_tx.descriptor.script_code(), |
|
||||
commit_tx.amount.as_sat(), |
|
||||
SigHashType::All, |
|
||||
); |
|
||||
|
|
||||
Ok(Self { |
|
||||
inner: tx, |
|
||||
index_nonce_pairs, |
|
||||
sighash, |
|
||||
commit_descriptor: commit_tx.descriptor(), |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
pub(crate) fn encsign( |
|
||||
&self, |
|
||||
sk: SecretKey, |
|
||||
oracle_pk: &schnorrsig::PublicKey, |
|
||||
) -> Result<EcdsaAdaptorSignature> { |
|
||||
let adaptor_point = compute_adaptor_pk(oracle_pk, &self.index_nonce_pairs)?; |
|
||||
|
|
||||
Ok(EcdsaAdaptorSignature::encrypt( |
|
||||
SECP256K1, |
|
||||
&self.sighash.to_message(), |
|
||||
&sk, |
|
||||
&adaptor_point, |
|
||||
)) |
|
||||
} |
|
||||
|
|
||||
pub(crate) fn into_inner(self) -> Transaction { |
|
||||
self.inner |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[derive(Debug, Clone)] |
|
||||
pub(crate) struct RefundTransaction { |
|
||||
inner: Transaction, |
|
||||
sighash: SigHash, |
|
||||
commit_output_descriptor: Descriptor<PublicKey>, |
|
||||
} |
|
||||
|
|
||||
impl RefundTransaction { |
|
||||
/// Expected size of signed transaction in virtual bytes, plus a
|
|
||||
/// buffer to account for different signature lengths.
|
|
||||
const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; |
|
||||
|
|
||||
pub(crate) fn new( |
|
||||
commit_tx: &CommitTransaction, |
|
||||
relative_locktime_in_blocks: u32, |
|
||||
maker_address: &Address, |
|
||||
taker_address: &Address, |
|
||||
maker_amount: Amount, |
|
||||
taker_amount: Amount, |
|
||||
) -> Self { |
|
||||
let commit_input = TxIn { |
|
||||
previous_output: commit_tx.outpoint(), |
|
||||
sequence: relative_locktime_in_blocks, |
|
||||
..Default::default() |
|
||||
}; |
|
||||
|
|
||||
let maker_output = TxOut { |
|
||||
value: maker_amount.as_sat(), |
|
||||
script_pubkey: maker_address.script_pubkey(), |
|
||||
}; |
|
||||
|
|
||||
let taker_output = TxOut { |
|
||||
value: taker_amount.as_sat(), |
|
||||
script_pubkey: taker_address.script_pubkey(), |
|
||||
}; |
|
||||
|
|
||||
let mut tx = Transaction { |
|
||||
version: 2, |
|
||||
lock_time: 0, |
|
||||
input: vec![commit_input], |
|
||||
output: vec![maker_output, taker_output], |
|
||||
}; |
|
||||
|
|
||||
let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; |
|
||||
fee += commit_tx.fee() as f64; |
|
||||
tx.output[0].value -= (fee / 2.0) as u64; |
|
||||
tx.output[1].value -= (fee / 2.0) as u64; |
|
||||
|
|
||||
let commit_output_descriptor = commit_tx.descriptor(); |
|
||||
|
|
||||
let sighash = SigHashCache::new(&tx).signature_hash( |
|
||||
0, |
|
||||
&commit_tx.descriptor().script_code(), |
|
||||
commit_tx.amount().as_sat(), |
|
||||
SigHashType::All, |
|
||||
); |
|
||||
|
|
||||
Self { |
|
||||
inner: tx, |
|
||||
sighash, |
|
||||
commit_output_descriptor, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
pub(crate) fn sighash(&self) -> SigHash { |
|
||||
self.sighash |
|
||||
} |
|
||||
|
|
||||
pub(crate) fn into_inner(self) -> Transaction { |
|
||||
self.inner |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// Build a transaction which closes the CFD contract.
|
|
||||
///
|
|
||||
/// This transaction spends directly from the lock transaction. Both
|
|
||||
/// parties must agree on the split of coins between `maker_amount`
|
|
||||
/// and `taker_amount`.
|
|
||||
pub fn close_transaction( |
|
||||
lock_descriptor: &Descriptor<PublicKey>, |
|
||||
lock_outpoint: OutPoint, |
|
||||
lock_amount: Amount, |
|
||||
(maker_address, maker_amount): (&Address, Amount), |
|
||||
(taker_address, taker_amount): (&Address, Amount), |
|
||||
) -> Result<(Transaction, secp256k1_zkp::Message)> { |
|
||||
/// Expected size of signed transaction in virtual bytes, plus a
|
|
||||
/// buffer to account for different signature lengths.
|
|
||||
const SIGNED_VBYTES: f64 = 167.5 + (3.0 * 2.0) / 4.0; |
|
||||
|
|
||||
let lock_input = TxIn { |
|
||||
previous_output: lock_outpoint, |
|
||||
..Default::default() |
|
||||
}; |
|
||||
|
|
||||
// TODO: The fee could take into account the network state in this
|
|
||||
// case, since this transaction is to be broadcast immediately
|
|
||||
// after building and signing it
|
|
||||
let (maker_fee, taker_fee) = Fee::new(SIGNED_VBYTES).split(); |
|
||||
|
|
||||
let maker_output = TxOut { |
|
||||
value: maker_amount.as_sat() - maker_fee, |
|
||||
script_pubkey: maker_address.script_pubkey(), |
|
||||
}; |
|
||||
let taker_output = TxOut { |
|
||||
value: taker_amount.as_sat() - taker_fee, |
|
||||
script_pubkey: taker_address.script_pubkey(), |
|
||||
}; |
|
||||
|
|
||||
let tx = Transaction { |
|
||||
version: 2, |
|
||||
lock_time: 0, |
|
||||
input: vec![lock_input], |
|
||||
output: vec![maker_output, taker_output], |
|
||||
}; |
|
||||
|
|
||||
let sighash = SigHashCache::new(&tx) |
|
||||
.signature_hash( |
|
||||
0, |
|
||||
&lock_descriptor.script_code(), |
|
||||
lock_amount.as_sat(), |
|
||||
SigHashType::All, |
|
||||
) |
|
||||
.to_message(); |
|
||||
|
|
||||
Ok((tx, sighash)) |
|
||||
} |
|
||||
|
|
||||
pub fn punish_transaction( |
|
||||
commit_descriptor: &Descriptor<PublicKey>, |
|
||||
address: &Address, |
|
||||
encsig: EcdsaAdaptorSignature, |
|
||||
sk: SecretKey, |
|
||||
revocation_them_sk: SecretKey, |
|
||||
pub_them_pk: PublicKey, |
|
||||
revoked_commit_tx: &Transaction, |
|
||||
) -> Result<Transaction> { |
|
||||
/// Expected size of signed transaction in virtual bytes, plus a
|
|
||||
/// buffer to account for different signature lengths.
|
|
||||
const SIGNED_VBYTES: f64 = 219.5 + (3.0 * 3.0) / 4.0; |
|
||||
|
|
||||
let input = revoked_commit_tx |
|
||||
.input |
|
||||
.clone() |
|
||||
.into_iter() |
|
||||
.exactly_one() |
|
||||
.context("commit transaction inputs != 1")?; |
|
||||
|
|
||||
let publish_them_sk = input |
|
||||
.find_map_signature(|sig| encsig.recover(SECP256K1, &sig, &pub_them_pk.key).ok()) |
|
||||
.context("could not recover publish sk from commit tx")?; |
|
||||
|
|
||||
let commit_outpoint = revoked_commit_tx |
|
||||
.outpoint(&commit_descriptor.script_pubkey()) |
|
||||
.expect("to find commit output in commit tx"); |
|
||||
let commit_amount = revoked_commit_tx.output[commit_outpoint.vout as usize].value; |
|
||||
|
|
||||
let mut punish_tx = { |
|
||||
let output = TxOut { |
|
||||
value: commit_amount, |
|
||||
script_pubkey: address.script_pubkey(), |
|
||||
}; |
|
||||
let mut tx = Transaction { |
|
||||
version: 2, |
|
||||
lock_time: 0, |
|
||||
input: vec![TxIn { |
|
||||
previous_output: commit_outpoint, |
|
||||
..Default::default() |
|
||||
}], |
|
||||
output: vec![output], |
|
||||
}; |
|
||||
|
|
||||
let fee = SIGNED_VBYTES * SATS_PER_VBYTE; |
|
||||
tx.output[0].value = commit_amount - fee as u64; |
|
||||
|
|
||||
tx |
|
||||
}; |
|
||||
|
|
||||
let sighash = SigHashCache::new(&punish_tx).signature_hash( |
|
||||
0, |
|
||||
&commit_descriptor.script_code(), |
|
||||
commit_amount, |
|
||||
SigHashType::All, |
|
||||
); |
|
||||
|
|
||||
let satisfier = { |
|
||||
let pk = { |
|
||||
let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &sk); |
|
||||
PublicKey { |
|
||||
compressed: true, |
|
||||
key, |
|
||||
} |
|
||||
}; |
|
||||
let pk_hash = pk.pubkey_hash().as_hash(); |
|
||||
let sig_sk = SECP256K1.sign(&sighash.to_message(), &sk); |
|
||||
|
|
||||
let pub_them_pk_hash = pub_them_pk.pubkey_hash().as_hash(); |
|
||||
let sig_pub_them = SECP256K1.sign(&sighash.to_message(), &publish_them_sk); |
|
||||
|
|
||||
let rev_them_pk = { |
|
||||
let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &revocation_them_sk); |
|
||||
PublicKey { |
|
||||
compressed: true, |
|
||||
key, |
|
||||
} |
|
||||
}; |
|
||||
let rev_them_pk_hash = rev_them_pk.pubkey_hash().as_hash(); |
|
||||
let sig_rev_them = SECP256K1.sign(&sighash.to_message(), &revocation_them_sk); |
|
||||
|
|
||||
let sighash_all = SigHashType::All; |
|
||||
HashMap::from_iter(vec![ |
|
||||
(pk_hash, (pk, (sig_sk, sighash_all))), |
|
||||
(pub_them_pk_hash, (pub_them_pk, (sig_pub_them, sighash_all))), |
|
||||
(rev_them_pk_hash, (rev_them_pk, (sig_rev_them, sighash_all))), |
|
||||
]) |
|
||||
}; |
|
||||
|
|
||||
commit_descriptor.satisfy(&mut punish_tx.input[0], satisfier)?; |
|
||||
|
|
||||
Ok(punish_tx) |
|
||||
} |
|
||||
|
|
||||
#[derive(Clone, Debug)] |
|
||||
struct Fee { |
|
||||
fee: f64, |
|
||||
} |
|
||||
|
|
||||
impl Fee { |
|
||||
fn new(signed_vbytes: f64) -> Self { |
|
||||
let fee = signed_vbytes * SATS_PER_VBYTE; |
|
||||
Self { fee } |
|
||||
} |
|
||||
|
|
||||
#[must_use] |
|
||||
fn add(self, number: f64) -> Fee { |
|
||||
Fee { |
|
||||
fee: self.fee + number, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
fn as_u64(&self) -> u64 { |
|
||||
// Ceil to prevent going lower than the min relay fee
|
|
||||
self.fee.ceil() as u64 |
|
||||
} |
|
||||
|
|
||||
fn split(&self) -> (u64, u64) { |
|
||||
let half = self.as_u64() / 2; |
|
||||
(half as u64, self.as_u64() - half) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#[cfg(test)] |
|
||||
mod tests { |
|
||||
use super::*; |
|
||||
use proptest::prelude::*; |
|
||||
|
|
||||
proptest! { |
|
||||
#[test] |
|
||||
fn test_fee_always_above_min_relay_fee(signed_vbytes in 1.0f64..100_000_000.0f64) { |
|
||||
let fee = Fee::new(signed_vbytes); |
|
||||
let (maker_fee, taker_fee) = fee.split(); |
|
||||
|
|
||||
prop_assert!(signed_vbytes <= fee.as_u64() as f64); |
|
||||
prop_assert!(signed_vbytes <= (maker_fee + taker_fee) as f64); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// A bunch of tests illustrating how fees are split
|
|
||||
|
|
||||
#[test] |
|
||||
fn test_splitting_fee_1_0() { |
|
||||
const SIGNED_VBYTES_TEST: f64 = 1.0; |
|
||||
|
|
||||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|
||||
let (maker_fee, taker_fee) = fee.split(); |
|
||||
|
|
||||
assert_eq!(fee.as_u64(), 1); |
|
||||
assert_eq!(maker_fee, 0); |
|
||||
assert_eq!(taker_fee, 1); |
|
||||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|
||||
} |
|
||||
|
|
||||
#[test] |
|
||||
fn test_splitting_fee_2_0() { |
|
||||
const SIGNED_VBYTES_TEST: f64 = 2.0; |
|
||||
|
|
||||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|
||||
let (maker_fee, taker_fee) = fee.split(); |
|
||||
|
|
||||
assert_eq!(fee.as_u64(), 2); |
|
||||
assert_eq!(maker_fee, 1); |
|
||||
assert_eq!(taker_fee, 1); |
|
||||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|
||||
} |
|
||||
|
|
||||
#[test] |
|
||||
fn test_splitting_fee_2_1() { |
|
||||
const SIGNED_VBYTES_TEST: f64 = 2.1; |
|
||||
|
|
||||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|
||||
let (maker_fee, taker_fee) = fee.split(); |
|
||||
|
|
||||
assert_eq!(fee.as_u64(), 3); |
|
||||
assert_eq!(maker_fee, 1); |
|
||||
assert_eq!(taker_fee, 2); |
|
||||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|
||||
} |
|
||||
|
|
||||
#[test] |
|
||||
fn test_splitting_fee_2_6() { |
|
||||
const SIGNED_VBYTES_TEST: f64 = 2.6; |
|
||||
|
|
||||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|
||||
let (maker_fee, taker_fee) = fee.split(); |
|
||||
|
|
||||
assert_eq!(fee.as_u64(), 3); |
|
||||
assert_eq!(maker_fee, 1); |
|
||||
assert_eq!(taker_fee, 2); |
|
||||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|
||||
} |
|
||||
} |
|
@ -1,23 +0,0 @@ |
|||||
use bdk::bitcoin::secp256k1::Signature; |
|
||||
use bdk::bitcoin::TxIn; |
|
||||
|
|
||||
pub(crate) trait TxInExt { |
|
||||
fn find_map_signature<F, R>(&self, f: F) -> Option<R> |
|
||||
where |
|
||||
F: Fn(Signature) -> Option<R>; |
|
||||
} |
|
||||
|
|
||||
impl TxInExt for TxIn { |
|
||||
fn find_map_signature<F, R>(&self, f: F) -> Option<R> |
|
||||
where |
|
||||
F: Fn(Signature) -> Option<R>, |
|
||||
{ |
|
||||
self.witness |
|
||||
.iter() |
|
||||
.filter_map(|elem| { |
|
||||
let elem = elem.as_slice(); |
|
||||
Signature::from_der(&elem[..elem.len() - 1]).ok() |
|
||||
}) |
|
||||
.find_map(f) |
|
||||
} |
|
||||
} |
|
File diff suppressed because it is too large
Loading…
Reference in new issue