Skip to content

Commit 5c6db60

Browse files
committed
unify coinbase abstractions
the functions for serializing `coinbase_tx_prefix` and `coinbase_tx_suffix` did not take into consideration that they must strip the coinbase from BIP141 data (marker + flag + witness) before calculating the merkle root this was patched via PR #488 although it fixed the perceived issues, it also introduced API fragmentation, since there's two different pairs of methods for serializing these fields... this is very confusing for the user we have: - `fn coinbase_tx_prefix(...)` vs `StrippedCoinbaseTx::into_coinbase_tx_prefix(...)` - `fn coinbase_tx_suffix(...)` vs `StrippedCoinbaseTx::into_coinbase_tx_suffix(...)` one includes BIP141 data, while the other removes it... and only the `coinbase_tx_prefix` without BIP141 is actually used for merkle root calculation this commit introduces a new `struct Coinbase`, which unifies coinbase abstractions, effectively replacing: - `struct StrippedCoinbase` (plus methods) - `fn coinbase -> Result<Transaction, Error>` (plus accompanying prefix and suffix functions) in practice, this means that now we **always** remove BIP141 data when serializing `coinbase_tx_prefix` and `coinbase_tx_suffix`... and most importantly: there's only one function responsible for this task, leaving no room for confusion by API users.
1 parent eb08cfa commit 5c6db60

File tree

2 files changed

+206
-212
lines changed

2 files changed

+206
-212
lines changed

protocols/v2/roles-logic-sv2/src/job_creator.rs

Lines changed: 65 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,20 @@
22
//!
33
//! This module provides logic to create extended mining jobs given a template from
44
//! a template provider as well as logic to clean up old templates when new blocks are mined.
5-
use crate::{errors, utils::Id, Error};
6-
use binary_sv2::B064K;
5+
use crate::{
6+
utils::{Coinbase, Id},
7+
Error,
8+
};
79
use mining_sv2::NewExtendedMiningJob;
810
use nohash_hasher::BuildNoHashHasher;
911
use std::{collections::HashMap, convert::TryInto};
1012
use template_distribution_sv2::{NewTemplate, SetNewPrevHash};
1113
use tracing::debug;
1214

