-
Notifications
You must be signed in to change notification settings - Fork 829
Add Coinbase(Transaction)
newtype to distinguish coinbase transactions
#4563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
🚨 API BREAKING CHANGE DETECTED To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section. |
Pull Request Test Coverage Report for Build 15969096369Details
💛 - Coveralls |
Why is this draft mate? Are you chasing concept ack or full review? |
I just marked it as a draft to double-check a few things before opening it up. it's ready for full review now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to give an unhelpful review: something doesn't quite feel right about this but I can't work out what. Perhaps one of the other fellas can shed light on it. The fact that we map all the errors using |_|
is a hint I think. I'll re-review after the error variant thing is fixed, maybe something will fall out.
bitcoin/src/blockdata/transaction.rs
Outdated
|
||
|
||
impl<'a> TryFrom<&'a Transaction> for &'a Coinbase { | ||
type Error = CoinbaseError; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error includes a variant that is never returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like a better approach for error handling would be to rename Bip34Error
to something more generic and use it for both methods: coinbase()
and bip34_block_height()
That way I can have all error variants related to coinbase operations in one place instead of creating two separate error types !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this repo we favour having exact errors that only include variants that are returned by the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks for the clarification
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At minimum, I want the transparent_newtype
macro to be used so that we don't have to burn brain power on thinking about unsafe
. The remaining requests are fairly important though might be controversial.
bitcoin/src/blockdata/block.rs
Outdated
#[non_exhaustive] | ||
pub enum CoinbaseError { | ||
/// Block has no transactions. | ||
NoTransactions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this is acceptable for now but we really should have a type invariant that a block must have at least one tx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
We're not using CoinbaseError
anymore. Moved error handling to block-level validation with NoTransactions
and InvalidCoinbase
in InvalidBlockError
instead of a separate CoinbaseError
bitcoin/src/blockdata/transaction.rs
Outdated
/// This type exists to distinguish coinbase transactions from regular ones at the type level. | ||
#[derive(Clone, PartialEq, Eq, Debug, Hash)] | ||
#[repr(transparent)] | ||
pub struct Coinbase(Transaction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to use the transparent_newtype
macro from internals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
bitcoin/src/blockdata/transaction.rs
Outdated
fn deref(&self) -> &Self::Target { | ||
&self.0 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really convinced about this. It's generally an anti-pattern to do this kind of "inheritance". I'd rather conservatively remove it now and perhaps add it later if not having it is way too annoying (but most likely it's not correct to add it anyway).
bitcoin/src/blockdata/transaction.rs
Outdated
|
||
fn try_from(tx: &'a Transaction) -> Result<Self, Self::Error> { | ||
if tx.is_coinbase() { | ||
Ok(unsafe { &*(tx as *const Transaction as *const Coinbase) }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transparent_newtype
would avoid this.
Anyway, I'm not sure if we should have this impl. I think that being coinbase is more a property of a block rather than transaction. If we want to have a method I think it should be something like assume_first_transaction(tx: Transaction) -> Coinbase
, and _ref
for references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Used transparent_newtype
macro and removed the try_from
approach
bitcoin/src/blockdata/block.rs
Outdated
@@ -315,7 +315,7 @@ impl BlockCheckedExt for Block<Checked> { | |||
return Err(Bip34Error::Unsupported); | |||
} | |||
|
|||
let cb = self.coinbase().map_err(|_| Bip34Error::NotPresent)?; | |||
let cb = self.coinbase().map_err(|_| Bip34Error::NoCoinbase)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it more, I think having a mostly-valid coinbase should be an invariant of Block
. The only invalidity I'd allow is precisely BIP-34 check because that one requires block height which needs external information to check.
Apologise for the delay in addressing your feedback. I think all review comments have been addressed:
|
bitcoin/src/blockdata/block.rs
Outdated
|
||
impl fmt::Display for CoinbaseError { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
use CoinbaseError::*; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6dce354:
Please don't do wildcard imports like this. Just use Self::
prefixes on the variants in your match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted. Also, I'm not using CoinbaseError
anymore, I’ve already removed it
|
||
impl Coinbase { | ||
/// Computes the [`Txid`] of this coinbase transaction. | ||
pub fn compute_txid(&self) -> Txid { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6dce354:
Why have these methods? They are available through Deref
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved away from using implicit inheritance via Deref
to avoid inheriting the entire Transaction API. Instead, we now expose only the methods that make sense for coinbase transactions.
inner()
serves as an explicit escape hatch when full Transaction access is needed. This forces callers to be intentional about when they need the underlying Transaction functionalities.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why shouldn't we inherit the entire Transaction
API? A coinbase is a transaction.
primitives/src/block.rs
Outdated
@@ -76,6 +76,12 @@ impl Block<Unchecked> { | |||
Block { header, transactions, witness_root: None, marker: PhantomData::<Unchecked> } | |||
} | |||
|
|||
/// Adds a transaction to the block. | |||
#[inline] | |||
pub fn push_transaction(&mut self, transaction: Transaction) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 1babd2c:
This method does not update the merkle root in the block header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method isn't used in this PR. Presumably you want to drop it, since adding it properly would be a bit involved (I guess we want to maintain at least the rightmost merkle path in the block?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it fits better into block::Builder
where the merkle root is only computed once all transactions were added.
Can you squash the review responses into the commits that they affect? It's very hard to review a "grab bag" commit of stuff that undoes changes from previous commits. |
7dfd510
to
dfc9c4b
Compare
Thanks for the review. I believe I’ve addressed all the feedback. You can also find the reasoning behind each change in the commit messages |
Being a mere mortal I cannot read that macro without devoting a whole bunch of time to it, which I don't want to right now but AFAICT the current usage does not enforce any invariants. So the |
/// Computes the [`Wtxid`] of this coinbase transaction. | ||
pub fn compute_wtxid(&self) -> Wtxid { | ||
self.0.compute_wtxid() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API suggestion:
/// Returns a reference to the inner transaction.
pub fn as_inner(&self) -> &Transaction { &self.0 }
/// Returns the inner transaction.
pub fn into_inner(self) -> Transaction { self.0 }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, added as_inner
and into_inner
as suggested
thanks for pointing to the guidelines
bitcoin/src/blockdata/transaction.rs
Outdated
|
||
impl Coinbase { | ||
/// Creates a reference to `Coinbase` from a reference to the inner `Transaction`. | ||
pub fn from_ref(inner: &_) -> &Self; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From looking at other call sites I think this should probably be from_transaction_ref
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or if there is not supposed to be an invariant perhaps it should be from_first_transaction_ref
or something (to match the naming of the other constructors).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's definitely an invariant that the transaction is valid coinbase (including being zeroth in the block - something it cannot check). It wouldn't solve anything otherwise.
The name should be something like assume_coinbase_ref
. It definitely needs the assume
word (or unchecked
but that is often used with UB). But I wonder if we need it to be public - it should only need to be called inside Block
and some kind of CoinbaseBuilder
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be public. People will create coinbase transactions in all sorts of ways, including by reading them off the wire in ad-hoc mining protocols.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed the method to assume_coinbase_ref
to reflect that the caller must ensure it's a valid coinbase.
I Kept it public as per the feedback on ad-hoc mining protocols. Open to further changes if needed!
dfc9c4b
to
0abadc3
Compare
Can you move the last commit into the commit that originally creates the function please mate. This is repository specific, other repos may, and will, have different strategies. In this repo after review we make changes to the patch set and then force push. Its a reasonably common strategy, you should be able to find someone online explaining it. Or this prompt into opus gave me a pretty good response "when working on github with git why do some projects fix patches and force push instead of adding new patches onto the end after review". |
Coinbase transactions have unique consensus rules and exceptions that distinguish them from regular txs. We represent them using a dedicated Coinbase(Transaction) new type We use the transparent_newtype macro from internals for safe reference conversion (&Transaction -> &Coinbase) without manual unsafe code also it automatically adds #[repr(transparent)] which guarantees that Coinbase has the exact same memory layout as Transaction
We provide explicit convenience methods (compute_txid, compute_wtxid) rather than implementing Deref to only expose methods that make sense for coinbase transactions. This prevents inheritance of the entire Transaction API. inner() serves as an escape hatch when full Transaction access is needed. This forces the user to be explicit about when they need the underlying Transaction functionalities. Following this approach, we avoid implicit inheritance through Deref. We also added assume_* methods to make it clear that the caller is responsible for ensuring the transaction is actually a coinbase transaction in the first position of a block
…action> This change has semantic meaning: it guarantees that every checked Block has a coinbase as its first transaction. The method is now infallible for validated blocks.
Add validation checks to Block::validate() to ensure blocks have a valid coinbase: - Check for empty transaction list (NoTransactions error) - Verify first transaction is coinbase (InvalidCoinbase error) The validation now happens during Block::validate() rather than requiring every caller to check coinbase presence separately.
Remove unnecessary error handling from bip34_block_height since coinbase() now returns &Coinbase instead of Option<&Transaction>. Use cb.inner() to access the underlying Transaction fields for input processing.
Add thorough test coverage for the new Coinbase type and validation logic
42d5517
to
23ef6c4
Compare
Done
Appreciate the time you take to explain these best practices |
/// Returns the coinbase transaction, if one is present. | ||
fn coinbase(&self) -> Option<&Transaction> { self.transactions().first() } | ||
/// Returns the coinbase transaction. | ||
/// | ||
/// This method is infallible because validation ensures a valid coinbase transaction is always present. | ||
fn coinbase(&self) -> &Coinbase { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: On the trait method impls we don't need to repeat the docs. If you are keen you could add a patch at the front that removes the original line also the one on bip34_block_height
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In ba5fb26
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 23ef6c4
This has shaped up to be a nice PR. Process comment: I ack'd so you can leave it as is if you want. Or you can see to the nit if you want to. Its trivial for me to re-ack since I posted the commit hash and can use |
/// | ||
/// This method does not validate that the transaction is actually a coinbase transaction. | ||
/// The caller must ensure that this transaction is indeed the first transaction in a valid block. | ||
pub fn assume_first_transaction(tx: Transaction) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be just assume_coinbase
. Coinbase has more validity requirements than just being the first, so simply saying "first" is misleading and enumerating all of them in the method name would make the name insanely long.
/// The caller must ensure that this transaction is indeed the first transaction in a valid block. | ||
pub fn assume_first_transaction_ref(tx: &Transaction) -> &Self { | ||
Self::assume_coinbase_ref(tx) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't needed, the assume_coinbase_ref
method already exists.
} | ||
|
||
/// Returns a reference to the inner transaction. | ||
pub fn as_inner(&self) -> &Transaction { &self.0 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want this name here? Maybe as_transaction
or as_any_transaction
or something along these lines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could go either way on this. Leaning toward agreeing with you since we've generally moved away from *_inner
naming in this library.
Whatever we choose we should doc-alias it to the other.
} | ||
|
||
/// Computes the [`Wtxid`] of this coinbase transaction. | ||
pub fn compute_wtxid(&self) -> Wtxid { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC coinbase transactions have a constant Wtxid
so this method looks misleading and probably shouldn't exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, let's put just wtxid
here but make sure there's a doc(alias="compute_wtxid")
. I'm not sure if the compiler is smart enough to make use of doc aliases in its error messages.
Self::assume_coinbase_ref(tx) | ||
} | ||
|
||
/// Returns a reference to the inner transaction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps should contain a disclaimer that accessing this might enable nonsensical usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How could it? A coinbase is a transaction.
return Err(InvalidBlockError::NoTransactions); | ||
} | ||
|
||
if !transactions[0].is_coinbase() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FTR, perhaps the method name should be changed to looks_like_coinbase
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think is_coinbase
is fine. Though I think we should have an explicit conversion method that does Transaction
-> Coinbase
and this should be redefined just as self.to_coinbase().is_ok()
.
In principle any coinbase-looking transaction "is a coinbase" in the sense that you could make a block with it as the first transaction.
let cb = self.coinbase().ok_or(Bip34Error::NotPresent)?; | ||
let input = cb.input.first().ok_or(Bip34Error::NotPresent)?; | ||
let cb = self.coinbase(); | ||
let input = cb.as_inner().input.first().ok_or(Bip34Error::NotPresent)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understand it this commit is not standalone and thus a previous one will not compile. We require that each commit can be compiled so that git bisect
can be used reliably.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At minimum, the compile error needs to be fixed.
Coinbase transactions are structurally and semantically different from normal transactions.
We introduce a distinct
Coinbase(Transaction)
newtype to make this distinction explicit at the type level, helping to catch logic that shouldn't apply to coinbases (e.g attempting to look up coinbase inputs in the UTXO set).This PR is split into several patches to keep changes reviewable and to document the rationale behind each step.