add integration test using regtest

This test validates the basic spending paths are enforceable.
It covers the split TX, followed by three different resolutions
for each individual payout contract. We test to confirm:
- a player can claim winnings using their ticket preimage
- the market maker can reclaim winnings using the payout preimage
- the market maker can reclaim winnings after the timeout
This commit is contained in:
conduition
2024-03-16 01:36:14 +00:00
parent b32bab6c14
commit 61c4be2a68
4 changed files with 715 additions and 0 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target /target
*.old.rs *.old.rs
.env

139
Cargo.lock generated
View File

@@ -8,6 +8,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "bech32" name = "bech32"
version = "0.10.0-beta" version = "0.10.0-beta"
@@ -26,6 +32,7 @@ dependencies = [
"hex-conservative", "hex-conservative",
"hex_lit", "hex_lit",
"secp256k1", "secp256k1",
"serde",
] ]
[[package]] [[package]]
@@ -33,6 +40,9 @@ name = "bitcoin-internals"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"
@@ -42,6 +52,31 @@ checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
dependencies = [ dependencies = [
"bitcoin-internals", "bitcoin-internals",
"hex-conservative", "hex-conservative",
"serde",
]
[[package]]
name = "bitcoincore-rpc"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb70725a621848c83b3809913d5314c0d20ca84877d99dd909504b564edab00"
dependencies = [
"bitcoincore-rpc-json",
"jsonrpc",
"log",
"serde",
"serde_json",
]
[[package]]
name = "bitcoincore-rpc-json"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "856ffbee2e492c23bca715d72ea34aae80d58400f2bda26a82015d6bc2ec3662"
dependencies = [
"bitcoin",
"serde",
"serde_json",
] ]
[[package]] [[package]]
@@ -100,6 +135,8 @@ name = "dlctix"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"bitcoin", "bitcoin",
"bitcoincore-rpc",
"dotenv",
"hex", "hex",
"musig2", "musig2",
"rand", "rand",
@@ -108,6 +145,12 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -156,12 +199,35 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "jsonrpc"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2"
dependencies = [
"base64",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.153" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "musig2" name = "musig2"
version = "0.0.8" version = "0.0.8"
@@ -190,6 +256,24 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -220,6 +304,12 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]] [[package]]
name = "secp" name = "secp"
version = "0.2.2" version = "0.2.2"
@@ -242,6 +332,7 @@ dependencies = [
"bitcoin_hashes", "bitcoin_hashes",
"rand", "rand",
"secp256k1-sys", "secp256k1-sys",
"serde",
] ]
[[package]] [[package]]
@@ -253,6 +344,37 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "serde"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.8" version = "0.10.8"
@@ -270,12 +392,29 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View File

@@ -18,6 +18,10 @@ secp = { version = "0.2.1" }
secp256k1 = { version = "0.28.2", features = ["global-context"] } secp256k1 = { version = "0.28.2", features = ["global-context"] }
sha2 = "0.10.8" sha2 = "0.10.8"
[dev-dependencies]
bitcoincore-rpc = "0.18.0"
dotenv = "0.15.0"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]

571
tests/regtest.rs Normal file
View File

