Skip to content

Commit

Permalink
feat: Support posv (#1815)
Browse files Browse the repository at this point in the history
* serialize/ deserialize PoSV transaction

* sign posv transaction

* PoSV supporting configuration

* code fixes and missing n_time

* fix fmt in utxo.rs

* fix fmt in transaction.rs

* add changelog entry

---------

Co-authored-by: John Nash <john@redd.ink>
Co-authored-by: shamardy <shamardy@yahoo.com>
Reviewed-by: cipig, shamardy <shamardy@yahoo.com>
  • Loading branch information
3 people committed May 15, 2023
1 parent aea18eb commit cbf312d
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- CI flows for `adex-cli` added [#1818](https://github.com/KomodoPlatform/atomicDEX-API/pull/1818)
- Detect a chain reorganization, if it occurs, redownload and revalidate the new best chain headers for SPV [#1728](https://github.com/KomodoPlatform/atomicDEX-API/pull/1728)
- Fix moralis request in wasm target, add moralis tests [#1817](https://github.com/KomodoPlatform/atomicDEX-API/pull/1817)
- PoSV support for UTXO coins was added in [#1815](https://github.com/KomodoPlatform/atomicDEX-API/pull/1815)


## v1.0.3-beta - 2023-04-28

Expand Down
4 changes: 4 additions & 0 deletions mm2src/coins/utxo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,9 @@ pub struct UtxoCoinConf {
/// https://en.bitcoin.it/wiki/Proof_of_work
/// The actual meaning of this is nTime field is used in transaction
pub is_pos: bool,
/// Defines if coin uses PoSV transaction format (Reddcoin, Potcoin, et al).
/// n_time field is appended to end of transaction
pub is_posv: bool,
/// Special field for Zcash and it's forks
/// Defines if Overwinter network upgrade was activated
/// https://z.cash/upgrade/overwinter/
Expand Down Expand Up @@ -791,6 +794,7 @@ impl UtxoCoinFields {
shielded_spends: vec![],
shielded_outputs: vec![],
zcash: self.conf.zcash,
posv: self.conf.is_posv,
str_d_zeel,
hash_algo: self.tx_hash_algo.into(),
}
Expand Down
1 change: 1 addition & 0 deletions mm2src/coins/utxo/slp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ impl SlpToken {
join_split_sig: Default::default(),
binding_sig: Default::default(),
zcash: unsigned.zcash,
posv: unsigned.posv,
str_d_zeel: unsigned.str_d_zeel,
tx_hash_algo: self.platform_coin.as_ref().tx_hash_algo,
};
Expand Down
4 changes: 4 additions & 0 deletions mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ impl<'a> UtxoConfBuilder<'a> {
let mature_confirmations = self.mature_confirmations();

let is_pos = self.is_pos();
let is_posv = self.is_posv();
let segwit = self.segwit();
let force_min_relay_fee = self.conf["force_min_relay_fee"].as_bool().unwrap_or(false);
let mtp_block_count = self.mtp_block_count();
Expand All @@ -95,6 +96,7 @@ impl<'a> UtxoConfBuilder<'a> {
Ok(UtxoCoinConf {
ticker: self.ticker.to_owned(),
is_pos,
is_posv,
requires_notarization,
overwintered,
pub_addr_prefix,
Expand Down Expand Up @@ -266,6 +268,8 @@ impl<'a> UtxoConfBuilder<'a> {

fn is_pos(&self) -> bool { self.conf["isPoS"].as_u64() == Some(1) }

fn is_posv(&self) -> bool { self.conf["isPoSV"].as_u64() == Some(1) }

fn segwit(&self) -> bool { self.conf["segwit"].as_bool().unwrap_or(false) }

fn mtp_block_count(&self) -> NonZeroU64 {
Expand Down
2 changes: 2 additions & 0 deletions mm2src/coins/utxo/utxo_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,7 @@ pub async fn p2sh_spending_tx<T: UtxoCommonOps>(coin: &T, input: P2SHSpendingTxI
version_group_id: coin.as_ref().conf.version_group_id,
consensus_branch_id: coin.as_ref().conf.consensus_branch_id,
zcash: coin.as_ref().conf.zcash,
posv: coin.as_ref().conf.is_posv,
str_d_zeel,
hash_algo,
};
Expand Down Expand Up @@ -1170,6 +1171,7 @@ pub async fn p2sh_spending_tx<T: UtxoCommonOps>(coin: &T, input: P2SHSpendingTxI
join_split_sig: H512::default(),
join_split_pubkey: H256::default(),
zcash: coin.as_ref().conf.zcash,
posv: coin.as_ref().conf.is_posv,
str_d_zeel: unsigned.str_d_zeel,
tx_hash_algo: unsigned.hash_algo.into(),
})
Expand Down
1 change: 1 addition & 0 deletions mm2src/coins/utxo/utxo_common_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ pub(super) fn utxo_coin_fields_for_test(
UtxoCoinFields {
conf: UtxoCoinConf {
is_pos: false,
is_posv: false,
requires_notarization: false.into(),
overwintered: true,
segwit: true,
Expand Down
1 change: 1 addition & 0 deletions mm2src/coins/utxo_signer/src/sign_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub(crate) fn complete_tx(unsigned: TransactionInputSigner, signed_inputs: Vec<T
join_split_sig: H512::default(),
join_split_pubkey: H256::default(),
zcash: unsigned.zcash,
posv: unsigned.posv,
str_d_zeel: unsigned.str_d_zeel,
tx_hash_algo: unsigned.hash_algo.into(),
}
Expand Down
70 changes: 65 additions & 5 deletions mm2src/mm2_bitcoin/chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ pub struct Transaction {
pub join_split_sig: H512,
pub binding_sig: H512,
pub zcash: bool,
pub posv: bool,
/// https://github.com/navcoin/navcoin-core/blob/556250920fef9dc3eddd28996329ba316de5f909/src/primitives/transaction.h#L497
pub str_d_zeel: Option<String>,
pub tx_hash_algo: TxHashAlgo,
Expand Down Expand Up @@ -358,15 +359,23 @@ impl Serializable for Transaction {
stream.append(&self.version_group_id);
}

if let Some(n_time) = self.n_time {
stream.append(&n_time);
if !self.posv {
if let Some(n_time) = self.n_time {
stream.append(&n_time);
}
}

stream
.append_list(&self.inputs)
.append_list(&self.outputs)
.append(&self.lock_time);

if self.posv {
if let Some(n_time) = self.n_time {
stream.append(&n_time);
}
}

if self.overwintered && self.version >= 3 {
stream.append(&self.expiry_height);
if self.version >= 4 {
Expand Down Expand Up @@ -418,6 +427,11 @@ pub enum TxType {
StandardWithWitness,
Zcash,
PosWithNTime,
PosvWithNTime,
}

impl TxType {
fn uses_witness(&self) -> bool { matches!(self, TxType::StandardWithWitness | TxType::PosvWithNTime) }
}

pub fn deserialize_tx<T>(reader: &mut Reader<T>, tx_type: TxType) -> Result<Transaction, Error>
Expand All @@ -433,13 +447,13 @@ where
version_group_id = reader.read()?;
}

let n_time = if tx_type == TxType::PosWithNTime {
let mut n_time = if tx_type == TxType::PosWithNTime {
Some(reader.read()?)
} else {
None
};
let mut inputs: Vec<TransactionInput> = reader.read_list_max(MAX_LIST_SIZE)?;
let read_witness = if inputs.is_empty() && !overwintered && tx_type == TxType::StandardWithWitness {
let read_witness = if inputs.is_empty() && !overwintered && tx_type.uses_witness() {
let witness_flag: u8 = reader.read()?;
if witness_flag != WITNESS_FLAG {
return Err(Error::MalformedData);
Expand All @@ -451,7 +465,7 @@ where
false
};
let outputs = reader.read_list_max(MAX_LIST_SIZE)?;
if outputs.is_empty() && tx_type == TxType::StandardWithWitness {
if outputs.is_empty() && tx_type.uses_witness() {
return Err(Error::Custom("Transaction has no output".into()));
}
if read_witness {
Expand All @@ -462,6 +476,14 @@ where

let lock_time = reader.read()?;

let mut posv = false;
n_time = if tx_type == TxType::PosvWithNTime {
posv = true;
Some(reader.read()?)
} else {
n_time
};

let mut expiry_height = 0;
let mut value_balance = 0;
let mut shielded_spends = vec![];
Expand Down Expand Up @@ -528,6 +550,7 @@ where
shielded_spends,
shielded_outputs,
zcash,
posv,
str_d_zeel,
tx_hash_algo: TxHashAlgo::DSHA256,
})
Expand All @@ -545,6 +568,9 @@ impl Deserializable for Transaction {
// specific use case
let mut buffer = vec![];
reader.read_to_end(&mut buffer)?;
if let Ok(t) = deserialize_tx(&mut Reader::from_read(buffer.as_slice()), TxType::PosvWithNTime) {
return Ok(t);
}
if let Ok(t) = deserialize_tx(&mut Reader::from_read(buffer.as_slice()), TxType::StandardWithWitness) {
return Ok(t);
}
Expand Down Expand Up @@ -836,6 +862,7 @@ mod tests {
}],
lock_time: 0x00000011,
zcash: false,
posv: false,
str_d_zeel: None,
tx_hash_algo: TxHashAlgo::DSHA256,
};
Expand Down Expand Up @@ -1021,6 +1048,7 @@ mod tests {
}],
lock_time: 1632875267,
zcash: false,
posv: false,
str_d_zeel: None,
tx_hash_algo: TxHashAlgo::DSHA256,
};
Expand All @@ -1034,4 +1062,36 @@ mod tests {
let ext_tx = ExtTransaction::from(tx.clone());
assert_eq!(tx.hash().reversed().to_string(), ext_tx.txid().to_string());
}

#[test]
fn n_time_posv_transaction() {
let raw = "0200000001fa402b05b9108ec4762247d74c48a2ff303dd832d24c341c486e32cef0434177010000004847304402207a5283cc0fe6fc384744545cb600206ec730d0cdfa6a5e1479cb509fda536ee402202bec1e79b90638f1c608d805b2877fefc8fa6d0df279f58f0a70883e0e0609ce01ffffffff030000000000000000006a734110a10a0000232102fa0ecb032c7cb7be378efd03a84532b5cf1795996bfad854f042dc521616bfdcacd57f643201000000232103c8fc5c87f00bcc32b5ce5c036957f8befeff05bf4d88d2dcde720249f78d9313ac00000000dfcb3c64";
let t: Transaction = raw.into();

assert_eq!(t.version, 2);
assert_eq!(t.lock_time, 0);
assert_eq!(t.inputs.len(), 1);
assert_eq!(t.outputs.len(), 3);
assert_eq!(t.n_time, Some(1681705951));
assert!(t.posv);

let serialized = serialize(&t);
assert_eq!(Bytes::from(raw), serialized);
}

#[test]
fn n_time_posv_transaction_locktime() {
let raw = "0200000001a471828d6290f5ca7935bc26a9d07cda37227ca3fb0e8d1282296ea839c134810100000048473044022067bbc1176fe4fa8681db854d9fce8d47d5613d01820ef78a6ab76c4f067500990220560d00098fcb69eb8587873f8072c11bcf832308fdcf5712d0e4aea4b761060e01fdffffff02940237c31d0600001976a9147fb8384a3f328148137447cdf6b1c3c1f0a8559588ac00e40b54020000001976a91485ee21a7f8cdd9034fb55004e0d8ed27db1c03c288acbd8d05002b584e63";
let t: Transaction = raw.into();

assert_eq!(t.version, 2);
assert_eq!(t.lock_time, 363965);
assert_eq!(t.inputs.len(), 1);
assert_eq!(t.outputs.len(), 2);
assert_eq!(t.n_time, Some(1666078763));
assert!(t.posv);

let serialized = serialize(&t);
assert_eq!(Bytes::from(raw), serialized);
}
}
62 changes: 61 additions & 1 deletion mm2src/mm2_bitcoin/script/src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ pub struct TransactionInputSigner {
pub shielded_spends: Vec<ShieldedSpend>,
pub shielded_outputs: Vec<ShieldedOutput>,
pub zcash: bool,
pub posv: bool,
pub str_d_zeel: Option<String>,
pub hash_algo: SignerHashAlgo,
}
Expand All @@ -181,6 +182,7 @@ impl From<Transaction> for TransactionInputSigner {
shielded_spends: t.shielded_spends.clone(),
shielded_outputs: t.shielded_outputs.clone(),
zcash: t.zcash,
posv: t.posv,
str_d_zeel: t.str_d_zeel,
hash_algo: t.tx_hash_algo.into(),
}
Expand Down Expand Up @@ -214,6 +216,7 @@ impl From<TransactionInputSigner> for Transaction {
shielded_spends: t.shielded_spends.clone(),
shielded_outputs: t.shielded_outputs.clone(),
zcash: t.zcash,
posv: t.posv,
binding_sig: H512::default(),
join_split_pubkey: H256::default(),
join_split_sig: H512::default(),
Expand Down Expand Up @@ -344,11 +347,14 @@ impl TransactionInputSigner {
SighashBase::None => Vec::new(),
};

// PoSV transactions have n_time truncated when creating signed inputs
let n_time: Option<u32> = if self.posv { None } else { self.n_time };

let tx = Transaction {
inputs,
outputs,
version: self.version,
n_time: self.n_time,
n_time,
lock_time: self.lock_time,
binding_sig: H512::default(),
expiry_height: 0,
Expand All @@ -361,6 +367,7 @@ impl TransactionInputSigner {
value_balance: 0,
version_group_id: 0,
zcash: self.zcash,
posv: self.posv,
str_d_zeel: self.str_d_zeel.clone(),
tx_hash_algo: self.hash_algo.into(),
};
Expand Down Expand Up @@ -658,6 +665,59 @@ mod tests {
shielded_spends: vec![],
shielded_outputs: vec![],
zcash: false,
posv: false,
str_d_zeel: None,
hash_algo: SignerHashAlgo::DSHA256,
};

let hash = input_signer.signature_hash(0, 0, &previous_output, SignatureVersion::Base, SighashBase::All.into());
assert_eq!(hash, expected_signature_hash);
}

#[test]
fn test_signature_hash_posv() {
let _private: Private = "cSQJp8ymcCUZCceowcTr5L1rr7tRbeB8uj3pDFRfRaMuRP6yDqfa".into();
let previous_tx_hash =
H256::from_reversed_str("0bc54ed426950f50bf2c2776034a03592e844757b42330eb908eb04492dad2c6");
let previous_output_index = 1;
let to: Address = "msj7SEQmH7pUCUx8YU6R87DrAHYzcABdzw".into();
assert!(to.hash.is_address_hash());
let previous_output = "76a914df3bd30160e6c6145baaf2c88a8844c13a00d1d588ac".into();
let current_output: Bytes = "76a91485ee21a7f8cdd9034fb55004e0d8ed27db1c03c288ac".into();
let value = 100000000;
let expected_signature_hash: H256 = "21d91397ba4e4bfaf73584300804cf9f9fd11cabe43f1bb38f7986cea5ef5519".into();

let unsigned_input = UnsignedTransactionInput {
sequence: 0xffff_ffff,
previous_output: OutPoint {
index: previous_output_index,
hash: previous_tx_hash,
},
amount: 100,
witness: vec![Vec::new()],
};

let output = TransactionOutput {
value,
script_pubkey: current_output,
};

let input_signer = TransactionInputSigner {
version: 2,
n_time: Some(1682050928),
overwintered: false,
version_group_id: 0,
consensus_branch_id: 0,
expiry_height: 0,
value_balance: 0,
lock_time: 0,
inputs: vec![unsigned_input],
outputs: vec![output],
join_splits: vec![],
shielded_spends: vec![],
shielded_outputs: vec![],
zcash: false,
posv: true,
str_d_zeel: None,
hash_algo: SignerHashAlgo::DSHA256,
};
Expand Down

0 comments on commit cbf312d

Please sign in to comment.