Skip to content
5 changes: 4 additions & 1 deletion mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ impl<'a> UtxoConfBuilder<'a> {
}

fn signature_version(&self) -> SignatureVersion {
let default_signature_version = if self.ticker == "BCH" || self.fork_id() != 0 {
let default_signature_version = if self.conf["protocol"]["chain_variant"].as_str() == Some("RXD") {
SignatureVersion::ForkIdRxd
} else if self.ticker == "BCH" || self.fork_id() != 0 {
SignatureVersion::ForkId
} else {
SignatureVersion::Base
Expand All @@ -279,6 +281,7 @@ impl<'a> UtxoConfBuilder<'a> {
fn fork_id(&self) -> u32 {
let default_fork_id = match self.ticker {
"BCH" => "0x40",
_ if self.conf["protocol"]["chain_variant"].as_str() == Some("RXD") => "0x40",
_ => "0x0",
};
let hex_string = self.conf["fork_id"].as_str().unwrap_or(default_fork_id);
Expand Down
217 changes: 212 additions & 5 deletions mm2src/mm2_bitcoin/script/src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use hash::{H256, H512};
use keys::KeyPair;
use ser::Stream;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::convert::TryInto;
use {Builder, Script};

Expand All @@ -29,6 +30,8 @@ pub enum SignatureVersion {
WitnessV0,
#[serde(rename = "fork_id")]
ForkId,
#[serde(rename = "fork_id_rxd")]
ForkIdRxd,
}

#[derive(Debug, PartialEq, Clone, Copy)]
Expand Down Expand Up @@ -79,7 +82,7 @@ impl Sighash {
pub fn is_defined(version: SignatureVersion, u: u32) -> bool {
// reset anyone_can_pay && fork_id (if applicable) bits
let u = match version {
SignatureVersion::ForkId => u & !(0x40 | 0x80),
SignatureVersion::ForkId | SignatureVersion::ForkIdRxd => u & !(0x40 | 0x80),
_ => u & !(0x80),
};

Expand All @@ -90,7 +93,7 @@ impl Sighash {
/// Creates Sighash from any u, even if is_defined() == false
pub fn from_u32(version: SignatureVersion, u: u32) -> Self {
let anyone_can_pay = (u & 0x80) == 0x80;
let fork_id = version == SignatureVersion::ForkId && (u & 0x40) == 0x40;
let fork_id = matches!(version, SignatureVersion::ForkId | SignatureVersion::ForkIdRxd) && (u & 0x40) == 0x40;
let base = match u & 0x1f {
2 => SighashBase::None,
3 => SighashBase::Single,
Expand Down Expand Up @@ -246,7 +249,10 @@ impl TransactionInputSigner {
SignatureVersion::ForkId if sighash.fork_id => {
self.signature_hash_fork_id(input_index, input_amount, script_pubkey, sighashtype, sighash)
},
SignatureVersion::Base | SignatureVersion::ForkId => {
SignatureVersion::ForkIdRxd if sighash.fork_id => {
self.signature_hash_fork_id_rxd(input_index, input_amount, script_pubkey, sighashtype, sighash)
},
SignatureVersion::Base | SignatureVersion::ForkId | SignatureVersion::ForkIdRxd => {
self.signature_hash_original(input_index, script_pubkey, sighashtype, sighash)
},
SignatureVersion::WitnessV0 => {
Expand Down Expand Up @@ -435,6 +441,43 @@ impl TransactionInputSigner {
self.signature_hash_witness0(input_index, input_amount, script_pubkey, sighashtype, sighash)
}

fn signature_hash_fork_id_rxd(
&self,
input_index: usize,
input_amount: u64,
script_pubkey: &Script,
sighashtype: u32,
sighash: Sighash,
) -> H256 {
if input_index >= self.inputs.len() {
return 1u8.into();
}

if sighash.base == SighashBase::Single && input_index >= self.outputs.len() {
return 1u8.into();
}

let hash_prevouts = compute_hash_prevouts(sighash, &self.inputs);
let hash_sequence = compute_hash_sequence(sighash, &self.inputs);
let hash_output_hashes = compute_hash_output_hashes_rxd(sighash, input_index, &self.outputs);
let hash_outputs = compute_hash_outputs(sighash, input_index, &self.outputs);

let mut stream = Stream::default();
stream.append(&self.version);
stream.append(&hash_prevouts);
stream.append(&hash_sequence);
stream.append(&self.inputs[input_index].previous_output);
stream.append_list(script_pubkey);
stream.append(&input_amount);
stream.append(&self.inputs[input_index].sequence);
stream.append(&hash_output_hashes);
stream.append(&hash_outputs);
stream.append(&self.lock_time);
stream.append(&sighashtype);
let out = stream.out();
dhash256(&out)
}

/// https://github.com/zcash/zips/blob/master/zip-0243.rst#notes
/// This method doesn't cover all possible Sighash combinations so it doesn't fully match the
/// specification, however I don't need other cases yet as BarterDEX marketmaker always uses
Expand Down Expand Up @@ -615,6 +658,126 @@ fn compute_hash_outputs(sighash: Sighash, input_index: usize, outputs: &[Transac
}
}

fn compute_hash_output_hashes_rxd(sighash: Sighash, input_index: usize, outputs: &[TransactionOutput]) -> H256 {
let mut stream = Stream::default();

match sighash.base {
SighashBase::All => {
for output in outputs {
append_output_data_summary_rxd(&mut stream, output);
}
},
SighashBase::Single if input_index < outputs.len() => {
append_output_data_summary_rxd(&mut stream, &outputs[input_index]);
},
_ => return 0u8.into(),
}

dhash256(&stream.out())
}

fn append_output_data_summary_rxd(stream: &mut Stream, output: &TransactionOutput) {
let script_pubkey_hash = dhash256(output.script_pubkey.as_ref());
let sorted_push_refs = sorted_push_refs_rxd(output.script_pubkey.as_ref());
let total_refs: u32 = sorted_push_refs.len() as u32;
let refs_hash = if sorted_push_refs.is_empty() {
0u8.into()
} else {
let refs_concat: Vec<u8> = sorted_push_refs.into_iter().flatten().collect();
dhash256(&refs_concat)
};

stream.append(&output.value);
stream.append(&script_pubkey_hash);
stream.append(&total_refs);
stream.append(&refs_hash);
}

fn sorted_push_refs_rxd(script_pubkey: &[u8]) -> Vec<[u8; 36]> {
const OP_PUSHDATA1: u8 = 0x4c;
const OP_PUSHDATA2: u8 = 0x4d;
const OP_PUSHDATA4: u8 = 0x4e;
const OP_PUSHINPUTREF: u8 = 0xd0;
const OP_REQUIREINPUTREF: u8 = 0xd1;
const OP_DISALLOWPUSHINPUTREF: u8 = 0xd2;
const OP_DISALLOWPUSHINPUTREFSIBLING: u8 = 0xd3;
const OP_PUSHINPUTREFSINGLETON: u8 = 0xd8;

let mut refs = BTreeSet::new();
let mut pos = 0usize;

while pos < script_pubkey.len() {
let opcode = script_pubkey[pos];
pos += 1;

match opcode {
0x01..=0x4b => {
let push_len = opcode as usize;
if pos + push_len > script_pubkey.len() {
break;
}
pos += push_len;
},
OP_PUSHDATA1 => {
if pos + 1 > script_pubkey.len() {
break;
}
let push_len = script_pubkey[pos] as usize;
pos += 1;
if pos + push_len > script_pubkey.len() {
break;
}
pos += push_len;
},
OP_PUSHDATA2 => {
if pos + 2 > script_pubkey.len() {
break;
}
let push_len = u16::from_le_bytes([script_pubkey[pos], script_pubkey[pos + 1]]) as usize;
pos += 2;
if pos + push_len > script_pubkey.len() {
break;
}
pos += push_len;
},
OP_PUSHDATA4 => {
if pos + 4 > script_pubkey.len() {
break;
}
let push_len = u32::from_le_bytes([
script_pubkey[pos],
script_pubkey[pos + 1],
script_pubkey[pos + 2],
script_pubkey[pos + 3],
]) as usize;
pos += 4;
if pos + push_len > script_pubkey.len() {
break;
}
pos += push_len;
},
OP_PUSHINPUTREF
| OP_PUSHINPUTREFSINGLETON
| OP_REQUIREINPUTREF
| OP_DISALLOWPUSHINPUTREF
| OP_DISALLOWPUSHINPUTREFSIBLING => {
if pos + 36 > script_pubkey.len() {
break;
}
if opcode == OP_PUSHINPUTREF || opcode == OP_PUSHINPUTREFSINGLETON {
let mut reference = [0u8; 36];
reference.copy_from_slice(&script_pubkey[pos..pos + 36]);
refs.insert(reference);
}
pos += 36;
},
_ => (),
}
}

refs.into_iter().collect()
}

fn blake_2b_256_personal(input: &[u8], personal: &[u8]) -> Result<H256, String> {
let bytes: [u8; 32] = Blake2b::new()
.hash_length(32)
Expand All @@ -631,14 +794,15 @@ fn blake_2b_256_personal(input: &[u8], personal: &[u8]) -> Result<H256, String>
#[cfg(test)]
mod tests {
use super::{
blake_2b_256_personal, Sighash, SighashBase, SignatureVersion, TransactionInputSigner, UnsignedTransactionInput,
blake_2b_256_personal, Sighash, SighashBase, SignatureVersion, TransactionInputSigner,
UnsignedTransactionInput,
};
use bytes::Bytes;
use chain::{OutPoint, Transaction, TransactionOutput};
use hash::{H160, H256};
use keys::{
prefixes::{BTC_PREFIXES, T_BTC_PREFIXES},
Address, AddressHashEnum, Private,
Address, AddressHashEnum, Private, Public, Signature,
};
use script::Script;
use ser::deserialize;
Expand Down Expand Up @@ -792,6 +956,49 @@ mod tests {
assert!(Sighash::is_defined(SignatureVersion::ForkId, 0x00000081));
assert!(Sighash::is_defined(SignatureVersion::ForkId, 0x000000C2));
assert!(Sighash::is_defined(SignatureVersion::ForkId, 0x00000043));

assert!(Sighash::is_defined(SignatureVersion::ForkIdRxd, 0x00000081));
assert!(Sighash::is_defined(SignatureVersion::ForkIdRxd, 0x000000C2));
assert!(Sighash::is_defined(SignatureVersion::ForkIdRxd, 0x00000043));
}

#[test]
fn test_rxd_forkid_sighash_vector() {
const SPEND_TX_HEX: &str = "0100000002502d0525588143dee5d3857b6b901805fb4ec53cca16b374f25bf8c3e12644ee010000006a47304402204715bf639b322d08cfabb2b1b7af1ba8b6b3d571a3629a2ae5faa39b8483942f0220424747e9e1cda6e034501171292fac690da050d5221cbfd318d880eb9701405541210275c802fa50d9a1f2b89c7e43b74c77e8826209b8aa79ed144c6768b9a6f262a1ffffffff457a0a555e6fbdaf5e5f2c241847026e1cf38ee3f523ebcd0dc27683fdcd4c55000000006a47304402200be359814746c94556d80fae90f02cc9bcff50a60a1a87dadce7579eb986d783022028f7d7a39561fbacde025a91b41b6279d4ffda991d9ef69804bde990f663c1df41210275c802fa50d9a1f2b89c7e43b74c77e8826209b8aa79ed144c6768b9a6f262a1ffffffff0300ca9a3b0000000017a914cf53278b47afd12ffd864a00090b6df471d22a16870000000000000000166a14bddadc147d635060f518c7d59e481090dc066f8bdb1ae329010000001976a914d4466bdfcc471b6207a108289b847d562044539288acd8c89169";
const INPUT_INDEX: usize = 0;
const INPUT_AMOUNT_SATS: u64 = 998_119_699;
const PREVOUT_SCRIPT_CODE_HEX: &str = "76a914d4466bdfcc471b6207a108289b847d562044539288ac";
const SIGHASH_U32: u32 = 0x41;
const DER_SIGNATURE_HEX: &str =
"304402204715bf639b322d08cfabb2b1b7af1ba8b6b3d571a3629a2ae5faa39b8483942f0220424747e9e1cda6e034501171292fac690da050d5221cbfd318d880eb97014055";
const PUBKEY_HEX: &str = "0275c802fa50d9a1f2b89c7e43b74c77e8826209b8aa79ed144c6768b9a6f262a1";
const EXPECTED_SIGHASH_HEX: &str = "201a2654d0ed04643080bbdda4acdf40a3d31a6a2e0c486270476fd3c6409187";

let tx: Transaction = SPEND_TX_HEX.into();
let mut signer: TransactionInputSigner = tx.into();
signer.inputs[INPUT_INDEX].amount = INPUT_AMOUNT_SATS;

let script_code: Script = PREVOUT_SCRIPT_CODE_HEX.into();
let sighash = signer.signature_hash(
INPUT_INDEX,
INPUT_AMOUNT_SATS,
&script_code,
SignatureVersion::ForkIdRxd,
SIGHASH_U32,
);

let signature: Signature = DER_SIGNATURE_HEX.into();
let pubkey_bytes: Bytes = PUBKEY_HEX.into();
let pubkey = Public::from_slice(&pubkey_bytes).expect("valid compressed pubkey");

assert!(pubkey.verify(&sighash, &signature).expect("signature verification should not error"));

let expected_sighash: H256 = EXPECTED_SIGHASH_HEX.into();
assert_eq!(
sighash, expected_sighash,
"RXD sighash mismatch: computed {}, expected {}",
sighash, expected_sighash
);
}

#[test]
Expand Down
6 changes: 6 additions & 0 deletions mm2src/mm2_bitcoin/serialization/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub enum ChainVariant {
MORTY,
RVN,
PIVX,
RXD,
}

impl ChainVariant {
Expand All @@ -92,6 +93,10 @@ impl ChainVariant {
pub fn is_pivx(&self) -> bool {
matches!(self, ChainVariant::PIVX)
}

pub fn is_rxd(&self) -> bool {
matches!(self, ChainVariant::RXD)
}
}

impl TryFrom<&str> for ChainVariant {
Expand All @@ -107,6 +112,7 @@ impl TryFrom<&str> for ChainVariant {
"MORTY" => Ok(ChainVariant::MORTY),
"RVN" => Ok(ChainVariant::RVN),
"PIVX" => Ok(ChainVariant::PIVX),
"RXD" => Ok(ChainVariant::RXD),
_ => Err(format!("Unknown chain variant: {}", value)),
}
}
Expand Down