13-
use stratum_common::{
14-
bitcoin,
15-
bitcoin::{
16-
blockdata::{
17-
transaction::{OutPoint, Transaction, TxIn, TxOut},
18-
witness::Witness,
19-
},
20-
consensus::Decodable,
21-
util::psbt::serialize::{Deserialize, Serialize},
22-
},
15+
use stratum_common::bitcoin::{
16+
blockdata::transaction::{Transaction, TxOut},
17+
consensus::Decodable,
18+
util::psbt::serialize::Deserialize,
2319
};
2420

2521
#[derive(Debug)]
@@ -204,17 +200,16 @@ fn new_extended_job(
204200
.map_err(|_| Error::TxVersionTooBig)?;
205201

206202
let script_sig_prefix = new_template.coinbase_prefix.to_vec();
207-
let script_sig_prefix_len = script_sig_prefix.len() + additional_coinbase_script_data.len();
208203

209-
let coinbase = coinbase(
204+
let coinbase = Coinbase::new(
210205
script_sig_prefix,
211206
tx_version,
212207
new_template.coinbase_tx_locktime,
213208
new_template.coinbase_tx_input_sequence,
214-
coinbase_outputs,
209+
coinbase_outputs.to_vec(),
215210
additional_coinbase_script_data,
216211
extranonce_len,
217-
)?;
212+
);
218213

219214
let min_ntime = binary_sv2::Sv2Option::new(if new_template.future_template {
220215
None
@@ -229,8 +224,8 @@ fn new_extended_job(
229224
version: new_template.version,
230225
version_rolling_allowed,
231226
merkle_path: new_template.merkle_path.clone().into_static(),
232-
coinbase_tx_prefix: coinbase_tx_prefix(&coinbase, script_sig_prefix_len)?,
233-
coinbase_tx_suffix: coinbase_tx_suffix(&coinbase, extranonce_len, script_sig_prefix_len)?,
227+
coinbase_tx_prefix: coinbase.clone().coinbase_tx_prefix()?,
228+
coinbase_tx_suffix: coinbase.coinbase_tx_suffix()?,
234229
};
235230

236231
debug!(
@@ -240,89 +235,6 @@ fn new_extended_job(
240235
Ok(new_extended_mining_job)
241236
}
242237

243-
// Used to extract the coinbase transaction prefix for extended jobs
244-
// so the extranonce search space can be introduced
245-
fn coinbase_tx_prefix(
246-
coinbase: &Transaction,
247-
script_sig_prefix_len: usize,
248-
) -> Result<B064K<'static>, Error> {
249-
let encoded = coinbase.serialize();
250-
// If script_sig_prefix_len is not 0 we are not in a test environment and the coinbase will have
251-
// the 0 witness
252-
let segwit_bytes = match script_sig_prefix_len {
253-
0 => 0,
254-
_ => 2,
255-
};
256-
let index = 4 // tx version
257-
+ segwit_bytes
258-
+ 1 // number of inputs TODO can be also 3
259-
+ 32 // prev OutPoint
260-
+ 4 // index
261-
+ 1 // bytes in script TODO can be also 3
262-
+ script_sig_prefix_len; // script_sig_prefix
263-
let r = encoded[0..index].to_vec();
264-
r.try_into().map_err(Error::BinarySv2Error)
265-
}
266-
267-
// Used to extract the coinbase transaction suffix for extended jobs
268-
// so the extranonce search space can be introduced
269-
fn coinbase_tx_suffix(
270-
coinbase: &Transaction,
271-
extranonce_len: u8,
272-
script_sig_prefix_len: usize,
273-
) -> Result<B064K<'static>, Error> {
274-
let encoded = coinbase.serialize();
275-
// If script_sig_prefix_len is not 0 we are not in a test environment and the coinbase have the
276-
// 0 witness
277-
let segwit_bytes = match script_sig_prefix_len {
278-
0 => 0,
279-
_ => 2,
280-
};
281-
let r = encoded[4 // tx version
282-
+ segwit_bytes
283-
+ 1 // number of inputs TODO can be also 3
284-
+ 32 // prev OutPoint
285-
+ 4 // index
286-
+ 1 // bytes in script TODO can be also 3
287-
+ script_sig_prefix_len // script_sig_prefix
288-
+ (extranonce_len as usize)..]
289-
.to_vec();
290-
r.try_into().map_err(Error::BinarySv2Error)
291-
}
292-
293-
// try to build a Transaction coinbase
294-
fn coinbase(
295-
script_sig_prefix: Vec<u8>,
296-
version: i32,
297-
lock_time: u32,
298-
sequence: u32,
299-
coinbase_outputs: &[TxOut],
300-
additional_coinbase_script_data: Vec<u8>,
301-
extranonce_len: u8,
302-
) -> Result<Transaction, Error> {
303-
// If script_sig_prefix_len is not 0 we are not in a test environment and the coinbase have the
304-
// 0 witness
305-
let witness = match script_sig_prefix.len() {
306-
0 => Witness::from_vec(vec![]),
307-
_ => Witness::from_vec(vec![vec![0; 32]]),
308-
};
309-
let mut script_sig = script_sig_prefix;
310-
script_sig.extend_from_slice(&additional_coinbase_script_data);
311-
script_sig.extend_from_slice(&vec![0; extranonce_len as usize]);
312-
let tx_in = TxIn {
313-
previous_output: OutPoint::null(),
314-
script_sig: script_sig.into(),
315-
sequence: bitcoin::Sequence(sequence),
316-
witness,
317-
};
318-
Ok(Transaction {
319-
version,
320-
lock_time: bitcoin::PackedLockTime(lock_time),
321-
input: vec![tx_in],
322-
output: coinbase_outputs.to_vec(),
323-
})
324-
}
325-
326238
/// Helper type to strip a segwit data from the coinbase_tx_prefix and coinbase_tx_suffix
327239
/// to ensure miners are hashing with the correct coinbase
328240
pub fn extended_job_to_non_segwit(
@@ -335,8 +247,20 @@ pub fn extended_job_to_non_segwit(
335247
let extranonce = vec![0_u8; full_extranonce_len];
336248
encoded.extend_from_slice(&extranonce[..]);
337249
encoded.extend_from_slice(job.coinbase_tx_suffix.inner_as_ref());
338-
let coinbase = Transaction::deserialize(&encoded).map_err(|_| Error::InvalidCoinbase)?;
339-
let stripped_tx = StrippedCoinbaseTx::from_coinbase(coinbase, full_extranonce_len)?;
250+
let tx = Transaction::deserialize(&encoded).map_err(|_| Error::InvalidCoinbase)?;
251+
252+
let coinbase = Coinbase {
253+
tx,
254+
script_sig_prefix_len: job.coinbase_tx_prefix.len()
255+
- (
256+
4 + // version
257+
1 + // input count
258+
32 + // outpoint
259+
4 + // input index
260+
1
261+
// script length byte
262+
),
263+
};
340264

341265
Ok(NewExtendedMiningJob {
342266
channel_id: job.channel_id,
@@ -345,95 +269,10 @@ pub fn extended_job_to_non_segwit(
345269
version: job.version,
346270
version_rolling_allowed: job.version_rolling_allowed,
347271
merkle_path: job.merkle_path,
348-
coinbase_tx_prefix: stripped_tx.into_coinbase_tx_prefix()?,
349-
coinbase_tx_suffix: stripped_tx.into_coinbase_tx_suffix()?,
272+
coinbase_tx_prefix: coinbase.clone().coinbase_tx_prefix()?,
273+
coinbase_tx_suffix: coinbase.coinbase_tx_suffix()?,
350274
})
351275
}
352-
// Helper type to strip a segwit data from the coinbase_tx_prefix and coinbase_tx_suffix
353-
// to ensure miners are hashing with the correct coinbase
354-
struct StrippedCoinbaseTx {
355-
version: u32,
356-
inputs: Vec<Vec<u8>>,
357-
outputs: Vec<Vec<u8>>,
358-
lock_time: u32,
359-
// helper field
360-
bip141_bytes_len: usize,
361-
}
362-
363-
impl StrippedCoinbaseTx {
364-
// create
365-
fn from_coinbase(tx: Transaction, full_extranonce_len: usize) -> Result<Self, Error> {
366-
let bip141_bytes_len = tx
367-
.input
368-
.last()
369-
.ok_or(Error::BadPayloadSize)?
370-
.script_sig
371-
.len()
372-
- full_extranonce_len;
373-
Ok(Self {
374-
version: tx.version as u32,
375-
inputs: tx
376-
.input
377-
.iter()
378-
.map(|txin| {
379-
let mut ser: Vec<u8> = vec![];
380-
ser.extend_from_slice(&txin.previous_output.txid);
381-
ser.extend_from_slice(&txin.previous_output.vout.to_le_bytes());
382-
ser.push(txin.script_sig.len() as u8);
383-
ser.extend_from_slice(txin.script_sig.as_bytes());
384-
ser.extend_from_slice(&txin.sequence.0.to_le_bytes());
385-
ser
386-
})
387-
.collect(),
388-
outputs: tx.output.iter().map(|o| o.serialize()).collect(),
389-
lock_time: tx.lock_time.into(),
390-
bip141_bytes_len,
391-
})
392-
}
393-
394-
// The coinbase tx prefix is the LE bytes concatenation of the tx version and all
395-
// of the tx inputs minus the 32 bytes after the script_sig_prefix bytes
396-
// and the last input's sequence (used as the first entry in the coinbase tx suffix).
397-
// The last 32 bytes after the bip34 bytes in the script will be used to allow extranonce
398-
// space for the miner. We remove the bip141 marker and flag since it is only used for
399-
// computing the `wtxid` and the legacy `txid` is what is used for computing the merkle root
400-
// clippy allow because we don't want to consume self
401-
#[allow(clippy::wrong_self_convention)]
402-
fn into_coinbase_tx_prefix(&self) -> Result<B064K<'static>, errors::Error> {
403-
let mut inputs = self.inputs.clone();
404-
let last_input = inputs.last_mut().ok_or(Error::BadPayloadSize)?;
405-
let new_last_input_len =
406-
32 // outpoint
407-
+ 4 // vout
408-
+ 1 // script length byte -> TODO can be also 3 (based on TODO in `coinbase_tx_prefix()`)
409-
+ self.bip141_bytes_len // space for bip34 bytes
410-
;
411-
last_input.truncate(new_last_input_len);
412-
let mut prefix: Vec<u8> = vec![];
413-
prefix.extend_from_slice(&self.version.to_le_bytes());
414-
prefix.push(self.inputs.len() as u8);
415-
prefix.extend_from_slice(&inputs.concat());
416-
prefix.try_into().map_err(Error::BinarySv2Error)
417-
}
418-
419-
// This coinbase tx suffix is the sequence of the last tx input plus
420-
// the serialized tx outputs and the lock time. Note we do not use the witnesses
421-
// (placed between txouts and lock time) since it is only used for
422-
// computing the `wtxid` and the legacy `txid` is what is used for computing the merkle root
423-
// clippy allow because we don't want to consume self
424-
#[allow(clippy::wrong_self_convention)]
425-
fn into_coinbase_tx_suffix(&self) -> Result<B064K<'static>, errors::Error> {
426-
let mut suffix: Vec<u8> = vec![];
427-
let last_input = self.inputs.last().ok_or(Error::BadPayloadSize)?;
428-
// only take the last intput's sequence u32 (bytes after the extranonce space)
429-
let last_input_sequence = &last_input[last_input.len() - 4..];
430-
suffix.extend_from_slice(last_input_sequence);
431-
suffix.push(self.outputs.len() as u8);
432-
suffix.extend_from_slice(&self.outputs.concat());
433-
suffix.extend_from_slice(&self.lock_time.to_le_bytes());
434-
suffix.try_into().map_err(Error::BinarySv2Error)
435-
}
436-
}
437276

438277
// Test
439278
#[cfg(test)]
@@ -682,15 +521,27 @@ pub mod tests {
682521
235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
683522
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
684523
];
685-
let coinbase = Transaction::deserialize(encoded).unwrap();
686-
let stripped = StrippedCoinbaseTx::from_coinbase(coinbase.clone(), 32).unwrap();
687-
let prefix = stripped.into_coinbase_tx_prefix().unwrap().to_vec();
688-
let suffix = stripped.into_coinbase_tx_suffix().unwrap().to_vec();
524+
525+
let tx = Transaction::deserialize(encoded).unwrap();
526+
527+
// Calculate script_sig_prefix_len by examining the actual script_sig length
528+
// and subtracting the extranonce length (32 in this case)
529+
let extranonce_len = 32;
530+
let script_sig_len = tx.input[0].script_sig.len();
531+
let script_sig_prefix_len = script_sig_len - extranonce_len;
532+
533+
let coinbase = Coinbase {
534+
tx,
535+
script_sig_prefix_len,
536+
};
537+
538+
let prefix = coinbase.clone().coinbase_tx_prefix().unwrap().to_vec();
539+
let suffix = coinbase.clone().coinbase_tx_suffix().unwrap().to_vec();
689540
let extranonce = &[0_u8; 32];
690541
let path: &[binary_sv2::U256] = &[];
691542
let stripped_merkle_root =
692543
merkle_root_from_path(&prefix[..], &suffix[..], extranonce, path).unwrap();
693-
let og_merkle_root = coinbase.txid().to_vec();
544+
let og_merkle_root = coinbase.tx.txid().to_vec();
694545
assert!(
695546
stripped_merkle_root == og_merkle_root,
696547
"stripped tx hash is not the same as bitcoin crate"
@@ -720,15 +571,24 @@ pub mod tests {
720571
let mut encoded_clone = encoded.clone();
721572
encoded_clone.extend_from_slice(&extranonce[..]);
722573
encoded_clone.extend_from_slice(coinbase_suffix);
723-
// let mut i = 1;
724-
// while let Err(_) = Transaction::deserialize(&encoded_clone) {
725-
// encoded_clone = encoded.clone();
726-
// extranonce.push(0);
727-
// encoded_clone.extend_from_slice(&extranonce[..]);
728-
// encoded_clone.extend_from_slice(coinbase_suffix);
729-
// i+=1;
730-
// }
731-
// println!("SIZE: {:?}", i);
732-
Transaction::deserialize(&encoded_clone).unwrap();
574+
575+
let parsed_tx = Transaction::deserialize(&encoded_clone).unwrap();
576+
577+
let coinbase = Coinbase {
578+
tx: parsed_tx.clone(),
579+
script_sig_prefix_len: 60,
580+
};
581+
582+
let prefix = coinbase.clone().coinbase_tx_prefix().unwrap().to_vec();
583+
let suffix = coinbase.clone().coinbase_tx_suffix().unwrap().to_vec();
584+
585+
let mut encoded_b = vec![];
586+
encoded_b.extend_from_slice(&prefix);
587+
encoded_b.extend_from_slice(&extranonce[..]);
588+
encoded_b.extend_from_slice(&suffix);
589+
590+
// parse one last time to ensure that Coinbase::coinbase_tx_prefix and
591+
// Coinbase::coinbase_tx_suffix functions are sane
592+
let _parsed_tx_b = Transaction::deserialize(&encoded_b).unwrap();
733593
}
734594
}

0 commit comments

Comments
 (0)