Browse Source

Depend on cfd protocol from `maia` repository

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.
rollover-test
Mariusz Klochowicz 3 years ago
parent
commit
b3abfd027b
No known key found for this signature in database GPG Key ID: 470C865699C8D4D
  1. 76
      Cargo.lock
  2. 3
      Cargo.toml
  3. 17
      cfd_protocol/Cargo.toml
  4. 157
      cfd_protocol/src/interval.rs
  5. 459
      cfd_protocol/src/interval/digit_decomposition.rs
  6. 12
      cfd_protocol/src/lib.rs
  7. 40
      cfd_protocol/src/oracle.rs
  8. 573
      cfd_protocol/src/protocol.rs
  9. 15
      cfd_protocol/src/protocol/sighash_ext.rs
  10. 26
      cfd_protocol/src/protocol/transaction_ext.rs
  11. 596
      cfd_protocol/src/protocol/transactions.rs
  12. 23
      cfd_protocol/src/protocol/txin_ext.rs
  13. 1184
      cfd_protocol/tests/cfds.rs
  14. 2
      daemon/Cargo.toml
  15. 2
      daemon/src/lib.rs
  16. 2
      daemon/src/maker_cfd.rs
  17. 8
      daemon/src/model/cfd.rs
  18. 8
      daemon/src/oracle.rs
  19. 2
      daemon/src/payout_curve.rs
  20. 8
      daemon/src/setup_contract.rs
  21. 3
      daemon/src/wallet.rs
  22. 4
      daemon/src/wire.rs
  23. 2
      daemon/tests/happy_path.rs
  24. 6
      daemon/tests/harness/maia.rs
  25. 2
      daemon/tests/harness/mocks/oracle.rs
  26. 6
      daemon/tests/harness/mocks/wallet.rs
  27. 2
      daemon/tests/harness/mod.rs

76
Cargo.lock

@ -264,7 +264,6 @@ dependencies = [
"base64-compat",
"bech32",
"bitcoin_hashes 0.10.0",
"bitcoinconsensus",
"secp256k1",
"serde",
]
@ -284,16 +283,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bitcoinconsensus"
version = "0.19.0-3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a8aa43b5cd02f856cb126a9af819e77b8910fdd74dd1407be649f2f5fe3a1b5"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -368,21 +357,6 @@ dependencies = [
"jobserver",
]
[[package]]
name = "cfd_protocol"
version = "0.1.0"
dependencies = [
"anyhow",
"bdk",
"bit-vec",
"bitcoin",
"itertools",
"proptest",
"rand 0.6.5",
"secp256k1-zkp",
"thiserror",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
@ -634,7 +608,6 @@ dependencies = [
"atty",
"bdk",
"bytes",
"cfd_protocol",
"chrono",
"clap",
"derive_more",
@ -643,6 +616,7 @@ dependencies = [
"hkdf",
"http-api-problem",
"itertools",
"maia",
"mockall",
"mockall_derive",
"nalgebra",
@ -1470,6 +1444,20 @@ dependencies = [
"serde_json",
]
[[package]]
name = "maia"
version = "0.1.0"
source = "git+https://github.com/comit-network/maia?rev=70fc548da0fe4f34478fb34ec437fa9a434c7ee3#70fc548da0fe4f34478fb34ec437fa9a434c7ee3"
dependencies = [
"anyhow",
"bdk",
"bit-vec",
"itertools",
"rand 0.6.5",
"secp256k1-zkp",
"thiserror",
]
[[package]]
name = "matchers"
version = "0.0.1"
@ -2021,29 +2009,6 @@ dependencies = [
"yansi",
]
[[package]]
name = "proptest"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
dependencies = [
"bitflags",
"byteorder",
"lazy_static",
"num-traits",
"quick-error",
"rand 0.8.4",
"rand_chacha 0.3.1",
"rand_xorshift 0.3.0",
"regex-syntax",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "1.0.10"
@ -2068,7 +2033,7 @@ dependencies = [
"rand_jitter",
"rand_os",
"rand_pcg",
"rand_xorshift 0.1.1",
"rand_xorshift",
"winapi 0.3.9",
]
@ -2240,15 +2205,6 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core 0.6.3",
]
[[package]]
name = "rawpointer"
version = "0.2.1"

3
Cargo.toml

@ -1,8 +1,9 @@
[workspace]
members = ["cfd_protocol", "daemon", "xtra_productivity"]
members = ["daemon", "xtra_productivity"]
resolver = "2"
[patch.crates-io]
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.
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" }

17
cfd_protocol/Cargo.toml

@ -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"] }

157
cfd_protocol/src/interval.rs

@ -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())
}
}
}

459
cfd_protocol/src/interval/digit_decomposition.rs

@ -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
)
);
}
}
}

12
cfd_protocol/src/lib.rs

@ -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;

40
cfd_protocol/src/oracle.rs

@ -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
}

573
cfd_protocol/src/protocol.rs

@ -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))
);
}
}
}

15
cfd_protocol/src/protocol/sighash_ext.rs

@ -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()
}
}

26
cfd_protocol/src/protocol/transaction_ext.rs

@ -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,
})
}
}

596
cfd_protocol/src/protocol/transactions.rs

@ -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);
}
}

23
cfd_protocol/src/protocol/txin_ext.rs

@ -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)
}
}

1184
cfd_protocol/tests/cfds.rs

File diff suppressed because it is too large

2
daemon/Cargo.toml

@ -9,7 +9,6 @@ async-trait = "0.1.51"
atty = "0.2"
bdk = { version = "0.13", default-features = false, features = ["sqlite", "electrum"] }
bytes = "1"
cfd_protocol = { path = "../cfd_protocol" }
chrono = { version = "0.4", features = ["serde"] }
clap = "3.0.0-beta.5"
derive_more = { version = "0.99.16", default-features = false, features = ["display"] }
@ -18,6 +17,7 @@ hex = "0.4"
hkdf = "0.11"
http-api-problem = { version = "0.51.0", features = ["rocket"] }
itertools = "0.10"
maia = "0.1.0"
nalgebra = { version = "0.29", default-features = false, features = ["std"] }
ndarray = "0.15.3"
ndarray_einsum_beta = "0.7.0"

2
daemon/src/lib.rs

@ -4,8 +4,8 @@ use crate::maker_cfd::{FromTaker, NewTakerOnline};
use crate::model::cfd::{Cfd, Order, UpdateCfdProposals};
use crate::oracle::Attestation;
use anyhow::Result;
use cfd_protocol::secp256k1_zkp::schnorrsig;
use futures::Stream;
use maia::secp256k1_zkp::schnorrsig;
use sqlx::SqlitePool;
use std::collections::HashMap;
use std::future::Future;

2
daemon/src/maker_cfd.rs

