mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-03 09:29:46 +00:00
Use TX IDs for Bitcoin Eventualities
They're a bit more binding, smaller, provided by the Rust bitcoin library, sane, and we don't have to worry about malleability since all of our inputs are SegWit.
This commit is contained in:
parent
62fa31de07
commit
3a6c7ad796
5 changed files with 27 additions and 49 deletions
|
@ -149,11 +149,11 @@ impl Rpc {
|
||||||
|
|
||||||
/// Get the hash of a block by the block's number.
|
/// Get the hash of a block by the block's number.
|
||||||
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
||||||
let mut hash = *self
|
let mut hash = self
|
||||||
.rpc_call::<BlockHash>("getblockhash", json!([number]))
|
.rpc_call::<BlockHash>("getblockhash", json!([number]))
|
||||||
.await?
|
.await?
|
||||||
.as_raw_hash()
|
.as_raw_hash()
|
||||||
.as_byte_array();
|
.to_byte_array();
|
||||||
// bitcoin stores the inner bytes in reverse order.
|
// bitcoin stores the inner bytes in reverse order.
|
||||||
hash.reverse();
|
hash.reverse();
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
|
|
|
@ -13,6 +13,7 @@ use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
|
||||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
|
hashes::Hash,
|
||||||
sighash::{TapSighashType, SighashCache, Prevouts},
|
sighash::{TapSighashType, SighashCache, Prevouts},
|
||||||
absolute::LockTime,
|
absolute::LockTime,
|
||||||
script::{PushBytesBuf, ScriptBuf},
|
script::{PushBytesBuf, ScriptBuf},
|
||||||
|
@ -245,6 +246,13 @@ impl SignableTransaction {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the TX ID of the transaction this will create.
|
||||||
|
pub fn txid(&self) -> [u8; 32] {
|
||||||
|
let mut res = self.tx.txid().to_byte_array();
|
||||||
|
res.reverse();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the outputs this transaction will create.
|
/// Returns the outputs this transaction will create.
|
||||||
pub fn outputs(&self) -> &[TxOut] {
|
pub fn outputs(&self) -> &[TxOut] {
|
||||||
&self.tx.output
|
&self.tx.output
|
||||||
|
|
|
@ -279,6 +279,7 @@ async_sequential! {
|
||||||
FEE
|
FEE
|
||||||
).unwrap();
|
).unwrap();
|
||||||
let needed_fee = tx.needed_fee();
|
let needed_fee = tx.needed_fee();
|
||||||
|
let expected_id = tx.txid();
|
||||||
let tx = sign(&keys, tx);
|
let tx = sign(&keys, tx);
|
||||||
|
|
||||||
assert_eq!(tx.output.len(), 3);
|
assert_eq!(tx.output.len(), 3);
|
||||||
|
@ -322,6 +323,7 @@ async_sequential! {
|
||||||
let mut hash = *tx.txid().as_raw_hash().as_byte_array();
|
let mut hash = *tx.txid().as_raw_hash().as_byte_array();
|
||||||
hash.reverse();
|
hash.reverse();
|
||||||
assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap());
|
assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap());
|
||||||
|
assert_eq!(expected_id, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_data() {
|
async fn test_data() {
|
||||||
|
|
|
@ -21,7 +21,7 @@ use bitcoin_serai::{
|
||||||
consensus::{Encodable, Decodable},
|
consensus::{Encodable, Decodable},
|
||||||
script::Instruction,
|
script::Instruction,
|
||||||
address::{NetworkChecked, Address as BAddress},
|
address::{NetworkChecked, Address as BAddress},
|
||||||
OutPoint, TxOut, Transaction, Block, Network as BNetwork,
|
Transaction, Block, Network as BNetwork,
|
||||||
},
|
},
|
||||||
wallet::{
|
wallet::{
|
||||||
tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError,
|
tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError,
|
||||||
|
@ -37,7 +37,7 @@ use bitcoin_serai::bitcoin::{
|
||||||
sighash::{EcdsaSighashType, SighashCache},
|
sighash::{EcdsaSighashType, SighashCache},
|
||||||
script::{PushBytesBuf, Builder},
|
script::{PushBytesBuf, Builder},
|
||||||
absolute::LockTime,
|
absolute::LockTime,
|
||||||
Sequence, Script, Witness, TxIn, Amount as BAmount,
|
Amount as BAmount, Sequence, Script, Witness, OutPoint, TxOut, TxIn,
|
||||||
transaction::Version,
|
transaction::Version,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -206,32 +206,22 @@ impl TransactionTrait<Bitcoin> for Transaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Eventuality {
|
pub struct Eventuality([u8; 32]);
|
||||||
// We need to bind to the plan. While we could bind to the plan ID via an OP_RETURN, plans will
|
|
||||||
// use distinct inputs and this is accordingly valid as a binding to a specific plan.
|
|
||||||
plan_binding_input: OutPoint,
|
|
||||||
outputs: Vec<TxOut>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventualityTrait for Eventuality {
|
impl EventualityTrait for Eventuality {
|
||||||
fn lookup(&self) -> Vec<u8> {
|
fn lookup(&self) -> Vec<u8> {
|
||||||
let mut buf = Vec::with_capacity(32 + 4);
|
self.0.to_vec()
|
||||||
self.plan_binding_input.consensus_encode(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
let plan_binding_input = OutPoint::consensus_decode(reader)
|
let mut id = [0; 32];
|
||||||
.map_err(|_| io::Error::other("couldn't decode outpoint in eventuality"))?;
|
reader
|
||||||
let outputs = Vec::<TxOut>::consensus_decode(reader)
|
.read_exact(&mut id)
|
||||||
.map_err(|_| io::Error::other("couldn't decode outputs in eventuality"))?;
|
.map_err(|_| io::Error::other("couldn't decode ID in eventuality"))?;
|
||||||
Ok(Eventuality { plan_binding_input, outputs })
|
Ok(Eventuality(id))
|
||||||
}
|
}
|
||||||
fn serialize(&self) -> Vec<u8> {
|
fn serialize(&self) -> Vec<u8> {
|
||||||
let mut buf = Vec::with_capacity(32 + 4 + 4 + (self.outputs.len() * (8 + 32)));
|
self.0.to_vec()
|
||||||
self.plan_binding_input.consensus_encode(&mut buf).unwrap();
|
|
||||||
self.outputs.consensus_encode(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -653,23 +643,7 @@ impl Network for Bitcoin {
|
||||||
res: &mut HashMap<[u8; 32], (usize, Transaction)>,
|
res: &mut HashMap<[u8; 32], (usize, Transaction)>,
|
||||||
) {
|
) {
|
||||||
for tx in &block.txdata[1 ..] {
|
for tx in &block.txdata[1 ..] {
|
||||||
let input = &tx.input[0].previous_output;
|
if let Some((plan, _)) = eventualities.map.remove(tx.id().as_slice()) {
|
||||||
let mut lookup = Vec::with_capacity(4 + 32);
|
|
||||||
input.consensus_encode(&mut lookup).unwrap();
|
|
||||||
if let Some((plan, eventuality)) = eventualities.map.remove(&lookup) {
|
|
||||||
// Sanity, as this is guaranteed by how the lookup is performed
|
|
||||||
assert_eq!(input, &eventuality.plan_binding_input);
|
|
||||||
// If the multisig is honest, then the Eventuality's outputs should match the outputs of
|
|
||||||
// this transaction
|
|
||||||
// This panic is fine as this multisig being dishonest will require intervention on
|
|
||||||
// Substrate to trigger a slash, and then an update to the processor to handle the exact
|
|
||||||
// adjustments needed
|
|
||||||
// Panicking here is effectively triggering the halt we need to perform anyways
|
|
||||||
assert_eq!(
|
|
||||||
tx.output, eventuality.outputs,
|
|
||||||
"dishonest multisig spent input on distinct set of outputs"
|
|
||||||
);
|
|
||||||
|
|
||||||
res.insert(plan, (eventualities.block_number, tx.clone()));
|
res.insert(plan, (eventualities.block_number, tx.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -744,13 +718,8 @@ impl Network for Bitcoin {
|
||||||
RecommendedTranscript::new(b"Serai Processor Bitcoin Transaction Transcript");
|
RecommendedTranscript::new(b"Serai Processor Bitcoin Transaction Transcript");
|
||||||
transcript.append_message(b"plan", plan_id);
|
transcript.append_message(b"plan", plan_id);
|
||||||
|
|
||||||
let plan_binding_input = *inputs[0].output.outpoint();
|
let eventuality = Eventuality(signable.txid());
|
||||||
let outputs = signable.outputs().to_vec();
|
(SignableTransaction { transcript, actual: signable }, eventuality)
|
||||||
|
|
||||||
(
|
|
||||||
SignableTransaction { transcript, actual: signable },
|
|
||||||
Eventuality { plan_binding_input, outputs },
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -785,8 +754,7 @@ impl Network for Bitcoin {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_completion(&self, eventuality: &Self::Eventuality, tx: &Transaction) -> bool {
|
fn confirm_completion(&self, eventuality: &Self::Eventuality, tx: &Transaction) -> bool {
|
||||||
(eventuality.plan_binding_input == tx.input[0].previous_output) &&
|
eventuality.0 == tx.id()
|
||||||
(eventuality.outputs == tx.output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -35,7 +35,7 @@ async fn mint_and_burn_test() {
|
||||||
|
|
||||||
// Helper to mine a block on each network
|
// Helper to mine a block on each network
|
||||||
async fn mine_blocks(
|
async fn mine_blocks(
|
||||||
handles: &Vec<Handles>,
|
handles: &[Handles],
|
||||||
ops: &DockerOperations,
|
ops: &DockerOperations,
|
||||||
producer: &mut usize,
|
producer: &mut usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
|
Loading…
Reference in a new issue