Skip to content
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

Merged
merged 10 commits into from
Feb 28, 2019
Merged

Conversation

dongcarl
Copy link
Member

@dongcarl dongcarl commented Jul 6, 2018

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:

  1. Could I add the type aliases I'm defining here to the relevant modules? (e.g. adding KeyID to util::bip32::ExtendedPubKey)
  2. Does psbt.rs belong in src/network or some other module?

First time contributing... Excited 😄

@dongcarl
Copy link
Member Author

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:

  1. This code is much DRY-er than the bitcoind implementation thanks to macros and traits (yay rust)
  2. The decode/encode process has an intermediary type: PSBTRawKVType, which is inserted/extracted from PSBTMaps
  3. Currently the PartiallySignedTransaction type can merge (as a combiner) and implements Into<Transaction> (as a extractor), although this code is not as robust as the encoding/decoding code

Any initial comments are welcome as I attempt to finalize this

@TheBlueMatt
Copy link
Member

With all the new serialization/deserialization code this almost certainly needs new entries in fuzz/fuzz_targets.

@dongcarl dongcarl force-pushed the psbt branch 3 times, most recently from 320948e to d64fcec Compare August 2, 2018 22:08
@dongcarl
Copy link
Member Author

dongcarl commented Aug 3, 2018

@TheBlueMatt Works now, tests passed, fuzz tests are there.

@dongcarl dongcarl changed the title WIP: BIP174 (de)serialization support Ready for Review: BIP174 (de)serialization support Aug 3, 2018
@dongcarl
Copy link
Member Author

@TheBlueMatt @apoelstra Cleaned up my commits... First 3 commits which are non-PSBT are also available as cherry-picks at #122 #123 #124

@dongcarl
Copy link
Member Author

Made PSBT-specific deserialization not influence other modules

Copy link
Contributor

@jeandudey jeandudey left a 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),
Copy link
Contributor

@jeandudey jeandudey Aug 10, 2018

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).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 7a6df92 and 5168c7c, will rebase

Copy link
Contributor

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).

Copy link
Member Author

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 {
Copy link
Contributor

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).

Copy link
Member Author

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 impls?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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 Froms at once

Copy link
Member

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.

Copy link
Member

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.

}
}

impl From<Transaction> for Result<PSBTGlobal, util::Error> {
Copy link
Contributor

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> {
        ....
    }
}

Copy link
Member Author

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?

Copy link
Member Author

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

@apoelstra
Copy link
Member

Needs rebasing on the new ChildNumber semantics.

}
}

impl From<Transaction> for Result<PartiallySignedTransaction, util::Error> {
Copy link
Member

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.

Copy link
Contributor

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

Copy link
Member Author

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.

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 {
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Much cleaner...


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)?;
Copy link
Member

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)?

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()),
Copy link
Member

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.

@apoelstra
Copy link
Member

I'd like if none of the types had Psbt prefixing them; you can pub use them in util/psbt/mod.rs so that users can import the module then use them like psbt::Global rather than needing to import the full type and writing PsbtGlobal.

match tx_raw_kv_pair.len() {
0 => Err(D::Error::from(Error::MustHaveUnsignedTx)),
1 => {
let tx: Transaction = deserialize(&tx_raw_kv_pair[0].value[..])?;
Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

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?

@TheBlueMatt
Copy link
Member

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).

@dongcarl
Copy link
Member Author

@TheBlueMatt Good thing we have PSBT specific serialization/deserialization traits... Will do.

src/util/psbt/map/global.rs Show resolved Hide resolved
}
}

if self.final_script_sig.is_none() && self.final_script_witness.is_none() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are we sure we want to enforce this like this? Instead of making the caller decide to provide this redundant information?

Or is this a protocol requirement?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the BIP:

The Input Finalizer must only accept a PSBT. For each input, the Input Finalizer determines if the input has enough data to pass validation. If it does, it must construct the scriptSig and scriptWitness and place them into the input key-value map. All other data except the UTXO and unknown fields in the input key-value map should be cleared from the PSBT. The UTXO should be kept to allow Transaction Extractors to verify the final network serialized transaction.

Perhaps the function could be renamed in the future, it's more providing an intermediary representation where everything will be serialized rather than just spitting out all the Pairs (which is what get_pairs seems to imply).

Copy link
Collaborator

@stevenroose stevenroose Feb 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think get_pairs doesn't qualify as the "Input Finalizer" from the spec.

I'd be more in favor of an additional method finalize or remove_redundant_fields hat strips all no-longer-relevant information from it. Now this would break round-tripping PSBT structs that have redundant information as get_pairs is used for serializing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @stevenroose. These if statements also prevent PSBTs from round-tripping which makes fuzzing harder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevenroose @apoelstra How about we remove these if statements for now, and in a future PR introduce finalize?

type_value: 0u8,
key: vec![],
},
value: self.unsigned_tx.serialize(),
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Collaborator

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.

Copy link
Member

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).

Copy link
Member

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?

Copy link
Contributor

@achow101 achow101 Feb 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes (maybe)

@apoelstra
Copy link
Member

Couple comments which don't really match any lines of code:

  1. As I mentioned on IRC, secp256k1::PublicKey should be replaced everywhere by util::key::PublicKey to ensure compressedness is preserved.
  2. We can't test perfect round-tripping because hashmaps don't have guaranteed orders; but it would be good to reserialize PSBTs in our fuzz tests and at least test that the lengths and some sort of order-independent checksum remain the same. (I'd accept a sum of all the bytes as an "order-independent checksum".)

@dongcarl
Copy link
Member Author

Key types are replaced and manual serialization done.

stevenroose
stevenroose previously approved these changes Feb 28, 2019
Copy link
Collaborator

@stevenroose stevenroose left a 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
Copy link
Collaborator

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.

@stevenroose stevenroose self-requested a review February 28, 2019 12:37
- 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
@dongcarl
Copy link
Member Author

I think that the following changes can be done in separate PRs, and I currently don't have the bandwidth to complete them:

  1. Some kind of finalize method as detailed by @stevenroose here: BIP174 (de)serialization support #103 (comment) (I will update this current PR to be dumb when serializing/deserializing)
  2. Improved fuzzing as detailed by @apoelstra here: BIP174 (de)serialization support #103 (comment)

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
@apoelstra
Copy link
Member

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..)

@apoelstra apoelstra self-requested a review February 28, 2019 16:47
Copy link
Collaborator

@stevenroose stevenroose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK e5b5912

@apoelstra apoelstra merged commit 67e4671 into rust-bitcoin:master Feb 28, 2019
Tibo-lg pushed a commit to p2pderivatives/rust-bitcoin that referenced this pull request Sep 1, 2020
Davidson-Souza pushed a commit to Davidson-Souza/rust-bitcoin that referenced this pull request Jul 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants