Skip to content

Commit 937d4db

Browse files
committed
Merge #431: fix(types): use f64 for DecodePsbt fee field
3113bf1 fix(types): use f64 for DecodePsbt fee field (deadmanoz) Pull request description: As per raised in #429, addressing type discrepancies between `corepc-types` and Bitcoin Core Bitcoin Core returns the `fee` field in the `decodepsbt` RPC response as a floating-point (double) BTC value, not as satoshis (integer). The current implementation uses `u64` for the fee field and converts it with `Amount::from_sat()`, which causes a deserialisation failure. Minimal repro with a PSBT from Bitcoin Core's functional tests. https://github.com/bitcoin/bitcoin/blob/13891a8a685d255cb13dd5018e3d5ccc18b07c34/test/functional/data/rpc_psbt.json#L96 ```rust use corepc_client::client_sync::v28::Client; use corepc_client::client_sync::Auth; use corepc_client::types::v28::DecodePsbt; fn main() -> Result<(), Box<dyn std::error::Error>> { // First valid PSBT from Bitcoin Core's test/functional/data/rpc_psbt.json (valid[0]). let psbt = concat!( "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////", "AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxH", "BQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/", "hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6Uwpy", "N+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLr", "CwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m8", "0GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc", "0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/j", "nF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4O", "FyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkz", "gHNEZPhPKrMAAAAAAAAA", ); let auth = Auth::UserPass("bitcoin".to_string(), "bitcoin".to_string()); let client = Client::new_with_auth("http://127.0.0.1:8332", auth)?; let decoded: DecodePsbt = client.call("decodepsbt", &[serde_json::json!(psbt)])?; println!("fee: {:?}", decoded.fee); Ok(()) } ``` ``` Error: JsonRpc(Json(Error("invalid type: floating point `3.01e-6`, expected u64", line: 1, column: 2896))) ``` This change: - Changes the fee field type from `u64` to `f64` in `DecodePsbt` struct (as required in v17, v23, v24, v30) - Updates the conversion to use `Amount::from_btc()` instead of `Amount::from_sat()` - Adds a `Fee(ParseAmountError)` error variant to `DecodePsbtError` for proper error handling of the fee - Adds an integration test to verify correct deserialisation ACKs for top commit: tcharding: ACK 3113bf1 Tree-SHA512: de6ec1c23869df79bfb7317831531d42a1c08602af77ff1575a514f47356112f55f5327a9d6fa7923fddb5e3253768ae5013454439ff29a4fe1aa4075df1c66c
2 parents cff690d + 3113bf1 commit 937d4db

File tree

14 files changed

+83
-38
lines changed

14 files changed

+83
-38
lines changed

integration_test/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
33
use std::path::PathBuf;
44

5+
use bitcoin::bip32::{Fingerprint, Xpriv, Xpub};
6+
use bitcoin::secp256k1::{Secp256k1, XOnlyPublicKey};
7+
use bitcoin::Network;
58
use node::{Conf, P2P};
69
use rand::distributions::Alphanumeric;
710
use rand::Rng;
@@ -148,3 +151,25 @@ pub fn three_node_network() -> (Node, Node, Node) {
148151

149152
(node1, node2, node3)
150153
}
154+
155+
/// BIP32 key set for testing.
156+
pub struct TestKeys {
157+
pub xprv: Xpriv,
158+
pub xpub: Xpub,
159+
pub fingerprint: Fingerprint,
160+
pub x_only_public_key: XOnlyPublicKey,
161+
}
162+
163+
/// Returns deterministic test keys derived from a zero seed.
164+
pub fn test_keys() -> TestKeys {
165+
let secp = Secp256k1::new();
166+
let seed = [0u8; 32];
167+
let xprv = Xpriv::new_master(Network::Regtest, &seed).unwrap();
168+
let xpub = Xpub::from_priv(&secp, &xprv);
169+
TestKeys {
170+
xprv,
171+
xpub,
172+
fingerprint: xpub.fingerprint(),
173+
x_only_public_key: xprv.private_key.x_only_public_key(&secp).0,
174+
}
175+
}

integration_test/tests/raw_transactions.rs

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
#![allow(non_snake_case)] // Test names intentionally use double underscore.
66
#![allow(unused_imports)] // Because of feature gated tests.
77

8+
use bitcoin::bip32::DerivationPath;
89
use bitcoin::consensus::encode;
910
use bitcoin::hex::FromHex as _;
1011
use bitcoin::opcodes::all::*;
1112
use bitcoin::{
1213
absolute, consensus, hex, psbt, script, transaction, Amount, ScriptBuf, Transaction, TxOut,
1314
};
14-
use integration_test::{Node, NodeExt as _, Wallet};
15+
use integration_test::{test_keys, Node, NodeExt as _, Wallet};
1516
use node::vtype::*;
1617
use node::{mtype, Input, Output}; // All the version specific types.
1718

@@ -127,57 +128,57 @@ fn raw_transactions__create_raw_transaction__modelled() {
127128
create_sign_send(&node);
128129
}
129130

