-
Notifications
You must be signed in to change notification settings - Fork 729
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
BIP174 (de)serialization support #103
Conversation
After a few days of grinding away, I have an implementation with tests here Too tired right now to write out the entire documentation but here are some comments:
Any initial comments are welcome as I attempt to finalize this |
With all the new serialization/deserialization code this almost certainly needs new entries in fuzz/fuzz_targets. |
320948e
to
d64fcec
Compare
@TheBlueMatt Works now, tests passed, fuzz tests are there. |
@TheBlueMatt @apoelstra Cleaned up my commits... First 3 commits which are non-PSBT are also available as cherry-picks at #122 #123 #124 |
Made PSBT-specific deserialization not influence other modules |
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.
Greate work! I left my two cents 👍
src/util/mod.rs
Outdated
UnsupportedWitnessVersion(u8) | ||
UnsupportedWitnessVersion(u8), | ||
/// PSBT-related error | ||
PSBT(psbt::Error), |
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.
Shouldn't this variant be named Psbt
? All types with acronyms in the codebase only have the first letter uppercased e.g. Sha256dHash
(C-CASE).
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 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 that guideline applies to all types with the PSBT
prefix (PSBTGlobal
and such ones).
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
src/util/mod.rs
Outdated
@@ -165,3 +171,8 @@ impl From<secp256k1::Error> for Error { | |||
} | |||
} | |||
|
|||
impl From<psbt::Error> for Error { |
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.
Please add a #[doc(hidden)]
attribute (C-HIDDEN).
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.
Should that be added to all the From<*> for Error
impl
s?
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.
Ideally
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.
@jeandudey let's open up another PR after this gets merged to fix all of them?
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.
Yes, it's up for another PR.
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.
Going to wait until this one's merged to do this for all From
s at once
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.
Maybe we should change SimpleDecoder
to always use util::Error
rather than being parameterized, and add a pub use util::Error
at the top-level, in the same PR.
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.
#127 added #[doc(hidden)]
everywhere - we should do so in this PR then as well.
src/util/psbt/psbtmap/psbtglobal.rs
Outdated
} | ||
} | ||
|
||
impl From<Transaction> for Result<PSBTGlobal, util::Error> { |
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.
It seems weird to implement this to a std
type, isn't it more clear to have a from_transaction
method in PSBTGlobal
?
Something along the lines:
impl PSBTGlobal {
pub fn from_transaction(tx: Transaction) -> Result<PSBTGlobal, util::Error> {
....
}
}
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 about I add this and leave the From
version as well?
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 for both PsbtGlobal
and PartiallySignedTransaction
9b17114
to
866c2b5
Compare
Needs rebasing on the new |
src/util/psbt/mod.rs
Outdated
} | ||
} | ||
|
||
impl From<Transaction> for Result<PartiallySignedTransaction, util::Error> { |
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.
Is it a standard Rust idiom to impl From
on a Result
? I find it very confusing, From
is supposed to be for things that are conceptually trivial conversions or reinterpretations. But a Transaction
is nothing at all like a Result<T, E>
.
I would prefer that PartiallySignedTransaction
had a from_transaction
method on it.
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 find it weird too, seems confunsing
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, I was trying to emulate the new TryFrom
trait that isn't in 1.14.0, but will just use the dedicated method.
src/util/psbt/mod.rs
Outdated
fn consensus_decode(d: &mut D) -> Result<Self, D::Error> { | ||
let magic: [u8; 4] = ConsensusDecodable::consensus_decode(d)?; | ||
|
||
if [b'p', b's', b'b', b't'] != magic { |
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: you can use *b"psbt"
rather than an explicit array here
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.
Awesome! Much cleaner...
src/util/psbt/mod.rs
Outdated
|
||
impl<S: SimpleEncoder<Error = util::Error>> ConsensusEncodable<S> for PartiallySignedTransaction { | ||
fn consensus_encode(&self, s: &mut S) -> Result<(), S::Error> { | ||
[b'p', b's', b'b', b't'].consensus_encode(s)?; |
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: b"psbt".consensus_encode(s)?
src/util/psbt/mod.rs
Outdated
pub fn from_unsigned_tx(tx: Transaction) -> Result<Self, util::Error> { | ||
Ok(PartiallySignedTransaction { | ||
inputs: Vec::with_capacity(tx.input.len()), | ||
outputs: Vec::with_capacity(tx.output.len()), |
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 this is wrong - search " The transaction format is specified as follows:" in BIP 174 which says that there must be a map for every input and output. Using with_capacity()
will create empty vectors, so that serializing a fresh PartiallySignedTransaction
will result in an invalid PSBT.
I think providing empty maps with vec![Default::default(); tx.input.len()]
should be fine; then an Updater can add actual info.
I'd like if none of the types had |
src/util/psbt/psbtmap/psbtglobal.rs
Outdated
match tx_raw_kv_pair.len() { | ||
0 => Err(D::Error::from(Error::MustHaveUnsignedTx)), | ||
1 => { | ||
let tx: Transaction = deserialize(&tx_raw_kv_pair[0].value[..])?; |
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.
You need to check at some point that raw_kv_pair.key.key.is_empty()
, because as written this will deserialize any key with the type_value 0x00, no matter its length or contents, which the BIP forbids.
Also, Transaction::deserialize
will ignore trailing bytes, which you should flag an error on (and also flag an error if it's signed, has a witness array, etc). Annoyingly to catch trailing bytes you need to copy the RawDecoder
logic out of network::serialize::deserialize
, unwrap the rawdecoder when its done, and check that the underlying cursor is spent.
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.
Hmmm... Anything else that will ignore trailing bytes? I'd be happy to check for all of them
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.
Yes, the deserializer will ignore trailing bytes on all Bitcoin objects because they all self-describe their sizes, and the deserializer is meant to work with streams where "no more bytes" is not always well-defined.
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.
Hmmm... I think that makes sense for ConsensusDecodable
, but perhaps deserialize
should error if the cursor is not consumed as the caller is explicitly saying that the bytes provided should deserialize
to a certain type?
da0bf67
to
da36b34
Compare
After long IRC back-and-forth, it seems we need to have a separate Transaction decoder for PSBT - because we want to support 0-input-decoding in non-witness-serialization-format in PSBT, but probably cant do it in non-PSBT (to fix the SegWit-ambiguity-bug, the easiest thing to do is to encode 0-input txn with witnesses always). |
@TheBlueMatt Good thing we have PSBT specific serialization/deserialization traits... Will do. |
src/util/psbt/map/global.rs
Outdated
type_value: 0u8, | ||
key: vec![], | ||
}, | ||
value: self.unsigned_tx.serialize(), |
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 be replaced with manual serialization code to ensure 0-input transactions are serialized without witnesses. (And the corresponding unit test needs to be changed.)
+ value: {
+ let mut ret = vec![];
+ self.unsigned_tx.version.consensus_encode(&mut ret).unwrap();
+ self.unsigned_tx.input.consensus_encode(&mut ret).unwrap();
+ self.unsigned_tx.output.consensus_encode(&mut ret).unwrap();
+ self.unsigned_tx.lock_time.consensus_encode(&mut ret).unwrap();
+ ret
+ },
});
works.
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.
Same for the non_witneess_utxo
in Input? Would it make sense if I just made sure that psbt::Serialize
for Transaction
always manually serializes?
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.
Hmm, non_witness_utxo
doesn't necessarily mean that the previous tx doesn't have witnesses, right? Not sure if they are entirely irrelevant. They are for txid generation, which I think this field is primarily used for, but I wouldn't dare claim they are entirely unused.
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.
@dongcarl the test vectors have witnesses in non_witness_utxo
. You need to support segwit witnesses. (And this is fine, everything is unambiguous since the referenced transactions need to be valid, i.e. have a nonzero number of outputs).
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.
@achow101 out of curiosity, was this deliberate?
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.
Yes (maybe)
Couple comments which don't really match any lines of code:
|
Key types are replaced and manual serialization done. |
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.
Looks good! Just the nit about adding an explanatory comment.
self.unsigned_tx.input.consensus_encode(&mut ret)?; | ||
self.unsigned_tx.output.consensus_encode(&mut ret)?; | ||
self.unsigned_tx.lock_time.consensus_encode(&mut ret)?; | ||
ret |
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.
Only nit: Add a comment explaining why you're doing the manual serialization here. New readers will be confused otherwise.
- Implement psbt::Error data type - Implement conversion from psbt::Error to util::Error - Create util::psbt module - Create non-public util::psbt::error module
- Add (en)decoding logic for said data types
- Implement psbt::Map trait for psbt::Global - Add converting constructor logic from Transaction for psbt::Global - Add (en)decoding logic for psbt::Global - Always deserialize unsigned_tx as non-witness - Add trait for PSBT (de)serialization - Implement PSBT (de)serialization trait for relevant psbt::Global types - Add macros for consensus::encode-backed PSBT (de)serialization implementations - Add macro for implementing encoding logic for PSBT key-value maps
- Implement psbt::Map trait for psbt::Output - Add (en)decoding logic for psbt::Output - Implement PSBT (de)serialization trait for relevant psbt::Output types - Add macro for merging fields for PSBT key-value maps - Add macro for implementing decoding logic for PSBT key-value maps - Add convenience macro for implementing both encoding and decoding logic for PSBT key-value maps - Add macro for inserting raw PSBT key-value pairs into PSBT key-value maps - Add macro for getting raw PSBT key-value pairs from PSBT key-value maps
I think that the following changes can be done in separate PRs, and I currently don't have the bandwidth to complete them:
Let's get minimal functionality merged in, then we can take our time expanding on it. |
- Implement psbt::Map trait for psbt::Input - Add (en)decoding logic for psbt::Input - Implement PSBT (de)serialization trait for relevant psbt::Input types
- Add merging logic for PartiallySignedTransactions - Add (en)decoding logic for PartiallySignedTransaction - Add converting constructor logic from Transaction for PartiallySignedTransaction - Add extracting constructor logic from PartiallySignedTransaction for Transaction Squashed in fixes from stevenroose <stevenroose@gmail.com> - Prevent PSBT::extract_tx from panicking - Make PartiallySignedTransaction fields public
- Add macro for decoding and unwrapping PartiallySignedTransaction from hex string
Sounds good. I can do fuzzing. Probably it is blocked on deciding how to parse hybrid keys (currently we interpret them as uncompressed and therefore reserialize them wrong; maybe we should reject them, but they are permissible on the blockchain..) |
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 e5b5912
Fix minimum_sum_amount field name
Relax cc dependency requirements.
This is currently a non-working work-in-progress to add BIP174 (de)serialization support to rust-bitcoin.
I'm wondering about the conventions of this project so I can conform to them better, namely:
KeyID
toutil::bip32::ExtendedPubKey
)psbt.rs
belong insrc/network
or some other module?First time contributing... Excited 😄