@ -12,9 +12,9 @@ use crate::{log_error, maker_inc_connections, monitor, oracle, setup_contract, w
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use bdk::bitcoin::secp256k1::schnorrsig;
use cfd_protocol::secp256k1_zkp::Signature;
use futures::channel::mpsc;
use futures::{future, SinkExt};
use maia::secp256k1_zkp::Signature;
use sqlx::pool::PoolConnection;
use sqlx::Sqlite;
use std::collections::HashMap;

8
daemon/src/model/cfd.rs

@ -8,8 +8,8 @@ use bdk::bitcoin::secp256k1::{SecretKey, Signature};
use bdk::bitcoin::{Address, Amount, PublicKey, Script, SignedAmount, Transaction, Txid};
use bdk::descriptor::Descriptor;
use bdk::miniscript::DescriptorTrait;
use cfd_protocol::secp256k1_zkp::{self, EcdsaAdaptorSignature, SECP256K1};
use cfd_protocol::{finalize_spend_transaction, spending_tx_sighash, TransactionExt};
use maia::secp256k1_zkp::{self, EcdsaAdaptorSignature, SECP256K1};
use maia::{finalize_spend_transaction, spending_tx_sighash, TransactionExt};
use rocket::request::FromParam;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
@ -1715,7 +1715,7 @@ impl Dlc {
(outpoint, amount)
};
let (tx, sighash) = cfd_protocol::close_transaction(
let (tx, sighash) = maia::close_transaction(
lock_desc,
lock_outpoint,
lock_amount,
@ -1740,7 +1740,7 @@ impl Dlc {
));
let (_, lock_desc) = &self.lock;
let spend_tx = cfd_protocol::finalize_spend_transaction(
let spend_tx = maia::finalize_spend_transaction(
close_tx,
lock_desc,
(own_pk, own_sig),

8
daemon/src/oracle.rs

@ -3,7 +3,7 @@ use crate::model::BitMexPriceEventId;
use crate::{log_error, tokio_ext, try_continue};
use anyhow::{Context, Result};
use async_trait::async_trait;
use cfd_protocol::secp256k1_zkp::{schnorrsig, SecretKey};
use maia::secp256k1_zkp::{schnorrsig, SecretKey};
use rocket::time::{OffsetDateTime, Time};
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
@ -278,9 +278,9 @@ pub struct Announcement {
pub nonce_pks: Vec<schnorrsig::PublicKey>,
}
impl From<Announcement> for cfd_protocol::Announcement {
impl From<Announcement> for maia::Announcement {
fn from(announcement: Announcement) -> Self {
cfd_protocol::Announcement {
maia::Announcement {
id: announcement.id.to_string(),
nonce_pks: announcement.nonce_pks,
}
@ -300,7 +300,7 @@ impl xtra::Message for NewAttestationFetched {
mod olivia_api {
use crate::model::BitMexPriceEventId;
use anyhow::Context;
use cfd_protocol::secp256k1_zkp::{schnorrsig, SecretKey};
use maia::secp256k1_zkp::{schnorrsig, SecretKey};
use std::convert::TryFrom;
use time::OffsetDateTime;

2
daemon/src/payout_curve.rs

@ -4,8 +4,8 @@ use crate::model::{Leverage, Price, Usd};
use crate::payout_curve::curve::Curve;
use anyhow::{Context, Result};
use bdk::bitcoin;
use cfd_protocol::{generate_payouts, Payout};
use itertools::Itertools;
use maia::{generate_payouts, Payout};
use ndarray::prelude::*;
use num::{FromPrimitive, ToPrimitive};
use rust_decimal::Decimal;

8
daemon/src/setup_contract.rs

@ -10,14 +10,14 @@ use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
use bdk::bitcoin::{Amount, PublicKey, Transaction};
use bdk::descriptor::Descriptor;
use bdk::miniscript::DescriptorTrait;
use cfd_protocol::secp256k1_zkp::EcdsaAdaptorSignature;
use cfd_protocol::{
use futures::stream::FusedStream;
use futures::{Sink, SinkExt, StreamExt};
use maia::secp256k1_zkp::EcdsaAdaptorSignature;
use maia::{
commit_descriptor, compute_adaptor_pk, create_cfd_transactions, interval, lock_descriptor,
renew_cfd_transactions, secp256k1_zkp, spending_tx_sighash, Announcement, PartyParams,
PunishParams,
};
use futures::stream::FusedStream;
use futures::{Sink, SinkExt, StreamExt};
use std::collections::HashMap;
use std::iter::FromIterator;
use std::ops::RangeInclusive;

3
daemon/src/wallet.rs

@ -7,8 +7,9 @@ use bdk::bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid};
use bdk::blockchain::{ElectrumBlockchain, NoopProgress};
use bdk::wallet::AddressIndex;
use bdk::{electrum_client, FeeRate, KeychainKind, SignOptions};
use cfd_protocol::{PartyParams, WalletExt};
use maia::{PartyParams, WalletExt};
use rocket::serde::json::Value;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;

4
daemon/src/wire.rs

@ -6,8 +6,8 @@ use bdk::bitcoin::secp256k1::Signature;
use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
use bdk::bitcoin::{Address, Amount, PublicKey};
use bytes::BytesMut;
use cfd_protocol::secp256k1_zkp::{EcdsaAdaptorSignature, SecretKey};
use cfd_protocol::{CfdTransactions, PartyParams, PunishParams};
use maia::secp256k1_zkp::{EcdsaAdaptorSignature, SecretKey};
use maia::{CfdTransactions, PartyParams, PunishParams};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use snow::TransportState;

2
daemon/tests/happy_path.rs

@ -3,12 +3,12 @@ use crate::harness::mocks::oracle::dummy_announcement;
use crate::harness::mocks::wallet::build_party_params;
use crate::harness::start_both;
use anyhow::Context;
use cfd_protocol::secp256k1_zkp::schnorrsig;
use daemon::maker_cfd;
use daemon::model::cfd::{Cfd, CfdState, Order, Origin};
use daemon::model::{Price, Usd};
use daemon::tokio_ext::FutureExt;
use harness::bdk::dummy_tx_id;
use maia::secp256k1_zkp::schnorrsig;
use rust_decimal_macros::dec;
use std::time::Duration;
use tokio::sync::{watch, MutexGuard};

6
daemon/tests/harness/cfd_protocol.rs → daemon/tests/harness/maia.rs

@ -2,9 +2,9 @@ use anyhow::Result;
use bdk::bitcoin;
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
use bdk::bitcoin::{Amount, Network};
use cfd_protocol::secp256k1_zkp::rand::{CryptoRng, RngCore};
use cfd_protocol::secp256k1_zkp::{schnorrsig, SecretKey};
use cfd_protocol::Announcement;
use maia::secp256k1_zkp::rand::{CryptoRng, RngCore};
use maia::secp256k1_zkp::{schnorrsig, SecretKey};
use maia::Announcement;
use std::str::FromStr;
pub fn dummy_wallet(

2
daemon/tests/harness/mocks/oracle.rs

@ -1,4 +1,4 @@
use crate::harness::cfd_protocol::OliviaData;
use crate::harness::maia::OliviaData;
use daemon::model::BitMexPriceEventId;
use daemon::oracle;
use mockall::*;

6
daemon/tests/harness/mocks/wallet.rs

@ -1,11 +1,11 @@
use crate::harness::cfd_protocol::dummy_wallet;
use crate::harness::maia::dummy_wallet;
use anyhow::Result;
use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
use bdk::bitcoin::{ecdsa, Amount, Txid};
use cfd_protocol::secp256k1_zkp::Secp256k1;
use cfd_protocol::{PartyParams, WalletExt};
use daemon::model::{Timestamp, WalletInfo};
use daemon::wallet::{self};
use maia::secp256k1_zkp::Secp256k1;
use maia::{PartyParams, WalletExt};
use mockall::*;
use rand::thread_rng;
use std::sync::Arc;

2
daemon/tests/harness/mod.rs

@ -16,7 +16,7 @@ use xtra::spawn::TokioGlobalSpawnExt;
use xtra::Actor;
pub mod bdk;
pub mod cfd_protocol;
pub mod maia;
pub mod mocks;
pub async fn start_both() -> (Maker, Taker) {

Loading…
Cancel
Save