130-
// Notes on testing decoding of PBST.
131-
//
132-
// - `bip32_derivs` field in the input list of the decoded PSBT changes shape a bunch of times.
133-
// - In v23 a bunch of additional fields are added.
134-
// - In v24 taproot fields are added.
135-
//
136-
// All this should still be handled by `into_model` because `bitcoin::Psbt` has all optional fields.
131+
// Tests PSBT decoding across Bitcoin Core versions.
132+
// Version-specific assertions are gated below.
137133
#[test]
138134
fn raw_transactions__decode_psbt__modelled() {
139135
let node = Node::with_wallet(Wallet::Default, &["-txindex"]);
140136
node.fund_wallet();
141137

138+
// v17: utxoupdatepsbt unavailable
139+
#[cfg(feature = "v17")]
142140
let mut psbt = create_a_psbt(&node);
143141

144-
// A bunch of new fields got added in v23.
145-
//
146-
// Add an arbitrary global xpub to see if it decodes. Before v23 this will end up in `unknown`,
147-
// from v23 onwards in should be in its own field.
148-
{
149-
use std::collections::BTreeMap;
150-
151-
use bitcoin::bip32::{DerivationPath, Fingerprint, Xpub};
142+
// v18+: utxoupdatepsbt available
143+
#[cfg(not(feature = "v17"))]
144+
let mut psbt = {
145+
let psbt = create_a_psbt(&node);
146+
let updated: UtxoUpdatePsbt = node.client.utxo_update_psbt(&psbt).expect("utxoupdatepsbt");
147+
updated.into_model().expect("UtxoUpdatePsbt into model").0
148+
};
152149

153-
let mut map = BTreeMap::default();
154-
// Some arbitrary xpub I grabbed from rust-bitcoin.
155-
let xpub = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL";
156-
let xpub = xpub.parse::<Xpub>().expect("failed to parse xpub");
157-
let fp = Fingerprint::from([1u8, 2, 3, 42]);
158-
let path =
159-
"m/84'/0'/0'/0/1".parse::<DerivationPath>().expect("failed to parse derivation path");
160-
map.insert(xpub, (fp, path));
150+
let keys = test_keys();
151+
let path: DerivationPath = "m/84'/0'/0'/0/1".parse().expect("valid derivation path");
152+
psbt.xpub.insert(keys.xpub, (keys.fingerprint, path));
161153

162-
psbt.xpub = map;
154+
// v24+: Taproot fields
155+
#[cfg(not(feature = "v23_and_below"))]
156+
{
157+
psbt.inputs[0].tap_internal_key = Some(keys.x_only_public_key);
163158
}
164159

165160
let encoded = psbt.to_string();
166-
167161
let json: DecodePsbt = node.client.decode_psbt(&encoded).expect("decodepsbt");
168162
let model: Result<mtype::DecodePsbt, DecodePsbtError> = json.into_model();
169-
170-
#[allow(unused_variables)]
171163
let decoded = model.unwrap();
172164

173-
// Before Core v23 global xpubs was not a known keypair.
165+
// v18/v19: utxoupdatepsbt can't populate UTXO data, so fee is None
166+
#[cfg(feature = "v19_and_below")]
167+
assert!(decoded.fee.is_none());
168+
169+
// v20+: utxoupdatepsbt allows fee to be calculated
170+
#[cfg(not(feature = "v19_and_below"))]
171+
assert!(decoded.fee.expect("fee should be present").to_sat() > 0);
172+
173+
// v23+: dedicated xpub field; earlier versions store in `unknown`.
174174
#[cfg(feature = "v22_and_below")]
175175
assert_eq!(decoded.psbt.unknown.len(), 1);
176176

177177
#[cfg(not(feature = "v22_and_below"))]
178178
assert_eq!(decoded.psbt.xpub.len(), 1);
179179

180-
// TODO: Add a taproot field and test it with v24
180+
#[cfg(not(feature = "v23_and_below"))]
181+
assert_eq!(decoded.psbt.inputs[0].tap_internal_key, Some(keys.x_only_public_key));
181182
}
182183

183184
#[test]

types/src/v17/raw_transactions/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub enum DecodePsbtError {
2424
Inputs(PsbtInputError),
2525
/// Conversion of one of the PSBT outputs failed.
2626
Outputs(PsbtOutputError),
27+
/// Conversion of the `fee` field failed.
28+
Fee(ParseAmountError),
2729
}
2830

2931
impl fmt::Display for DecodePsbtError {
@@ -35,6 +37,7 @@ impl fmt::Display for DecodePsbtError {
3537
Self::Inputs(ref e) => write_err!(f, "conversion of one of the PSBT inputs failed"; e),
3638
Self::Outputs(ref e) =>
3739
write_err!(f, "conversion of one of the PSBT outputs failed"; e),
40+
Self::Fee(ref e) => write_err!(f, "conversion of the `fee` field failed"; e),
3841
}
3942
}
4043
}
@@ -47,6 +50,7 @@ impl std::error::Error for DecodePsbtError {
4750
Self::Unknown(ref e) => Some(e),
4851
Self::Inputs(ref e) => Some(e),
4952
Self::Outputs(ref e) => Some(e),
53+
Self::Fee(ref e) => Some(e),
5054
}
5155
}
5256
}