@@ -0,0 +1,571 @@
use bitcoincore_rpc::{jsonrpc::serde_json, Auth, Client as BitcoinClient, RpcApi};
use dlctix::*;
use bitcoin::{
blockdata::transaction::predict_weight,
key::TweakedPublicKey,
locktime::absolute::LockTime,
sighash::{Prevouts, SighashCache, TapSighashType},
Address, Amount, FeeRate, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut,
};
use musig2::{CompactSignature, LiftedSignature, PartialSignature, PubNonce};
use rand::{CryptoRng, RngCore};
use secp::{Point, Scalar};
use std::collections::{BTreeMap, BTreeSet};
const P2TR_SCRIPT_PUBKEY_SIZE: usize = 34;
/// Generate a P2TR address which pays to the given pubkey (no tweak added).
fn p2tr_address(pubkey: Point) -> Address {
let (xonly, _) = pubkey.into();
let tweaked = TweakedPublicKey::dangerous_assume_tweaked(xonly);
Address::p2tr_tweaked(tweaked, Network::Regtest)
}
/// Generate a P2TR script pubkey which pays to the given pubkey (no tweak added).
fn p2tr_script_pubkey(pubkey: Point) -> ScriptBuf {
let (xonly, _) = pubkey.into();
let tweaked = TweakedPublicKey::dangerous_assume_tweaked(xonly);
ScriptBuf::new_p2tr_tweaked(tweaked)
}
/// Build a bitcoind RPC client for regtest. Expects the following environment variables
/// to be defined:
///
/// - `BITCOIND_RPC_URL`
/// - `BITCOIND_RPC_AUTH_USERNAME`
/// - `BITCOIND_RPC_AUTH_PASSWORD`
fn new_rpc_client() -> BitcoinClient {
dotenv::dotenv().unwrap();
let bitcoind_rpc_url =
std::env::var("BITCOIND_RPC_URL").unwrap_or_else(|_| "http://127.0.0.1:18443".to_string());
let bitcoind_auth_username =
std::env::var("BITCOIND_RPC_AUTH_USERNAME").expect("missing BITCOIND_RPC_AUTH_USERNAME");
let bitcoind_auth_password =
std::env::var("BITCOIND_RPC_AUTH_PASSWORD").expect("missing BITCOIND_RPC_AUTH_PASSWORD");
let auth = Auth::UserPass(bitcoind_auth_username, bitcoind_auth_password);
BitcoinClient::new(&bitcoind_rpc_url, auth).expect("failed to create bitcoind RPC client")
}
/// Take some money from the regtest node and deposit it into the given address.
/// Return the outpoint and prevout.
fn take_usable_utxo(rpc: &BitcoinClient, address: &Address, amount: Amount) -> (OutPoint, TxOut) {
let txid: bitcoin::Txid = rpc
.call(
"sendtoaddress",
&[
serde_json::Value::String(address.to_string()),
serde_json::Value::Number(serde_json::Number::from_f64(amount.to_btc()).unwrap()),
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::Value::Null,
// must specify fee rate or the regtest node will fail to estimate it
serde_json::Value::Number(1.into()),
],
)
.unwrap();
let sent_tx = rpc.get_raw_transaction(&txid, None).unwrap();
let (vout, prevout) = sent_tx
.output
.into_iter()
.enumerate()
.find(|(_, output)| output.script_pubkey == address.script_pubkey())
.unwrap();
let outpoint = OutPoint {
txid,
vout: vout as u32,
};
(outpoint, prevout)
}
fn mine_blocks(rpc: &BitcoinClient, n_blocks: u16) -> Result<(), bitcoincore_rpc::Error> {
let address = rpc
.get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m))?
.require_network(bitcoin::Network::Regtest)
.unwrap();
rpc.generate_to_address(n_blocks as u64, &address)?;
Ok(())
}
/// Construct and sign the funding transaction.
fn signed_funding_tx(
market_maker_seckey: Scalar,
funding_output: TxOut,
mm_utxo_outpoint: OutPoint,
mm_utxo_prevout: &TxOut,
) -> Transaction {
let mut funding_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: mm_utxo_outpoint,
..TxIn::default()
}],
output: vec![funding_output],
};
let funding_tx_sighash = SighashCache::new(&funding_tx)
.taproot_key_spend_signature_hash(
0,
&Prevouts::All(&[mm_utxo_prevout]),
TapSighashType::Default,
)
.unwrap();
let signature: CompactSignature =
musig2::deterministic::sign_solo(market_maker_seckey, &funding_tx_sighash);
funding_tx.input[0].witness.push(signature.serialize());
funding_tx
}
/// Represents a simulated DLC player, including the ticket preimage which a player
/// herself may not actually know in a real DLC until having purchased it.
struct SimulatedPlayer {
seckey: Scalar,
ticket_preimage: hashlock::Preimage,
payout_preimage: hashlock::Preimage,
player: Player,
}
impl SimulatedPlayer {
fn random<R: RngCore + CryptoRng>(rng: &mut R) -> SimulatedPlayer {
let seckey = Scalar::random(rng);
let payout_preimage = hashlock::preimage_random(rng);
let ticket_preimage = hashlock::preimage_random(rng);
SimulatedPlayer {
seckey,
payout_preimage,
ticket_preimage,
player: Player {
pubkey: seckey.base_point_mul(),
ticket_hash: hashlock::sha256(&ticket_preimage),
payout_hash: hashlock::sha256(&payout_preimage),
},
}
}
}
/// Cooperatively sign a `TicketedDLC` using the secret keys of every player
/// and the market maker. The order of secret keys in the `all_seckeys` iterator
/// does not matter.
fn musig_sign_ticketed_dlc<R: RngCore + CryptoRng>(
ticketed_dlc: &TicketedDLC,
all_seckeys: impl IntoIterator<Item = Scalar>,
rng: &mut R,
) -> SignedContract {
let signing_sessions: BTreeMap<Point, SigningSession<NonceSharingRound>> = all_seckeys
.into_iter()
.map(|seckey| {
let session = SigningSession::new(ticketed_dlc.clone(), rng, seckey)
.expect("error creating SigningSession");
(seckey.base_point_mul(), session)
})
.collect();
let pubnonces_by_sender: BTreeMap<Point, SigMap<PubNonce>> = signing_sessions
.iter()
.map(|(&sender_pubkey, session)| (sender_pubkey, session.our_public_nonces().clone()))
.collect();
let signing_sessions: BTreeMap<Point, SigningSession<PartialSignatureSharingRound>> =
signing_sessions
.into_iter()
.map(|(pubkey, session)| {
let new_session = session
.compute_partial_signatures(pubnonces_by_sender.clone())
.expect("failed to compute partial signatures");
(pubkey, new_session)
})
.collect();
let partial_sigs_by_sender: BTreeMap<Point, SigMap<PartialSignature>> = signing_sessions
.iter()
.map(|(&sender_pubkey, session)| (sender_pubkey, session.our_partial_signatures().clone()))
.collect();
// Everyone's signatures can be verified by everyone else.
for session in signing_sessions.values() {
for (&sender_pubkey, partial_sigs) in &partial_sigs_by_sender {
session
.verify_partial_signatures(sender_pubkey, partial_sigs)
.expect("valid partial signatures should be verified as OK");
}
}
let mut signed_contracts: BTreeMap<Point, SignedContract> = signing_sessions
.into_iter()
.map(|(pubkey, session)| {
let signed_contract = session
.aggregate_all_signatures(partial_sigs_by_sender.clone())
.expect("error during signature aggregation");
(pubkey, signed_contract)
})
.collect();
// Everyone should have computed the same set of signatures.
for contract1 in signed_contracts.values() {
for contract2 in signed_contracts.values() {
assert_eq!(contract1.all_signatures(), contract2.all_signatures());
}
}
let (_, contract) = signed_contracts.pop_first().unwrap();
contract
}
const FUNDING_VALUE: Amount = Amount::from_sat(200_000);
/// Make sure we're on the regtest network and we have enough bitcoins
/// in the regtest node wallet, otherwise the actual test will not work.
#[test]
fn check_regtest_wallet() {
let rpc_client = new_rpc_client();
let info = rpc_client
.get_mining_info()
.expect("failed to get network info from remote node");
assert_eq!(
info.chain,
bitcoin::Network::Regtest,
"node should be running in regtest mode, found {} instead",
info.chain
);
let mut wallet_info = rpc_client.get_wallet_info().unwrap_or_else(|_| {
if let Some(wallet_name) = rpc_client.list_wallets().unwrap().into_iter().next() {
rpc_client.load_wallet(&wallet_name).unwrap();
} else {
rpc_client
.create_wallet("dlctix_market_maker", None, None, None, None)
.unwrap();
}
rpc_client.get_wallet_info().unwrap()
});
while wallet_info.balance < FUNDING_VALUE + Amount::from_sat(100_000) {
mine_blocks(&rpc_client, 101).expect("error mining blocks");
wallet_info = rpc_client.get_wallet_info().unwrap();
}
}
#[test]
fn simple_ticketed_dlc_simulation() {
let mut rng = rand::thread_rng();
// Oracle
let oracle_seckey = Scalar::random(&mut rng);
let oracle_secnonce = Scalar::random(&mut rng);
// Market maker
let market_maker_seckey = Scalar::random(&mut rng);
let market_maker = MarketMaker {
pubkey: market_maker_seckey.base_point_mul(),
};
let market_maker_address = p2tr_address(market_maker.pubkey);
// players
let alice = SimulatedPlayer::random(&mut rng);
let bob = SimulatedPlayer::random(&mut rng);
let carol = SimulatedPlayer::random(&mut rng);
let dave = SimulatedPlayer::random(&mut rng);
let players = BTreeSet::from([
alice.player.clone(),
bob.player.clone(),
carol.player.clone(),
dave.player.clone(),
]);
let player_indexes: BTreeMap<Player, PlayerIndex> = players
.iter()
.enumerate()
.map(|(i, player)| (player.clone(), i))
.collect();
let rpc = new_rpc_client();
let block_height = rpc.get_block_count().unwrap();
let outcome_payouts = BTreeMap::<Outcome, PayoutWeights>::from([
(
Outcome::Attestation(0),
PayoutWeights::from([
(player_indexes[&alice.player], 1),
(player_indexes[&bob.player], 2),
(player_indexes[&carol.player], 1),
]),
),
(
Outcome::Attestation(1),
PayoutWeights::from([
(player_indexes[&carol.player], 1),
(player_indexes[&dave.player], 3),
]),
),
(
Outcome::Expiry,
PayoutWeights::from([(player_indexes[&alice.player], 1)]),
),
]);
let contract_params = ContractParameters {
market_maker,
players,
event: EventAnnouncement {
oracle_pubkey: oracle_seckey.base_point_mul(),
nonce_point: oracle_secnonce.base_point_mul(),
outcome_messages: vec![
Vec::from(b"alice and bob win"),
Vec::from(b"carol and dave win"),
],
expiry: u32::try_from(block_height + 2000).unwrap(),
},
outcome_payouts,
fee_rate: FeeRate::from_sat_per_vb_unchecked(100),
funding_value: FUNDING_VALUE,
relative_locktime_block_delta: 25,
};
// Fund the market maker
let (mm_utxo_outpoint, mm_utxo_prevout) = take_usable_utxo(
&rpc,
&market_maker_address,
FUNDING_VALUE + Amount::from_sat(50_000),
);
// Prepare a funding transaction
let funding_tx = signed_funding_tx(
market_maker_seckey,
contract_params.funding_output().unwrap(),
mm_utxo_outpoint,
&mm_utxo_prevout,
);
let funding_outpoint = OutPoint {
txid: funding_tx.txid(),
vout: 0,
};
// Construct all the DLC transactions.
let ticketed_dlc = TicketedDLC::new(contract_params, funding_outpoint)
.expect("failed to constructed ticketed DLC transactions");
// Sign all the transactions.
let seckeys = [
market_maker_seckey,
alice.seckey,
bob.seckey,
carol.seckey,
dave.seckey,
];
let signed_contract = musig_sign_ticketed_dlc(&ticketed_dlc, seckeys, &mut rng);
// At this point, the market maker is confident they'll be able to reclaim their
// capital if needed, and the players know they'll be able to enforce the DLC outcome
// if they purchase their ticket preimage.
//
// The market maker can now broadcast the funding TX.
rpc.send_raw_transaction(&funding_tx)
.expect("failed to broadcast funding TX");
mine_blocks(&rpc, 1).unwrap();
let event: &EventAnnouncement = &signed_contract.params().event;
let outcome_index: usize = 0;
// The oracle attests to outcome zero, where Alice, Bob, and Carol are winners.
let oracle_attestation = event
.attestation_secret(outcome_index, oracle_seckey, oracle_secnonce)
.unwrap();
// The attestation should be a valid BIP340 signature by the oracle's pubkey.
{
let oracle_signature = LiftedSignature::new(event.nonce_point, oracle_attestation);
musig2::verify_single(
event.oracle_pubkey,
oracle_signature,
&event.outcome_messages[outcome_index],
)
.expect("invalid oracle signature");
}
// Anyone can unlock and broadcast an outcome TX if they know the attestation.
let outcome_tx = signed_contract
.signed_outcome_tx(outcome_index, oracle_attestation)
.expect("failed to sign outcome TX");
rpc.send_raw_transaction(&outcome_tx)
.expect("failed to broadcast outcome TX");
// Assume Alice bought her ticket preimage. She can now
// use it to unlock the split transaction.
let alice_win_cond = WinCondition {
outcome: Outcome::Attestation(outcome_index),
player_index: player_indexes[&alice.player],
};
let split_tx = signed_contract
.signed_split_tx(&alice_win_cond, alice.ticket_preimage)
.expect("failed to sign split TX");
// Alice should not be able to broadcast the split TX right away,
// due to the relative locktime on the split TX.
let err = rpc
.send_raw_transaction(&split_tx)
.expect_err("early broadcast of split TX should fail");
assert_eq!(
err.to_string(),
"JSON-RPC error: RPC error response: RpcError { code: -26, \
message: \"non-BIP68-final\", data: None }",
);
// Only after a block delay of `delta` should Alice be able to
// broadcast the split TX.
mine_blocks(&rpc, signed_contract.params().relative_locktime_block_delta).unwrap();
rpc.send_raw_transaction(&split_tx)
.expect("failed to broadcast split TX");
// Alice, Bob, and Carol now have separate payout contracts with the market maker.
// Alice paid for her ticket preimage, but wishes to receive a payout off-chain,
// by selling her payout preimage to the market maker. The market maker uses the
// payout preimage to sign a sellback TX which reclaims Alice's winnings before
// she will have a chance to sweep them.
let (alice_split_input, alice_split_prevout) = signed_contract
.split_sellback_tx_input_and_prevout(&alice_win_cond)
.unwrap();
let mut sellback_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::ZERO,
input: vec![alice_split_input],
output: vec![TxOut {
script_pubkey: p2tr_script_pubkey(alice.player.pubkey),
value: {
let sellback_tx_weight = predict_weight(
[signed_contract.split_sellback_tx_input_weight()],
[P2TR_SCRIPT_PUBKEY_SIZE],
);
let fee = sellback_tx_weight * FeeRate::from_sat_per_vb_unchecked(20);
alice_split_prevout.value - fee
},
}],
};
signed_contract
.sign_split_sellback_tx_input(
&alice_win_cond,
&mut sellback_tx,
0, // input index
&Prevouts::All(&[alice_split_prevout]),
alice.payout_preimage,
market_maker_seckey,
)
.unwrap();
// The sellback TX has no relative locktime; it can be broadcast immediately.
rpc.send_raw_transaction(&sellback_tx)
.expect("failed to broadcast the sellback TX");
// Bob will try to claim his winnings using the ticket preimage he bought.
let bob_win_cond = WinCondition {
outcome: Outcome::Attestation(outcome_index),
player_index: player_indexes[&bob.player],
};
let (bob_split_input, bob_split_prevout) = signed_contract
.split_win_tx_input_and_prevout(&bob_win_cond)
.unwrap();
// TODO test OP_CSV by spending without correct min sequence number
let mut bob_win_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::ZERO,
input: vec![bob_split_input],
output: vec![TxOut {
script_pubkey: p2tr_script_pubkey(bob.player.pubkey),
value: {
let win_tx_weight = predict_weight(
[signed_contract.split_win_tx_input_weight()],
[P2TR_SCRIPT_PUBKEY_SIZE],
);
let fee = win_tx_weight * FeeRate::from_sat_per_vb_unchecked(20);
bob_split_prevout.value - fee
},
}],
};
signed_contract
.sign_split_win_tx_input(
&bob_win_cond,
&mut bob_win_tx,
0, // input index
&Prevouts::All(&[bob_split_prevout]),
bob.ticket_preimage,
bob.seckey,
)
.expect("failed to sign win TX");
// Only after a block delay of `delta` should Bob be able to
// broadcast the win TX.
mine_blocks(&rpc, signed_contract.params().relative_locktime_block_delta).unwrap();
rpc.send_raw_transaction(&bob_win_tx)
.expect("failed to broadcast Bob's win TX");
// Carol never bought her preimage, and so her winnings will return to the market maker
// `2*delta` blocks after the split TX is mined.
let carol_win_cond = WinCondition {
outcome: Outcome::Attestation(outcome_index),
player_index: player_indexes[&carol.player],
};
let (carol_split_input, carol_split_prevout) = signed_contract
.split_reclaim_tx_input_and_prevout(&carol_win_cond)
.unwrap();
// TODO test OP_CSV encumberance on reclaim script
let mut reclaim_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::ZERO,
input: vec![carol_split_input],
output: vec![TxOut {
script_pubkey: p2tr_script_pubkey(signed_contract.params().market_maker.pubkey),
value: {
let reclaim_tx_weight = predict_weight(
[signed_contract.split_reclaim_tx_input_weight()],
[P2TR_SCRIPT_PUBKEY_SIZE],
);
let fee = reclaim_tx_weight * FeeRate::from_sat_per_vb_unchecked(20);
carol_split_prevout.value - fee
},
}],
};
signed_contract
.sign_split_reclaim_tx_input(
&carol_win_cond,
&mut reclaim_tx,
0, // input index
&Prevouts::All(&[carol_split_prevout]),
market_maker_seckey,
)
.expect("failed to sign reclaim TX");
// Only after a block delay of `2*delta` can the market maker
// broadcast the split TX.
mine_blocks(&rpc, signed_contract.params().relative_locktime_block_delta).unwrap();
rpc.send_raw_transaction(&reclaim_tx)
.expect("failed to broadcast reclaim TX");
}