types/src/v17/raw_transactions/into.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ impl DecodePsbt {
124124

125125
let psbt =
126126
bitcoin::Psbt { unsigned_tx, version, xpub, proprietary, unknown, inputs, outputs };
127-
let fee = self.fee.map(Amount::from_sat);
127+
let fee = self.fee.map(Amount::from_btc).transpose().map_err(E::Fee)?;
128128

129129
Ok(model::DecodePsbt { psbt, fee })
130130
}

types/src/v17/raw_transactions/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ pub struct DecodePsbt {
143143
/// Array of transaction outputs.
144144
pub outputs: Vec<PsbtOutput>,
145145
/// The transaction fee paid if all UTXOs slots in the PSBT have been filled.
146-
pub fee: Option<u64>,
146+
pub fee: Option<f64>,
147147
}
148148

149149
/// An input in a partially signed Bitcoin transaction. Part of `decodepsbt`.

types/src/v23/raw_transactions/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use core::fmt;
44

5+
use bitcoin::amount::ParseAmountError;
56
use bitcoin::{address, bip32, hex, sighash};
67

78
use super::{Bip32DerivError, PartialSignatureError, RawTransactionError, WitnessUtxoError};
@@ -22,6 +23,8 @@ pub enum DecodePsbtError {
2223
Inputs(PsbtInputError),
2324
/// Conversion of one of the PSBT outputs failed.
2425
Outputs(PsbtOutputError),
26+
/// Conversion of the `fee` field failed.
27+
Fee(ParseAmountError),
2528
}
2629

2730
impl fmt::Display for DecodePsbtError {
@@ -37,6 +40,7 @@ impl fmt::Display for DecodePsbtError {
3740
Self::Inputs(ref e) => write_err!(f, "conversion of one of the PSBT inputs failed"; e),
3841
Self::Outputs(ref e) =>
3942
write_err!(f, "conversion of one of the PSBT outputs failed"; e),
43+
Self::Fee(ref e) => write_err!(f, "conversion of the `fee` field failed"; e),
4044
}
4145
}
4246
}
@@ -51,6 +55,7 @@ impl std::error::Error for DecodePsbtError {
5155
Self::Unknown(ref e) => Some(e),
5256
Self::Inputs(ref e) => Some(e),
5357
Self::Outputs(ref e) => Some(e),
58+
Self::Fee(ref e) => Some(e),
5459
}
5560
}
5661
}

types/src/v23/raw_transactions/into.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl DecodePsbt {
6767
inputs,
6868
outputs,
6969
};
70-
let fee = self.fee.map(Amount::from_sat);
70+
let fee = self.fee.map(Amount::from_btc).transpose().map_err(E::Fee)?;
7171

7272
Ok(model::DecodePsbt { psbt, fee })
7373
}

types/src/v23/raw_transactions/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub struct DecodePsbt {
4747
/// Array of transaction outputs.
4848
pub outputs: Vec<PsbtOutput>,
4949
/// The transaction fee paid if all UTXOs slots in the PSBT have been filled.
50-
pub fee: Option<u64>,
50+
pub fee: Option<f64>,
5151
}
5252

5353
/// An item from the global xpubs list. Part of `decodepsbt`.

types/src/v24/raw_transactions/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use core::fmt;
44

5+
use bitcoin::amount::ParseAmountError;
56
use bitcoin::taproot::{IncompleteBuilderError, TaprootBuilderError, TaprootError};
67
use bitcoin::{bip32, hex, secp256k1, sighash};
78

@@ -23,6 +24,8 @@ pub enum DecodePsbtError {
2324
Inputs(PsbtInputError),
2425
/// Conversion of one of the PSBT outputs failed.
2526
Outputs(PsbtOutputError),
27+
/// Conversion of the `fee` field failed.
28+
Fee(ParseAmountError),
2629
}
2730

2831
impl fmt::Display for DecodePsbtError {
@@ -38,6 +41,7 @@ impl fmt::Display for DecodePsbtError {
3841
Self::Inputs(ref e) => write_err!(f, "conversion of one of the PSBT inputs failed"; e),
3942
Self::Outputs(ref e) =>
4043
write_err!(f, "conversion of one of the PSBT outputs failed"; e),
44+
Self::Fee(ref e) => write_err!(f, "conversion of the `fee` field failed"; e),
4145
}
4246
}
4347
}
@@ -52,6 +56,7 @@ impl std::error::Error for DecodePsbtError {
5256
Self::Unknown(ref e) => Some(e),
5357
Self::Inputs(ref e) => Some(e),
5458
Self::Outputs(ref e) => Some(e),
59+
Self::Fee(ref e) => Some(e),
5560
}
5661
}
5762
}

types/src/v24/raw_transactions/into.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl DecodePsbt {
7272
inputs,
7373
outputs,
7474
};
75-
let fee = self.fee.map(Amount::from_sat);
75+
let fee = self.fee.map(Amount::from_btc).transpose().map_err(E::Fee)?;
7676

7777
Ok(model::DecodePsbt { psbt, fee })
7878
}

0 commit comments

Comments
 (0)