From c5bac55d0b80dc416f55e66553172c15e2f38fbb Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 16 Jun 2026 20:21:50 +0000 Subject: [PATCH 01/41] manually update nightly version to 2026-06-11. Clippy removed the `from_iter_instead_of_collect` lint due to being "problematic". I don't know that we ever used this, but we had explicitly enabled it. Clippy also added a new `needless_return_with_question_mark` lint which fires numerous times throughout the crate. Supercedes #274. --- Cargo.toml | 3 +-- src/blech32/decode.rs | 2 +- src/pset/map/global.rs | 10 +++++----- src/pset/map/input.rs | 2 +- src/pset/mod.rs | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a1d5f4af..45237dd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" rust-version = "1.74.0" [workspace.metadata.rbmt.toolchains] -nightly = "nightly-2026-05-14" +nightly = "nightly-2026-06-11" stable = "1.96.0" [features] @@ -97,7 +97,6 @@ explicit_iter_loop = "warn" filter_map_next = "warn" flat_map_option = "warn" fn_params_excessive_bools = "warn" -from_iter_instead_of_collect = "warn" if_not_else = "warn" ignored_unit_patterns = "warn" implicit_clone = "warn" diff --git a/src/blech32/decode.rs b/src/blech32/decode.rs index 2dcea3e6..8a504bbb 100644 --- a/src/blech32/decode.rs +++ b/src/blech32/decode.rs @@ -257,7 +257,7 @@ impl<'s> CheckedHrpstring<'s> { let padding_len = fe_iter.len() * 5 % 8; if padding_len > 4 { - return Err(PaddingError::TooMuch)?; + return Err(PaddingError::TooMuch); } let last_fe = fe_iter.last().expect("checked above"); diff --git a/src/pset/map/global.rs b/src/pset/map/global.rs index 0c1cb018..8051cc39 100644 --- a/src/pset/map/global.rs +++ b/src/pset/map/global.rs @@ -174,7 +174,7 @@ impl Map for Global { } self.scalars.push(scalar); } else { - return Err(Error::InvalidKey(raw_key))?; + return Err(Error::InvalidKey(raw_key).into()); } } else if prop_key.is_pset_key() && prop_key.subtype == PSBT_ELEMENTS_GLOBAL_TX_MODIFIABLE @@ -182,7 +182,7 @@ impl Map for Global { if prop_key.key.is_empty() && raw_value.len() == 1 { self.elements_tx_modifiable_flag = Some(raw_value[0]); } else { - return Err(Error::InvalidKey(raw_key))?; + return Err(Error::InvalidKey(raw_key).into()); } } else { match self.proprietary.entry(prop_key) { @@ -467,7 +467,7 @@ impl Decodable for Global { } scalars.push(scalar); } else { - return Err(Error::InvalidKey(raw_key))?; + return Err(Error::InvalidKey(raw_key).into()); } } else if prop_key.is_pset_key() && prop_key.subtype == PSBT_ELEMENTS_GLOBAL_TX_MODIFIABLE @@ -475,7 +475,7 @@ impl Decodable for Global { if prop_key.key.is_empty() && raw_value.len() == 1 { elements_tx_modifiable_flag = Some(raw_value[0]); } else { - return Err(Error::InvalidKey(raw_key))?; + return Err(Error::InvalidKey(raw_key).into()); } } else { match proprietary.entry(prop_key) { @@ -506,7 +506,7 @@ impl Decodable for Global { // Mandatory fields let version = version.ok_or(Error::IncorrectPsetVersion)?; if version != 2 { - return Err(Error::IncorrectPsetVersion)?; + return Err(Error::IncorrectPsetVersion.into()); } let tx_version = tx_version.ok_or(Error::MissingTxVersion)?; let input_count = input_count.ok_or(Error::MissingInputCount)?.0 as usize; diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index ca54d56a..70fc1674 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -688,7 +688,7 @@ impl Map for Input { )?; } PSET_IN_PREVIOUS_TXID | PSET_IN_OUTPUT_INDEX => { - return Err(Error::DuplicateKey(raw_key))?; + return Err(Error::DuplicateKey(raw_key).into()); } PSET_IN_SEQUENCE => { impl_pset_insert_pair! { diff --git a/src/pset/mod.rs b/src/pset/mod.rs index b0f976cf..5c03ca61 100644 --- a/src/pset/mod.rs +++ b/src/pset/mod.rs @@ -747,7 +747,7 @@ impl Decodable for PartiallySignedTransaction { // Maximum pset input size supported if inputs_len > 10_000 { - return Err(Error::TooLargePset)?; + return Err(Error::TooLargePset.into()); } let mut inputs: Vec = Vec::with_capacity(inputs_len); @@ -764,7 +764,7 @@ impl Decodable for PartiallySignedTransaction { // Maximum pset input size supported if outputs_len > 10_000 { - return Err(Error::TooLargePset)?; + return Err(Error::TooLargePset.into()); } let mut outputs: Vec = Vec::with_capacity(outputs_len); From 47d01d13dee717b6f204201662b552fa63c938ca Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 27 Jun 2026 13:11:48 +0000 Subject: [PATCH 02/41] fuzz: gitignore cargo-fuzz corpus/artifact files If we want to keep seeds from these directories we should move them to the qa-assets repo and make some effort to minimize them. It's easy to get 10s of 000s of these files if you're just running the fuzzer without making an effort to clean them up. --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2e47091b..42dea11a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ Cargo.lock *~ #fuzz -fuzz/hfuzz_target -fuzz/hfuzz_workspace +fuzz/corpus +fuzz/artifacts +fuzz/*.log From 9b950fd4896a51b49d366924cb6d7b1cae6b1fa5 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 27 Jun 2026 13:13:34 +0000 Subject: [PATCH 03/41] fuzz: update generate-files.sh You should be able to run this script cleanly. Update it to include a recent Cargo.toml change, and add a big header which will hopefully remind me not to edit the Cargo.toml file directly. --- .github/workflows/cron-daily-fuzz.yml | 5 ++++- fuzz/Cargo.toml | 4 ++++ fuzz/generate-files.sh | 10 +++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml index 6154bf60..7a4f4314 100644 --- a/.github/workflows/cron-daily-fuzz.yml +++ b/.github/workflows/cron-daily-fuzz.yml @@ -1,4 +1,7 @@ -# Automatically generated by fuzz/generate-files.sh +###### +## DO NOT EDIT THIS FILE DIRECTLY. It is generated by generate-fuzz.sh. +## Edit that script instead and re-run it. +###### name: Fuzz on: schedule: diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 899e9a24..ddd191b4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,3 +1,7 @@ +###### +## DO NOT EDIT THIS FILE DIRECTLY. It is generated by generate-fuzz.sh. +## Edit that script instead and re-run it. +###### [package] name = "elements-fuzz" edition = "2021" diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh index 9eb98e0e..cbe54f11 100755 --- a/fuzz/generate-files.sh +++ b/fuzz/generate-files.sh @@ -10,6 +10,10 @@ source "$REPO_DIR/fuzz/fuzz-util.sh" # 1. Generate fuzz/Cargo.toml cat > "$REPO_DIR/fuzz/Cargo.toml" < "$REPO_DIR/.github/workflows/cron-daily-fuzz.yml" < Date: Sat, 27 Jun 2026 13:16:07 +0000 Subject: [PATCH 04/41] fuzz: copy fuzz.sh, cycle.sh and README.md from rust-bitcoin The README has a few references to rust-bitcoin. I left these alone because all the content of the doc should be the same between the two repos, and it is probably a useful hint to somebody reading the README that the fuzz infrastructure was copied from rust-bitcoin. --- fuzz/README.md | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ fuzz/cycle.sh | 26 ++++++++ fuzz/fuzz.sh | 62 +++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 fuzz/README.md create mode 100755 fuzz/cycle.sh create mode 100755 fuzz/fuzz.sh diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..001f1ef0 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,161 @@ +# Fuzzing + +`rust-bitcoin` has fuzzing harnesses setup for use with +`cargo-fuzz`. + +To run the fuzz-tests as in CI -- briefly fuzzing every target -- simply +run + +```bash +./fuzz.sh +``` + +in this directory. + +By default, `fuzz.sh` runs each target for 100 seconds. Pass +`-max_total_time` to run for longer or shorter: + +```bash +./fuzz.sh -max_total_time=300 +``` + +## Fuzzing with weak cryptography + +You may wish to replace the hashing and signing code with broken crypto, +which will be faster and enable the fuzzer to do otherwise impossible +things such as forging signatures or finding preimages to hashes. + +Doing so may result in spurious bug reports since the broken crypto does +not respect the encoding or algebraic invariants upheld by the real crypto. We +would like to improve this, but it's a nontrivial problem -- though not +beyond the abilities of a motivated student with a few months of time. +Please let us know if you are interested in taking this on! + +Meanwhile, to use the broken crypto, simply compile (and run the fuzzing +scripts) with + +```bash +RUSTFLAGS="--cfg=hashes_fuzz --cfg=secp256k1_fuzz" +``` + +which will replace the hashing library with broken hashes, and the +`secp256k1` library with broken cryptography. + +Needless to say, NEVER COMPILE REAL CODE WITH THESE FLAGS because if a +fuzzer can break your crypto, so can anybody. + +## Long-term fuzzing + +To see the full list of targets, the most straightforward way is to run + +```bash +cargo fuzz list +``` + +To run each of them for an hour, run + +```bash +./cycle.sh +``` +This script uses the `chrt` utility to try to reduce the priority of the +jobs. If you would like to run for longer, the most straightforward way +is to edit `cycle.sh` before starting. To run the fuzz-tests in parallel, +you will need to implement a custom harness. + +To run a single fuzztest indefinitely, run + +```bash +cargo +nightly fuzz run "" +``` + +## Adding fuzz tests + +All fuzz tests can be found in the `fuzz_target/` directory. Adding a new +one is as simple as copying an existing one and editing the `do_test` +function to do what you want. + +If your test clearly belongs to a specific crate, please put it in that +crate's directory. Otherwise, you can put it directly in `fuzz_target/`. + +If you need to add dependencies, edit the file `generate-files.sh` to add +it to the generated `Cargo.toml`. + +Once you've added a fuzztest, regenerate the `Cargo.toml` and CI job by +running + +```bash +./generate-files.sh +``` + +Then to test your fuzztest, run + +```bash +./fuzz.sh +``` + +If it is working, you will see a rapid stream of data for many seconds +(you can hit Ctrl+C to stop it early) that looks something like this: +```text +INFO: Running with entropic power schedule (0xFF, 100). +INFO: Seed: 2953319389 +INFO: Loaded 1 modules (9121 inline 8-bit counters): 9121 [0x104132ea0, 0x104135241), +INFO: Loaded 1 PC tables (9121 PCs): 9121 [0x104135248,0x104158c58), +INFO: 0 files found in /some/path/to/rust-bitcoin/fuzz/corpus/units_arbitrary_weight +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes +INFO: A corpus is not provided, starting from an empty corpus +#2 INITED cov: 42 ft: 42 corp: 1/1b exec/s: 0 rss: 36Mb +#411 NEW cov: 43 ft: 43 corp: 2/9b lim: 8 exec/s: 0 rss: 37Mb L: 8/8 MS: 4 ChangeBinInt-ShuffleBytes-ShuffleBytes-InsertRepeatedBytes- +#1329 NEW cov: 43 ft: 44 corp: 3/26b lim: 17 exec/s: 0 rss: 37Mb L: 17/17 MS: 3 InsertRepeatedBytes-CMP-CopyPart- DE: "\001\000\000\000"- +#1357 REDUCE cov: 43 ft: 44 corp: 3/25b lim: 17 exec/s: 0 rss: 37Mb L: 16/16 MS: 3 CopyPart-CMP-EraseBytes- DE: "\000\000\000\000\000\000\000\000"- +... +``` +If you don't see this, you should quickly see an error. + +## Reproducing Failures + +If a fuzztest fails, it will exit with a summary which looks something like +```text +... +thread '' (3001874) panicked at units/src/weight.rs:103:25: +attempt to multiply with overflow +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +==66478== ERROR: libFuzzer: deadly signal + #0 0x0001049fd3c4 in __sanitizer_print_stack_trace+0x28 (librustc-nightly_rt.asan.dylib:arm64+0x5d3c4) + #1 0x000104078b90 in fuzzer::PrintStackTrace()+0x30 (units_arbitrary_weight:arm64+0x100070b90) + #2 0x00010406d074 in fuzzer::Fuzzer::CrashCallback()+0x54 (units_arbitrary_weight:arm64+0x100065074) + #3 0x000180d26740 in _sigtramp+0x34 (libsystem_platform.dylib:arm64+0x3740) + ... +``` +This will tell you where the test failed and is followed by information about how to reproduce the crash. +It will look something like this: + +```text +... +NOTE: libFuzzer has rudimentary signal handlers. + Combine libFuzzer with AddressSanitizer or similar for better crash reports. +SUMMARY: libFuzzer: deadly signal +MS: 2 ChangeByte-CopyPart-; base unit: 25058c6b0d02cd1d71a030ad61c46b7396ddcdb9 +0x5e,0x5e,0x5e,0x5e,0x5e,0x44,0x0,0x0,0x0,0x0,0x0,0x5d,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x5e,0xa,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0xa5,0x1,0x1,0x1, +^^^^^D\000\000\000\000\000]\001\000\000\000\000\000\000^\012\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\245\001\001\001 +artifact_prefix='/some/path/to/rust-bitcoin/fuzz/artifacts/units_arbitrary_weight/'; Test unit written to /some/path/to/rust-bitcoin/fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 +Base64: Xl5eXl5EAAAAAABdAQAAAAAAAF4KAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBpQEBAQ== +──────────────────────────────────────────────────────────────────────────────── + +Failing input: + + fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 + +Output of `std::fmt::Debug`: + + [94, 94, 94, 94, 94, 68, 0, 0, 0, 0, 0, 93, 1, 0, 0, 0, 0, 0, 0, 94, 10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 165, 1, 1, 1] + +Reproduce with: + + cargo fuzz run units_arbitrary_weight fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 + +Minimize test case with: + + cargo fuzz tmin units_arbitrary_weight fuzz/artifacts/units_arbitrary_weight/crash-1b454523d38a6c3f45d453dfea4099f3cb574822 + +──────────────────────────────────────────────────────────────────────────────── +``` diff --git a/fuzz/cycle.sh b/fuzz/cycle.sh new file mode 100755 index 00000000..84c1cb6a --- /dev/null +++ b/fuzz/cycle.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Continuously cycle over fuzz targets running each for 1 hour. +# It uses chrt SCHED_IDLE so that other process takes priority. +# +# For cargo-fuzz usage see https://github.com/rust-fuzz/cargo-fuzz?tab=readme-ov-file#usage + +set -euo pipefail + +REPO_DIR=$(git rev-parse --show-toplevel) +# can't find the file because of the ENV var +# shellcheck source=/dev/null +source "$REPO_DIR/fuzz/fuzz-util.sh" + +while : +do + for targetFile in $(listTargetFiles); do + targetName=$(targetFileToName "$targetFile") + echo "Fuzzing target $targetName ($targetFile)" + + # fuzz for one hour + chrt -i 0 cargo +nightly fuzz run "$targetName" -- -max_total_time=3600 + cargo +nightly fuzz cmin "$targetName" + done +done + diff --git a/fuzz/fuzz.sh b/fuzz/fuzz.sh new file mode 100755 index 00000000..3a4aa602 --- /dev/null +++ b/fuzz/fuzz.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# This script is used to briefly fuzz every target when no target is provided. Otherwise, it will briefly fuzz the +# provided target + +set -euox pipefail + +REPO_DIR=$(git rev-parse --show-toplevel) + +# can't find the file because of the ENV var +# shellcheck source=/dev/null +source "$REPO_DIR/fuzz/fuzz-util.sh" + +target= +max_total_time=100 + +for arg in "$@"; do + case "$arg" in + -max_total_time=*) + max_total_time="${arg#-max_total_time=}" + ;; + -*) + echo "Unknown option: $arg" + exit 2 + ;; + *) + if [ -n "$target" ]; then + echo "Unexpected argument: $arg" + exit 2 + fi + target="$arg" + ;; + esac +done + +case "$max_total_time" in + ''|*[!0-9]*) + echo "-max_total_time must be a non-negative integer number of seconds" + exit 2 + ;; +esac + +# Check that input files are correct Windows file names +checkWindowsFiles + +if [ -z "$target" ]; then + targetFiles="$(listTargetFiles)" +else + targetFiles=fuzz_targets/"$target".rs +fi + +cargo --version +rustc --version + +# Testing +cargo install --force --locked --version 0.12.0 cargo-fuzz +for targetFile in $targetFiles; do + targetName=$(targetFileToName "$targetFile") + echo "Fuzzing target $targetName ($targetFile) for $max_total_time seconds" + # cargo-fuzz will check for the corpus at fuzz/corpus/ + cargo +nightly fuzz run "$targetName" -- -max_total_time="$max_total_time" + checkReport "$targetName" +done From 3c958e2a8701b0d3250a23637d015311a7072f5b Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 16 Jun 2026 14:30:26 +0000 Subject: [PATCH 05/41] blind: encapsulate RangeProofMessage and improve error types In #279 we fixed some slicing bugs in a fairly ugly way so that we could easily backport the bugfixes without API breakage. However, the better solution is to close the RangeProofMessage type, add a dedicated error for parsing from an array, and update all callers. This changes the CT validation logic to allow rangeproofs to contain embedded asset IDs alongside explicit assets. This differs from Elements which only allows unblinding an output if both its value and asset commitment are confidential. However, if the confidential asset is explicit, it seems like the "right thing to do" to embed the asset ID and a zero blinding factor. This matches what we do in this library in TxIn::blind_issuances_with_bfs for example. --- clippy.toml | 1 + src/blind.rs | 223 ++++++++++++++++++++++++++++++++++-------------- src/pset/mod.rs | 5 +- 3 files changed, 163 insertions(+), 66 deletions(-) diff --git a/clippy.toml b/clippy.toml index 5f304987..c4931941 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,2 @@ avoid-breaking-exported-api = true +large-error-threshold = 192 # default 128 complains about two public keys in RangeProofMessageError diff --git a/src/blind.rs b/src/blind.rs index 7954ddcc..d8e26e6a 100644 --- a/src/blind.rs +++ b/src/blind.rs @@ -15,7 +15,6 @@ //! # Transactions Blinding //! -use internals::array::ArrayExt as _; use internals::slice::SliceExt; use std::{self, collections::BTreeMap, fmt}; @@ -201,26 +200,144 @@ impl From for ConfidentialTxOutError { ConfidentialTxOutError::Upstream(from) } } -/// The Rangeproof message -#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] -pub struct RangeProofMessage { - /// The asset id - pub asset: AssetId, - /// The asset blinding factor - pub bf: AssetBlindingFactor, -} -impl RangeProofMessage { - /// Converts the message to bytes - pub fn to_bytes(&self) -> [u8; 64] { - let mut message = [0u8; 64]; +mod range_proof_message { + use core::fmt; + use internals::array::ArrayExt; + use secp256k1_zkp::{Generator, Signing, Secp256k1}; + use super::{Asset, AssetId, AssetBlindingFactor}; + + /// The Rangeproof message + #[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] + pub struct RangeProofMessage { + /// The asset id + asset_id: AssetId, + /// The asset blinding factor + asset_bf: AssetBlindingFactor, + } + + impl RangeProofMessage { + /// Constructs a [`RangeProofMessage`] from an asset ID and blinding factor. + pub fn new(asset_id: AssetId, asset_bf: AssetBlindingFactor) -> Self { + Self { asset_id, asset_bf } + } + + /// The asset ID embedded in the rangeproof message. + pub fn asset_id(&self) -> &AssetId { + &self.asset_id + } + + /// The asset blinding factor embedded in the rangeproof message. + pub fn blinding_factor(&self) -> &AssetBlindingFactor { + &self.asset_bf + } - message[..32].copy_from_slice(self.asset.into_tag().as_ref()); - message[32..].copy_from_slice(self.bf.into_inner().as_ref()); + /// Computes the commmitment of the rangeproof message. + pub fn commitment(&self, secp: &Secp256k1) -> Generator { + Generator::new_blinded(secp, self.asset_id.into_tag(), self.asset_bf.into_inner()) + } - message + /// Parses a message from bytes + pub fn from_byte_array( + secp: &Secp256k1, + inner: [u8; 64], + expected_asset: &Asset, + ) -> Result { + let (asset_id, asset_bf) = inner.split_array::<32, 32>(); + let ret = Self { + asset_id: AssetId::from_byte_array(*asset_id), + asset_bf: AssetBlindingFactor::from_byte_array(*asset_bf) + .map_err(RangeProofMessageError::BlindingFactorOutOfRange)?, + }; + + match expected_asset { + Asset::Null => return Err(RangeProofMessageError::NullExpectedAsset), + Asset::Explicit(asset_id) => { + if ret.asset_id != *asset_id { + return Err(RangeProofMessageError::ExplicitAssetMismatch { + in_txout: *asset_id, + in_message: ret.asset_id, + }) + } + if ret.asset_bf != AssetBlindingFactor::zero() { + return Err(RangeProofMessageError::ExplicitAssetNonzeroBf { + blinding_factor: ret.asset_bf, + }); + } + } + Asset::Confidential(commitment) => { + let ret_commitment = ret.commitment(secp); + if ret_commitment != *commitment { + return Err(RangeProofMessageError::ConfidentialAssetMismatch { + in_txout: *commitment, + in_message: ret_commitment, + }) + } + } + } + + Ok(ret) + } + + /// Converts the message to bytes + pub fn to_byte_array(&self) -> [u8; 64] { + let mut message = [0u8; 64]; + message[..32].copy_from_slice(self.asset_id.into_tag().as_ref()); + message[32..].copy_from_slice(self.asset_bf.into_inner().as_ref()); + message + } + } + + #[non_exhaustive] + #[derive(PartialEq, Eq, Clone, Debug)] + pub enum RangeProofMessageError { + NullExpectedAsset, + ExplicitAssetMismatch { + in_txout: AssetId, + in_message: AssetId, + }, + ExplicitAssetNonzeroBf { + blinding_factor: AssetBlindingFactor, + }, + ConfidentialAssetMismatch { + in_txout: Generator, + in_message: Generator, + }, + BlindingFactorOutOfRange(secp256k1_zkp::Error), + } + + impl fmt::Display for RangeProofMessageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::NullExpectedAsset => f.write_str("rangeproof associated with null asset"), + Self::ExplicitAssetMismatch { in_txout, in_message } => { + write!(f, "txout had explicit asset ID {in_txout}, but rangeproof encoded asset ID {in_message}") + } + Self::ExplicitAssetNonzeroBf { blinding_factor } => { + write!(f, "txout had explicit asset ID, but rangeproof encoded a nonzero asset blinding factor {blinding_factor}") + } + Self::ConfidentialAssetMismatch { in_txout, in_message } => { + write!(f, "txout had asset commitment {in_txout}, but rangeproof encoded asset commitment {in_message}") + } + Self::BlindingFactorOutOfRange(ref e) => e.fmt(f), + } + } + } + + impl std::error::Error for RangeProofMessageError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match *self { + Self::NullExpectedAsset => None, + Self::ExplicitAssetMismatch { .. } => None, + Self::ExplicitAssetNonzeroBf { .. } => None, + Self::ConfidentialAssetMismatch { .. } => None, + Self::BlindingFactorOutOfRange(ref e) => Some(e), + } + + } } } +pub use self::range_proof_message::{RangeProofMessage, RangeProofMessageError}; /// Information about Transaction Input Asset #[cfg_attr( @@ -432,8 +549,7 @@ impl Value { let value = self .explicit() .ok_or(ConfidentialTxOutError::ExpectedExplicitValue)?; - let out_asset_commitment = - Generator::new_blinded(secp, msg.asset.into_tag(), msg.bf.into_inner()); + let out_asset_commitment = msg.commitment(secp); let value_commitment = Value::new_confidential(secp, value, out_asset_commitment, vbf); let rangeproof = RangeProof::new( @@ -442,7 +558,7 @@ impl Value { value_commitment.commitment().expect("confidential value"), value, vbf.into_inner(), - &msg.to_bytes(), + &msg.to_byte_array(), spk.as_bytes(), shared_secret, TxOut::RANGEPROOF_EXP_SHIFT, @@ -536,10 +652,10 @@ impl TxOut { let (out_asset, surjection_proof) = exp_asset.blind(rng, secp, out_secrets.asset_bf, spent_utxo_secrets)?; - let msg = RangeProofMessage { - asset: out_secrets.asset, - bf: out_secrets.asset_bf, - }; + let msg = RangeProofMessage::new( + out_secrets.asset, + out_secrets.asset_bf, + ); let exp_value = Value::Explicit(out_secrets.value); let (out_value, nonce, range_proof) = exp_value.blind( secp, @@ -735,7 +851,7 @@ impl TxOut { /// Unblinds a transaction output, if it is confidential. /// /// It returns the secret elements of the value and asset Pedersen commitments. - pub fn unblind( + pub fn unblind( &self, secp: &Secp256k1, blinding_key: SecretKey, @@ -760,34 +876,22 @@ impl TxOut { shared_secret, self.script_pubkey.as_bytes(), additional_generator, - )?; + ).map_err(UnblindError::Rewind)?; - // Use `MissingRangeproof` error because it's available so does not require - // API breaks. In a later PR we should extend that enum and add #[non_exhaustive] - // to it. The maybe-better `MalformedAssetId` error requires we start with a - // std `FromSliceError` which we don't have. + let value = opening.value; + let value_bf = ValueBlindingFactor(opening.blinding_factor); let asset_and_bf = SliceExt::split_first_chunk::<64>(opening.message.as_ref()) .ok_or(UnblindError::MissingRangeproof)? .0; - let (asset_id, asset_bf) = asset_and_bf.split_array(); - - let asset_id = AssetId::from_byte_array(*asset_id); - let asset_bf = AssetBlindingFactor::from_byte_array(*asset_bf)?; - if let Asset::Confidential(own_asset) = self.asset { - let secp = Secp256k1::signing_only(); // needed to avoid API break - let asset = Generator::new_blinded(&secp, asset_id.into_tag(), asset_bf.into_inner()); - if asset != own_asset { - // See above about use of MissingRangeproof. - return Err(UnblindError::MissingRangeproof); - } - } - - let value = opening.value; - let value_bf = ValueBlindingFactor(opening.blinding_factor); + let message = RangeProofMessage::from_byte_array( + secp, + *asset_and_bf, + &self.asset, + ).map_err(UnblindError::RangeProofMessage)?; Ok(TxOutSecrets { - asset: asset_id, - asset_bf, + asset: *message.asset_id(), + asset_bf: *message.blinding_factor(), value, value_bf, }) @@ -796,6 +900,7 @@ impl TxOut { /// Errors encountered when unblinding `TxOut`s. #[derive(Debug)] +#[non_exhaustive] pub enum UnblindError { /// The `TxOut` is not fully confidential. NotConfidential, @@ -803,18 +908,18 @@ pub enum UnblindError { MissingNonce, /// Transaction output does not have a rangeproof. MissingRangeproof, - /// Malformed asset ID. - MalformedAssetId(core::array::TryFromSliceError), + /// Malformed rangeproof message. + RangeProofMessage(RangeProofMessageError), /// Error originated in `secp256k1_zkp`. - Upstream(secp256k1_zkp::Error), + Rewind(secp256k1_zkp::Error), } impl fmt::Display for UnblindError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { UnblindError::MissingNonce => write!(f, "missing nonce in txout"), - UnblindError::MalformedAssetId(_) => write!(f, "malformed asset id"), - UnblindError::Upstream(e) => write!(f, "{}", e), + UnblindError::RangeProofMessage(_) => f.write_str("failed to parse message embedded in rangeproof"), + UnblindError::Rewind(_) => f.write_str("failed to rewind rangeproof"), UnblindError::NotConfidential => write!(f, "cannot unblind non-confidential txout"), UnblindError::MissingRangeproof => write!(f, "missing rangeproof in txout"), } @@ -825,20 +930,14 @@ impl std::error::Error for UnblindError { fn cause(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { UnblindError::MissingNonce => None, - UnblindError::MalformedAssetId(e) => Some(e), - UnblindError::Upstream(e) => Some(e), + UnblindError::RangeProofMessage(e) => Some(e), + UnblindError::Rewind(e) => Some(e), UnblindError::NotConfidential => None, UnblindError::MissingRangeproof => None, } } } -impl From for UnblindError { - fn from(from: secp256k1_zkp::Error) -> Self { - UnblindError::Upstream(from) - } -} - impl TxIn { /// Blind issuances for this [`TxIn`]. Asset amount and token amount must be /// set in [`AssetIssuance`](crate::AssetIssuance) field for this input @@ -871,10 +970,10 @@ impl TxIn { Value::Explicit(v) => Value::Explicit(v), }; let spk = Script::new(); - let msg = RangeProofMessage { + let msg = RangeProofMessage::new( asset, - bf: AssetBlindingFactor::zero(), - }; + AssetBlindingFactor::zero(), + ); let (comm, prf) = v.blind_with_shared_secret(secp, bf, blind_sk, &spk, &msg)?; if i == 0 { self.asset_issuance.amount = comm; diff --git a/src/pset/mod.rs b/src/pset/mod.rs index b0f976cf..aecd77b6 100644 --- a/src/pset/mod.rs +++ b/src/pset/mod.rs @@ -654,10 +654,7 @@ impl PartiallySignedTransaction { .ok_or(PsetBlindError::MustHaveExplicitTxOut(last_out_index))?; let ephemeral_sk = SecretKey::new(rng); let spk = &self.outputs[last_out_index].script_pubkey; - let msg = RangeProofMessage { - asset: asset_id, - bf: out_abf, - }; + let msg = RangeProofMessage::new(asset_id, out_abf); let blind_res = exp_value.blind( secp, final_vbf, From 400650d323c49a0318602c346850ae98a25143ad Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 1 Jun 2026 17:49:43 +0000 Subject: [PATCH 06/41] add 'hashes 1.0' dependency --- Cargo-recent.lock | 28 +++++++++++++++++++++++++--- Cargo.toml | 2 ++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 5a99fb08..f83c2120 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -27,7 +27,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ "bitcoin-internals 0.3.0", - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", ] [[package]] @@ -69,13 +69,22 @@ dependencies = [ "bitcoin-internals 0.3.0", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", "hex-conservative 0.2.1", "hex_lit", "secp256k1", "serde", ] +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" +dependencies = [ + "bitcoin-internals 0.5.0", +] + [[package]] name = "bitcoin-internals" version = "0.3.0" @@ -124,6 +133,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin_hashes" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f70c29ac06e7effa19682e91318deae86bdb46c4fd1bbd0f12fd196ff427ab0" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals 0.5.0", + "hex-conservative 1.1.0", + "serde", +] + [[package]] name = "bitcoincore-rpc" version = "0.19.0" @@ -210,6 +231,7 @@ dependencies = [ "bincode", "bitcoin", "bitcoin-internals 0.5.0", + "bitcoin_hashes 1.0.0", "getrandom 0.2.16", "hex-conservative 1.1.0", "rand", @@ -490,7 +512,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", "rand", "secp256k1-sys", "serde", diff --git a/Cargo.toml b/Cargo.toml index a1d5f4af..53c380b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ json-contract = ["serde_json"] "dep:serde", "bitcoin/serde", "bitcoin/serde", + "hashes/serde", "secp256k1-zkp/serde", ] base64 = ["bitcoin/base64"] @@ -29,6 +30,7 @@ base64 = ["bitcoin/base64"] [dependencies] bech32 = "0.11.0" bitcoin = "0.32.2" +hashes = { package = "bitcoin_hashes", version = "1.0", features = [ "hex" ] } internals = { package = "bitcoin-internals", version = "0.5" } secp256k1-zkp = { version = "0.11.0", features = ["global-context", "hashes"] } From be0c59d62072b50fc6cdf677e7544a8c27d8d901 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Fri, 19 Jun 2026 11:43:43 +0000 Subject: [PATCH 07/41] genesis: eliminate a bunch of allocations and panic paths Oops :) apparently I did not review this code as well as I should have. --- src/genesis.rs | 23 ++++++++++++++--------- src/issuance.rs | 4 ++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/genesis.rs b/src/genesis.rs index 74c7d4d9..1dc7d71a 100644 --- a/src/genesis.rs +++ b/src/genesis.rs @@ -19,7 +19,6 @@ use secp256k1_zkp::Tweak; use crate::hashes::{sha256, sha256d, Hash, HashEngine}; use crate::opcodes::all::OP_RETURN; use crate::opcodes::OP_TRUE; -use crate::pset::serialize::Serialize; use crate::{confidential, script, AssetId, Block, BlockExtData, BlockHash, BlockHeader, LockTime, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness}; use crate::{AssetIssuance, ContractHash, OutPoint, Txid}; use crate::confidential::Nonce; @@ -89,12 +88,18 @@ impl NetworkParams { } /// Hash commitment of network parameters for a given Network -pub fn commit_to_custom_network_parameters(params: &NetworkParams) -> Vec { +pub fn commit_to_custom_network_parameters(params: &NetworkParams) -> sha256::Hash { + use hex::{BytesToHexIter, Case}; + let mut eng = sha256::Hash::engine(); eng.input(params.network_id.clone().as_bytes()); - eng.input(format!("{:x}", params.fedpeg_script).as_bytes()); - eng.input(format!("{:x}", params.sign_block_script).as_bytes()); - sha256::Hash::from_engine(eng).serialize() + for ch in BytesToHexIter::new(params.fedpeg_script[..].iter(), Case::Lower).flatten() { + eng.input(&[ch.into()]); + } + for ch in BytesToHexIter::new(params.sign_block_script[..].iter(), Case::Lower).flatten() { + eng.input(&[ch.into()]); + } + sha256::Hash::from_engine(eng) } /// Produce the genesis transaction for a given elements Network @@ -105,7 +110,7 @@ fn liquid_genesis_tx(network_params: &NetworkParams) -> Transaction { previous_output: OutPoint::default(), is_pegin: false, script_sig: script::Builder::new() - .push_slice(commit.as_slice()) + .push_slice(commit.as_byte_array()) .into_script(), sequence: Sequence::default(), asset_issuance: AssetIssuance::default(), @@ -135,7 +140,7 @@ fn liquid_genesis_asset_tx(network_params: &NetworkParams) -> Option Block { let merkle_root: sha256d::Hash = if let Some(asset_tx) = liquid_genesis_asset_tx(params) { txdata.push(asset_tx.clone()); - let tx_hashes = vec![tx.txid().to_raw_hash(), asset_tx.txid().to_raw_hash()]; - bitcoin::merkle_tree::calculate_root(tx_hashes.into_iter()) + let tx_hashes = [tx.txid().to_raw_hash(), asset_tx.txid().to_raw_hash()]; + bitcoin::merkle_tree::calculate_root(tx_hashes.iter().copied()) .expect("merkle root") } else { tx.txid().to_raw_hash() diff --git a/src/issuance.rs b/src/issuance.rs index f1f789ee..27c7b30d 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -167,8 +167,8 @@ impl AssetId { /// a Regtest parent network fn pegged_asset_id_for_params_and_parent_chain_hash(params: &NetworkParams, parent_chainhash: bitcoin::blockdata::constants::ChainHash) -> AssetId { let commit = commit_to_custom_network_parameters(params); - let asset_outpoint = OutPoint::new(Txid::from_slice(commit.as_slice()).expect("txid"), 0); - let asset_entropy = AssetId::generate_asset_entropy(asset_outpoint, ContractHash::from_slice(parent_chainhash.to_bytes().as_slice()).unwrap()); + let asset_outpoint = OutPoint::new(Txid::from_byte_array(commit.to_byte_array()), 0); + let asset_entropy = AssetId::generate_asset_entropy(asset_outpoint, ContractHash::from_byte_array(*parent_chainhash.as_ref())); AssetId::from_entropy(asset_entropy) } } From 9f47b7422df77cf42711db0b7192fdb41a27c574 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 1 Jun 2026 17:52:36 +0000 Subject: [PATCH 08/41] use bitcoin_hashes 1.0 throughout the codebase This is a fairly big commit but it's mostly mechanical and it didn't seem super helpful to pull it out into multiple commits. The bulk of the prepatory work was done in #278. --- Cargo.toml | 1 + elementsd-tests/Cargo.toml | 1 + elementsd-tests/src/pset.rs | 1 - elementsd-tests/src/taproot.rs | 5 +- fuzz/Cargo.toml | 1 + src/address.rs | 26 +++++----- src/block.rs | 6 +-- src/confidential.rs | 2 +- src/dynafed.rs | 10 ++-- src/encode.rs | 8 ++- src/fast_merkle_root.rs | 24 +++++---- src/genesis.rs | 27 ++++++---- src/hash_types.rs | 39 ++++++++++---- src/internal_macros.rs | 2 +- src/issuance.rs | 30 ++++++----- src/lib.rs | 4 +- src/pset/elip100.rs | 1 - src/pset/map/input.rs | 25 +++++---- src/script.rs | 17 +++--- src/sighash.rs | 39 +++++++------- src/taproot.rs | 94 +++++++++++++++++----------------- src/transaction.rs | 17 +++--- 22 files changed, 208 insertions(+), 172 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 53c380b2..9208dbdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,6 +191,7 @@ zero_sized_map_values = "warn" [package.metadata.rbmt.lint] allowed_duplicates = [ + "bitcoin_hashes", "bitcoin-internals", "hex-conservative", ] diff --git a/elementsd-tests/Cargo.toml b/elementsd-tests/Cargo.toml index 5912f723..d08aa099 100644 --- a/elementsd-tests/Cargo.toml +++ b/elementsd-tests/Cargo.toml @@ -16,6 +16,7 @@ rand = "0.8" [package.metadata.rbmt.lint] # FIXME the bulk of these are because elementsd/bitcoind is much older than rust-bitcoin. allowed_duplicates = [ + "bitcoin_hashes", "bitcoin-internals", "hex-conservative", "base64", diff --git a/elementsd-tests/src/pset.rs b/elementsd-tests/src/pset.rs index 7ce1d95b..7b907278 100644 --- a/elementsd-tests/src/pset.rs +++ b/elementsd-tests/src/pset.rs @@ -8,7 +8,6 @@ use crate::{setup, Call}; use bitcoin::{self, Address, Amount}; use elements::encode::serialize; -use elements::hashes::Hash; use elements::hex::DisplayHex as _; use elements::pset::PartiallySignedTransaction; use elements::{AssetId, ContractHash}; diff --git a/elementsd-tests/src/taproot.rs b/elementsd-tests/src/taproot.rs index 8dcc6b13..0bfdc0c9 100644 --- a/elementsd-tests/src/taproot.rs +++ b/elementsd-tests/src/taproot.rs @@ -10,7 +10,6 @@ use bitcoin::Amount; use elements::hex; use elements::confidential::{AssetBlindingFactor, ValueBlindingFactor}; use elements::encode::{deserialize, serialize_hex}; -use elements::hashes::Hash; use elements::script::Builder; use elements::secp256k1_zkp; use elements::sighash::{self, SighashCache}; @@ -215,7 +214,7 @@ fn taproot_spend_test( ); let tweak = secp256k1_zkp::Scalar::from_be_bytes(tweak.to_byte_array()).expect("hash value greater than curve order"); let sig = secp.sign_schnorr( - &secp256k1_zkp::Message::from_digest_slice(&sighash_msg[..]).unwrap(), + &secp256k1_zkp::Message::from_digest(sighash_msg.to_byte_array()), &output_keypair.add_xonly_tweak(secp, &tweak).unwrap(), ); @@ -239,7 +238,7 @@ fn taproot_spend_test( .unwrap(); let sig = secp.sign_schnorr( - &secp256k1_zkp::Message::from_digest_slice(&sighash_msg[..]).unwrap(), + &secp256k1_zkp::Message::from_digest(sighash_msg.to_byte_array()), &test_data.leaf1_keypair, ); diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index ddd191b4..bf452ae6 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -26,6 +26,7 @@ use_self = "warn" [package.metadata.rbmt.lint] allowed_duplicates = [ + "bitcoin_hashes", "bitcoin-internals", "hex-conservative", "getrandom", diff --git a/src/address.rs b/src/address.rs index 7480ccf9..55ea613c 100644 --- a/src/address.rs +++ b/src/address.rs @@ -15,7 +15,7 @@ //! # Addresses //! -use std::convert::TryFrom as _; +use std::convert::{TryFrom as _, TryInto as _}; use std::error; use std::fmt; use std::fmt::Write as _; @@ -23,8 +23,8 @@ use std::str::FromStr; use bech32::{Bech32, Bech32m, ByteIterExt, Fe32, Fe32IterExt, Hrp}; use crate::blech32::{Blech32, Blech32m}; -use crate::hashes::Hash; use bitcoin::base58; +use bitcoin::hashes::Hash as _; use bitcoin::PublicKey; use internals::array::ArrayExt as _; use internals::slice::SliceExt; @@ -329,12 +329,12 @@ impl Address { ) -> Address { let ws = script::Builder::new() .push_int(0) - .push_slice(&WScriptHash::hash(&script[..])[..]) + .push_slice(WScriptHash::hash_script(script).as_ref()) .into_script(); Address { params, - payload: Payload::ScriptHash(ScriptHash::hash(&ws[..])), + payload: Payload::ScriptHash(ScriptHash::hash_script(&ws)), blinding_pubkey: blinder, } } @@ -386,9 +386,9 @@ impl Address { ) -> Option
{ Some(Address { payload: if script.is_p2pkh() { - Payload::PubkeyHash(Hash::from_slice(&script.as_bytes()[3..23]).unwrap()) + Payload::PubkeyHash(PubkeyHash::from_byte_array(script.as_bytes()[3..23].try_into().unwrap())) } else if script.is_p2sh() { - Payload::ScriptHash(Hash::from_slice(&script.as_bytes()[2..22]).unwrap()) + Payload::ScriptHash(ScriptHash::from_byte_array(script.as_bytes()[2..22].try_into().unwrap())) } else if script.is_v0_p2wpkh() { Payload::WitnessProgram { version: Fe32::Q, @@ -418,12 +418,12 @@ impl Address { Payload::PubkeyHash(ref hash) => script::Builder::new() .push_opcode(opcodes::all::OP_DUP) .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&hash[..]) + .push_slice(hash.as_ref()) .push_opcode(opcodes::all::OP_EQUALVERIFY) .push_opcode(opcodes::all::OP_CHECKSIG), Payload::ScriptHash(ref hash) => script::Builder::new() .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&hash[..]) + .push_slice(hash.as_byte_array()) .push_opcode(opcodes::all::OP_EQUAL), Payload::WitnessProgram { version: witver, @@ -566,12 +566,12 @@ impl fmt::Display for Address { prefixed[0] = self.params.blinded_prefix; prefixed[1] = self.params.p2pkh_prefix; prefixed[2..35].copy_from_slice(&blinder.serialize()); - prefixed[35..].copy_from_slice(&hash[..]); + prefixed[35..].copy_from_slice(hash.as_ref()); base58::encode_check_to_fmt(fmt, &prefixed[..]) } else { let mut prefixed = [0; 21]; prefixed[0] = self.params.p2pkh_prefix; - prefixed[1..].copy_from_slice(&hash[..]); + prefixed[1..].copy_from_slice(hash.as_ref()); base58::encode_check_to_fmt(fmt, &prefixed[..]) } } @@ -581,12 +581,12 @@ impl fmt::Display for Address { prefixed[0] = self.params.blinded_prefix; prefixed[1] = self.params.p2sh_prefix; prefixed[2..35].copy_from_slice(&blinder.serialize()); - prefixed[35..].copy_from_slice(&hash[..]); + prefixed[35..].copy_from_slice(hash.as_byte_array()); base58::encode_check_to_fmt(fmt, &prefixed[..]) } else { let mut prefixed = [0; 21]; prefixed[0] = self.params.p2sh_prefix; - prefixed[1..].copy_from_slice(&hash[..]); + prefixed[1..].copy_from_slice(hash.as_byte_array()); base58::encode_check_to_fmt(fmt, &prefixed[..]) } } @@ -937,7 +937,7 @@ mod test { "93c7378d96518a75448821c4f7c8f4bae7ce60f804d03d1f0628dd5dd0f5de51", ) .unwrap(); - let tap_node_hash = TapNodeHash::all_zeros(); + let tap_node_hash = TapNodeHash::from_byte_array([0; 32]); let mut expected = IntoIterator::into_iter([ "2dszRCFv8Ub4ytKo1Q1vXXGgSx7mekNDwSJ", diff --git a/src/block.rs b/src/block.rs index 0f9b1ba5..f022f1ae 100644 --- a/src/block.rs +++ b/src/block.rs @@ -21,7 +21,7 @@ use std::io; #[cfg(feature = "serde")] use std::fmt; use crate::dynafed; -use crate::hashes::Hash; +use crate::hashes::{HashEngine as _, sha256d}; use crate::Transaction; use crate::encode::{self, serialize, Decodable, Encodable, VarInt}; use crate::{BlockHash, Script, TxMerkleNode}; @@ -235,7 +235,7 @@ impl BlockHeader { }; // Everything except the signblock witness goes into the hash - let mut enc = BlockHash::engine(); + let mut enc = sha256d::Hash::engine(); version.consensus_encode(&mut enc).unwrap(); self.prev_blockhash.consensus_encode(&mut enc).unwrap(); self.merkle_root.consensus_encode(&mut enc).unwrap(); @@ -250,7 +250,7 @@ impl BlockHeader { proposed.consensus_encode(&mut enc).unwrap(); }, } - BlockHash::from_engine(enc) + BlockHash(enc.finalize()) } /// Returns true if this is a block with dynamic federations enabled. diff --git a/src/confidential.rs b/src/confidential.rs index ad3066b0..2e0ed39a 100644 --- a/src/confidential.rs +++ b/src/confidential.rs @@ -17,7 +17,7 @@ //! Structures representing Pedersen commitments of various types //! -use crate::hashes::{sha256d, Hash}; +use crate::hashes::sha256d; use secp256k1_zkp::{self, CommitmentSecrets, Generator, PedersenCommitment, PublicKey, Secp256k1, SecretKey, Signing, Tweak, ZERO_TWEAK, compute_adaptive_blinding_factor, diff --git a/src/dynafed.rs b/src/dynafed.rs index e5e29d14..c2f0227c 100644 --- a/src/dynafed.rs +++ b/src/dynafed.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::ser::{SerializeSeq, SerializeStruct}; use crate::encode::{self, Encodable, Decodable}; -use crate::hashes::{Hash, sha256d}; +use crate::hashes::sha256d; use crate::Script; impl_sha256_midstate_wrapper! { @@ -149,7 +149,7 @@ impl FullParams { let compact_root = crate::fast_merkle_root::fast_merkle_root(&leaves[..]); let leaves = [ - compact_root.to_byte_array(), + compact_root.to_parts().0, self.extra_root().to_byte_array(), ]; ParamsRoot::from_midstate(crate::fast_merkle_root::fast_merkle_root(&leaves[..])) @@ -364,7 +364,7 @@ impl Params { serialize_hash(self.signblockscript().unwrap()).to_byte_array(), serialize_hash(&self.signblock_witness_limit().unwrap()).to_byte_array(), ]; - let compact_root = crate::fast_merkle_root::fast_merkle_root(&leaves[..]); + let compact_root = ElidedRoot::from_midstate(crate::fast_merkle_root::fast_merkle_root(&leaves[..])); let leaves = [ compact_root.to_byte_array(), @@ -718,8 +718,8 @@ mod tests { signblock_witness: vec![], }, version: Default::default(), - prev_blockhash: BlockHash::all_zeros(), - merkle_root: TxMerkleNode::all_zeros(), + prev_blockhash: BlockHash::GENESIS_PREVIOUS_BLOCK_HASH, + merkle_root: TxMerkleNode::from_byte_array([0; 32]), time: Default::default(), height: Default::default(), }; diff --git a/src/encode.rs b/src/encode.rs index a0bbc0af..068a3c94 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -298,16 +298,14 @@ impl Decodable for bitcoin::ScriptBuf { } } -impl Encodable for bitcoin::hashes::sha256d::Hash { +impl Encodable for hashes::sha256d::Hash { fn consensus_encode(&self, mut w: W) -> Result { self.as_byte_array().consensus_encode(&mut w) } } -impl Decodable for bitcoin::hashes::sha256d::Hash { +impl Decodable for hashes::sha256d::Hash { fn consensus_decode(d: D) -> Result { - Ok(Self::from_byte_array( - <::Bytes>::consensus_decode(d)?, - )) + Ok(Self::from_byte_array(<[u8; 32]>::consensus_decode(d)?)) } } diff --git a/src/fast_merkle_root.rs b/src/fast_merkle_root.rs index 4d88ea81..7d7c5abe 100644 --- a/src/fast_merkle_root.rs +++ b/src/fast_merkle_root.rs @@ -12,7 +12,7 @@ // If not, see . // -use crate::hashes::{sha256, Hash, HashEngine}; +use crate::hashes::{sha256, HashEngine}; /// Calculate a single sha256 midstate hash of the given left and right leaves. #[inline] @@ -20,7 +20,7 @@ fn sha256midstate(left: &[u8], right: &[u8]) -> sha256::Midstate { let mut engine = sha256::Hash::engine(); engine.input(left); engine.input(right); - engine.midstate() + engine.midstate().expect("hashing exactly 64 bytes") } /// Compute the Merkle root of the give hashes using mid-state only. @@ -45,14 +45,14 @@ pub fn fast_merkle_root(leaves: &[[u8; 32]]) -> sha256::Midstate { let mut inner: [sha256::Midstate; 32] = Default::default(); let mut count: u32 = 0; while (count as usize) < leaves.len() { - let mut temp_hash = sha256::Midstate::from_byte_array(leaves[count as usize]); + let mut temp_hash = sha256::Midstate::new(leaves[count as usize], 64); count += 1; // For each of the lower bits in count that are 0, do 1 step. Each // corresponds to an inner value that existed before processing the // current leaf, and each needs a hash to combine it. let mut level = 0; while count & (1u32 << level) == 0 { - temp_hash = sha256midstate(&inner[level][..], &temp_hash[..]); + temp_hash = sha256midstate(inner[level].as_parts().0, temp_hash.as_parts().0); level += 1; } // Store the resulting hash at inner position level. @@ -81,7 +81,7 @@ pub fn fast_merkle_root(leaves: &[[u8; 32]]) -> sha256::Midstate { count += 1 << level; level += 1; while count & (1u32 << level) == 0 { - result_hash = sha256midstate(&inner[level][..], &result_hash[..]); + result_hash = sha256midstate(inner[level].as_parts().0, result_hash.as_parts().0); level += 1; } } @@ -92,11 +92,15 @@ pub fn fast_merkle_root(leaves: &[[u8; 32]]) -> sha256::Midstate { #[cfg(test)] mod tests { use super::fast_merkle_root; - use crate::hashes::sha256; - use std::str::FromStr; #[test] fn test_fast_merkle_root() { + fn decode_hex(hex: &str) -> [u8; 32] { + let mut ret = hex::decode_to_array(hex).unwrap(); + ret.reverse(); + ret + } + // unit test vectors from Elements Core let test_leaves = [ "b66b041650db0f297b53f8d93c0e8706925bf3323f8c59c14a6fac37bfdcd06f", @@ -116,9 +120,9 @@ mod tests { let mut leaves = vec![]; for i in 0..4 { let root = fast_merkle_root(&leaves); - assert_eq!(root, FromStr::from_str(test_roots[i]).unwrap(), "root #{}", i); - leaves.push(sha256::Midstate::from_str(test_leaves[i]).unwrap().to_byte_array()); + assert_eq!(root.to_parts().0, decode_hex(test_roots[i]), "root #{i}"); + leaves.push(decode_hex(test_leaves[i])); } - assert_eq!(fast_merkle_root(&leaves), FromStr::from_str(test_roots[4]).unwrap()); + assert_eq!(fast_merkle_root(&leaves).to_parts().0, decode_hex(test_roots[4])); } } diff --git a/src/genesis.rs b/src/genesis.rs index 1dc7d71a..4aed10ae 100644 --- a/src/genesis.rs +++ b/src/genesis.rs @@ -16,7 +16,7 @@ use bitcoin::secp256k1::impl_array_newtype; use secp256k1_zkp::Tweak; -use crate::hashes::{sha256, sha256d, Hash, HashEngine}; +use crate::hashes::{sha256, HashEngine}; use crate::opcodes::all::OP_RETURN; use crate::opcodes::OP_TRUE; use crate::{confidential, script, AssetId, Block, BlockExtData, BlockHash, BlockHeader, LockTime, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness}; @@ -185,21 +185,31 @@ pub fn genesis_block(params: &NetworkParams) -> Block { let tx = liquid_genesis_tx(params); let mut txdata = vec![tx.clone()]; - let merkle_root: sha256d::Hash = + let merkle_root: crate::TxMerkleNode = if let Some(asset_tx) = liquid_genesis_asset_tx(params) { + // To use `bitcoin::merkle_tree::calculate_root` from bitcoin 0.32, we need a hash + // with fairly specific trait bounds, even though it's just used as a byte array. + // sha256d::Hash from the rust-bitcoin version of bitcoin_hashes works. + use bitcoin::hashes::sha256d::Hash as MerkleHash; + use bitcoin::hashes::Hash as _; + txdata.push(asset_tx.clone()); - let tx_hashes = [tx.txid().to_raw_hash(), asset_tx.txid().to_raw_hash()]; - bitcoin::merkle_tree::calculate_root(tx_hashes.iter().copied()) - .expect("merkle root") + let tx_hashes = [ + MerkleHash::from_byte_array(tx.txid().to_byte_array()), + MerkleHash::from_byte_array(asset_tx.txid().to_byte_array()), + ]; + let root = bitcoin::merkle_tree::calculate_root(tx_hashes.iter().copied()) + .expect("merkle root"); + crate::TxMerkleNode::from_byte_array(root.to_byte_array()) } else { - tx.txid().to_raw_hash() + crate::TxMerkleNode::from_byte_array(tx.txid().to_byte_array()) }; Block { header: BlockHeader { version: 1, - prev_blockhash: BlockHash::all_zeros(), - merkle_root: merkle_root.into(), + prev_blockhash: BlockHash::GENESIS_PREVIOUS_BLOCK_HASH, + merkle_root, time: 1_296_688_602, height: 0, ext: BlockExtData::Proof { @@ -239,7 +249,6 @@ impl ChainHash { #[cfg(test)] mod test { use crate::genesis::{genesis_block, ChainHash, NetworkParams}; - use crate::hashes::Hash; #[test] fn genesis_block_hash() { diff --git a/src/hash_types.rs b/src/hash_types.rs index b4ce1861..cf0b157f 100644 --- a/src/hash_types.rs +++ b/src/hash_types.rs @@ -18,7 +18,7 @@ //! to avoid mixing data of the same hash format (like `SHA256d`) but of different meaning //! (transaction id, block hash etc). -use crate::hashes::{hash160, hash_newtype, sha256, sha256d, Hash}; +use crate::hashes::{hash160, sha256, sha256d}; // Re-export bitcoin's pubkeyhash types. We already re-export bitcoin's `PublicKey` type. pub use bitcoin::{PubkeyHash, WPubkeyHash}; @@ -30,7 +30,7 @@ macro_rules! impl_hashencode { &self, w: W, ) -> Result { - self.0.consensus_encode(w) + self.as_byte_array().consensus_encode(w) } } @@ -44,32 +44,53 @@ macro_rules! impl_hashencode { }; } -hash_newtype! { +hashes::hash_newtype! { /// An elements transaction ID - pub struct Txid(sha256d::Hash); + pub struct Txid(pub(crate) sha256d::Hash); /// An elements witness transaction ID - pub struct Wtxid(sha256d::Hash); + pub struct Wtxid(pub(crate) sha256d::Hash); /// An elements blockhash - pub struct BlockHash(sha256d::Hash); + pub struct BlockHash(pub(crate) sha256d::Hash); /// "Hash of the transaction according to the signature algorithm" - pub struct Sighash(sha256d::Hash); + pub struct Sighash(pub(crate) sha256d::Hash); /// A hash of Bitcoin Script bytecode. - pub struct ScriptHash(hash160::Hash); + pub struct ScriptHash(pub(crate) hash160::Hash); /// SegWit version of a Bitcoin Script bytecode hash. - pub struct WScriptHash(sha256::Hash); + pub struct WScriptHash(pub(crate) sha256::Hash); /// A hash of the Merkle tree branch or root for transactions pub struct TxMerkleNode(sha256d::Hash); } + +hashes::impl_hex_for_newtype!(Txid, Wtxid, BlockHash, ScriptHash, WScriptHash, TxMerkleNode); +#[cfg(feature = "serde")] +hashes::impl_serde_for_newtype!(Txid, Wtxid, BlockHash, ScriptHash, WScriptHash, TxMerkleNode); +// We do not implement serde or display/fromstr for 'Sighash'. In general it's dangerous +// to deserialize sighashes; they must be the output of a cryptographic hash function. +hashes::impl_debug_only_for_newtype!(Sighash); + impl_hashencode!(Txid); impl_hashencode!(Wtxid); impl_hashencode!(Sighash); impl_hashencode!(BlockHash); impl_hashencode!(TxMerkleNode); +impl BlockHash { + /// Dummy hash used as the previous blockhash of the genesis block. + pub const GENESIS_PREVIOUS_BLOCK_HASH: Self = Self::from_byte_array([0; 32]); +} + +impl Txid { + /// The `Txid` used in a coinbase prevout. + /// + /// This is used as the "txid" of the dummy input of a coinbase transaction. This is not a real + /// TXID and should not be used in any other contexts. + pub const COINBASE_PREVOUT: Self = Self::from_byte_array([0; 32]); +} + impl ScriptHash { /// Computes the `ScriptHash` of a script. pub fn hash_script(s: &crate::Script) -> Self { diff --git a/src/internal_macros.rs b/src/internal_macros.rs index 7dfbcf38..3e2b7431 100644 --- a/src/internal_macros.rs +++ b/src/internal_macros.rs @@ -422,7 +422,7 @@ macro_rules! impl_sha256_midstate_wrapper { /// (Private) convert a sha256 midstate to an object. fn from_midstate(value: crate::hashes::sha256::Midstate) -> Self { - Self(value.to_byte_array()) + Self(value.to_parts().0) } } diff --git a/src/issuance.rs b/src/issuance.rs index 27c7b30d..82e31e26 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -17,7 +17,7 @@ use std::io; use crate::encode::{self, Encodable, Decodable}; -use crate::hashes::{hash_newtype, sha256, sha256d, Hash}; +use crate::hashes::{hash_newtype, sha256, sha256d}; use crate::fast_merkle_root::fast_merkle_root; use secp256k1_zkp::Tag; use crate::genesis::{commit_to_custom_network_parameters, NetworkParams}; @@ -42,7 +42,9 @@ hash_newtype!( #[hash_newtype(backward)] pub struct ContractHash(sha256::Hash); ); - +hashes::impl_hex_for_newtype!(ContractHash); +#[cfg(feature = "serde")] +hashes::impl_serde_for_newtype!(ContractHash); impl_sha256_midstate_wrapper! { /// A hash of some data used as "asset entropy" to seed the ID of a new asset. @@ -62,15 +64,17 @@ impl ContractHash { /// the hash. #[cfg(feature = "json-contract")] pub fn from_json_contract(json: &str) -> Result { + use crate::hashes::HashEngine as _; + // Parsing the JSON into a BTreeMap will recursively order object keys // lexicographically. This order is respected when we later serialize // it again. let ordered: ::std::collections::BTreeMap = ::serde_json::from_str(json)?; - let mut engine = ContractHash::engine(); + let mut engine = sha256::Hash::engine(); ::serde_json::to_writer(&mut engine, &ordered).expect("engines don't error"); - Ok(ContractHash::from_engine(engine)) + Ok(ContractHash(engine.finalize())) } } @@ -263,28 +267,28 @@ mod test { let tether = ContractHash::from_str("3c7f0a53c2ff5b99590620d7f6604a7a3a7bfbaaa6aa61f7bfc7833ca03cde82").unwrap(); let correct = r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDt","version":0}"#; - let expected = ContractHash::hash(correct.as_bytes()); - assert_eq!(tether, expected); - assert_eq!(expected, ContractHash::from_json_contract(correct).unwrap()); + let expected = sha256::Hash::hash(correct.as_bytes()).to_byte_array(); + assert_eq!(tether.to_byte_array(), expected); + assert_eq!(expected, ContractHash::from_json_contract(correct).unwrap().to_byte_array()); let invalid_json = r#"{"entity":{"domain":"tether.to"},"issuer_pubkey:"#; assert!(ContractHash::from_json_contract(invalid_json).is_err()); let unordered = r#"{"precision":8,"ticker":"USDt","entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","version":0}"#; - assert_eq!(expected, ContractHash::from_json_contract(unordered).unwrap()); + assert_eq!(expected, ContractHash::from_json_contract(unordered).unwrap().to_byte_array()); let unordered = r#"{"precision":8,"name":"Tether USD","ticker":"USDt","entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","version":0}"#; - assert_eq!(expected, ContractHash::from_json_contract(unordered).unwrap()); + assert_eq!(expected, ContractHash::from_json_contract(unordered).unwrap().to_byte_array()); let spaces = r#"{"precision":8, "name" : "Tether USD", "ticker":"USDt", "entity":{"domain":"tether.to" }, "issuer_pubkey" :"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","version":0} "#; - assert_eq!(expected, ContractHash::from_json_contract(spaces).unwrap()); + assert_eq!(expected, ContractHash::from_json_contract(spaces).unwrap().to_byte_array()); let nested_correct = r#"{"entity":{"author":"Tether Inc","copyright":2020,"domain":"tether.to","hq":"Mars"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDt","version":0}"#; - let nested_expected = ContractHash::hash(nested_correct.as_bytes()); - assert_eq!(nested_expected, ContractHash::from_json_contract(nested_correct).unwrap()); + let nested_expected = sha256::Hash::hash(nested_correct.as_bytes()).to_byte_array(); + assert_eq!(nested_expected, ContractHash::from_json_contract(nested_correct).unwrap().to_byte_array()); let nested_unordered = r#"{"ticker":"USDt","entity":{"domain":"tether.to","hq":"Mars","author":"Tether Inc","copyright":2020},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"version":0}"#; - assert_eq!(nested_expected, ContractHash::from_json_contract(nested_unordered).unwrap()); + assert_eq!(nested_expected, ContractHash::from_json_contract(nested_unordered).unwrap().to_byte_array()); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 64b74ebe..dfc51d3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ /// Re-export of bitcoin crate pub extern crate bitcoin; +/// Re-export of `bitcoin_hashes` crate +pub extern crate hashes; /// Re-export of hex crate pub extern crate hex; /// Re-export of secp256k1-zkp crate @@ -74,8 +76,6 @@ mod transaction; mod endian; pub mod genesis; -// re-export bitcoin deps which we re-use -pub use bitcoin::hashes; // export everything at the top level so it can be used as `elements::Transaction` etc. pub use crate::address::{Address, AddressError, AddressParams}; pub use crate::blind::{ diff --git a/src/pset/elip100.rs b/src/pset/elip100.rs index 56c1db00..d95e9166 100644 --- a/src/pset/elip100.rs +++ b/src/pset/elip100.rs @@ -221,7 +221,6 @@ mod test { use crate::encode::serialize; use crate::{OutPoint, Txid}; - use bitcoin::hashes::Hash; use hex::DisplayHex as _; use crate::{ diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index ca54d56a..c5216bac 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -332,7 +332,7 @@ impl Default for Input { sha256_preimages: BTreeMap::new(), hash160_preimages: BTreeMap::new(), hash256_preimages: BTreeMap::new(), - previous_txid: Txid::all_zeros(), + previous_txid: Txid::COINBASE_PREVOUT, previous_output_index: 0, sequence: None, required_time_locktime: None, @@ -656,7 +656,7 @@ impl Map for Input { } } PSET_IN_RIPEMD160 => { - pset_insert_hash_pair( + pset_insert_hash_pair::( &mut self.ripemd160_preimages, raw_key, &raw_value, @@ -664,7 +664,7 @@ impl Map for Input { )?; } PSET_IN_SHA256 => { - pset_insert_hash_pair( + pset_insert_hash_pair::( &mut self.sha256_preimages, raw_key, &raw_value, @@ -672,7 +672,7 @@ impl Map for Input { )?; } PSET_IN_HASH160 => { - pset_insert_hash_pair( + pset_insert_hash_pair::( &mut self.hash160_preimages, raw_key, &raw_value, @@ -680,7 +680,7 @@ impl Map for Input { )?; } PSET_IN_HASH256 => { - pset_insert_hash_pair( + pset_insert_hash_pair::( &mut self.hash256_preimages, raw_key, &raw_value, @@ -1171,26 +1171,29 @@ impl Decodable for Input { } } -fn pset_insert_hash_pair( - map: &mut BTreeMap>, +fn pset_insert_hash_pair( + map: &mut BTreeMap>, raw_key: raw::Key, raw_value: &[u8], hash_type: error::PsetHash, ) -> Result<(), encode::Error> where - H: hashes::Hash + serialize::Deserialize, + Eng: hashes::HashEngine + Default, + Eng::Hash: serialize::Deserialize, { if raw_key.key.is_empty() { return Err(pset::Error::InvalidKey(raw_key).into()); } - let key_val: H = serialize::Deserialize::deserialize(&raw_key.key)?; + let key_val: Eng::Hash = serialize::Deserialize::deserialize(&raw_key.key)?; match map.entry(key_val) { Entry::Vacant(empty_key) => { let val: Vec = serialize::Deserialize::deserialize(raw_value)?; - if ::hash(&val) != key_val { + let mut eng = Eng::default(); + eng.input(&val); + if eng.finalize() != key_val { return Err(pset::Error::InvalidPreimageHashPair { preimage: val, - hash: Vec::from(key_val.borrow()), + hash: key_val.as_byte_array().as_ref().to_vec(), hash_type, } .into()); diff --git a/src/script.rs b/src/script.rs index 0b0a4507..5388463f 100644 --- a/src/script.rs +++ b/src/script.rs @@ -31,7 +31,6 @@ use secp256k1_zkp::{Verification, Secp256k1}; #[cfg(feature = "serde")] use serde; use crate::encode::{self, Decodable, Encodable}; -use crate::hashes::Hash; use crate::{opcodes, ScriptHash, WScriptHash, PubkeyHash, WPubkeyHash}; use bitcoin::PublicKey; @@ -241,7 +240,7 @@ impl Script { Builder::new() .push_opcode(opcodes::all::OP_DUP) .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&pubkey_hash[..]) + .push_slice(pubkey_hash.as_ref()) .push_opcode(opcodes::all::OP_EQUALVERIFY) .push_opcode(opcodes::all::OP_CHECKSIG) .into_script() @@ -251,19 +250,19 @@ impl Script { pub fn new_p2sh(script_hash: &ScriptHash) -> Script { Builder::new() .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&script_hash[..]) + .push_slice(script_hash.as_byte_array()) .push_opcode(opcodes::all::OP_EQUAL) .into_script() } /// Generates P2WPKH-type of scriptPubkey pub fn new_v0_wpkh(pubkey_hash: &WPubkeyHash) -> Script { - Script::new_witness_program(bech32::Fe32::Q, &pubkey_hash.to_raw_hash().to_byte_array()) + Script::new_witness_program(bech32::Fe32::Q, pubkey_hash.as_ref()) } /// Generates P2WSH-type of scriptPubkey with a given hash of the redeem script pub fn new_v0_wsh(script_hash: &WScriptHash) -> Script { - Script::new_witness_program(bech32::Fe32::Q, &script_hash.to_raw_hash().to_byte_array()) + Script::new_witness_program(bech32::Fe32::Q, script_hash.as_byte_array()) } /// Generates P2TR for script spending path using an internal public key and some optional @@ -302,12 +301,12 @@ impl Script { /// Returns 160-bit hash of the script pub fn script_hash(&self) -> ScriptHash { - ScriptHash::hash(self.as_bytes()) + ScriptHash(hashes::hash160::hash(self.as_bytes())) } /// Returns 256-bit hash of the script for P2WSH outputs pub fn wscript_hash(&self) -> WScriptHash { - WScriptHash::hash(self.as_bytes()) + WScriptHash(hashes::sha256::hash(self.as_bytes())) } /// The length in bytes of the script @@ -329,7 +328,7 @@ impl Script { #[must_use] pub fn to_p2sh(&self) -> Script { Builder::new().push_opcode(opcodes::all::OP_HASH160) - .push_slice(&ScriptHash::hash(&self.0)[..]) + .push_slice(self.script_hash().as_byte_array()) .push_opcode(opcodes::all::OP_EQUAL) .into_script() } @@ -339,7 +338,7 @@ impl Script { #[must_use] pub fn to_v0_p2wsh(&self) -> Script { Builder::new().push_int(0) - .push_slice(&WScriptHash::hash(&self.0)[..]) + .push_slice(self.wscript_hash().as_byte_array()) .into_script() } diff --git a/src/sighash.rs b/src/sighash.rs index 21f6f840..78b408c7 100644 --- a/src/sighash.rs +++ b/src/sighash.rs @@ -22,7 +22,7 @@ use std::borrow::Borrow; use crate::encode::{self, Encodable}; use crate::hash_types::Sighash; -use crate::hashes::{sha256d, Hash, sha256}; +use crate::hashes::{sha256d, sha256t, sha256, HashEngine as _}; use crate::script::Script; use std::ops::{Deref, DerefMut}; use std::io; @@ -427,7 +427,7 @@ impl> SighashCache { sighash_type: SchnorrSighashType, genesis_hash: BlockHash, ) -> Result { - let mut enc = TapSighashHash::engine(); + let mut enc = sha256t::Hash::engine(); self.taproot_encode_signing_data_to( &mut enc, input_index, @@ -437,7 +437,7 @@ impl> SighashCache { sighash_type, genesis_hash, )?; - Ok(TapSighashHash::from_engine(enc)) + Ok(TapSighashHash(enc.finalize())) } /// Compute the BIP341 sighash for a key spend @@ -448,7 +448,7 @@ impl> SighashCache { sighash_type: SchnorrSighashType, genesis_hash: BlockHash, ) -> Result { - let mut enc = TapSighashHash::engine(); + let mut enc = sha256t::Hash::engine(); self.taproot_encode_signing_data_to( &mut enc, input_index, @@ -458,7 +458,7 @@ impl> SighashCache { sighash_type, genesis_hash, )?; - Ok(TapSighashHash::from_engine(enc)) + Ok(TapSighashHash(enc.finalize())) } /// Compute the BIP341 sighash for a script spend @@ -473,7 +473,7 @@ impl> SighashCache { sighash_type: SchnorrSighashType, genesis_hash: BlockHash, ) -> Result { - let mut enc = TapSighashHash::engine(); + let mut enc = sha256t::Hash::engine(); self.taproot_encode_signing_data_to( &mut enc, input_index, @@ -483,7 +483,7 @@ impl> SighashCache { sighash_type, genesis_hash )?; - Ok(TapSighashHash::from_engine(enc)) + Ok(TapSighashHash(enc.finalize())) } /// Encode the BIP143 signing data for any flag type into a given object implementing a @@ -505,7 +505,7 @@ impl> SighashCache { value: confidential::Value, sighash_type: EcdsaSighashType, ) -> Result<(), encode::Error> { - let zero_hash = sha256d::Hash::all_zeros(); + let zero_hash = [0u8; 32]; let (sighash, anyone_can_pay) = sighash_type.split_anyonecanpay_flag(); @@ -548,9 +548,9 @@ impl> SighashCache { if sighash != EcdsaSighashType::Single && sighash != EcdsaSighashType::None { self.segwit_cache().outputs.consensus_encode(&mut writer)?; } else if sighash == EcdsaSighashType::Single && input_index < self.tx.output.len() { - let mut single_enc = Sighash::engine(); + let mut single_enc = sha256d::Hash::engine(); self.tx.output[input_index].consensus_encode(&mut single_enc)?; - Sighash::from_engine(single_enc).consensus_encode(&mut writer)?; + Sighash(single_enc.finalize()).consensus_encode(&mut writer)?; } else { zero_hash.consensus_encode(&mut writer)?; } @@ -576,10 +576,10 @@ impl> SighashCache { value: confidential::Value, sighash_type: EcdsaSighashType ) -> Sighash { - let mut enc = Sighash::engine(); + let mut enc = sha256d::Hash::engine(); self.encode_segwitv0_signing_data_to(&mut enc, input_index, script_code, value, sighash_type) .expect("engines don't error"); - Sighash::from_engine(enc) + Sighash(enc.finalize()) } /// Encodes the signing data from which a signature hash for a given input index with a given @@ -689,10 +689,10 @@ impl> SighashCache { script_pubkey: &Script, sighash_type: EcdsaSighashType, ) -> Sighash { - let mut engine = Sighash::engine(); + let mut engine = sha256d::Hash::engine(); self.encode_legacy_signing_data_to(&mut engine, input_index, script_pubkey, sighash_type) .expect("engines don't error"); - Sighash::from_engine(engine) + Sighash(engine.finalize()) } #[inline] @@ -965,14 +965,12 @@ impl std::str::FromStr for SchnorrSighashType { mod tests{ use super::*; use crate::encode::deserialize; - use std::str::FromStr; fn test_segwit_sighash(tx: &str, script: &str, input_index: usize, value: &str, hash_type: EcdsaSighashType, expected_result: &str) { let tx: Transaction = deserialize(&hex::decode_to_vec(tx).unwrap()).unwrap(); let script = Script::from(hex::decode_to_vec(script).unwrap()); - // A hack to parse sha256d strings are sha256 so that we don't reverse them... - let raw_expected = crate::hashes::sha256::Hash::from_str(expected_result).unwrap(); - let expected_result = Sighash::from_slice(&raw_expected[..]).unwrap(); + let raw_expected = hex::decode_to_array(expected_result).unwrap(); + let expected_result = Sighash::from_byte_array(raw_expected); let mut cache = SighashCache::new(&tx); let value : confidential::Value = deserialize(&hex::decode_to_vec(value).unwrap()).unwrap(); @@ -1004,9 +1002,8 @@ mod tests{ fn test_legacy_sighash(tx: &str, script: &str, input_index: usize, hash_type: EcdsaSighashType, expected_result: &str) { let tx: Transaction = deserialize(&hex::decode_to_vec(tx).unwrap()).unwrap(); let script = Script::from(hex::decode_to_vec(script).unwrap()); - // A hack to parse sha256d strings are sha256 so that we don't reverse them... - let raw_expected = crate::hashes::sha256::Hash::from_str(expected_result).unwrap(); - let expected_result = Sighash::from_slice(&raw_expected[..]).unwrap(); + let raw_expected = hex::decode_to_array(expected_result).unwrap(); + let expected_result = Sighash::from_byte_array(raw_expected); let sighash_cache = SighashCache::new(&tx); let actual_result = sighash_cache.legacy_sighash(input_index, &script, hash_type); assert_eq!(actual_result, expected_result); diff --git a/src/taproot.rs b/src/taproot.rs index 1f906c29..e6496cca 100644 --- a/src/taproot.rs +++ b/src/taproot.rs @@ -16,36 +16,49 @@ use std::cmp::Reverse; use std::{error, io, fmt}; -use crate::hashes::{sha256t_hash_newtype, Hash, HashEngine}; +use crate::hashes::HashEngine as _; use crate::schnorr::{UntweakedPublicKey, TweakedPublicKey, TapTweak}; use crate::Script; use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; +use hashes::sha256t; use secp256k1_zkp::{self, Secp256k1, Scalar}; use crate::encode::Encodable; -// Taproot test vectors from BIP-341 state the hashes without any reversing -sha256t_hash_newtype! { +hashes::sha256t_tag! { pub struct TapLeafTag = hash_str("TapLeaf/elements"); +} +hashes::sha256t_tag! { + pub struct TapBranchTag = hash_str("TapBranch/elements"); +} +hashes::sha256t_tag! { + pub struct TapTweakTag = hash_str("TapTweak/elements"); +} +hashes::sha256t_tag! { + pub struct TapSighashTag = hash_str("TapSighash/elements"); +} + +// Taproot test vectors from BIP-341 state the hashes without any reversing +hashes::hash_newtype! { /// Taproot-tagged hash for elements tapscript Merkle tree leafs. - #[hash_newtype(forward)] - pub struct TapLeafHash(_); + pub struct TapLeafHash(sha256t::Hash::); - pub struct TapBranchTag = hash_str("TapBranch/elements"); /// Tagged hash used in taproot trees; see BIP-340 for tagging rules. - #[hash_newtype(forward)] - pub struct TapNodeHash(_); + pub struct TapNodeHash(sha256t::Hash::); - pub struct TapTweakTag = hash_str("TapTweak/elements"); /// Taproot-tagged hash for elements public key tweaks. - #[hash_newtype(forward)] - pub struct TapTweakHash(_); + pub struct TapTweakHash(sha256t::Hash::); - pub struct TapSighashTag = hash_str("TapSighash/elements"); /// Taproot-tagged hash for the elements taproot signature hash. - #[hash_newtype(forward)] - pub struct TapSighashHash(_); + pub struct TapSighashHash(pub(crate) sha256t::Hash::); } +hashes::impl_hex_for_newtype!(TapLeafHash, TapNodeHash, TapTweakHash); +#[cfg(feature = "serde")] +hashes::impl_serde_for_newtype!(TapLeafHash, TapNodeHash, TapTweakHash); +// We do not implement serde or display/fromstr for 'Sighash'. In general it's dangerous +// to deserialize sighashes; they must be the output of a cryptographic hash function. +hashes::impl_debug_only_for_newtype!(TapSighashHash); + impl TapTweakHash { /// Create a new BIP341 [`TapTweakHash`] from key and tweak @@ -54,7 +67,7 @@ impl TapTweakHash { internal_key: UntweakedPublicKey, merkle_root: Option, ) -> TapTweakHash { - let mut eng = TapTweakHash::engine(); + let mut eng = sha256t::Hash::engine(); // always hash the key eng.input(&internal_key.serialize()); if let Some(h) = merkle_root { @@ -62,7 +75,7 @@ impl TapTweakHash { } else { // nothing to hash } - TapTweakHash::from_engine(eng) + TapTweakHash(eng.finalize()) } /// Converts a `TapTweakHash` into a `Scalar` ready for use with key tweaking API. @@ -75,14 +88,14 @@ impl TapTweakHash { impl TapLeafHash { /// function to compute leaf hash from components pub fn from_script(script: &Script, ver: LeafVersion) -> TapLeafHash { - let mut eng = TapLeafHash::engine(); + let mut eng = sha256t::Hash::engine(); ver.as_u8() .consensus_encode(&mut eng) .expect("engines don't error"); script .consensus_encode(&mut eng) .expect("engines don't error"); - TapLeafHash::from_engine(eng) + TapLeafHash(eng.finalize()) } } @@ -481,7 +494,7 @@ impl NodeInfo { b_leaf.merkle_branch.push(a.hash)?; // add hashing partner all_leaves.push(b_leaf); } - let mut eng = TapNodeHash::engine(); + let mut eng = sha256t::Hash::engine(); if a.hash < b.hash { eng.input(a.hash.as_ref()); eng.input(b.hash.as_ref()); @@ -490,7 +503,7 @@ impl NodeInfo { eng.input(a.hash.as_ref()); }; Ok(Self { - hash: TapNodeHash::from_engine(eng), + hash: TapNodeHash(eng.finalize()), leaves: all_leaves, }) } @@ -548,11 +561,12 @@ impl TaprootMerkleBranch { )) } else { let inner = sl - // TODO: Use chunks_exact after MSRV changes to 1.31 - .chunks(TAPROOT_CONTROL_NODE_SIZE) + // TODO: Use array_chunks after this stabilizes + .chunks_exact(TAPROOT_CONTROL_NODE_SIZE) .map(|chunk| { - TapNodeHash::from_slice(chunk) - .expect("chunk exact always returns the correct size") + use core::convert::TryFrom as _; + let chunk = <&[u8; 32]>::try_from(chunk).expect("need array_chunks in stdlib"); + TapNodeHash::from_byte_array(*chunk) }) .collect(); Ok(TaprootMerkleBranch(inner)) @@ -687,7 +701,7 @@ impl ControlBlock { let mut curr_hash = TapNodeHash::from_byte_array(leaf_hash.to_byte_array()); // Verify the proof for elem in self.merkle_branch.as_inner() { - let mut eng = TapNodeHash::engine(); + let mut eng = sha256t::Hash::engine(); if curr_hash.as_byte_array() < elem.as_byte_array() { eng.input(curr_hash.as_ref()); eng.input(elem.as_ref()); @@ -696,7 +710,7 @@ impl ControlBlock { eng.input(curr_hash.as_ref()); } // Recalculate the curr hash as parent hash - curr_hash = TapNodeHash::from_engine(eng); + curr_hash = TapNodeHash(eng.finalize()); } // compute the taptweak let tweak = TapTweakHash::from_key_and_tweak(self.internal_key, Some(curr_hash)); @@ -858,36 +872,22 @@ impl error::Error for TaprootError {} mod tests{ use super::*; use crate::hashes::{sha256, HashEngine}; - use crate::hashes::sha256t::Tag; use std::str::FromStr; fn tag_engine(tag_name: &str) -> sha256::HashEngine { let mut engine = sha256::Hash::engine(); let tag_hash = sha256::Hash::hash(tag_name.as_bytes()); - engine.input(&tag_hash[..]); - engine.input(&tag_hash[..]); + engine.input(tag_hash.as_byte_array()); + engine.input(tag_hash.as_byte_array()); engine } #[test] - fn test_midstates() { - // check that hash creation is the same as building into the same engine - fn empty_hash(tag_name: &str) -> [u8; 32] { - let mut e = tag_engine(tag_name); - e.input(&[]); - sha256::Hash::from_engine(e).to_byte_array() - } - - // test that engine creation roundtrips - assert_eq!(tag_engine("TapLeaf/elements").midstate(), TapLeafTag::engine().midstate()); - assert_eq!(tag_engine("TapBranch/elements").midstate(), TapBranchTag::engine().midstate()); - assert_eq!(tag_engine("TapTweak/elements").midstate(), TapTweakTag::engine().midstate()); - assert_eq!(tag_engine("TapSighash/elements").midstate(), TapSighashTag::engine().midstate()); - - assert_eq!(empty_hash("TapLeaf/elements"), TapLeafHash::hash(&[]).to_byte_array()); - assert_eq!(empty_hash("TapBranch/elements"), TapNodeHash::hash(&[]).to_byte_array()); - assert_eq!(empty_hash("TapTweak/elements"), TapTweakHash::hash(&[]).to_byte_array()); - assert_eq!(empty_hash("TapSighash/elements"), TapSighashHash::hash(&[]).to_byte_array()); + fn test_engine_initialization() { + assert_eq!(tag_engine("TapLeaf/elements").finalize().to_byte_array(), sha256t::Hash::::engine().finalize().to_byte_array()); + assert_eq!(tag_engine("TapBranch/elements").finalize().to_byte_array(), sha256t::Hash::::engine().finalize().to_byte_array()); + assert_eq!(tag_engine("TapTweak/elements").finalize().to_byte_array(), sha256t::Hash::::engine().finalize().to_byte_array()); + assert_eq!(tag_engine("TapSighash/elements").finalize().to_byte_array(), sha256t::Hash::::engine().finalize().to_byte_array()); } #[test] diff --git a/src/transaction.rs b/src/transaction.rs index 367174b7..1ba454b7 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -20,8 +20,9 @@ use std::collections::HashMap; use std::convert::TryFrom; use bitcoin::{self, VarInt}; +use bitcoin::hashes::Hash as _; use internals::slice::SliceExt; -use crate::hashes::Hash; +use crate::hashes::{sha256d, HashEngine as _}; use crate::{confidential, ContractHash}; use crate::encode::{self, Encodable, Decodable}; @@ -99,7 +100,7 @@ impl OutPoint { #[inline] pub fn null() -> OutPoint { OutPoint { - txid: Txid::all_zeros(), + txid: Txid::COINBASE_PREVOUT, vout: u32::MAX, } } @@ -151,7 +152,7 @@ impl ::std::str::FromStr for OutPoint { } let bitcoin_outpoint = bitcoin::OutPoint::from_str(s)?; Ok(OutPoint { - txid: Txid::from(bitcoin_outpoint.txid.to_raw_hash()), + txid: Txid::from_byte_array(bitcoin_outpoint.txid.to_byte_array()), vout: bitcoin_outpoint.vout, }) } @@ -792,7 +793,7 @@ impl TxOut { // Parse destination chain's genesis block let genesis_hash = bitcoin::BlockHash::from_raw_hash( - crate::hashes::Hash::from_slice(iter.next()?.ok()?.push_bytes()?).ok()? + bitcoin::hashes::Hash::from_slice(iter.next()?.ok()?.push_bytes()?).ok()? ); // Parse destination scriptpubkey @@ -1027,20 +1028,20 @@ impl Transaction { /// The txid of the transaction. pub fn txid(&self) -> Txid { - let mut enc = Txid::engine(); + let mut enc = sha256d::Hash::engine(); self.version.consensus_encode(&mut enc).unwrap(); 0u8.consensus_encode(&mut enc).unwrap(); self.input.consensus_encode(&mut enc).unwrap(); self.output.consensus_encode(&mut enc).unwrap(); self.lock_time.consensus_encode(&mut enc).unwrap(); - Txid::from_engine(enc) + Txid(enc.finalize()) } /// Get the witness txid of the transaction. pub fn wtxid(&self) -> Wtxid { - let mut enc = Txid::engine(); + let mut enc = sha256d::Hash::engine(); self.consensus_encode(&mut enc).unwrap(); - Wtxid::from_engine(enc) + Wtxid(enc.finalize()) } /// Get the total transaction fee in the given asset. From ad4788ee9889c0b997eb8fc52bc6601983d7e57b Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 28 Jun 2026 23:05:35 +0000 Subject: [PATCH 09/41] rustfmt src/lib.rs Copy the (nightly-only) rustfmt.toml from rust-bitcoin, and add an ignore block which scopes it specifically to src/lib.rs. As I add new files, I will add them to the format whitelist. Because I am going to be adding a ton of new exports to src/lib.rs, add that file for now. To make this work with jj-fix, run jj config edit --repo and add [fix.tools.cargo-fmt] command = ["/usr/bin/env", "rustfmt", "+nightly", "--edition", "2018"] patterns = [ "./src/lib.rs", "./src/confidential/range_proof.rs", "./src/confidential/surjection_proof.rs", "./src/transaction/decoders.rs", "./src/transaction/encoders.rs", "./src/transaction/pegin_witness.rs", ] --- rustfmt.toml | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 11 ++++---- 2 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..db148418 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,80 @@ +ignore = [ + "/", + "!/src/lib.rs" +] +hard_tabs = false +tab_spaces = 4 +newline_style = "Auto" +indent_style = "Block" + +max_width = 100 # This is the number of characters. +# `use_small_heuristics` is ignored if the granular width config values are explicitly set. +use_small_heuristics = "Max" # "Max" == All granular width settings same as `max_width`. +# # Granular width configuration settings. These are percentages of `max_width`. +# fn_call_width = 60 +# attr_fn_like_width = 70 +# struct_lit_width = 18 +# struct_variant_width = 35 +# array_width = 60 +# chain_width = 60 +# single_line_if_else_max_width = 50 + +wrap_comments = false +format_code_in_doc_comments = false +comment_width = 100 # Default 80 +normalize_comments = false +normalize_doc_attributes = false +format_strings = false +format_macro_matchers = false +format_macro_bodies = true +hex_literal_case = "Preserve" +empty_item_single_line = true +struct_lit_single_line = true +fn_single_line = true # Default false +where_single_line = false +imports_indent = "Block" +imports_layout = "Mixed" +imports_granularity = "Module" # Default "Preserve" +group_imports = "StdExternalCrate" # Default "Preserve" +reorder_imports = true +reorder_modules = true +reorder_impl_items = false +type_punctuation_density = "Wide" +space_before_colon = false +space_after_colon = true +spaces_around_ranges = false +binop_separator = "Front" +remove_nested_parens = true +combine_control_expr = true +overflow_delimited_expr = false +struct_field_align_threshold = 0 +enum_discrim_align_threshold = 0 +match_arm_blocks = false # Default true +match_arm_leading_pipes = "Never" +force_multiline_blocks = false +fn_params_layout = "Tall" +brace_style = "SameLineWhere" +control_brace_style = "AlwaysSameLine" +trailing_semicolon = true +trailing_comma = "Vertical" +match_block_trailing_comma = false +blank_lines_upper_bound = 1 +blank_lines_lower_bound = 0 +edition = "2021" +style_edition = "2021" +inline_attribute_width = 0 +format_generated_files = true +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true +condense_wildcard_suffixes = false +color = "Auto" +unstable_features = false +disable_all_formatting = false +skip_children = false +show_parse_errors = true +error_on_line_overflow = false +error_on_unformatted = false +emit_mode = "Files" +make_backup = false diff --git a/src/lib.rs b/src/lib.rs index dfc51d3a..2fc14b6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,11 +79,11 @@ pub mod genesis; // export everything at the top level so it can be used as `elements::Transaction` etc. pub use crate::address::{Address, AddressError, AddressParams}; pub use crate::blind::{ - BlindAssetProofs, BlindError, BlindValueProofs, ConfidentialTxOutError, RangeProofMessage, - SurjectionInput, TxOutError, TxOutSecrets, UnblindError, VerificationError, CtLocation, CtLocationType, + BlindAssetProofs, BlindError, BlindValueProofs, ConfidentialTxOutError, CtLocation, + CtLocationType, RangeProofMessage, SurjectionInput, TxOutError, TxOutSecrets, UnblindError, + VerificationError, }; -pub use crate::block::ExtData as BlockExtData; -pub use crate::block::{Block, BlockHeader, DynafedRoot}; +pub use crate::block::{Block, BlockHeader, DynafedRoot, ExtData as BlockExtData}; pub use crate::ext::{ReadExt, WriteExt}; pub use crate::fast_merkle_root::fast_merkle_root; pub use crate::hash_types::*; @@ -92,8 +92,7 @@ pub use crate::locktime::LockTime; pub use crate::schnorr::{SchnorrSig, SchnorrSigError}; pub use crate::script::Script; pub use crate::sighash::SchnorrSighashType; -pub use crate::transaction::Sequence; pub use crate::transaction::{ - AssetIssuance, EcdsaSighashType, OutPoint, PeginData, PegoutData, Transaction, TxIn, + AssetIssuance, EcdsaSighashType, OutPoint, PeginData, PegoutData, Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness, }; From f3a0afe7f0d3c5f2e65300088293afb9245f4062 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 29 Jun 2026 13:20:25 +0000 Subject: [PATCH 10/41] fuzz: fix typo in generate-files.sh --- .github/workflows/cron-daily-fuzz.yml | 2 +- fuzz/Cargo.toml | 2 +- fuzz/generate-files.sh | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml index 7a4f4314..7f3af38e 100644 --- a/.github/workflows/cron-daily-fuzz.yml +++ b/.github/workflows/cron-daily-fuzz.yml @@ -1,5 +1,5 @@ ###### -## DO NOT EDIT THIS FILE DIRECTLY. It is generated by generate-fuzz.sh. +## DO NOT EDIT THIS FILE DIRECTLY. It is generated by generate-files.sh. ## Edit that script instead and re-run it. ###### name: Fuzz diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index bf452ae6..14d0784b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,5 +1,5 @@ ###### -## DO NOT EDIT THIS FILE DIRECTLY. It is generated by generate-fuzz.sh. +## DO NOT EDIT THIS FILE DIRECTLY. It is generated by generate-files.sh. ## Edit that script instead and re-run it. ###### [package] diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh index cbe54f11..dc19fb28 100755 --- a/fuzz/generate-files.sh +++ b/fuzz/generate-files.sh @@ -11,7 +11,7 @@ source "$REPO_DIR/fuzz/fuzz-util.sh" # 1. Generate fuzz/Cargo.toml cat > "$REPO_DIR/fuzz/Cargo.toml" < "$REPO_DIR/.github/workflows/cron-daily-fuzz.yml" < Date: Wed, 24 Jun 2026 15:29:06 +0000 Subject: [PATCH 11/41] remove Encodable/Decodable impls for PedersenCommitment/Generator/PublicKey These impls shouldn't really be in the public API; we don't ever consensus encode bare secp256k1-zkp objects. We only consensus-encode values and assets and nonces, which have their own dedicated types. Also, as a practical problem, we are going to remove our Encodable/Decodable trait in favor of the bitcoin-consensus-encoding Encode/Decode traits. Since we own neither the secp256k1-zkp objects nor the traits, we won't be able to retain these. --- src/confidential.rs | 58 ++++++++++----------------------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/src/confidential.rs b/src/confidential.rs index 2e0ed39a..4a91b960 100644 --- a/src/confidential.rs +++ b/src/confidential.rs @@ -141,18 +141,14 @@ impl Encodable for Value { 1u8.consensus_encode(&mut s)?; Ok(1 + u64::swap_bytes(n).consensus_encode(&mut s)?) } - Value::Confidential(commitment) => commitment.consensus_encode(&mut s), + Value::Confidential(commitment) => { + s.write_all(&commitment.serialize())?; + Ok(33) + } } } } -impl Encodable for PedersenCommitment { - fn consensus_encode(&self, mut e: W) -> Result { - e.write_all(&self.serialize())?; - Ok(33) - } -} - impl Decodable for Value { fn consensus_decode(mut d: D) -> Result { let prefix = u8::consensus_decode(&mut d)?; @@ -174,13 +170,6 @@ impl Decodable for Value { } } -impl Decodable for PedersenCommitment { - fn consensus_decode(d: D) -> Result { - let bytes = <[u8; 33]>::consensus_decode(d)?; - Ok(PedersenCommitment::from_slice(&bytes)?) - } -} - #[cfg(feature = "serde")] impl Serialize for Value { fn serialize(&self, s: S) -> Result { @@ -363,18 +352,14 @@ impl Encodable for Asset { 1u8.consensus_encode(&mut s)?; Ok(1 + n.consensus_encode(&mut s)?) } - Asset::Confidential(generator) => generator.consensus_encode(&mut s) + Asset::Confidential(generator) => { + s.write_all(&generator.serialize())?; + Ok(33) + } } } } -impl Encodable for Generator { - fn consensus_encode(&self, mut e: W) -> Result { - e.write_all(&self.serialize())?; - Ok(33) - } -} - impl Decodable for Asset { fn consensus_decode(mut d: D) -> Result { let prefix = u8::consensus_decode(&mut d)?; @@ -396,14 +381,6 @@ impl Decodable for Asset { } } -impl Decodable for Generator { - fn consensus_decode(d: D) -> Result { - let bytes = <[u8; 33]>::consensus_decode(d)?; - Ok(Generator::from_slice(&bytes)?) - } -} - - #[cfg(feature = "serde")] impl Serialize for Asset { fn serialize(&self, s: S) -> Result { @@ -615,18 +592,14 @@ impl Encodable for Nonce { 1u8.consensus_encode(&mut s)?; Ok(1 + n.consensus_encode(&mut s)?) } - Nonce::Confidential(commitment) => commitment.consensus_encode(&mut s), + Nonce::Confidential(commitment) => { + s.write_all(&commitment.serialize())?; + Ok(33) + } } } } -impl Encodable for PublicKey { - fn consensus_encode(&self, mut e: W) -> Result { - e.write_all(&self.serialize())?; - Ok(33) - } -} - impl Decodable for Nonce { fn consensus_decode(mut d: D) -> Result { let prefix = u8::consensus_decode(&mut d)?; @@ -648,13 +621,6 @@ impl Decodable for Nonce { } } -impl Decodable for PublicKey { - fn consensus_decode(d: D) -> Result { - let bytes = <[u8; 33]>::consensus_decode(d)?; - Ok(PublicKey::from_slice(&bytes)?) - } -} - #[cfg(feature = "serde")] impl Serialize for Nonce { fn serialize(&self, s: S) -> Result { From 9b949f4249f8b3b2cc07d4c22d20003be342adec Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 14:06:58 +0000 Subject: [PATCH 12/41] rename confidential.rs to confidential/mod.rs --- src/{confidential.rs => confidential/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{confidential.rs => confidential/mod.rs} (100%) diff --git a/src/confidential.rs b/src/confidential/mod.rs similarity index 100% rename from src/confidential.rs rename to src/confidential/mod.rs From 06f4575846191f10709dfb4c220e028244ce07a0 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 7 Jul 2026 14:10:43 +0000 Subject: [PATCH 13/41] pset: add unit test from Claude A previous version of the two following commits cause this unit test to fail. Add it here so we can confirm that the current version does not. --- src/pset/map/input.rs | 48 ++++++++++++++++++++++++++++++++++++ src/pset/map/output.rs | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index ae67f854..6bfd432f 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -1204,3 +1204,51 @@ where Entry::Occupied(_) => Err(pset::Error::DuplicateKey(raw_key).into()), } } + +#[cfg(test)] +mod tests { + use secp256k1_zkp::ZERO_TWEAK; + + use crate::confidential; + use crate::pset::PartiallySignedTransaction; + use crate::{AssetIssuance, LockTime, Transaction, TxIn, TxInWitness}; + + // See `pset::map::output::tests::from_tx_does_not_spuriously_set_proofs_on_unblinded_outputs`. + // Same principle, but for the asset issuance rangeproofs in the input witnesses. + #[test] + fn from_tx_does_not_spuriously_set_proofs_on_explicit_issuance() { + let txin = TxIn { + asset_issuance: AssetIssuance { + asset_blinding_nonce: ZERO_TWEAK, + asset_entropy: [1u8; 32], + amount: confidential::Value::Explicit(1000), + inflation_keys: confidential::Value::Null, + }, + witness: TxInWitness::empty(), + ..TxIn::default() + }; + assert!(txin.has_issuance()); + assert!(txin.witness.amount_rangeproof.is_none()); + assert!(txin.witness.inflation_keys_rangeproof.is_none()); + + let tx = Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![txin], + output: vec![], + }; + + let pset = PartiallySignedTransaction::from_tx(tx); + let input = &pset.inputs()[0]; + assert!( + input.issuance_value_rangeproof.is_none(), + "issuance_value_rangeproof should be None for an explicit issuance, got {:?}", + input.issuance_value_rangeproof, + ); + assert!( + input.issuance_keys_rangeproof.is_none(), + "issuance_keys_rangeproof should be None for an explicit issuance, got {:?}", + input.issuance_keys_rangeproof, + ); + } +} diff --git a/src/pset/map/output.rs b/src/pset/map/output.rs index 69e361c4..707df9bc 100644 --- a/src/pset/map/output.rs +++ b/src/pset/map/output.rs @@ -584,3 +584,59 @@ impl Decodable for Output { Ok(rv) } } + +#[cfg(test)] +mod tests { + use crate::pset::{raw, Map, PartiallySignedTransaction}; + use crate::{encode, Transaction}; + + // Regression test from Claude that the next two commits do not cause + // `Pset::from_tx` to insert 0-length proofs when given unblinded outputs. + // It should insert no proofs at all. + #[test] + fn from_tx_does_not_spuriously_set_proofs_on_unblinded_outputs() { + // Fully-explicit transaction from `test_pset` in mod.rs + let tx_hex = "010000000001715df5ccebaf02ff18d6fae7263fa69fed5de59c900f4749556eba41bc7bf2af0000000000000000000201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000124101100001f5175517551755175517551755175517551755175517551755175517551755101230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000005f5e100000000000000"; + let tx: Transaction = + encode::deserialize(&hex::decode_to_vec(tx_hex).unwrap()).unwrap(); + assert!(tx.output.iter().all(|o| o.witness.is_empty()), "fixture must be unblinded"); + + let pset = PartiallySignedTransaction::from_tx(tx); + for (i, out) in pset.outputs().iter().enumerate() { + assert!( + out.value_rangeproof.is_none(), + "output {i}: value_rangeproof should be None for an unblinded output, got {:?}", + out.value_rangeproof, + ); + assert!( + out.asset_surjection_proof.is_none(), + "output {i}: asset_surjection_proof should be None for an unblinded output, got {:?}", + out.asset_surjection_proof, + ); + } + + // Wire-format check: an unblinded output's key-value pairs must not + // include the value-rangeproof / asset-surjection-proof proprietary + // keys at all (subtypes 0x04/0x05 per PSBT_ELEMENTS_OUT_VALUE_RANGEPROOF/ + // PSBT_ELEMENTS_OUT_ASSET_SURJECTION_PROOF in pset/map/output.rs), not + // even with an empty value. `Map::get_pairs` is exactly what + // `consensus_encode` iterates over (see `impl_pset_consensus_encoding!` + // in pset/macros.rs), so this checks the real wire format rather than + // grepping serialized bytes. + let value_rangeproof_key = raw::ProprietaryKey::from_pset_pair(0x04, vec![]).to_key(); + let asset_surjection_proof_key = raw::ProprietaryKey::from_pset_pair(0x05, vec![]).to_key(); + for (i, out) in pset.outputs().iter().enumerate() { + let pairs = Map::get_pairs(out).unwrap(); + assert!( + !pairs.iter().any(|p| p.key == value_rangeproof_key), + "output {i}: serialized pairs unexpectedly contain a value-rangeproof key: {:?}", + pairs, + ); + assert!( + !pairs.iter().any(|p| p.key == asset_surjection_proof_key), + "output {i}: serialized pairs unexpectedly contain an asset-surjection-proof key: {:?}", + pairs, + ); + } + } +} From de009482ad63c8c8b45d06fe62a733d21bab8d15 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 30 Jun 2026 15:19:51 +0000 Subject: [PATCH 14/41] confidential: encapsulate RangeProof This commit (and the next) are a bit bigger than I'd expected. Essentially, throughout the codebase we use Option> to represent rangerproofs, and similar for surjection proofs (done in the next commit). Aside from being very noisy, this causes a multitude of problems: * we needed the aux traits `BlindValueProofs` and `BlindAssetProofs` to add exact-value methods * there was a type confusion throughout the PSET code where we did not distinguish between empty rangeproofs and unset rangeproofs; we were using None for both * poor encapsulation in general; not only using literal None to mean "empty rangeproof" but mucking around with Option::as_ref and boxing/unboxing makes the code hard to read * we had to impl our Encodable/Decodable traits directly on the secp-zkp types, which we will be unable to do once we move to the new traits This commit replaces the Option> with a new rust-elements RangeProof type. This fixes all the above issues. Also, FYI for new files I am quietly relicensing from CC0 to MIT+Apache. I am not doing this in this non-license-related PR to be sneaky; I was just creating new files so it was an opportunity to use a new license header. See https://github.com/rust-bitcoin/rust-bitcoin/issues/5849 for motivation. --- rustfmt.toml | 3 +- src/blind.rs | 79 +------------ src/confidential/mod.rs | 4 + src/confidential/range_proof.rs | 192 ++++++++++++++++++++++++++++++++ src/lib.rs | 6 +- src/pset/map/input.rs | 44 +++++--- src/pset/map/output.rs | 22 ++-- src/pset/mod.rs | 30 +++-- src/pset/serialize.rs | 14 +-- src/transaction.rs | 60 +++------- 10 files changed, 287 insertions(+), 167 deletions(-) create mode 100644 src/confidential/range_proof.rs diff --git a/rustfmt.toml b/rustfmt.toml index db148418..77aa0184 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,6 +1,7 @@ ignore = [ "/", - "!/src/lib.rs" + "!/src/lib.rs", + "!/src/confidential/range_proof.rs", ] hard_tabs = false tab_spaces = 4 diff --git a/src/blind.rs b/src/blind.rs index d8e26e6a..f57e9dfe 100644 --- a/src/blind.rs +++ b/src/blind.rs @@ -23,9 +23,9 @@ use secp256k1_zkp::{ rand::{CryptoRng, RngCore}, PedersenCommitment, SecretKey, Tag, Tweak, Verification, ZERO_TWEAK, }; -use secp256k1_zkp::{Generator, RangeProof, Secp256k1, Signing, SurjectionProof}; +use secp256k1_zkp::{Generator, Secp256k1, Signing, SurjectionProof}; -use crate::{AddressParams, Script, TxIn}; +use crate::{AddressParams, RangeProof, Script, TxIn}; use crate::{ confidential::{Asset, AssetBlindingFactor, Nonce, Value, ValueBlindingFactor}, @@ -657,7 +657,7 @@ impl TxOut { out_secrets.asset_bf, ); let exp_value = Value::Explicit(out_secrets.value); - let (out_value, nonce, range_proof) = exp_value.blind( + let (out_value, nonce, rangeproof) = exp_value.blind( secp, out_secrets.value_bf, receiver_blinding_pk, @@ -673,7 +673,7 @@ impl TxOut { script_pubkey: spk, witness: TxOutWitness { surjection_proof: Some(Box::new(surjection_proof)), - rangeproof: Some(Box::new(range_proof)), + rangeproof, }, }; Ok(txout) @@ -977,10 +977,10 @@ impl TxIn { let (comm, prf) = v.blind_with_shared_secret(secp, bf, blind_sk, &spk, &msg)?; if i == 0 { self.asset_issuance.amount = comm; - self.witness.amount_rangeproof = Some(Box::new(prf)); + self.witness.amount_rangeproof = prf; } else { self.asset_issuance.inflation_keys = comm; - self.witness.inflation_keys_rangeproof = Some(Box::new(prf)); + self.witness.inflation_keys_rangeproof = prf; } } Ok(()) @@ -1365,73 +1365,6 @@ impl From for BlindError { } } -/// A trait to create and verify explicit rangeproofs -pub trait BlindValueProofs: Sized { - /// Outputs a `[RangeProof]` that blinded value - /// corresponfs to unblinded explicit value - fn blind_value_proof( - rng: &mut R, - secp: &Secp256k1, - explicit_val: u64, - value_commit: PedersenCommitment, - asset_gen: Generator, - vbf: ValueBlindingFactor, - ) -> Result; - - /// Verify that the Rangeproof proves that commitment - /// is actually bound to the explicit value - fn blind_value_proof_verify( - &self, - secp: &Secp256k1, - explicit_val: u64, - asset_gen: Generator, - value_commit: PedersenCommitment, - ) -> bool; -} - -impl BlindValueProofs for RangeProof { - /// Outputs a [`RangeProof`] that blinded `value_commit` - /// corresponds to explicit value - fn blind_value_proof( - rng: &mut R, - secp: &Secp256k1, - explicit_val: u64, - value_commit: PedersenCommitment, - asset_gen: Generator, - vbf: ValueBlindingFactor, - ) -> Result { - RangeProof::new( - secp, - explicit_val, // min_value - value_commit, // value_commit - explicit_val, // value - vbf.into_inner(), // blinding factor - &[], // message - &[], // add commitment - SecretKey::new(rng), // nonce - -1, // exp - 0, // min bits - asset_gen, // additional gen - ) - } - - /// Verify that the Rangeproof proves that commitment - /// is actually bound to the explicit value - fn blind_value_proof_verify( - &self, - secp: &Secp256k1, - explicit_val: u64, - asset_gen: Generator, - value_commit: PedersenCommitment, - ) -> bool { - let r = self.verify(secp, value_commit, &[], asset_gen); - match r { - Ok(e) => e.start == explicit_val && e.end - 1 == explicit_val, - Err(..) => false, - } - } -} - /// A trait to create and verify explicit surjection proofs pub trait BlindAssetProofs: Sized { /// Outputs a `[SurjectionProof]` that blinded asset diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 4a91b960..1765d734 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -17,6 +17,8 @@ //! Structures representing Pedersen commitments of various types //! +mod range_proof; + use crate::hashes::sha256d; use secp256k1_zkp::{self, CommitmentSecrets, Generator, PedersenCommitment, PublicKey, Secp256k1, SecretKey, Signing, Tweak, ZERO_TWEAK, @@ -31,6 +33,8 @@ use std::{fmt, io, ops::{AddAssign, Neg}, str}; use crate::encode::{self, Decodable, Encodable}; use crate::issuance::AssetId; +pub use self::range_proof::RangeProof; + /// A CT commitment to an amount #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] pub enum Value { diff --git a/src/confidential/range_proof.rs b/src/confidential/range_proof.rs new file mode 100644 index 00000000..e9a6c6ab --- /dev/null +++ b/src/confidential/range_proof.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Range Proofs + +use core::convert::TryInto; +use std::io; + +use secp256k1_zkp::rand::{CryptoRng, RngCore}; +use secp256k1_zkp::{self, Generator, PedersenCommitment, Secp256k1, SecretKey, Signing, Tweak}; +#[cfg(feature = "serde")] +use serde::{Deserializer, Serializer}; + +use crate::confidential::ValueBlindingFactor; +use crate::encode; + +/// A range proof, which represents a proof that a confidential value lies within +/// some range (typically `[0, 2^64)`). +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct RangeProof { + inner: Option>, +} + +impl RangeProof { + /// No range proof. + pub const EMPTY: Self = Self { inner: None }; + + /// Constructs a new [`RangeProof`]. + #[allow(clippy::too_many_arguments)] + pub fn new( + secp: &Secp256k1, + min_value: u64, + commitment: PedersenCommitment, + value: u64, + commitment_blinding: Tweak, + message: &[u8], + additional_commitment: &[u8], + sk: SecretKey, + exp: i32, + min_bits: u8, + additional_generator: Generator, + ) -> Result { + secp256k1_zkp::RangeProof::new( + secp, + min_value, + commitment, + value, + commitment_blinding, + message, + additional_commitment, + sk, + exp, + min_bits, + additional_generator, + ) + .map(|inner| Self { inner: Some(Box::new(inner)) }) + } + + /// Parses a [`RangeProof`] from a byte slice (with no length prefix). + pub fn from_slice(sl: &[u8]) -> Result { + if sl.is_empty() { + Ok(Self { inner: None }) + } else { + secp256k1_zkp::RangeProof::from_slice(sl) + .map(|inner| Self { inner: Some(Box::new(inner)) }) + } + } + + /// Outputs a [`RangeProof`] proving that a commitment matches an exact value. + pub fn blind_value_proof( + rng: &mut R, + secp: &Secp256k1, + explicit_val: u64, + value_commit: PedersenCommitment, + asset_gen: Generator, + vbf: ValueBlindingFactor, + ) -> Result { + secp256k1_zkp::RangeProof::new( + secp, + explicit_val, // min_value + value_commit, // value_commit + explicit_val, // value + vbf.into_inner(), // blinding factor + &[], // message + &[], // add commitment + SecretKey::new(rng), // nonce + -1, // exp + 0, // min bits + asset_gen, // additional gen + ) + .map(|inner| Self { inner: Some(Box::new(inner)) }) + } + + /// Verifies a [`RangeProof`] proving that a commitment matches an exact value. + pub fn blind_value_proof_verify( + &self, + secp: &Secp256k1, + explicit_val: u64, + asset_gen: Generator, + value_commit: PedersenCommitment, + ) -> bool { + let Some(inner) = self.inner.as_deref() else { + return false; + }; + if explicit_val == u64::MAX { + // FIXME upstream will panic on this input; we should be able to validate + // proofs with this value. + return false; + } + + let Ok(range) = inner.verify(secp, value_commit, &[], asset_gen) else { + return false; + }; + range == (explicit_val..explicit_val + 1) + } + + /// The length of the range proof (zero if it is empty/absent). + pub fn len(&self) -> usize { self.inner.as_deref().map_or(0, secp256k1_zkp::RangeProof::len) } + + /// Whether the range proof is absent. + pub fn is_empty(&self) -> bool { self.inner.is_none() } + + /// Serializes the range proof as a byte vector. + pub fn to_vec(&self) -> Vec { + match self.inner.as_deref() { + Some(prf) => secp256k1_zkp::RangeProof::serialize(prf), + None => Vec::new(), + } + } + + /// Extracts the minimum value encoded in the range proof. + pub fn minimim_value(&self) -> Option { + // inefficient, consider implementing index on rangeproof + let prf = self.to_vec(); + let byte0 = prf.first()?; + + let has_nonzero_range = byte0 & 64 == 64; + let has_min = byte0 & 32 == 32; + + if !has_min { + None + } else if has_nonzero_range { + let bytes: [u8; 8] = prf.get(2..10)?.try_into().ok()?; + Some(u64::from_be_bytes(bytes)) + } else { + let bytes: [u8; 8] = prf.get(1..9)?.try_into().ok()?; + Some(u64::from_be_bytes(bytes)) + } + } + + /// Obtains a reference to the underlying secp256k1-zkp object. + pub fn as_ref(&self) -> Option<&secp256k1_zkp::RangeProof> { self.inner.as_deref() } +} + +impl crate::encode::Encodable for RangeProof { + fn consensus_encode(&self, e: W) -> Result { + self.to_vec().consensus_encode(e) + } +} + +impl crate::encode::Decodable for RangeProof { + fn consensus_decode(d: D) -> Result { + let v = Vec::::consensus_decode(d)?; + if v.is_empty() { + Ok(Self { inner: None }) + } else { + secp256k1_zkp::RangeProof::from_slice(&v) + .map(|inner| Self { inner: Some(Box::new(inner)) }) + .map_err(encode::Error::Secp256k1zkp) + } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for RangeProof { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.inner.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for RangeProof { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer) + .map(|inner| Self { inner: inner.map(Box::new) }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 2fc14b6c..1b019567 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,11 +79,11 @@ pub mod genesis; // export everything at the top level so it can be used as `elements::Transaction` etc. pub use crate::address::{Address, AddressError, AddressParams}; pub use crate::blind::{ - BlindAssetProofs, BlindError, BlindValueProofs, ConfidentialTxOutError, CtLocation, - CtLocationType, RangeProofMessage, SurjectionInput, TxOutError, TxOutSecrets, UnblindError, - VerificationError, + BlindAssetProofs, BlindError, ConfidentialTxOutError, CtLocation, CtLocationType, + RangeProofMessage, SurjectionInput, TxOutError, TxOutSecrets, UnblindError, VerificationError, }; pub use crate::block::{Block, BlockHeader, DynafedRoot, ExtData as BlockExtData}; +pub use crate::confidential::RangeProof; pub use crate::ext::{ReadExt, WriteExt}; pub use crate::fast_merkle_root::fast_merkle_root; pub use crate::hash_types::*; diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index 6bfd432f..b5173b0d 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -32,10 +32,10 @@ use crate::pset::raw; use crate::pset::serialize; use crate::pset::{self, error, Error}; use crate::{transaction::SighashTypeParseError, SchnorrSighashType}; -use crate::{AssetIssuance, BlockHash, EcdsaSighashType, Script, Transaction, TxIn, TxOut, Txid}; +use crate::{AssetIssuance, BlockHash, EcdsaSighashType, RangeProof, Script, Transaction, TxIn, TxOut, Txid}; use bitcoin::bip32::KeySource; use bitcoin::{PublicKey, key::XOnlyPublicKey}; -use secp256k1_zkp::{self, RangeProof, SurjectionProof, Tweak, ZERO_TWEAK}; +use secp256k1_zkp::{self, SurjectionProof, Tweak, ZERO_TWEAK}; use crate::{OutPoint, Sequence}; @@ -262,9 +262,9 @@ pub struct Input { /// The issuance value commitment pub issuance_value_comm: Option, /// Issuance value rangeproof - pub issuance_value_rangeproof: Option>, + pub issuance_value_rangeproof: Option, /// Issuance keys rangeproof - pub issuance_keys_rangeproof: Option>, + pub issuance_keys_rangeproof: Option, /// Pegin Transaction. Should be a `bitcoin::Transaction` pub pegin_tx: Option, /// Pegin Transaction proof @@ -287,15 +287,15 @@ pub struct Input { /// Issuance asset entropy pub issuance_asset_entropy: Option<[u8; 32]>, /// input utxo rangeproof - pub in_utxo_rangeproof: Option>, + pub in_utxo_rangeproof: Option, /// Proof that blinded issuance matches the commitment - pub in_issuance_blind_value_proof: Option>, + pub in_issuance_blind_value_proof: Option, /// Proof that blinded inflation keys matches the corresponding commitment - pub in_issuance_blind_inflation_keys_proof: Option>, + pub in_issuance_blind_inflation_keys_proof: Option, /// The explicit amount of the input pub amount: Option, /// The blind value rangeproof - pub blind_value_proof: Option>, + pub blind_value_proof: Option, /// The input explicit asset pub asset: Option, /// The blind asset surjection proof @@ -540,8 +540,16 @@ impl Input { } // Witness - ret.issuance_keys_rangeproof = txin.witness.inflation_keys_rangeproof; - ret.issuance_value_rangeproof = txin.witness.amount_rangeproof; + ret.issuance_keys_rangeproof = if txin.witness.inflation_keys_rangeproof.is_empty() { + None + } else { + Some(txin.witness.inflation_keys_rangeproof) + }; + ret.issuance_value_rangeproof = if txin.witness.amount_rangeproof.is_empty() { + None + } else { + Some(txin.witness.amount_rangeproof) + }; } ret } @@ -746,10 +754,10 @@ impl Map for Input { impl_pset_prop_insert_pair!(self.issuance_value_comm <= | ); } PSBT_ELEMENTS_IN_ISSUANCE_VALUE_RANGEPROOF => { - impl_pset_prop_insert_pair!(self.issuance_value_rangeproof <= | >); + impl_pset_prop_insert_pair!(self.issuance_value_rangeproof <= | ); } PSBT_ELEMENTS_IN_ISSUANCE_KEYS_RANGEPROOF => { - impl_pset_prop_insert_pair!(self.issuance_keys_rangeproof <= | >); + impl_pset_prop_insert_pair!(self.issuance_keys_rangeproof <= | ); } PSBT_ELEMENTS_IN_PEG_IN_TX => { impl_pset_prop_insert_pair!(self.pegin_tx <= | ); @@ -783,19 +791,19 @@ impl Map for Input { impl_pset_prop_insert_pair!(self.issuance_asset_entropy <= | ); } PSBT_ELEMENTS_IN_UTXO_RANGEPROOF => { - impl_pset_prop_insert_pair!(self.in_utxo_rangeproof <= | >); + impl_pset_prop_insert_pair!(self.in_utxo_rangeproof <= | ); } PSBT_ELEMENTS_IN_ISSUANCE_BLIND_VALUE_PROOF => { - impl_pset_prop_insert_pair!(self.in_issuance_blind_value_proof <= | >); + impl_pset_prop_insert_pair!(self.in_issuance_blind_value_proof <= | ); } PSBT_ELEMENTS_IN_ISSUANCE_BLIND_INFLATION_KEYS_PROOF => { - impl_pset_prop_insert_pair!(self.in_issuance_blind_inflation_keys_proof <= | >); + impl_pset_prop_insert_pair!(self.in_issuance_blind_inflation_keys_proof <= | ); } PSBT_ELEMENTS_IN_EXPLICIT_VALUE => { impl_pset_prop_insert_pair!(self.amount <= | ); } PSBT_ELEMENTS_IN_VALUE_PROOF => { - impl_pset_prop_insert_pair!(self.blind_value_proof <= | >); + impl_pset_prop_insert_pair!(self.blind_value_proof <= | ); } PSBT_ELEMENTS_IN_EXPLICIT_ASSET => { impl_pset_prop_insert_pair!(self.asset <= | ); @@ -1228,8 +1236,8 @@ mod tests { ..TxIn::default() }; assert!(txin.has_issuance()); - assert!(txin.witness.amount_rangeproof.is_none()); - assert!(txin.witness.inflation_keys_rangeproof.is_none()); + assert!(txin.witness.amount_rangeproof.is_empty()); + assert!(txin.witness.inflation_keys_rangeproof.is_empty()); let tx = Transaction { version: 2, diff --git a/src/pset/map/output.rs b/src/pset/map/output.rs index 707df9bc..f4812e51 100644 --- a/src/pset/map/output.rs +++ b/src/pset/map/output.rs @@ -23,10 +23,10 @@ use crate::pset::map::Map; use crate::pset::raw; use crate::pset::Error; use crate::{confidential, pset}; -use crate::{encode, Script, TxOutWitness}; +use crate::{encode, RangeProof, Script, TxOutWitness}; use bitcoin::bip32::KeySource; use bitcoin::{PublicKey, key::XOnlyPublicKey}; -use secp256k1_zkp::{self, Generator, RangeProof, SurjectionProof}; +use secp256k1_zkp::{self, Generator, SurjectionProof}; use crate::issuance; @@ -116,7 +116,7 @@ pub struct Output { pub asset_comm: Option, // Proprietary key-value pairs for this output. /// Output value rangeproof - pub value_rangeproof: Option>, + pub value_rangeproof: Option, /// Output Asset surjection proof pub asset_surjection_proof: Option>, /// Blinding pubkey which is used in receiving address @@ -126,7 +126,7 @@ pub struct Output { /// The index of the input whose owner should blind this output pub blinder_index: Option, /// The blind value rangeproof - pub blind_value_proof: Option>, + pub blind_value_proof: Option, /// The blind asset surjection proof pub blind_asset_proof: Option>, /// Pset @@ -233,7 +233,11 @@ impl Output { }); } rv.script_pubkey = txout.script_pubkey; - rv.value_rangeproof = txout.witness.rangeproof; + rv.value_rangeproof = if txout.witness.rangeproof.is_empty() { + None + } else { + Some(txout.witness.rangeproof) + }; rv.asset_surjection_proof = txout.witness.surjection_proof; rv } @@ -262,7 +266,9 @@ impl Output { script_pubkey: self.script_pubkey.clone(), witness: TxOutWitness { surjection_proof: self.asset_surjection_proof.clone(), - rangeproof: self.value_rangeproof.clone(), + rangeproof: self.value_rangeproof + .clone() + .unwrap_or(RangeProof::EMPTY), }, } } @@ -354,7 +360,7 @@ impl Map for Output { impl_pset_prop_insert_pair!(self.asset_comm <= | ); } PSBT_ELEMENTS_OUT_VALUE_RANGEPROOF => { - impl_pset_prop_insert_pair!(self.value_rangeproof <= | >); + impl_pset_prop_insert_pair!(self.value_rangeproof <= | ); } PSBT_ELEMENTS_OUT_ASSET_SURJECTION_PROOF => { impl_pset_prop_insert_pair!(self.asset_surjection_proof <= | >); @@ -369,7 +375,7 @@ impl Map for Output { impl_pset_prop_insert_pair!(self.blinder_index <= | ); } PSBT_ELEMENTS_OUT_BLIND_VALUE_PROOF => { - impl_pset_prop_insert_pair!(self.blind_value_proof <= | >); + impl_pset_prop_insert_pair!(self.blind_value_proof <= | ); } PSBT_ELEMENTS_OUT_BLIND_ASSET_PROOF => { impl_pset_prop_insert_pair!(self.blind_asset_proof <= | >); diff --git a/src/pset/mod.rs b/src/pset/mod.rs index a61a3610..f51e23ce 100644 --- a/src/pset/mod.rs +++ b/src/pset/mod.rs @@ -38,20 +38,20 @@ mod str; #[cfg(feature = "base64")] pub use self::str::ParseError; -use crate::blind::{BlindAssetProofs, BlindValueProofs}; +use crate::blind::BlindAssetProofs; use crate::confidential; use crate::encode::{self, Decodable, Encodable}; use crate::{ blind::RangeProofMessage, confidential::{AssetBlindingFactor, ValueBlindingFactor}, - TxOutSecrets, + RangeProof, TxOutSecrets, }; use crate::{ LockTime, OutPoint, Sequence, SurjectionInput, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness, Txid, CtLocation, CtLocationType, }; use secp256k1_zkp::rand::{CryptoRng, RngCore}; -use secp256k1_zkp::{self, RangeProof, SecretKey, SurjectionProof}; +use secp256k1_zkp::{self, SecretKey, SurjectionProof}; pub use self::error::{Error, PsetBlindError, PsetHash}; use self::map::Map; @@ -300,8 +300,12 @@ impl PartiallySignedTransaction { sequence: psetin.sequence.unwrap_or(Sequence::MAX), asset_issuance: psetin.asset_issuance(), witness: TxInWitness { - amount_rangeproof: psetin.issuance_value_rangeproof.clone(), - inflation_keys_rangeproof: psetin.issuance_keys_rangeproof.clone(), + amount_rangeproof: psetin.issuance_value_rangeproof + .clone() + .unwrap_or(RangeProof::EMPTY), + inflation_keys_rangeproof: psetin.issuance_keys_rangeproof + .clone() + .unwrap_or(RangeProof::EMPTY), script_witness: psetin .final_script_witness .as_ref() @@ -336,7 +340,9 @@ impl PartiallySignedTransaction { script_pubkey: out.script_pubkey.clone(), witness: TxOutWitness { surjection_proof: out.asset_surjection_proof.clone(), - rangeproof: out.value_rangeproof.clone(), + rangeproof: out.value_rangeproof + .clone() + .unwrap_or(RangeProof::EMPTY), }, }; outputs.push(txout); @@ -518,7 +524,7 @@ impl PartiallySignedTransaction { // mutate the pset { - self.outputs[i].value_rangeproof = txout.witness.rangeproof; + self.outputs[i].value_rangeproof = Some(txout.witness.rangeproof); self.outputs[i].asset_surjection_proof = txout.witness.surjection_proof; self.outputs[i].amount_comm = txout.value.commitment(); self.outputs[i].asset_comm = txout.asset.commitment(); @@ -541,10 +547,10 @@ impl PartiallySignedTransaction { let value_comm = self.outputs[i] .amount_comm .expect("Blinding proof successful"); - self.outputs[i].blind_value_proof = Some(Box::new( + self.outputs[i].blind_value_proof = Some( RangeProof::blind_value_proof(rng, secp, value, value_comm, asset_gen, vbf) .map_err(|e| PsetBlindError::BlindingProofsCreationError(i, e))?, - )); + ); } // return blinding factors used let location = CtLocation{ input_index: i, ty: CtLocationType::Input}; @@ -670,7 +676,7 @@ impl PartiallySignedTransaction { // mutate the pset { - self.outputs[last_out_index].value_rangeproof = Some(Box::new(rangeproof)); + self.outputs[last_out_index].value_rangeproof = Some(rangeproof); self.outputs[last_out_index].asset_surjection_proof = Some(Box::new(surjection_proof)); self.outputs[last_out_index].amount_comm = value_commitment.commitment(); self.outputs[last_out_index].asset_comm = out_asset_commitment.commitment(); @@ -693,10 +699,10 @@ impl PartiallySignedTransaction { let value_comm = self.outputs[last_out_index] .amount_comm .expect("Blinding proof successful"); - self.outputs[last_out_index].blind_value_proof = Some(Box::new( + self.outputs[last_out_index].blind_value_proof = Some( RangeProof::blind_value_proof(rng, secp, value, value_comm, asset_gen, final_vbf) .map_err(|e| PsetBlindError::BlindingProofsCreationError(last_out_index, e))?, - )); + ); self.global.scalars.clear(); } diff --git a/src/pset/serialize.rs b/src/pset/serialize.rs index 5b152c04..6d247404 100644 --- a/src/pset/serialize.rs +++ b/src/pset/serialize.rs @@ -24,12 +24,12 @@ use crate::encode::{ self, deserialize, deserialize_partial, serialize, Decodable, Encodable, VarInt, }; use crate::hashes::{hash160, ripemd160, sha256, sha256d, Hash}; -use crate::{AssetId, BlockHash, Script, Transaction, TxOut, Txid}; +use crate::{AssetId, BlockHash, RangeProof, Script, Transaction, TxOut, Txid}; use bitcoin; use bitcoin::bip32::{ChildNumber, Fingerprint, KeySource}; use bitcoin::{key::XOnlyPublicKey, PublicKey}; use internals::slice::SliceExt; -use secp256k1_zkp::{self, RangeProof, SurjectionProof, Tweak}; +use secp256k1_zkp::{self, SurjectionProof, Tweak}; use super::map::{PsbtSighashType, TapTree}; use crate::schnorr; @@ -282,17 +282,15 @@ impl Deserialize for confidential::Asset { } } -impl Serialize for Box { +impl Serialize for RangeProof { fn serialize(&self) -> Vec { - RangeProof::serialize(self) + self.to_vec() } } -impl Deserialize for Box { +impl Deserialize for RangeProof { fn deserialize(bytes: &[u8]) -> Result { - let prf = RangeProof::from_slice(bytes) - .map_err(|_| encode::Error::ParseFailed("Invalid Rangeproof"))?; - Ok(Box::new(prf)) + Self::from_slice(bytes).map_err(encode::Error::Secp256k1zkp) } } diff --git a/src/transaction.rs b/src/transaction.rs index 1ba454b7..822a571e 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -30,10 +30,8 @@ use crate::issuance::{AssetEntropy, AssetId}; use crate::opcodes; use crate::parse::impl_parse_str_through_int; use crate::script::Instruction; -use crate::{LockTime, Script, Txid, Wtxid}; -use secp256k1_zkp::{ - RangeProof, SurjectionProof, Tweak, ZERO_TWEAK, -}; +use crate::{LockTime, RangeProof, Script, Txid, Wtxid}; +use secp256k1_zkp::{SurjectionProof, Tweak, ZERO_TWEAK}; /// Description of an asset issuance in a transaction input #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] @@ -362,9 +360,9 @@ impl std::error::Error for RelativeLockTimeError { #[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct TxInWitness { /// Amount rangeproof - pub amount_rangeproof: Option>, + pub amount_rangeproof: RangeProof, /// Rangeproof for inflation keys - pub inflation_keys_rangeproof: Option>, + pub inflation_keys_rangeproof: RangeProof, /// Traditional script witness pub script_witness: Vec>, /// Pegin witness, basically the same thing @@ -377,8 +375,8 @@ impl TxInWitness { /// Create an empty input witness. pub fn empty() -> Self { TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, + amount_rangeproof: RangeProof::EMPTY, + inflation_keys_rangeproof: RangeProof::EMPTY, script_witness: Vec::new(), pegin_witness: Vec::new(), } @@ -386,8 +384,8 @@ impl TxInWitness { /// Whether this witness is null pub fn is_empty(&self) -> bool { - self.amount_rangeproof.is_none() && - self.inflation_keys_rangeproof.is_none() && + self.amount_rangeproof.is_empty() && + self.inflation_keys_rangeproof.is_empty() && self.script_witness.is_empty() && self.pegin_witness.is_empty() } @@ -645,10 +643,8 @@ pub struct TxOutWitness { // We Box it because surjection proof internally is an array [u8; N] that // allocates on stack even when the surjection proof is empty pub surjection_proof: Option>, - /// Rangeproof showing that the value commitment is legitimate - // We Box it because range proof internally is an array [u8; N] that - // allocates on stack even when the range proof is empty - pub rangeproof: Option>, + /// Rangeproof showing that the value commitment is legitimate. + pub rangeproof: RangeProof, } serde_struct_impl!(TxOutWitness, surjection_proof, rangeproof); impl_consensus_encoding!(TxOutWitness, surjection_proof, rangeproof); @@ -658,18 +654,18 @@ impl TxOutWitness { pub fn empty() -> Self { TxOutWitness { surjection_proof: None, - rangeproof: None, + rangeproof: RangeProof::EMPTY, } } /// Whether this witness is null pub fn is_empty(&self) -> bool { - self.surjection_proof.is_none() && self.rangeproof.is_none() + self.surjection_proof.is_none() && self.rangeproof.is_empty() } /// The rangeproof len if is present, otherwise 0 pub fn rangeproof_len(&self) -> usize { - self.rangeproof.as_ref().map_or(0, |prf| prf.len()) + self.rangeproof.len() } /// The surjection proof len if is present, otherwise 0 @@ -839,29 +835,7 @@ impl TxOut { confidential::Value::Null => min_value, confidential::Value::Explicit(n) => n, confidential::Value::Confidential(..) => { - match &self.witness.rangeproof { - None => min_value, - Some(prf) => { - // inefficient, consider implementing index on rangeproof - let prf = prf.serialize(); - debug_assert!(prf.len() > 10); - - let has_nonzero_range = prf[0] & 64 == 64; - let has_min = prf[0] & 32 == 32; - - if !has_min { - min_value - } else if has_nonzero_range { - bitcoin::consensus::deserialize::(&prf[2..10]) - .expect("any 8 bytes is a u64") - .swap_bytes() // min-value is BE - } else { - bitcoin::consensus::deserialize::(&prf[1..9]) - .expect("any 8 bytes is a u64") - .swap_bytes() // min-value is BE - } - } - } + self.witness.rangeproof.minimim_value().unwrap_or(min_value) } } } @@ -974,10 +948,8 @@ impl Transaction { 0 } ) + if witness_flag { - let amt_prf_len = input.witness.amount_rangeproof.as_ref() - .map_or(0, |x| x.len()); - let keys_prf_len = input.witness.inflation_keys_rangeproof.as_ref() - .map_or(0, |x| x.len()); + let amt_prf_len = input.witness.amount_rangeproof.len(); + let keys_prf_len = input.witness.inflation_keys_rangeproof.len(); VarInt(amt_prf_len as u64).size() + amt_prf_len + From 273ed15c1e2ff163b4cffde47e81ed92261764e2 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 24 Jun 2026 15:40:13 +0000 Subject: [PATCH 15/41] confidential: encapsulate SurjectionProof All the justification from the previous commit message apply here. --- rustfmt.toml | 1 + src/blind.rs | 63 +----------- src/confidential/mod.rs | 2 + src/confidential/surjection_proof.rs | 145 +++++++++++++++++++++++++++ src/encode.rs | 56 +---------- src/lib.rs | 6 +- src/pset/map/input.rs | 8 +- src/pset/map/output.rs | 22 ++-- src/pset/mod.rs | 21 ++-- src/pset/serialize.rs | 14 ++- src/transaction.rs | 18 ++-- 11 files changed, 202 insertions(+), 154 deletions(-) create mode 100644 src/confidential/surjection_proof.rs diff --git a/rustfmt.toml b/rustfmt.toml index 77aa0184..13332480 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,6 +2,7 @@ ignore = [ "/", "!/src/lib.rs", "!/src/confidential/range_proof.rs", + "!/src/confidential/surjection_proof.rs", ] hard_tabs = false tab_spaces = 4 diff --git a/src/blind.rs b/src/blind.rs index f57e9dfe..65ae360e 100644 --- a/src/blind.rs +++ b/src/blind.rs @@ -23,9 +23,9 @@ use secp256k1_zkp::{ rand::{CryptoRng, RngCore}, PedersenCommitment, SecretKey, Tag, Tweak, Verification, ZERO_TWEAK, }; -use secp256k1_zkp::{Generator, Secp256k1, Signing, SurjectionProof}; +use secp256k1_zkp::{Generator, Secp256k1, Signing}; -use crate::{AddressParams, RangeProof, Script, TxIn}; +use crate::{AddressParams, RangeProof, Script, TxIn, SurjectionProof}; use crate::{ confidential::{Asset, AssetBlindingFactor, Nonce, Value, ValueBlindingFactor}, @@ -497,13 +497,7 @@ impl Asset { }) .collect::, _>>()?; - let surjection_proof = SurjectionProof::new( - secp, - rng, - asset.into_tag(), - asset_bf.into_inner(), - inputs.as_ref(), - )?; + let surjection_proof = SurjectionProof::new(secp, rng, asset, asset_bf, inputs)?; Ok((out_asset, surjection_proof)) } @@ -672,7 +666,7 @@ impl TxOut { nonce, script_pubkey: spk, witness: TxOutWitness { - surjection_proof: Some(Box::new(surjection_proof)), + surjection_proof, rangeproof, }, }; @@ -1365,55 +1359,6 @@ impl From for BlindError { } } -/// A trait to create and verify explicit surjection proofs -pub trait BlindAssetProofs: Sized { - /// Outputs a `[SurjectionProof]` that blinded asset - /// corresponfs to unblinded explicit asset - fn blind_asset_proof( - rng: &mut R, - secp: &Secp256k1, - asset: AssetId, - abf: AssetBlindingFactor, - ) -> Result; - - /// Verify that the Surjection proves that asset commitment - /// is actually bound to the explicit asset - fn blind_asset_proof_verify( - &self, - secp: &Secp256k1, - asset: AssetId, - asset_commit: Generator, - ) -> bool; -} - -impl BlindAssetProofs for SurjectionProof { - fn blind_asset_proof( - rng: &mut R, - secp: &Secp256k1, - asset: AssetId, - abf: AssetBlindingFactor, - ) -> Result { - let gen = Generator::new_unblinded(secp, asset.into_tag()); - SurjectionProof::new( - secp, - rng, - asset.into_tag(), - abf.into_inner(), - &[(gen, asset.into_tag(), ZERO_TWEAK)], - ) - } - - fn blind_asset_proof_verify( - &self, - secp: &Secp256k1, - asset: AssetId, - asset_commit: Generator, - ) -> bool { - let gen = Generator::new_unblinded(secp, asset.into_tag()); - self.verify(secp, asset_commit, &[gen]) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 1765d734..d85df050 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -18,6 +18,7 @@ //! mod range_proof; +mod surjection_proof; use crate::hashes::sha256d; use secp256k1_zkp::{self, CommitmentSecrets, Generator, PedersenCommitment, @@ -34,6 +35,7 @@ use crate::encode::{self, Decodable, Encodable}; use crate::issuance::AssetId; pub use self::range_proof::RangeProof; +pub use self::surjection_proof::SurjectionProof; /// A CT commitment to an amount #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] diff --git a/src/confidential/surjection_proof.rs b/src/confidential/surjection_proof.rs new file mode 100644 index 00000000..ad3d3e3f --- /dev/null +++ b/src/confidential/surjection_proof.rs @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Surjection Proofs + +use std::io; + +use secp256k1_zkp::rand::{CryptoRng, RngCore}; +use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; +#[cfg(feature = "serde")] +use serde::{Deserializer, Serializer}; + +use crate::confidential::{AssetBlindingFactor, AssetId}; +use crate::encode; + +/// A surjection proof, proving that an asset commitment commits to the same asset ID +/// as a commitment from a given set. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct SurjectionProof { + inner: Option>, +} + +impl SurjectionProof { + /// No surjection proof. + pub const EMPTY: Self = Self { inner: None }; + + /// Constructs a new [`SurjectionProof`]. + pub fn new( + secp: &Secp256k1, + rng: &mut R, + asset: AssetId, + asset_bf: AssetBlindingFactor, + inputs: S, + ) -> Result + where + R: RngCore + CryptoRng, + C: Signing, + S: AsRef<[(Generator, secp256k1_zkp::Tag, Tweak)]>, + { + secp256k1_zkp::SurjectionProof::new( + secp, + rng, + asset.into_tag(), + asset_bf.into_inner(), + inputs.as_ref(), + ) + .map(|inner| Self { inner: Some(Box::new(inner)) }) + } + + /// Parses a [`SurjectionProof`] from a byte slice (with no length prefix). + pub fn from_slice(sl: &[u8]) -> Result { + if sl.is_empty() { + Ok(Self { inner: None }) + } else { + secp256k1_zkp::SurjectionProof::from_slice(sl) + .map(|inner| Self { inner: Some(Box::new(inner)) }) + } + } + + /// Serializes the surjection proof as a byte vector. + pub fn to_vec(&self) -> Vec { + match self.inner.as_deref() { + Some(prf) => secp256k1_zkp::SurjectionProof::serialize(prf), + None => Vec::new(), + } + } + + /// Outputs a [`SurjectionProof`] proving that an asset matches an exact asset ID. + pub fn blind_asset_proof( + rng: &mut R, + secp: &Secp256k1, + asset: AssetId, + abf: AssetBlindingFactor, + ) -> Result { + let gen = Generator::new_unblinded(secp, asset.into_tag()); + SurjectionProof::new(secp, rng, asset, abf, [(gen, asset.into_tag(), ZERO_TWEAK)]) + } + + /// Verifies a [`SurjectionProof`] proving that an asset matches an exact asset ID. + pub fn blind_asset_proof_verify( + &self, + secp: &Secp256k1, + asset: AssetId, + asset_commit: Generator, + ) -> bool { + let gen = Generator::new_unblinded(secp, asset.into_tag()); + match self.inner.as_deref() { + Some(inner) => inner.verify(secp, asset_commit, &[gen]), + None => false, + } + } + + /// The length of the range proof (zero if it is empty/absent). + pub fn len(&self) -> usize { + self.inner.as_deref().map_or(0, secp256k1_zkp::SurjectionProof::len) + } + + /// Whether the surjectionproof is absent. + pub fn is_empty(&self) -> bool { self.inner.is_none() } + + /// Obtains a reference to the underlying secp256k1-zkp object. + pub fn as_ref(&self) -> Option<&secp256k1_zkp::SurjectionProof> { self.inner.as_deref() } +} + +impl crate::encode::Encodable for SurjectionProof { + fn consensus_encode(&self, e: W) -> Result { + match self.inner.as_ref() { + Some(prf) => secp256k1_zkp::SurjectionProof::serialize(prf).consensus_encode(e), + None => <[u8]>::consensus_encode(&[], e), + } + } +} + +impl crate::encode::Decodable for SurjectionProof { + fn consensus_decode(d: D) -> Result { + let v = Vec::::consensus_decode(d)?; + if v.is_empty() { + Ok(Self { inner: None }) + } else { + secp256k1_zkp::SurjectionProof::from_slice(&v) + .map(|inner| Self { inner: Some(Box::new(inner)) }) + .map_err(encode::Error::Secp256k1zkp) + } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SurjectionProof { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.inner.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for SurjectionProof { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer) + .map(|inner| Self { inner: inner.map(Box::new) }) + } +} diff --git a/src/encode.rs b/src/encode.rs index 068a3c94..a1204062 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -20,7 +20,7 @@ use std::{any, error, fmt, io, mem}; use bitcoin::ScriptBuf; use hex::{DecodeFixedLengthBytesError, DecodeVariableLengthBytesError}; -use secp256k1_zkp::{self, RangeProof, SurjectionProof, Tweak}; +use secp256k1_zkp::{self, Tweak}; use crate::hashes::{sha256, Hash}; use crate::pset; @@ -417,31 +417,6 @@ impl_array!(20); impl_array!(32); impl_array!(33); -macro_rules! impl_box_option { - ($type: ty) => { - impl Encodable for Option> { - #[inline] - fn consensus_encode(&self, e: W) -> Result { - match self { - None => Vec::::new().consensus_encode(e), - Some(v) => v.serialize().consensus_encode(e), - } - } - } - - impl Decodable for Option> { - #[inline] - fn consensus_decode(mut d: D) -> Result { - let v: Vec = Decodable::consensus_decode(&mut d)?; - if v.is_empty() { - Ok(None) - } else { - Ok(Some(Box::new(<$type>::from_slice(&v)?))) - } - } - } - }; -} // special implementations for elements only fields impl Encodable for Tweak { fn consensus_encode(&self, e: W) -> Result { @@ -455,32 +430,6 @@ impl Decodable for Tweak { } } -impl Encodable for RangeProof { - fn consensus_encode(&self, e: W) -> Result { - self.serialize().consensus_encode(e) - } -} - -impl Decodable for RangeProof { - fn consensus_decode(d: D) -> Result { - Ok(RangeProof::from_slice(&>::consensus_decode(d)?)?) - } -} - -impl Encodable for SurjectionProof { - fn consensus_encode(&self, e: W) -> Result { - self.serialize().consensus_encode(e) - } -} - -impl Decodable for SurjectionProof { - fn consensus_decode(d: D) -> Result { - Ok(SurjectionProof::from_slice(&>::consensus_decode( - d, - )?)?) - } -} - impl Encodable for sha256::Hash { fn consensus_encode(&self, s: S) -> Result { self.to_byte_array().consensus_encode(s) @@ -508,6 +457,3 @@ impl Decodable for TapLeafHash { )) } } - -impl_box_option!(RangeProof); -impl_box_option!(SurjectionProof); diff --git a/src/lib.rs b/src/lib.rs index 1b019567..18c92cea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,11 +79,11 @@ pub mod genesis; // export everything at the top level so it can be used as `elements::Transaction` etc. pub use crate::address::{Address, AddressError, AddressParams}; pub use crate::blind::{ - BlindAssetProofs, BlindError, ConfidentialTxOutError, CtLocation, CtLocationType, - RangeProofMessage, SurjectionInput, TxOutError, TxOutSecrets, UnblindError, VerificationError, + BlindError, ConfidentialTxOutError, CtLocation, CtLocationType, RangeProofMessage, + SurjectionInput, TxOutError, TxOutSecrets, UnblindError, VerificationError, }; pub use crate::block::{Block, BlockHeader, DynafedRoot, ExtData as BlockExtData}; -pub use crate::confidential::RangeProof; +pub use crate::confidential::{RangeProof, SurjectionProof}; pub use crate::ext::{ReadExt, WriteExt}; pub use crate::fast_merkle_root::fast_merkle_root; pub use crate::hash_types::*; diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index b5173b0d..53438a1a 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -32,10 +32,10 @@ use crate::pset::raw; use crate::pset::serialize; use crate::pset::{self, error, Error}; use crate::{transaction::SighashTypeParseError, SchnorrSighashType}; -use crate::{AssetIssuance, BlockHash, EcdsaSighashType, RangeProof, Script, Transaction, TxIn, TxOut, Txid}; +use crate::{AssetIssuance, BlockHash, EcdsaSighashType, RangeProof, Script, Transaction, TxIn, TxOut, Txid, SurjectionProof}; use bitcoin::bip32::KeySource; use bitcoin::{PublicKey, key::XOnlyPublicKey}; -use secp256k1_zkp::{self, SurjectionProof, Tweak, ZERO_TWEAK}; +use secp256k1_zkp::{self, Tweak, ZERO_TWEAK}; use crate::{OutPoint, Sequence}; @@ -299,7 +299,7 @@ pub struct Input { /// The input explicit asset pub asset: Option, /// The blind asset surjection proof - pub blind_asset_proof: Option>, + pub blind_asset_proof: Option, /// Whether the issuance is blinded pub blinded_issuance: Option, /// Other fields @@ -809,7 +809,7 @@ impl Map for Input { impl_pset_prop_insert_pair!(self.asset <= | ); } PSBT_ELEMENTS_IN_ASSET_PROOF => { - impl_pset_prop_insert_pair!(self.blind_asset_proof <= | >); + impl_pset_prop_insert_pair!(self.blind_asset_proof <= | ); } PSBT_ELEMENTS_IN_BLINDED_ISSUANCE => { impl_pset_prop_insert_pair!(self.blinded_issuance <= | ); diff --git a/src/pset/map/output.rs b/src/pset/map/output.rs index f4812e51..990f2816 100644 --- a/src/pset/map/output.rs +++ b/src/pset/map/output.rs @@ -23,10 +23,10 @@ use crate::pset::map::Map; use crate::pset::raw; use crate::pset::Error; use crate::{confidential, pset}; -use crate::{encode, RangeProof, Script, TxOutWitness}; +use crate::{encode, RangeProof, Script, TxOutWitness, SurjectionProof}; use bitcoin::bip32::KeySource; use bitcoin::{PublicKey, key::XOnlyPublicKey}; -use secp256k1_zkp::{self, Generator, SurjectionProof}; +use secp256k1_zkp::{self, Generator}; use crate::issuance; @@ -118,7 +118,7 @@ pub struct Output { /// Output value rangeproof pub value_rangeproof: Option, /// Output Asset surjection proof - pub asset_surjection_proof: Option>, + pub asset_surjection_proof: Option, /// Blinding pubkey which is used in receiving address pub blinding_key: Option, /// The ephermal pk sampled by sender @@ -128,7 +128,7 @@ pub struct Output { /// The blind value rangeproof pub blind_value_proof: Option, /// The blind asset surjection proof - pub blind_asset_proof: Option>, + pub blind_asset_proof: Option, /// Pset /// Other fields #[cfg_attr( @@ -238,7 +238,11 @@ impl Output { } else { Some(txout.witness.rangeproof) }; - rv.asset_surjection_proof = txout.witness.surjection_proof; + rv.asset_surjection_proof = if txout.witness.surjection_proof.is_empty() { + None + } else { + Some(txout.witness.surjection_proof) + }; rv } @@ -265,7 +269,9 @@ impl Output { .unwrap_or_default(), script_pubkey: self.script_pubkey.clone(), witness: TxOutWitness { - surjection_proof: self.asset_surjection_proof.clone(), + surjection_proof: self.asset_surjection_proof + .clone() + .unwrap_or(SurjectionProof::EMPTY), rangeproof: self.value_rangeproof .clone() .unwrap_or(RangeProof::EMPTY), @@ -363,7 +369,7 @@ impl Map for Output { impl_pset_prop_insert_pair!(self.value_rangeproof <= | ); } PSBT_ELEMENTS_OUT_ASSET_SURJECTION_PROOF => { - impl_pset_prop_insert_pair!(self.asset_surjection_proof <= | >); + impl_pset_prop_insert_pair!(self.asset_surjection_proof <= | ); } PSBT_ELEMENTS_OUT_BLINDING_PUBKEY => { impl_pset_prop_insert_pair!(self.blinding_key <= | ); @@ -378,7 +384,7 @@ impl Map for Output { impl_pset_prop_insert_pair!(self.blind_value_proof <= | ); } PSBT_ELEMENTS_OUT_BLIND_ASSET_PROOF => { - impl_pset_prop_insert_pair!(self.blind_asset_proof <= | >); + impl_pset_prop_insert_pair!(self.blind_asset_proof <= | ); } _ => match self.proprietary.entry(prop_key) { Entry::Vacant(empty_key) => { diff --git a/src/pset/mod.rs b/src/pset/mod.rs index f51e23ce..aeb3cea7 100644 --- a/src/pset/mod.rs +++ b/src/pset/mod.rs @@ -38,20 +38,19 @@ mod str; #[cfg(feature = "base64")] pub use self::str::ParseError; -use crate::blind::BlindAssetProofs; use crate::confidential; use crate::encode::{self, Decodable, Encodable}; use crate::{ blind::RangeProofMessage, confidential::{AssetBlindingFactor, ValueBlindingFactor}, - RangeProof, TxOutSecrets, + RangeProof, SurjectionProof, TxOutSecrets, }; use crate::{ LockTime, OutPoint, Sequence, SurjectionInput, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness, Txid, CtLocation, CtLocationType, }; use secp256k1_zkp::rand::{CryptoRng, RngCore}; -use secp256k1_zkp::{self, SecretKey, SurjectionProof}; +use secp256k1_zkp::{self, SecretKey}; pub use self::error::{Error, PsetBlindError, PsetHash}; use self::map::Map; @@ -339,7 +338,9 @@ impl PartiallySignedTransaction { .unwrap_or_default(), script_pubkey: out.script_pubkey.clone(), witness: TxOutWitness { - surjection_proof: out.asset_surjection_proof.clone(), + surjection_proof: out.asset_surjection_proof + .clone() + .unwrap_or(SurjectionProof::EMPTY), rangeproof: out.value_rangeproof .clone() .unwrap_or(RangeProof::EMPTY), @@ -525,7 +526,7 @@ impl PartiallySignedTransaction { // mutate the pset { self.outputs[i].value_rangeproof = Some(txout.witness.rangeproof); - self.outputs[i].asset_surjection_proof = txout.witness.surjection_proof; + self.outputs[i].asset_surjection_proof = Some(txout.witness.surjection_proof); self.outputs[i].amount_comm = txout.value.commitment(); self.outputs[i].asset_comm = txout.asset.commitment(); self.outputs[i].ecdh_pubkey = @@ -536,10 +537,10 @@ impl PartiallySignedTransaction { let asset_id = self.outputs[i] .asset .ok_or(PsetBlindError::MustHaveExplicitTxOut(i))?; - self.outputs[i].blind_asset_proof = Some(Box::new( + self.outputs[i].blind_asset_proof = Some( SurjectionProof::blind_asset_proof(rng, secp, asset_id, abf) .map_err(|e| PsetBlindError::BlindingProofsCreationError(i, e))?, - )); + ); let asset_gen = self.outputs[i] .asset_comm @@ -677,7 +678,7 @@ impl PartiallySignedTransaction { // mutate the pset { self.outputs[last_out_index].value_rangeproof = Some(rangeproof); - self.outputs[last_out_index].asset_surjection_proof = Some(Box::new(surjection_proof)); + self.outputs[last_out_index].asset_surjection_proof = Some(surjection_proof); self.outputs[last_out_index].amount_comm = value_commitment.commitment(); self.outputs[last_out_index].asset_comm = out_asset_commitment.commitment(); self.outputs[last_out_index].ecdh_pubkey = @@ -688,10 +689,10 @@ impl PartiallySignedTransaction { let asset_id = self.outputs[last_out_index] .asset .ok_or(PsetBlindError::MustHaveExplicitTxOut(last_out_index))?; - self.outputs[last_out_index].blind_asset_proof = Some(Box::new( + self.outputs[last_out_index].blind_asset_proof = Some( SurjectionProof::blind_asset_proof(rng, secp, asset_id, out_abf) .map_err(|e| PsetBlindError::BlindingProofsCreationError(last_out_index, e))?, - )); + ); let asset_gen = self.outputs[last_out_index] .asset_comm diff --git a/src/pset/serialize.rs b/src/pset/serialize.rs index 6d247404..16e3b8b1 100644 --- a/src/pset/serialize.rs +++ b/src/pset/serialize.rs @@ -24,12 +24,12 @@ use crate::encode::{ self, deserialize, deserialize_partial, serialize, Decodable, Encodable, VarInt, }; use crate::hashes::{hash160, ripemd160, sha256, sha256d, Hash}; -use crate::{AssetId, BlockHash, RangeProof, Script, Transaction, TxOut, Txid}; +use crate::{AssetId, BlockHash, RangeProof, Script, SurjectionProof, Transaction, TxOut, Txid}; use bitcoin; use bitcoin::bip32::{ChildNumber, Fingerprint, KeySource}; use bitcoin::{key::XOnlyPublicKey, PublicKey}; use internals::slice::SliceExt; -use secp256k1_zkp::{self, SurjectionProof, Tweak}; +use secp256k1_zkp::{self, Tweak}; use super::map::{PsbtSighashType, TapTree}; use crate::schnorr; @@ -294,17 +294,15 @@ impl Deserialize for RangeProof { } } -impl Serialize for Box { +impl Serialize for SurjectionProof { fn serialize(&self) -> Vec { - SurjectionProof::serialize(self) + self.to_vec() } } -impl Deserialize for Box { +impl Deserialize for SurjectionProof { fn deserialize(bytes: &[u8]) -> Result { - let prf = SurjectionProof::from_slice(bytes) - .map_err(|_| encode::Error::ParseFailed("Invalid SurjectionProof"))?; - Ok(Box::new(prf)) + Self::from_slice(bytes).map_err(encode::Error::Secp256k1zkp) } } diff --git a/src/transaction.rs b/src/transaction.rs index 822a571e..694c43ac 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -30,8 +30,10 @@ use crate::issuance::{AssetEntropy, AssetId}; use crate::opcodes; use crate::parse::impl_parse_str_through_int; use crate::script::Instruction; -use crate::{LockTime, RangeProof, Script, Txid, Wtxid}; -use secp256k1_zkp::{SurjectionProof, Tweak, ZERO_TWEAK}; +use crate::{LockTime, RangeProof, Script, SurjectionProof, Txid, Wtxid}; +use secp256k1_zkp::{ + Tweak, ZERO_TWEAK, +}; /// Description of an asset issuance in a transaction input #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] @@ -642,8 +644,10 @@ pub struct TxOutWitness { /// Surjection proof showing that the asset commitment is legitimate // We Box it because surjection proof internally is an array [u8; N] that // allocates on stack even when the surjection proof is empty - pub surjection_proof: Option>, - /// Rangeproof showing that the value commitment is legitimate. + pub surjection_proof: SurjectionProof, + /// Rangeproof showing that the value commitment is legitimate + // We Box it because range proof internally is an array [u8; N] that + // allocates on stack even when the range proof is empty pub rangeproof: RangeProof, } serde_struct_impl!(TxOutWitness, surjection_proof, rangeproof); @@ -653,14 +657,14 @@ impl TxOutWitness { /// Create an empty output witness. pub fn empty() -> Self { TxOutWitness { - surjection_proof: None, + surjection_proof: SurjectionProof::EMPTY, rangeproof: RangeProof::EMPTY, } } /// Whether this witness is null pub fn is_empty(&self) -> bool { - self.surjection_proof.is_none() && self.rangeproof.is_empty() + self.surjection_proof.is_empty() && self.rangeproof.is_empty() } /// The rangeproof len if is present, otherwise 0 @@ -670,7 +674,7 @@ impl TxOutWitness { /// The surjection proof len if is present, otherwise 0 pub fn surjectionproof_len(&self) -> usize { - self.surjection_proof.as_ref().map_or(0, |prf| prf.len()) + self.surjection_proof.len() } } From 278ad33abf60ca585fa3cdfb292d5ff2b4cbbea1 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 12:25:13 +0000 Subject: [PATCH 16/41] confidential: improve unit tests by defining constants --- src/confidential/mod.rs | 142 ++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index d85df050..23510c17 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -1086,83 +1086,125 @@ mod tests { #[cfg(feature = "serde")] use bincode; + const VALUE_EXPLICIT: [u8; 9] = [ 1, 0, 0, 0, 0, 0, 0, 3, 232 ]; + + const VALUE_COMMITMENT1: [u8; 33] = [ + 0x08, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ]; + + const VALUE_COMMITMENT2: [u8; 33] = [ + 0x09, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ]; + + const NONCE_EXPLICIT: [u8; 33] = [ + 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]; + + const NONCE_COMMITMENT1: [u8; 33] = [ + 0x02, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ]; + + const NONCE_COMMITMENT2: [u8; 33] = [ + 0x03, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ]; + + const ASSET_EXPLICIT: [u8; 33] = [ + 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]; + + const ASSET_COMMITMENT1: [u8; 33] = [ + 0x0a, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ]; + + const ASSET_COMMITMENT2: [u8; 33] = [ + 0x0b, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, + ]; + #[test] fn encode_length() { + let val_encodings = [ + vec![0], + VALUE_EXPLICIT.to_vec(), + VALUE_COMMITMENT1.to_vec(), + VALUE_COMMITMENT2.to_vec(), + ]; let vals = [ Value::Null, Value::Explicit(1000), - Value::from_commitment(&[ - 0x08, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, - ]) - .unwrap(), + Value::from_commitment(&VALUE_COMMITMENT1).unwrap(), + Value::from_commitment(&VALUE_COMMITMENT2).unwrap(), ]; - for v in &vals[..] { + for (v, enc) in vals.iter().zip(val_encodings.iter()) { let mut x = vec![]; assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); + assert_eq!(x, *enc); } + let nonce_encodings = [ + vec![0], + NONCE_EXPLICIT.to_vec(), + NONCE_COMMITMENT1.to_vec(), + NONCE_COMMITMENT2.to_vec(), + ]; let nonces = [ Nonce::Null, Nonce::Explicit([0; 32]), - Nonce::from_commitment(&[ - 0x02, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, - ]) - .unwrap(), + Nonce::from_commitment(&NONCE_COMMITMENT1).unwrap(), + Nonce::from_commitment(&NONCE_COMMITMENT2).unwrap(), ]; - for v in &nonces[..] { + for (v, enc) in nonces.iter().zip(nonce_encodings.iter()) { let mut x = vec![]; assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); + assert_eq!(x, *enc); } + let asset_encodings = [ + vec![0], + ASSET_EXPLICIT.to_vec(), + ASSET_COMMITMENT1.to_vec(), + ASSET_COMMITMENT2.to_vec(), + ]; let assets = [ Asset::Null, Asset::Explicit(AssetId::from_byte_array([0; 32])), - Asset::from_commitment(&[ - 0x0a, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, - ]) - .unwrap(), + Asset::from_commitment(&ASSET_COMMITMENT1).unwrap(), + Asset::from_commitment(&ASSET_COMMITMENT2).unwrap(), ]; - for v in &assets[..] { + for (v, enc) in assets.iter().zip(asset_encodings.iter()) { let mut x = vec![]; assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); + assert_eq!(x, *enc); } } #[test] fn commitments() { - let x = Value::from_commitment(&[ - 0x08, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - ]) - .unwrap(); + let x = Value::from_commitment(&VALUE_COMMITMENT1).unwrap(); let commitment = x.commitment().unwrap(); let mut commitment = commitment.serialize(); assert_eq!(x, Value::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Value::from_commitment(&commitment[..]).is_err()); - let x = Asset::from_commitment(&[ - 0x0a, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - ]) - .unwrap(); + let x = Asset::from_commitment(&ASSET_COMMITMENT1).unwrap(); let commitment = x.commitment().unwrap(); let mut commitment = commitment.serialize(); assert_eq!(x, Asset::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Asset::from_commitment(&commitment[..]).is_err()); - let x = Nonce::from_commitment(&[ - 0x02, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - ]) - .unwrap(); + let x = Nonce::from_commitment(&NONCE_COMMITMENT1).unwrap(); let commitment = x.commitment().unwrap(); let mut commitment = commitment.serialize(); assert_eq!(x, Nonce::from_commitment(&commitment[..]).unwrap()); @@ -1186,11 +1228,7 @@ mod tests { ] ); - let value = Value::from_commitment(&[ - 0x08, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - ]).unwrap(); + let value = Value::from_commitment(&VALUE_COMMITMENT1).unwrap(); assert_tokens( &value.readable(), &[ @@ -1207,13 +1245,7 @@ mod tests { &[ Token::Seq { len: Some(2) }, Token::U8(2), - Token::Bytes( - &[ - 8, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 - ] - ), + Token::Bytes(&VALUE_COMMITMENT1), Token::SeqEnd ] ); @@ -1264,11 +1296,7 @@ mod tests { ] ); - let asset = Asset::from_commitment(&[ - 0x0a, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - ]).unwrap(); + let asset = Asset::from_commitment(&ASSET_COMMITMENT1).unwrap(); assert_tokens( &asset.readable(), &[ @@ -1285,13 +1313,7 @@ mod tests { &[ Token::Seq { len: Some(2) }, Token::U8(2), - Token::Bytes( - &[ - 10, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 - ] - ), + Token::Bytes(&ASSET_COMMITMENT1), Token::SeqEnd ] ); @@ -1335,11 +1357,7 @@ mod tests { ] ); - let nonce = Nonce::from_commitment(&[ - 0x02, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - ]).unwrap(); + let nonce = Nonce::from_commitment(&NONCE_COMMITMENT1).unwrap(); assert_tokens( &nonce.readable(), &[ From 8cc6e02525ed8055dcb19b29430dd9c1a266b89b Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 14:10:14 +0000 Subject: [PATCH 17/41] confidential: move commitment types into their own modules The code for value, nonce, asset, have diverged in various ways. By splitting them into multiple files I hope to make diffing them easier. Code move only; the next commits will make chonges to bring the three files closer together. Also I am quietly relicensing this stuff from CC0 to MIT+Apache. I am not doing this to be sneaky; I was just creating new files so it was an opportunity to use a new license header. See https://github.com/rust-bitcoin/rust-bitcoin/issues/5849 --- src/confidential/asset.rs | 362 +++++++++++++ src/confidential/mod.rs | 1007 +------------------------------------ src/confidential/nonce.rs | 255 ++++++++++ src/confidential/value.rs | 416 +++++++++++++++ 4 files changed, 1044 insertions(+), 996 deletions(-) create mode 100644 src/confidential/asset.rs create mode 100644 src/confidential/nonce.rs create mode 100644 src/confidential/value.rs diff --git a/src/confidential/asset.rs b/src/confidential/asset.rs new file mode 100644 index 00000000..1b782f7a --- /dev/null +++ b/src/confidential/asset.rs @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Confiential Assets + +use core::{fmt, str}; +use std::io; + +use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; +use secp256k1_zkp::rand::Rng; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::encode::{self, Decodable, Encodable}; +use crate::issuance::AssetId; + +/// A CT commitment to an asset +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum Asset { + /// No value + #[default] + Null, + /// Asset entropy is explicitly encoded + Explicit(AssetId), + /// Asset is committed + Confidential(Generator), +} + +impl Asset { + /// Create asset commitment. + pub fn new_confidential( + secp: &Secp256k1, + asset: AssetId, + bf: AssetBlindingFactor, + ) -> Self { + Asset::Confidential(Generator::new_blinded( + secp, + asset.into_tag(), + bf.into_inner(), + )) + } + + /// Serialized length, in bytes + pub fn encoded_length(&self) -> usize { + match *self { + Asset::Null => 1, + Asset::Explicit(..) => 33, + Asset::Confidential(..) => 33, + } + } + + /// Create from commitment. + pub fn from_commitment(bytes: &[u8]) -> Result { + Ok(Asset::Confidential(Generator::from_slice(bytes)?)) + } + + /// Check if the object is null. + pub fn is_null(&self) -> bool { + matches!(*self, Asset::Null) + } + + /// Check if the object is explicit. + pub fn is_explicit(&self) -> bool { + matches!(*self, Asset::Explicit(_)) + } + + /// Check if the object is confidential. + pub fn is_confidential(&self) -> bool { + matches!(*self, Asset::Confidential(_)) + } + + /// Returns the explicit inner value. + /// Returns [None] if [`Asset::is_explicit`] returns false. + pub fn explicit(&self) -> Option { + match *self { + Asset::Explicit(i) => Some(i), + _ => None, + } + } + + /// Returns the confidential commitment in case of a confidential value. + /// Returns [None] if [`Asset::is_confidential`] returns false. + pub fn commitment(&self) -> Option { + match *self { + Asset::Confidential(i) => Some(i), + _ => None, + } + } + + /// Internally used function for getting the generator from asset + /// Used in the amount verification check + /// Returns [`None`] is the asset is [`Asset::Null`] + /// Converts a explicit asset into a generator and returns the confidential + /// generator as is. + pub fn into_asset_gen ( + self, + secp: &Secp256k1, + ) -> Option { + match self { + // Only error is Null error which is dealt with later + // when we have more context information about it. + Asset::Null => None, + Asset::Explicit(x) => { + Some(Generator::new_unblinded(secp, x.into_tag())) + } + Asset::Confidential(gen) => Some(gen), + } + } +} + +impl From for Asset { + fn from(from: Generator) -> Self { + Asset::Confidential(from) + } +} + +impl fmt::Display for Asset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Asset::Null => f.write_str("null"), + Asset::Explicit(n) => write!(f, "{}", n), + Asset::Confidential(generator) => write!(f, "{:02x}", generator), + } + } +} + +impl Encodable for Asset { + fn consensus_encode(&self, mut s: S) -> Result { + match *self { + Asset::Null => 0u8.consensus_encode(s), + Asset::Explicit(n) => { + 1u8.consensus_encode(&mut s)?; + Ok(1 + n.consensus_encode(&mut s)?) + } + Asset::Confidential(generator) => { + s.write_all(&generator.serialize())?; + Ok(33) + } + } + } +} + +impl Decodable for Asset { + fn consensus_decode(mut d: D) -> Result { + let prefix = u8::consensus_decode(&mut d)?; + + match prefix { + 0 => Ok(Asset::Null), + 1 => { + let explicit = Decodable::consensus_decode(&mut d)?; + Ok(Asset::Explicit(explicit)) + } + p if p == 0x0a || p == 0x0b => { + let mut comm = [0u8; 33]; + comm[0] = p; + d.read_exact(&mut comm[1..])?; + Ok(Asset::Confidential(Generator::from_slice(&comm[..])?)) + } + p => Err(encode::Error::InvalidConfidentialPrefix(p)), + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for Asset { + fn serialize(&self, s: S) -> Result { + use serde::ser::SerializeSeq; + + let seq_len = match *self { + Asset::Null => 1, + Asset::Explicit(_) | Asset::Confidential(_) => 2 + }; + let mut seq = s.serialize_seq(Some(seq_len))?; + + match *self { + Asset::Null => seq.serialize_element(&0u8)?, + Asset::Explicit(n) => { + seq.serialize_element(&1u8)?; + seq.serialize_element(&n)?; + } + Asset::Confidential(commitment) => { + seq.serialize_element(&2u8)?; + seq.serialize_element(&commitment)?; + } + } + seq.end() + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Asset { + fn deserialize>(d: D) -> Result { + use serde::de::{Error, SeqAccess, Visitor}; + struct CommitVisitor; + + impl<'de> Visitor<'de> for CommitVisitor { + type Value = Asset; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a committed value") + } + + fn visit_seq>(self, mut access: A) -> Result { + let prefix = access.next_element::()?; + match prefix { + Some(0) => Ok(Asset::Null), + Some(1) => { + match access.next_element()? { + Some(x) => Ok(Asset::Explicit(x)), + None => Err(A::Error::custom("missing explicit asset")), + } + } + Some(2) => { + match access.next_element()? { + Some(x) => Ok(Asset::Confidential(x)), + None => Err(A::Error::custom("missing generator")), + } + } + _ => Err(A::Error::custom("wrong or missing prefix")), + } + } + } + + d.deserialize_seq(CommitVisitor) + } +} + + +/// Blinding factor used for asset commitments. +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub struct AssetBlindingFactor(pub(crate) Tweak); + +impl AssetBlindingFactor { + /// Generate random asset blinding factor. + pub fn new(rng: &mut R) -> Self { + AssetBlindingFactor(Tweak::new(rng)) + } + + /// Parse a blinding factor from a 64-character hex string. + #[deprecated(since = "0.27.0", note = "use s.parse() instead")] + pub fn from_hex(s: &str) -> Result { + s.parse() + } + + /// Create from bytes. + pub fn from_byte_array(bytes: [u8; 32]) -> Result { + Ok(AssetBlindingFactor(Tweak::from_inner(bytes)?)) + } + + /// Create from bytes. + pub fn from_slice(bytes: &[u8]) -> Result { + Ok(AssetBlindingFactor(Tweak::from_slice(bytes)?)) + } + + /// Returns the inner value. + pub fn into_inner(self) -> Tweak { + self.0 + } + + /// Get a unblinded/zero `AssetBlinding` factor + pub fn zero() -> Self { + AssetBlindingFactor(ZERO_TWEAK) + } +} + +impl core::borrow::Borrow<[u8]> for AssetBlindingFactor { + fn borrow(&self) -> &[u8] { &self.0[..] } +} + +hex::impl_fmt_traits! { + #[display_backward(true)] + impl fmt_traits for AssetBlindingFactor { + const LENGTH: usize = 32; + } +} + +impl str::FromStr for AssetBlindingFactor { + type Err = encode::Error; + + fn from_str(s: &str) -> Result { + let mut slice: [u8; 32] = hex::decode_to_array(s)?; + slice.reverse(); + + let inner = Tweak::from_inner(slice)?; + Ok(AssetBlindingFactor(inner)) + } +} + +#[cfg(feature = "serde")] +impl Serialize for AssetBlindingFactor { + fn serialize(&self, s: S) -> Result { + if s.is_human_readable() { + s.collect_str(&self) + } else { + s.serialize_bytes(&self.0[..]) + } + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for AssetBlindingFactor { + fn deserialize>(d: D) -> Result { + if d.is_human_readable() { + struct HexVisitor; + + impl ::serde::de::Visitor<'_> for HexVisitor { + type Value = AssetBlindingFactor; + + fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + formatter.write_str("an ASCII hex string") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: ::serde::de::Error, + { + if let Ok(hex) = ::std::str::from_utf8(v) { + hex.parse().map_err(E::custom) + } else { + Err(E::invalid_value(::serde::de::Unexpected::Bytes(v), &self)) + } + } + + fn visit_str(self, v: &str) -> Result + where + E: ::serde::de::Error, + { + v.parse().map_err(E::custom) + } + } + + d.deserialize_str(HexVisitor) + } else { + struct BytesVisitor; + + impl ::serde::de::Visitor<'_> for BytesVisitor { + type Value = AssetBlindingFactor; + + fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + formatter.write_str("a bytestring") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: ::serde::de::Error, + { + use core::convert::TryFrom; + + match <[u8; 32]>::try_from(v) { + Ok(ret) => { + let inner = Tweak::from_inner(ret).map_err(E::custom)?; + Ok(AssetBlindingFactor(inner)) + } + Err(_) => Err(E::invalid_length(v.len(), &stringify!($len))), + } + } + } + + d.deserialize_bytes(BytesVisitor) + } + } +} + diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 23510c17..c10a50e7 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -17,679 +17,24 @@ //! Structures representing Pedersen commitments of various types //! +mod asset; +mod nonce; mod range_proof; mod surjection_proof; +mod value; -use crate::hashes::sha256d; -use secp256k1_zkp::{self, CommitmentSecrets, Generator, PedersenCommitment, - PublicKey, Secp256k1, SecretKey, Signing, Tweak, ZERO_TWEAK, - compute_adaptive_blinding_factor, - rand::{CryptoRng, Rng, RngCore} -}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use secp256k1_zkp; -use std::{fmt, io, ops::{AddAssign, Neg}, str}; +use core::fmt; -use crate::encode::{self, Decodable, Encodable}; +use crate::encode; use crate::issuance::AssetId; +pub use self::asset::{Asset, AssetBlindingFactor}; +pub use self::nonce::Nonce; pub use self::range_proof::RangeProof; pub use self::surjection_proof::SurjectionProof; - -/// A CT commitment to an amount -#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub enum Value { - /// No value - #[default] - Null, - /// Value is explicitly encoded - Explicit(u64), - /// Value is committed - Confidential(PedersenCommitment), -} - -impl Value { - /// Create value commitment. - pub fn new_confidential( - secp: &Secp256k1, - value: u64, - asset: Generator, - bf: ValueBlindingFactor, - ) -> Self { - Value::Confidential(PedersenCommitment::new(secp, value, bf.0, asset)) - } - - /// Create value commitment from assetID, asset blinding factor, - /// value and value blinding factor - pub fn new_confidential_from_assetid( - secp: &Secp256k1, - value: u64, - asset: AssetId, - v_bf: ValueBlindingFactor, - a_bf: AssetBlindingFactor, - ) -> Self { - let generator = Generator::new_blinded(secp, asset.into_tag(), a_bf.0); - let comm = PedersenCommitment::new(secp, value, v_bf.0, generator); - - Value::Confidential(comm) - } - - /// Serialized length, in bytes - pub fn encoded_length(&self) -> usize { - match *self { - Value::Null => 1, - Value::Explicit(..) => 9, - Value::Confidential(..) => 33, - } - } - - /// Create from commitment. - pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Value::Confidential(PedersenCommitment::from_slice(bytes)?)) - } - - /// Check if the object is null. - pub fn is_null(&self) -> bool { - matches!(*self, Value::Null) - } - - /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { - matches!(*self, Value::Explicit(_)) - } - - /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { - matches!(*self, Value::Confidential(_)) - } - - /// Returns the explicit inner value. - /// Returns [None] if [`Value::is_explicit`] returns false. - pub fn explicit(&self) -> Option { - match *self { - Value::Explicit(i) => Some(i), - _ => None, - } - } - - /// Returns the confidential commitment in case of a confidential value. - /// Returns [None] if [`Value::is_confidential`] returns false. - pub fn commitment(&self) -> Option { - match *self { - Value::Confidential(i) => Some(i), - _ => None, - } - } -} - -impl From for Value { - fn from(from: PedersenCommitment) -> Self { - Value::Confidential(from) - } -} - -impl fmt::Display for Value { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Value::Null => f.write_str("null"), - Value::Explicit(n) => write!(f, "{}", n), - Value::Confidential(commitment) => write!(f, "{:02x}", commitment), - } - } -} - -impl Encodable for Value { - fn consensus_encode(&self, mut s: S) -> Result { - match *self { - Value::Null => 0u8.consensus_encode(s), - Value::Explicit(n) => { - 1u8.consensus_encode(&mut s)?; - Ok(1 + u64::swap_bytes(n).consensus_encode(&mut s)?) - } - Value::Confidential(commitment) => { - s.write_all(&commitment.serialize())?; - Ok(33) - } - } - } -} - -impl Decodable for Value { - fn consensus_decode(mut d: D) -> Result { - let prefix = u8::consensus_decode(&mut d)?; - - match prefix { - 0 => Ok(Value::Null), - 1 => { - let explicit = u64::swap_bytes(Decodable::consensus_decode(&mut d)?); - Ok(Value::Explicit(explicit)) - } - p if p == 0x08 || p == 0x09 => { - let mut comm = [0u8; 33]; - comm[0] = p; - d.read_exact(&mut comm[1..])?; - Ok(Value::Confidential(PedersenCommitment::from_slice(&comm)?)) - } - p => Err(encode::Error::InvalidConfidentialPrefix(p)), - } - } -} - -#[cfg(feature = "serde")] -impl Serialize for Value { - fn serialize(&self, s: S) -> Result { - use serde::ser::SerializeSeq; - - let seq_len = match *self { - Value::Null => 1, - Value::Explicit(_) | Value::Confidential(_) => 2 - }; - let mut seq = s.serialize_seq(Some(seq_len))?; - - match *self { - Value::Null => seq.serialize_element(&0u8)?, - Value::Explicit(n) => { - seq.serialize_element(&1u8)?; - seq.serialize_element(&u64::swap_bytes(n))?; - } - Value::Confidential(commitment) => { - seq.serialize_element(&2u8)?; - seq.serialize_element(&commitment)?; - } - } - seq.end() - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Value { - fn deserialize>(d: D) -> Result { - use serde::de::{Error, SeqAccess, Visitor}; - struct CommitVisitor; - - impl<'de> Visitor<'de> for CommitVisitor { - type Value = Value; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("a committed value") - } - - fn visit_seq>(self, mut access: A) -> Result { - let prefix = access.next_element::()?; - match prefix { - Some(0) => Ok(Value::Null), - Some(1) => { - match access.next_element()? { - Some(x) => Ok(Value::Explicit(u64::swap_bytes(x))), - None => Err(A::Error::custom("missing explicit value")), - } - } - Some(2) => { - match access.next_element()? { - Some(x) => Ok(Value::Confidential(x)), - None => Err(A::Error::custom("missing pedersen commitment")), - } - } - _ => Err(A::Error::custom("wrong or missing prefix")), - } - } - } - - d.deserialize_seq(CommitVisitor) - } -} - -/// A CT commitment to an asset -#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub enum Asset { - /// No value - #[default] - Null, - /// Asset entropy is explicitly encoded - Explicit(AssetId), - /// Asset is committed - Confidential(Generator), -} - -impl Asset { - /// Create asset commitment. - pub fn new_confidential( - secp: &Secp256k1, - asset: AssetId, - bf: AssetBlindingFactor, - ) -> Self { - Asset::Confidential(Generator::new_blinded( - secp, - asset.into_tag(), - bf.into_inner(), - )) - } - - /// Serialized length, in bytes - pub fn encoded_length(&self) -> usize { - match *self { - Asset::Null => 1, - Asset::Explicit(..) => 33, - Asset::Confidential(..) => 33, - } - } - - /// Create from commitment. - pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Asset::Confidential(Generator::from_slice(bytes)?)) - } - - /// Check if the object is null. - pub fn is_null(&self) -> bool { - matches!(*self, Asset::Null) - } - - /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { - matches!(*self, Asset::Explicit(_)) - } - - /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { - matches!(*self, Asset::Confidential(_)) - } - - /// Returns the explicit inner value. - /// Returns [None] if [`Asset::is_explicit`] returns false. - pub fn explicit(&self) -> Option { - match *self { - Asset::Explicit(i) => Some(i), - _ => None, - } - } - - /// Returns the confidential commitment in case of a confidential value. - /// Returns [None] if [`Asset::is_confidential`] returns false. - pub fn commitment(&self) -> Option { - match *self { - Asset::Confidential(i) => Some(i), - _ => None, - } - } - - /// Internally used function for getting the generator from asset - /// Used in the amount verification check - /// Returns [`None`] is the asset is [`Asset::Null`] - /// Converts a explicit asset into a generator and returns the confidential - /// generator as is. - pub fn into_asset_gen ( - self, - secp: &Secp256k1, - ) -> Option { - match self { - // Only error is Null error which is dealt with later - // when we have more context information about it. - Asset::Null => None, - Asset::Explicit(x) => { - Some(Generator::new_unblinded(secp, x.into_tag())) - } - Asset::Confidential(gen) => Some(gen), - } - } -} - -impl From for Asset { - fn from(from: Generator) -> Self { - Asset::Confidential(from) - } -} - -impl fmt::Display for Asset { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Asset::Null => f.write_str("null"), - Asset::Explicit(n) => write!(f, "{}", n), - Asset::Confidential(generator) => write!(f, "{:02x}", generator), - } - } -} - -impl Encodable for Asset { - fn consensus_encode(&self, mut s: S) -> Result { - match *self { - Asset::Null => 0u8.consensus_encode(s), - Asset::Explicit(n) => { - 1u8.consensus_encode(&mut s)?; - Ok(1 + n.consensus_encode(&mut s)?) - } - Asset::Confidential(generator) => { - s.write_all(&generator.serialize())?; - Ok(33) - } - } - } -} - -impl Decodable for Asset { - fn consensus_decode(mut d: D) -> Result { - let prefix = u8::consensus_decode(&mut d)?; - - match prefix { - 0 => Ok(Asset::Null), - 1 => { - let explicit = Decodable::consensus_decode(&mut d)?; - Ok(Asset::Explicit(explicit)) - } - p if p == 0x0a || p == 0x0b => { - let mut comm = [0u8; 33]; - comm[0] = p; - d.read_exact(&mut comm[1..])?; - Ok(Asset::Confidential(Generator::from_slice(&comm[..])?)) - } - p => Err(encode::Error::InvalidConfidentialPrefix(p)), - } - } -} - -#[cfg(feature = "serde")] -impl Serialize for Asset { - fn serialize(&self, s: S) -> Result { - use serde::ser::SerializeSeq; - - let seq_len = match *self { - Asset::Null => 1, - Asset::Explicit(_) | Asset::Confidential(_) => 2 - }; - let mut seq = s.serialize_seq(Some(seq_len))?; - - match *self { - Asset::Null => seq.serialize_element(&0u8)?, - Asset::Explicit(n) => { - seq.serialize_element(&1u8)?; - seq.serialize_element(&n)?; - } - Asset::Confidential(commitment) => { - seq.serialize_element(&2u8)?; - seq.serialize_element(&commitment)?; - } - } - seq.end() - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Asset { - fn deserialize>(d: D) -> Result { - use serde::de::{Error, SeqAccess, Visitor}; - struct CommitVisitor; - - impl<'de> Visitor<'de> for CommitVisitor { - type Value = Asset; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("a committed value") - } - - fn visit_seq>(self, mut access: A) -> Result { - let prefix = access.next_element::()?; - match prefix { - Some(0) => Ok(Asset::Null), - Some(1) => { - match access.next_element()? { - Some(x) => Ok(Asset::Explicit(x)), - None => Err(A::Error::custom("missing explicit asset")), - } - } - Some(2) => { - match access.next_element()? { - Some(x) => Ok(Asset::Confidential(x)), - None => Err(A::Error::custom("missing generator")), - } - } - _ => Err(A::Error::custom("wrong or missing prefix")), - } - } - } - - d.deserialize_seq(CommitVisitor) - } -} - -/// A CT commitment to an output nonce (i.e. a public key) -#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub enum Nonce { - /// No value - #[default] - Null, - /// There should be no such thing as an "explicit nonce", but Elements will deserialize - /// such a thing (and insists that its size be 32 bytes). So we stick a 32-byte type here - /// that implements all the traits we need. - Explicit([u8; 32]), - /// Nonce is committed - Confidential(PublicKey), -} - -impl Nonce { - /// Create nonce commitment. - pub fn new_confidential( - rng: &mut R, - secp: &Secp256k1, - receiver_blinding_pk: &PublicKey, - ) -> (Self, SecretKey) { - let ephemeral_sk = SecretKey::new(rng); - Self::with_ephemeral_sk(secp, ephemeral_sk, receiver_blinding_pk) - } - - /// Similar to [`Nonce::new_confidential`], but with a given `ephemeral_sk` - /// instead of sampling it from rng. - pub fn with_ephemeral_sk( - secp: &Secp256k1, - ephemeral_sk: SecretKey, - receiver_blinding_pk: &PublicKey - ) -> (Self, SecretKey) { - let sender_pk = PublicKey::from_secret_key(secp, &ephemeral_sk); - let shared_secret = Self::make_shared_secret(receiver_blinding_pk, &ephemeral_sk); - (Nonce::Confidential(sender_pk), shared_secret) - } - - /// Calculate the shared secret. - pub fn shared_secret(&self, receiver_blinding_sk: &SecretKey) -> Option { - match self { - Nonce::Confidential(sender_pk) => { - Some(Self::make_shared_secret(sender_pk, receiver_blinding_sk)) - } - _ => None, - } - } - - /// Create the shared secret. - fn make_shared_secret(pk: &PublicKey, sk: &SecretKey) -> SecretKey { - let xy = secp256k1_zkp::ecdh::shared_secret_point(pk, sk); - let shared_secret = { - // Yes, what follows is the compressed representation of a Bitcoin public key. - // However, this is more by accident then by design, see here: https://github.com/rust-bitcoin/rust-secp256k1/pull/255#issuecomment-744146282 - - let mut dh_secret = [0u8; 33]; - dh_secret[0] = if xy.last().unwrap() % 2 == 0 { - 0x02 - } else { - 0x03 - }; - dh_secret[1..].copy_from_slice(&xy[0..32]); - - sha256d::Hash::hash(&dh_secret).to_byte_array() - }; - - SecretKey::from_slice(&shared_secret[..32]).expect("always has exactly 32 bytes") - } - - /// Serialized length, in bytes - pub fn encoded_length(&self) -> usize { - match *self { - Nonce::Null => 1, - Nonce::Explicit(..) => 33, - Nonce::Confidential(..) => 33, - } - } - - /// Create from commitment. - pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Nonce::Confidential( - PublicKey::from_slice(bytes).map_err(secp256k1_zkp::Error::Upstream)?, - )) - } - - /// Check if the object is null. - pub fn is_null(&self) -> bool { - matches!(*self, Nonce::Null) - } - - /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { - matches!(*self, Nonce::Explicit(_)) - } - - /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { - matches!(*self, Nonce::Confidential(_)) - } - - /// Returns the explicit inner value. - /// Returns [None] if [`Nonce::is_explicit`] returns false. - pub fn explicit(&self) -> Option<[u8; 32]> { - match *self { - Nonce::Explicit(i) => Some(i), - _ => None, - } - } - - /// Returns the confidential commitment in case of a confidential value. - /// Returns [None] if [`Nonce::is_confidential`] returns false. - pub fn commitment(&self) -> Option { - match *self { - Nonce::Confidential(i) => Some(i), - _ => None, - } - } -} - -impl From for Nonce { - fn from(from: PublicKey) -> Self { - Nonce::Confidential(from) - } -} - -impl fmt::Display for Nonce { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Nonce::Null => f.write_str("null"), - Nonce::Explicit(n) => { - for b in &n { - write!(f, "{:02x}", b)?; - } - Ok(()) - } - Nonce::Confidential(pk) => write!(f, "{:02x}", pk), - } - } -} - -impl Encodable for Nonce { - fn consensus_encode(&self, mut s: S) -> Result { - match *self { - Nonce::Null => 0u8.consensus_encode(s), - Nonce::Explicit(n) => { - 1u8.consensus_encode(&mut s)?; - Ok(1 + n.consensus_encode(&mut s)?) - } - Nonce::Confidential(commitment) => { - s.write_all(&commitment.serialize())?; - Ok(33) - } - } - } -} - -impl Decodable for Nonce { - fn consensus_decode(mut d: D) -> Result { - let prefix = u8::consensus_decode(&mut d)?; - - match prefix { - 0 => Ok(Nonce::Null), - 1 => { - let explicit = Decodable::consensus_decode(&mut d)?; - Ok(Nonce::Explicit(explicit)) - } - p if p == 0x02 || p == 0x03 => { - let mut comm = [0u8; 33]; - comm[0] = p; - d.read_exact(&mut comm[1..])?; - Ok(Nonce::Confidential(PublicKey::from_slice(&comm)?)) - } - p => Err(encode::Error::InvalidConfidentialPrefix(p)), - } - } -} - -#[cfg(feature = "serde")] -impl Serialize for Nonce { - fn serialize(&self, s: S) -> Result { - use serde::ser::SerializeSeq; - - let seq_len = match *self { - Nonce::Null => 1, - Nonce::Explicit(_) | Nonce::Confidential(_) => 2 - }; - let mut seq = s.serialize_seq(Some(seq_len))?; - - match *self { - Nonce::Null => seq.serialize_element(&0u8)?, - Nonce::Explicit(n) => { - seq.serialize_element(&1u8)?; - seq.serialize_element(&n)?; - } - Nonce::Confidential(commitment) => { - seq.serialize_element(&2u8)?; - seq.serialize_element(&commitment)?; - } - } - seq.end() - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Nonce { - fn deserialize>(d: D) -> Result { - use serde::de::{Error, SeqAccess, Visitor}; - struct CommitVisitor; - - impl<'de> Visitor<'de> for CommitVisitor { - type Value = Nonce; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("a committed value") - } - - fn visit_seq>(self, mut access: A) -> Result { - let prefix = access.next_element::()?; - match prefix { - Some(0) => Ok(Nonce::Null), - Some(1) => { - match access.next_element()? { - Some(x) => Ok(Nonce::Explicit(x)), - None => Err(A::Error::custom("missing explicit nonce")), - } - } - Some(2) => { - match access.next_element()? { - Some(x) => Ok(Nonce::Confidential(x)), - None => Err(A::Error::custom("missing nonce")), - } - } - _ => Err(A::Error::custom("wrong or missing prefix")) - } - } - } - - d.deserialize_seq(CommitVisitor) - } -} +pub use self::value::{Value, ValueBlindingFactor}; /// Error decoding hexadecimal string into tweak-like value. #[derive(Debug, Clone, PartialEq, Eq)] @@ -744,342 +89,12 @@ impl std::error::Error for TweakHexDecodeError { } } } - -/// Blinding factor used for asset commitments. -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] -pub struct AssetBlindingFactor(pub(crate) Tweak); - -impl AssetBlindingFactor { - /// Generate random asset blinding factor. - pub fn new(rng: &mut R) -> Self { - AssetBlindingFactor(Tweak::new(rng)) - } - - /// Parse a blinding factor from a 64-character hex string. - #[deprecated(since = "0.27.0", note = "use s.parse() instead")] - pub fn from_hex(s: &str) -> Result { - s.parse() - } - - /// Create from bytes. - pub fn from_byte_array(bytes: [u8; 32]) -> Result { - Ok(AssetBlindingFactor(Tweak::from_inner(bytes)?)) - } - - /// Create from bytes. - pub fn from_slice(bytes: &[u8]) -> Result { - Ok(AssetBlindingFactor(Tweak::from_slice(bytes)?)) - } - - /// Returns the inner value. - pub fn into_inner(self) -> Tweak { - self.0 - } - - /// Get a unblinded/zero `AssetBlinding` factor - pub fn zero() -> Self { - AssetBlindingFactor(ZERO_TWEAK) - } -} - -impl core::borrow::Borrow<[u8]> for AssetBlindingFactor { - fn borrow(&self) -> &[u8] { &self.0[..] } -} - -hex::impl_fmt_traits! { - #[display_backward(true)] - impl fmt_traits for AssetBlindingFactor { - const LENGTH: usize = 32; - } -} - -impl str::FromStr for AssetBlindingFactor { - type Err = encode::Error; - - fn from_str(s: &str) -> Result { - let mut slice: [u8; 32] = hex::decode_to_array(s)?; - slice.reverse(); - - let inner = Tweak::from_inner(slice)?; - Ok(AssetBlindingFactor(inner)) - } -} - -#[cfg(feature = "serde")] -impl Serialize for AssetBlindingFactor { - fn serialize(&self, s: S) -> Result { - if s.is_human_readable() { - s.collect_str(&self) - } else { - s.serialize_bytes(&self.0[..]) - } - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for AssetBlindingFactor { - fn deserialize>(d: D) -> Result { - if d.is_human_readable() { - struct HexVisitor; - - impl ::serde::de::Visitor<'_> for HexVisitor { - type Value = AssetBlindingFactor; - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - formatter.write_str("an ASCII hex string") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: ::serde::de::Error, - { - if let Ok(hex) = ::std::str::from_utf8(v) { - hex.parse().map_err(E::custom) - } else { - Err(E::invalid_value(::serde::de::Unexpected::Bytes(v), &self)) - } - } - - fn visit_str(self, v: &str) -> Result - where - E: ::serde::de::Error, - { - v.parse().map_err(E::custom) - } - } - - d.deserialize_str(HexVisitor) - } else { - struct BytesVisitor; - - impl ::serde::de::Visitor<'_> for BytesVisitor { - type Value = AssetBlindingFactor; - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - formatter.write_str("a bytestring") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: ::serde::de::Error, - { - use core::convert::TryFrom; - - match <[u8; 32]>::try_from(v) { - Ok(ret) => { - let inner = Tweak::from_inner(ret).map_err(E::custom)?; - Ok(AssetBlindingFactor(inner)) - } - Err(_) => Err(E::invalid_length(v.len(), &stringify!($len))), - } - } - } - - d.deserialize_bytes(BytesVisitor) - } - } -} - -/// Blinding factor used for value commitments. -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] -pub struct ValueBlindingFactor(pub(crate) Tweak); - -impl ValueBlindingFactor { - /// Generate random value blinding factor. - pub fn new(rng: &mut R) -> Self { - ValueBlindingFactor(Tweak::new(rng)) - } - - /// Parse a blinding factor from a 64-character hex string. - #[deprecated(since = "0.27.0", note = "use s.parse() instead")] - pub fn from_hex(s: &str) -> Result { - s.parse() - } - - /// Create the value blinding factor of the last output of a transaction. - pub fn last( - secp: &Secp256k1, - value: u64, - abf: AssetBlindingFactor, - inputs: &[(u64, AssetBlindingFactor, ValueBlindingFactor)], - outputs: &[(u64, AssetBlindingFactor, ValueBlindingFactor)], - ) -> Self { - let set_a = inputs - .iter() - .map(|(value, abf, vbf)| CommitmentSecrets { - value: *value, - value_blinding_factor: vbf.0, - generator_blinding_factor: abf.into_inner(), - }) - .collect::>(); - let set_b = outputs - .iter() - .map(|(value, abf, vbf)| CommitmentSecrets { - value: *value, - value_blinding_factor: vbf.0, - generator_blinding_factor: abf.into_inner(), - }) - .collect::>(); - - ValueBlindingFactor(compute_adaptive_blinding_factor( - secp, value, abf.0, &set_a, &set_b, - )) - } - - /// Create from bytes. - pub fn from_slice(bytes: &[u8]) -> Result { - Ok(ValueBlindingFactor(Tweak::from_slice(bytes)?)) - } - - /// Returns the inner value. - pub fn into_inner(self) -> Tweak { - self.0 - } - - /// Get a unblinded/zero `AssetBlinding` factor - pub fn zero() -> Self { - ValueBlindingFactor(ZERO_TWEAK) - } -} - -impl AddAssign for ValueBlindingFactor { - fn add_assign(&mut self, other: Self) { - if self.0.as_ref() == &[0u8; 32] { - *self = other; - } else if other.0.as_ref() == &[0u8; 32] { - // nothing to do - } else { - // Since libsecp does not expose low level APIs - // for scalar arethematic, we need to abuse secret key - // operations for this - let sk2 = SecretKey::from_slice(self.into_inner().as_ref()).expect("Valid key"); - let sk = SecretKey::from_slice(other.into_inner().as_ref()).expect("Valid key"); - // The only reason that secret key addition can fail - // is when the keys add up to zero since we have already checked - // keys are in valid secret keys - match sk.add_tweak(&sk2.into()) { - Ok(sk_tweaked) => *self = ValueBlindingFactor::from_slice(sk_tweaked.as_ref()).expect("Valid Tweak"), - Err(_) => *self = Self::zero(), - } - } - } -} - -impl Neg for ValueBlindingFactor { - type Output = Self; - - fn neg(self) -> Self::Output { - if self.0.as_ref() == &[0u8; 32] { - self - } else { - let sk = SecretKey::from_slice(self.into_inner().as_ref()).expect("Valid key").negate(); - ValueBlindingFactor::from_slice(sk.as_ref()).expect("Valid Tweak") - } - } -} - -impl core::borrow::Borrow<[u8]> for ValueBlindingFactor { - fn borrow(&self) -> &[u8] { &self.0[..] } -} - -hex::impl_fmt_traits! { - #[display_backward(true)] - impl fmt_traits for ValueBlindingFactor { - const LENGTH: usize = 32; - } -} - -impl str::FromStr for ValueBlindingFactor { - type Err = encode::Error; - - fn from_str(s: &str) -> Result { - let mut slice: [u8; 32] = hex::decode_to_array(s)?; - slice.reverse(); - - let inner = Tweak::from_inner(slice)?; - Ok(ValueBlindingFactor(inner)) - } -} - -#[cfg(feature = "serde")] -impl Serialize for ValueBlindingFactor { - fn serialize(&self, s: S) -> Result { - if s.is_human_readable() { - s.collect_str(&self) - } else { - s.serialize_bytes(&self.0[..]) - } - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for ValueBlindingFactor { - fn deserialize>(d: D) -> Result { - if d.is_human_readable() { - struct HexVisitor; - - impl ::serde::de::Visitor<'_> for HexVisitor { - type Value = ValueBlindingFactor; - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - formatter.write_str("an ASCII hex string") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: ::serde::de::Error, - { - if let Ok(hex) = ::std::str::from_utf8(v) { - hex.parse().map_err(E::custom) - } else { - Err(E::invalid_value(::serde::de::Unexpected::Bytes(v), &self)) - } - } - - fn visit_str(self, v: &str) -> Result - where - E: ::serde::de::Error, - { - v.parse().map_err(E::custom) - } - } - - d.deserialize_str(HexVisitor) - } else { - struct BytesVisitor; - - impl ::serde::de::Visitor<'_> for BytesVisitor { - type Value = ValueBlindingFactor; - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - formatter.write_str("a bytestring") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: ::serde::de::Error, - { - use core::convert::TryFrom; - - match <[u8; 32]>::try_from(v) { - Ok(ret) => { - let inner = Tweak::from_inner(ret).map_err(E::custom)?; - Ok(ValueBlindingFactor(inner)) - } - Err(_) => Err(E::invalid_length(v.len(), &stringify!($len))), - } - } - } - - d.deserialize_bytes(BytesVisitor) - } - } -} - #[cfg(test)] mod tests { use super::*; + use crate::encode::Encodable as _; + #[cfg(feature = "serde")] use std::str::FromStr; diff --git a/src/confidential/nonce.rs b/src/confidential/nonce.rs new file mode 100644 index 00000000..f3174997 --- /dev/null +++ b/src/confidential/nonce.rs @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Confiential Nonces + +use core::fmt; +use std::io; + +use secp256k1_zkp::{self, PublicKey, Secp256k1, SecretKey, Signing}; +use secp256k1_zkp::rand::{RngCore, CryptoRng}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::encode::{self, Decodable, Encodable}; +use crate::hashes::sha256d; + +/// A CT commitment to an output nonce (i.e. a public key) +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum Nonce { + /// No value + #[default] + Null, + /// There should be no such thing as an "explicit nonce", but Elements will deserialize + /// such a thing (and insists that its size be 32 bytes). So we stick a 32-byte type here + /// that implements all the traits we need. + Explicit([u8; 32]), + /// Nonce is committed + Confidential(PublicKey), +} + +impl Nonce { + /// Create nonce commitment. + pub fn new_confidential( + rng: &mut R, + secp: &Secp256k1, + receiver_blinding_pk: &PublicKey, + ) -> (Self, SecretKey) { + let ephemeral_sk = SecretKey::new(rng); + Self::with_ephemeral_sk(secp, ephemeral_sk, receiver_blinding_pk) + } + + /// Similar to [`Nonce::new_confidential`], but with a given `ephemeral_sk` + /// instead of sampling it from rng. + pub fn with_ephemeral_sk( + secp: &Secp256k1, + ephemeral_sk: SecretKey, + receiver_blinding_pk: &PublicKey + ) -> (Self, SecretKey) { + let sender_pk = PublicKey::from_secret_key(secp, &ephemeral_sk); + let shared_secret = Self::make_shared_secret(receiver_blinding_pk, &ephemeral_sk); + (Nonce::Confidential(sender_pk), shared_secret) + } + + /// Calculate the shared secret. + pub fn shared_secret(&self, receiver_blinding_sk: &SecretKey) -> Option { + match self { + Nonce::Confidential(sender_pk) => { + Some(Self::make_shared_secret(sender_pk, receiver_blinding_sk)) + } + _ => None, + } + } + + /// Create the shared secret. + fn make_shared_secret(pk: &PublicKey, sk: &SecretKey) -> SecretKey { + let xy = secp256k1_zkp::ecdh::shared_secret_point(pk, sk); + let shared_secret = { + // Yes, what follows is the compressed representation of a Bitcoin public key. + // However, this is more by accident then by design, see here: https://github.com/rust-bitcoin/rust-secp256k1/pull/255#issuecomment-744146282 + + let mut dh_secret = [0u8; 33]; + dh_secret[0] = if xy.last().unwrap() % 2 == 0 { + 0x02 + } else { + 0x03 + }; + dh_secret[1..].copy_from_slice(&xy[0..32]); + + sha256d::Hash::hash(&dh_secret).to_byte_array() + }; + + SecretKey::from_slice(&shared_secret[..32]).expect("always has exactly 32 bytes") + } + + /// Serialized length, in bytes + pub fn encoded_length(&self) -> usize { + match *self { + Nonce::Null => 1, + Nonce::Explicit(..) => 33, + Nonce::Confidential(..) => 33, + } + } + + /// Create from commitment. + pub fn from_commitment(bytes: &[u8]) -> Result { + Ok(Nonce::Confidential( + PublicKey::from_slice(bytes).map_err(secp256k1_zkp::Error::Upstream)?, + )) + } + + /// Check if the object is null. + pub fn is_null(&self) -> bool { + matches!(*self, Nonce::Null) + } + + /// Check if the object is explicit. + pub fn is_explicit(&self) -> bool { + matches!(*self, Nonce::Explicit(_)) + } + + /// Check if the object is confidential. + pub fn is_confidential(&self) -> bool { + matches!(*self, Nonce::Confidential(_)) + } + + /// Returns the explicit inner value. + /// Returns [None] if [`Nonce::is_explicit`] returns false. + pub fn explicit(&self) -> Option<[u8; 32]> { + match *self { + Nonce::Explicit(i) => Some(i), + _ => None, + } + } + + /// Returns the confidential commitment in case of a confidential value. + /// Returns [None] if [`Nonce::is_confidential`] returns false. + pub fn commitment(&self) -> Option { + match *self { + Nonce::Confidential(i) => Some(i), + _ => None, + } + } +} + +impl From for Nonce { + fn from(from: PublicKey) -> Self { + Nonce::Confidential(from) + } +} + +impl fmt::Display for Nonce { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Nonce::Null => f.write_str("null"), + Nonce::Explicit(n) => { + for b in &n { + write!(f, "{:02x}", b)?; + } + Ok(()) + } + Nonce::Confidential(pk) => write!(f, "{:02x}", pk), + } + } +} + +impl Encodable for Nonce { + fn consensus_encode(&self, mut s: S) -> Result { + match *self { + Nonce::Null => 0u8.consensus_encode(s), + Nonce::Explicit(n) => { + 1u8.consensus_encode(&mut s)?; + Ok(1 + n.consensus_encode(&mut s)?) + } + Nonce::Confidential(commitment) => { + s.write_all(&commitment.serialize())?; + Ok(33) + } + } + } +} + +impl Decodable for Nonce { + fn consensus_decode(mut d: D) -> Result { + let prefix = u8::consensus_decode(&mut d)?; + + match prefix { + 0 => Ok(Nonce::Null), + 1 => { + let explicit = Decodable::consensus_decode(&mut d)?; + Ok(Nonce::Explicit(explicit)) + } + p if p == 0x02 || p == 0x03 => { + let mut comm = [0u8; 33]; + comm[0] = p; + d.read_exact(&mut comm[1..])?; + Ok(Nonce::Confidential(PublicKey::from_slice(&comm)?)) + } + p => Err(encode::Error::InvalidConfidentialPrefix(p)), + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for Nonce { + fn serialize(&self, s: S) -> Result { + use serde::ser::SerializeSeq; + + let seq_len = match *self { + Nonce::Null => 1, + Nonce::Explicit(_) | Nonce::Confidential(_) => 2 + }; + let mut seq = s.serialize_seq(Some(seq_len))?; + + match *self { + Nonce::Null => seq.serialize_element(&0u8)?, + Nonce::Explicit(n) => { + seq.serialize_element(&1u8)?; + seq.serialize_element(&n)?; + } + Nonce::Confidential(commitment) => { + seq.serialize_element(&2u8)?; + seq.serialize_element(&commitment)?; + } + } + seq.end() + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Nonce { + fn deserialize>(d: D) -> Result { + use serde::de::{Error, SeqAccess, Visitor}; + struct CommitVisitor; + + impl<'de> Visitor<'de> for CommitVisitor { + type Value = Nonce; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a committed value") + } + + fn visit_seq>(self, mut access: A) -> Result { + let prefix = access.next_element::()?; + match prefix { + Some(0) => Ok(Nonce::Null), + Some(1) => { + match access.next_element()? { + Some(x) => Ok(Nonce::Explicit(x)), + None => Err(A::Error::custom("missing explicit nonce")), + } + } + Some(2) => { + match access.next_element()? { + Some(x) => Ok(Nonce::Confidential(x)), + None => Err(A::Error::custom("missing nonce")), + } + } + _ => Err(A::Error::custom("wrong or missing prefix")) + } + } + } + + d.deserialize_seq(CommitVisitor) + } +} + diff --git a/src/confidential/value.rs b/src/confidential/value.rs new file mode 100644 index 00000000..47483ae4 --- /dev/null +++ b/src/confidential/value.rs @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Confiential Values + +use core::{fmt, ops::{AddAssign, Neg}, str}; +use std::io; + +use secp256k1_zkp::{self, CommitmentSecrets, PedersenCommitment, Generator, Secp256k1, SecretKey, Signing, Tweak, ZERO_TWEAK}; +use secp256k1_zkp::compute_adaptive_blinding_factor; +use secp256k1_zkp::rand::Rng; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::confidential::{AssetBlindingFactor}; +use crate::encode::{self, Decodable, Encodable}; +use crate::issuance::AssetId; + +/// A CT commitment to an amount +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum Value { + /// No value + #[default] + Null, + /// Value is explicitly encoded + Explicit(u64), + /// Value is committed + Confidential(PedersenCommitment), +} + +impl Value { + /// Create value commitment. + pub fn new_confidential( + secp: &Secp256k1, + value: u64, + asset: Generator, + bf: ValueBlindingFactor, + ) -> Self { + Value::Confidential(PedersenCommitment::new(secp, value, bf.0, asset)) + } + + /// Create value commitment from assetID, asset blinding factor, + /// value and value blinding factor + pub fn new_confidential_from_assetid( + secp: &Secp256k1, + value: u64, + asset: AssetId, + v_bf: ValueBlindingFactor, + a_bf: AssetBlindingFactor, + ) -> Self { + let generator = Generator::new_blinded(secp, asset.into_tag(), a_bf.0); + let comm = PedersenCommitment::new(secp, value, v_bf.0, generator); + + Value::Confidential(comm) + } + + /// Serialized length, in bytes + pub fn encoded_length(&self) -> usize { + match *self { + Value::Null => 1, + Value::Explicit(..) => 9, + Value::Confidential(..) => 33, + } + } + + /// Create from commitment. + pub fn from_commitment(bytes: &[u8]) -> Result { + Ok(Value::Confidential(PedersenCommitment::from_slice(bytes)?)) + } + + /// Check if the object is null. + pub fn is_null(&self) -> bool { + matches!(*self, Value::Null) + } + + /// Check if the object is explicit. + pub fn is_explicit(&self) -> bool { + matches!(*self, Value::Explicit(_)) + } + + /// Check if the object is confidential. + pub fn is_confidential(&self) -> bool { + matches!(*self, Value::Confidential(_)) + } + + /// Returns the explicit inner value. + /// Returns [None] if [`Value::is_explicit`] returns false. + pub fn explicit(&self) -> Option { + match *self { + Value::Explicit(i) => Some(i), + _ => None, + } + } + + /// Returns the confidential commitment in case of a confidential value. + /// Returns [None] if [`Value::is_confidential`] returns false. + pub fn commitment(&self) -> Option { + match *self { + Value::Confidential(i) => Some(i), + _ => None, + } + } +} + +impl From for Value { + fn from(from: PedersenCommitment) -> Self { + Value::Confidential(from) + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Value::Null => f.write_str("null"), + Value::Explicit(n) => write!(f, "{}", n), + Value::Confidential(commitment) => write!(f, "{:02x}", commitment), + } + } +} + +impl Encodable for Value { + fn consensus_encode(&self, mut s: S) -> Result { + match *self { + Value::Null => 0u8.consensus_encode(s), + Value::Explicit(n) => { + 1u8.consensus_encode(&mut s)?; + Ok(1 + u64::swap_bytes(n).consensus_encode(&mut s)?) + } + Value::Confidential(commitment) => { + s.write_all(&commitment.serialize())?; + Ok(33) + } + } + } +} + +impl Decodable for Value { + fn consensus_decode(mut d: D) -> Result { + let prefix = u8::consensus_decode(&mut d)?; + + match prefix { + 0 => Ok(Value::Null), + 1 => { + let explicit = u64::swap_bytes(Decodable::consensus_decode(&mut d)?); + Ok(Value::Explicit(explicit)) + } + p if p == 0x08 || p == 0x09 => { + let mut comm = [0u8; 33]; + comm[0] = p; + d.read_exact(&mut comm[1..])?; + Ok(Value::Confidential(PedersenCommitment::from_slice(&comm)?)) + } + p => Err(encode::Error::InvalidConfidentialPrefix(p)), + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for Value { + fn serialize(&self, s: S) -> Result { + use serde::ser::SerializeSeq; + + let seq_len = match *self { + Value::Null => 1, + Value::Explicit(_) | Value::Confidential(_) => 2 + }; + let mut seq = s.serialize_seq(Some(seq_len))?; + + match *self { + Value::Null => seq.serialize_element(&0u8)?, + Value::Explicit(n) => { + seq.serialize_element(&1u8)?; + seq.serialize_element(&u64::swap_bytes(n))?; + } + Value::Confidential(commitment) => { + seq.serialize_element(&2u8)?; + seq.serialize_element(&commitment)?; + } + } + seq.end() + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Value { + fn deserialize>(d: D) -> Result { + use serde::de::{Error, SeqAccess, Visitor}; + struct CommitVisitor; + + impl<'de> Visitor<'de> for CommitVisitor { + type Value = Value; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a committed value") + } + + fn visit_seq>(self, mut access: A) -> Result { + let prefix = access.next_element::()?; + match prefix { + Some(0) => Ok(Value::Null), + Some(1) => { + match access.next_element()? { + Some(x) => Ok(Value::Explicit(u64::swap_bytes(x))), + None => Err(A::Error::custom("missing explicit value")), + } + } + Some(2) => { + match access.next_element()? { + Some(x) => Ok(Value::Confidential(x)), + None => Err(A::Error::custom("missing pedersen commitment")), + } + } + _ => Err(A::Error::custom("wrong or missing prefix")), + } + } + } + + d.deserialize_seq(CommitVisitor) + } +} + +/// Blinding factor used for value commitments. +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub struct ValueBlindingFactor(pub(crate) Tweak); + +impl ValueBlindingFactor { + /// Generate random value blinding factor. + pub fn new(rng: &mut R) -> Self { + ValueBlindingFactor(Tweak::new(rng)) + } + + /// Parse a blinding factor from a 64-character hex string. + #[deprecated(since = "0.27.0", note = "use s.parse() instead")] + pub fn from_hex(s: &str) -> Result { + s.parse() + } + + /// Create the value blinding factor of the last output of a transaction. + pub fn last( + secp: &Secp256k1, + value: u64, + abf: AssetBlindingFactor, + inputs: &[(u64, AssetBlindingFactor, ValueBlindingFactor)], + outputs: &[(u64, AssetBlindingFactor, ValueBlindingFactor)], + ) -> Self { + let set_a = inputs + .iter() + .map(|(value, abf, vbf)| CommitmentSecrets { + value: *value, + value_blinding_factor: vbf.0, + generator_blinding_factor: abf.into_inner(), + }) + .collect::>(); + let set_b = outputs + .iter() + .map(|(value, abf, vbf)| CommitmentSecrets { + value: *value, + value_blinding_factor: vbf.0, + generator_blinding_factor: abf.into_inner(), + }) + .collect::>(); + + ValueBlindingFactor(compute_adaptive_blinding_factor( + secp, value, abf.0, &set_a, &set_b, + )) + } + + /// Create from bytes. + pub fn from_slice(bytes: &[u8]) -> Result { + Ok(ValueBlindingFactor(Tweak::from_slice(bytes)?)) + } + + /// Returns the inner value. + pub fn into_inner(self) -> Tweak { + self.0 + } + + /// Get a unblinded/zero `AssetBlinding` factor + pub fn zero() -> Self { + ValueBlindingFactor(ZERO_TWEAK) + } +} + +impl AddAssign for ValueBlindingFactor { + fn add_assign(&mut self, other: Self) { + if self.0.as_ref() == &[0u8; 32] { + *self = other; + } else if other.0.as_ref() == &[0u8; 32] { + // nothing to do + } else { + // Since libsecp does not expose low level APIs + // for scalar arethematic, we need to abuse secret key + // operations for this + let sk2 = SecretKey::from_slice(self.into_inner().as_ref()).expect("Valid key"); + let sk = SecretKey::from_slice(other.into_inner().as_ref()).expect("Valid key"); + // The only reason that secret key addition can fail + // is when the keys add up to zero since we have already checked + // keys are in valid secret keys + match sk.add_tweak(&sk2.into()) { + Ok(sk_tweaked) => *self = ValueBlindingFactor::from_slice(sk_tweaked.as_ref()).expect("Valid Tweak"), + Err(_) => *self = Self::zero(), + } + } + } +} + +impl Neg for ValueBlindingFactor { + type Output = Self; + + fn neg(self) -> Self::Output { + if self.0.as_ref() == &[0u8; 32] { + self + } else { + let sk = SecretKey::from_slice(self.into_inner().as_ref()).expect("Valid key").negate(); + ValueBlindingFactor::from_slice(sk.as_ref()).expect("Valid Tweak") + } + } +} + +impl core::borrow::Borrow<[u8]> for ValueBlindingFactor { + fn borrow(&self) -> &[u8] { &self.0[..] } +} + +hex::impl_fmt_traits! { + #[display_backward(true)] + impl fmt_traits for ValueBlindingFactor { + const LENGTH: usize = 32; + } +} + +impl str::FromStr for ValueBlindingFactor { + type Err = encode::Error; + + fn from_str(s: &str) -> Result { + let mut slice: [u8; 32] = hex::decode_to_array(s)?; + slice.reverse(); + + let inner = Tweak::from_inner(slice)?; + Ok(ValueBlindingFactor(inner)) + } +} + +#[cfg(feature = "serde")] +impl Serialize for ValueBlindingFactor { + fn serialize(&self, s: S) -> Result { + if s.is_human_readable() { + s.collect_str(&self) + } else { + s.serialize_bytes(&self.0[..]) + } + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for ValueBlindingFactor { + fn deserialize>(d: D) -> Result { + if d.is_human_readable() { + struct HexVisitor; + + impl ::serde::de::Visitor<'_> for HexVisitor { + type Value = ValueBlindingFactor; + + fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + formatter.write_str("an ASCII hex string") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: ::serde::de::Error, + { + if let Ok(hex) = ::std::str::from_utf8(v) { + hex.parse().map_err(E::custom) + } else { + Err(E::invalid_value(::serde::de::Unexpected::Bytes(v), &self)) + } + } + + fn visit_str(self, v: &str) -> Result + where + E: ::serde::de::Error, + { + v.parse().map_err(E::custom) + } + } + + d.deserialize_str(HexVisitor) + } else { + struct BytesVisitor; + + impl ::serde::de::Visitor<'_> for BytesVisitor { + type Value = ValueBlindingFactor; + + fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + formatter.write_str("a bytestring") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: ::serde::de::Error, + { + use core::convert::TryFrom; + + match <[u8; 32]>::try_from(v) { + Ok(ret) => { + let inner = Tweak::from_inner(ret).map_err(E::custom)?; + Ok(ValueBlindingFactor(inner)) + } + Err(_) => Err(E::invalid_length(v.len(), &stringify!($len))), + } + } + } + + d.deserialize_bytes(BytesVisitor) + } + } +} + From 9a4c8b818def605b1448fb4338f8932533c64909 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 30 Jun 2026 15:49:48 +0000 Subject: [PATCH 18/41] run rustfmt on src/confidential/* tree --- rustfmt.toml | 3 +- src/confidential/asset.rs | 70 +++++++---------------- src/confidential/mod.rs | 114 ++++++++++++++------------------------ src/confidential/nonce.rs | 56 +++++++------------ src/confidential/value.rs | 76 ++++++++++--------------- 5 files changed, 112 insertions(+), 207 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index 13332480..09b6229f 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,8 +1,7 @@ ignore = [ "/", "!/src/lib.rs", - "!/src/confidential/range_proof.rs", - "!/src/confidential/surjection_proof.rs", + "!/src/confidential/*.rs", ] hard_tabs = false tab_spaces = 4 diff --git a/src/confidential/asset.rs b/src/confidential/asset.rs index 1b782f7a..7098bf76 100644 --- a/src/confidential/asset.rs +++ b/src/confidential/asset.rs @@ -5,8 +5,8 @@ use core::{fmt, str}; use std::io; -use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; use secp256k1_zkp::rand::Rng; +use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -32,11 +32,7 @@ impl Asset { asset: AssetId, bf: AssetBlindingFactor, ) -> Self { - Asset::Confidential(Generator::new_blinded( - secp, - asset.into_tag(), - bf.into_inner(), - )) + Asset::Confidential(Generator::new_blinded(secp, asset.into_tag(), bf.into_inner())) } /// Serialized length, in bytes @@ -54,19 +50,13 @@ impl Asset { } /// Check if the object is null. - pub fn is_null(&self) -> bool { - matches!(*self, Asset::Null) - } + pub fn is_null(&self) -> bool { matches!(*self, Asset::Null) } /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { - matches!(*self, Asset::Explicit(_)) - } + pub fn is_explicit(&self) -> bool { matches!(*self, Asset::Explicit(_)) } /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { - matches!(*self, Asset::Confidential(_)) - } + pub fn is_confidential(&self) -> bool { matches!(*self, Asset::Confidential(_)) } /// Returns the explicit inner value. /// Returns [None] if [`Asset::is_explicit`] returns false. @@ -91,7 +81,7 @@ impl Asset { /// Returns [`None`] is the asset is [`Asset::Null`] /// Converts a explicit asset into a generator and returns the confidential /// generator as is. - pub fn into_asset_gen ( + pub fn into_asset_gen( self, secp: &Secp256k1, ) -> Option { @@ -99,18 +89,14 @@ impl Asset { // Only error is Null error which is dealt with later // when we have more context information about it. Asset::Null => None, - Asset::Explicit(x) => { - Some(Generator::new_unblinded(secp, x.into_tag())) - } + Asset::Explicit(x) => Some(Generator::new_unblinded(secp, x.into_tag())), Asset::Confidential(gen) => Some(gen), } } } impl From for Asset { - fn from(from: Generator) -> Self { - Asset::Confidential(from) - } + fn from(from: Generator) -> Self { Asset::Confidential(from) } } impl fmt::Display for Asset { @@ -167,7 +153,7 @@ impl Serialize for Asset { let seq_len = match *self { Asset::Null => 1, - Asset::Explicit(_) | Asset::Confidential(_) => 2 + Asset::Explicit(_) | Asset::Confidential(_) => 2, }; let mut seq = s.serialize_seq(Some(seq_len))?; @@ -203,18 +189,14 @@ impl<'de> Deserialize<'de> for Asset { let prefix = access.next_element::()?; match prefix { Some(0) => Ok(Asset::Null), - Some(1) => { - match access.next_element()? { - Some(x) => Ok(Asset::Explicit(x)), - None => Err(A::Error::custom("missing explicit asset")), - } - } - Some(2) => { - match access.next_element()? { - Some(x) => Ok(Asset::Confidential(x)), - None => Err(A::Error::custom("missing generator")), - } - } + Some(1) => match access.next_element()? { + Some(x) => Ok(Asset::Explicit(x)), + None => Err(A::Error::custom("missing explicit asset")), + }, + Some(2) => match access.next_element()? { + Some(x) => Ok(Asset::Confidential(x)), + None => Err(A::Error::custom("missing generator")), + }, _ => Err(A::Error::custom("wrong or missing prefix")), } } @@ -224,22 +206,17 @@ impl<'de> Deserialize<'de> for Asset { } } - /// Blinding factor used for asset commitments. #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct AssetBlindingFactor(pub(crate) Tweak); impl AssetBlindingFactor { /// Generate random asset blinding factor. - pub fn new(rng: &mut R) -> Self { - AssetBlindingFactor(Tweak::new(rng)) - } + pub fn new(rng: &mut R) -> Self { AssetBlindingFactor(Tweak::new(rng)) } /// Parse a blinding factor from a 64-character hex string. #[deprecated(since = "0.27.0", note = "use s.parse() instead")] - pub fn from_hex(s: &str) -> Result { - s.parse() - } + pub fn from_hex(s: &str) -> Result { s.parse() } /// Create from bytes. pub fn from_byte_array(bytes: [u8; 32]) -> Result { @@ -252,14 +229,10 @@ impl AssetBlindingFactor { } /// Returns the inner value. - pub fn into_inner(self) -> Tweak { - self.0 - } + pub fn into_inner(self) -> Tweak { self.0 } /// Get a unblinded/zero `AssetBlinding` factor - pub fn zero() -> Self { - AssetBlindingFactor(ZERO_TWEAK) - } + pub fn zero() -> Self { AssetBlindingFactor(ZERO_TWEAK) } } impl core::borrow::Borrow<[u8]> for AssetBlindingFactor { @@ -359,4 +332,3 @@ impl<'de> Deserialize<'de> for AssetBlindingFactor { } } } - diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index c10a50e7..0c9556a6 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -23,18 +23,17 @@ mod range_proof; mod surjection_proof; mod value; -use secp256k1_zkp; - use core::fmt; -use crate::encode; -use crate::issuance::AssetId; +use secp256k1_zkp; pub use self::asset::{Asset, AssetBlindingFactor}; pub use self::nonce::Nonce; pub use self::range_proof::RangeProof; pub use self::surjection_proof::SurjectionProof; pub use self::value::{Value, ValueBlindingFactor}; +use crate::encode; +use crate::issuance::AssetId; /// Error decoding hexadecimal string into tweak-like value. #[derive(Debug, Clone, PartialEq, Eq)] @@ -60,16 +59,12 @@ impl fmt::Display for TweakHexDecodeError { #[doc(hidden)] impl From for TweakHexDecodeError { - fn from(err: hex::DecodeFixedLengthBytesError) -> Self { - TweakHexDecodeError::InvalidHex(err) - } + fn from(err: hex::DecodeFixedLengthBytesError) -> Self { TweakHexDecodeError::InvalidHex(err) } } #[doc(hidden)] impl From for TweakHexDecodeError { - fn from(err: secp256k1_zkp::Error) -> Self { - TweakHexDecodeError::InvalidTweak(err) - } + fn from(err: secp256k1_zkp::Error) -> Self { TweakHexDecodeError::InvalidTweak(err) } } impl From for encode::Error { @@ -91,17 +86,16 @@ impl std::error::Error for TweakHexDecodeError { } #[cfg(test)] mod tests { - use super::*; - - use crate::encode::Encodable as _; - #[cfg(feature = "serde")] use std::str::FromStr; #[cfg(feature = "serde")] use bincode; - const VALUE_EXPLICIT: [u8; 9] = [ 1, 0, 0, 0, 0, 0, 0, 3, 232 ]; + use super::*; + use crate::encode::Encodable as _; + + const VALUE_EXPLICIT: [u8; 9] = [1, 0, 0, 0, 0, 0, 0, 3, 232]; const VALUE_COMMITMENT1: [u8; 33] = [ 0x08, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, @@ -239,8 +233,8 @@ mod tests { Token::Seq { len: Some(2) }, Token::U8(1), Token::U64(63_601_271_583_539_200), - Token::SeqEnd - ] + Token::SeqEnd, + ], ); let value = Value::from_commitment(&VALUE_COMMITMENT1).unwrap(); @@ -249,11 +243,9 @@ mod tests { &[ Token::Seq { len: Some(2) }, Token::U8(2), - Token::Str( - "080101010101010101010101010101010101010101010101010101010101010101" - ), - Token::SeqEnd - ] + Token::Str("080101010101010101010101010101010101010101010101010101010101010101"), + Token::SeqEnd, + ], ); assert_tokens( &value.compact(), @@ -261,19 +253,12 @@ mod tests { Token::Seq { len: Some(2) }, Token::U8(2), Token::Bytes(&VALUE_COMMITMENT1), - Token::SeqEnd - ] + Token::SeqEnd, + ], ); let value = Value::Null; - assert_tokens( - &value, - &[ - Token::Seq { len: Some(1) }, - Token::U8(0), - Token::SeqEnd - ] - ); + assert_tokens(&value, &[Token::Seq { len: Some(1) }, Token::U8(0), Token::SeqEnd]); } #[cfg(feature = "serde")] @@ -281,34 +266,30 @@ mod tests { fn asset_serde() { use serde_test::{assert_tokens, Configure, Token}; - let asset_id = AssetId::from_str( - "630ed6f9b176af03c0cd3f8aa430f9e7b4d988cf2d0b2f204322488f03b00bf8" - ).unwrap(); + let asset_id = + AssetId::from_str("630ed6f9b176af03c0cd3f8aa430f9e7b4d988cf2d0b2f204322488f03b00bf8") + .unwrap(); let asset = Asset::Explicit(asset_id); assert_tokens( &asset.readable(), &[ Token::Seq { len: Some(2) }, Token::U8(1), - Token::Str( - "630ed6f9b176af03c0cd3f8aa430f9e7b4d988cf2d0b2f204322488f03b00bf8" - ), - Token::SeqEnd - ] + Token::Str("630ed6f9b176af03c0cd3f8aa430f9e7b4d988cf2d0b2f204322488f03b00bf8"), + Token::SeqEnd, + ], ); assert_tokens( &asset.compact(), &[ Token::Seq { len: Some(2) }, Token::U8(1), - Token::Bytes( - &[ - 248, 11, 176, 3, 143, 72, 34, 67, 32, 47, 11, 45, 207, 136, 217, 180, - 231, 249, 48, 164, 138, 63, 205, 192, 3, 175, 118, 177, 249, 214, 14, 99 - ] - ), - Token::SeqEnd - ] + Token::Bytes(&[ + 248, 11, 176, 3, 143, 72, 34, 67, 32, 47, 11, 45, 207, 136, 217, 180, 231, 249, + 48, 164, 138, 63, 205, 192, 3, 175, 118, 177, 249, 214, 14, 99, + ]), + Token::SeqEnd, + ], ); let asset = Asset::from_commitment(&ASSET_COMMITMENT1).unwrap(); @@ -317,11 +298,9 @@ mod tests { &[ Token::Seq { len: Some(2) }, Token::U8(2), - Token::Str( - "0a0101010101010101010101010101010101010101010101010101010101010101" - ), - Token::SeqEnd - ] + Token::Str("0a0101010101010101010101010101010101010101010101010101010101010101"), + Token::SeqEnd, + ], ); assert_tokens( &asset.compact(), @@ -329,23 +308,17 @@ mod tests { Token::Seq { len: Some(2) }, Token::U8(2), Token::Bytes(&ASSET_COMMITMENT1), - Token::SeqEnd - ] + Token::SeqEnd, + ], ); let asset = Asset::Null; - assert_tokens( - &asset, - &[ - Token::Seq { len: Some(1) }, - Token::U8(0), - Token::SeqEnd - ] - ); + assert_tokens(&asset, &[Token::Seq { len: Some(1) }, Token::U8(0), Token::SeqEnd]); } #[cfg(feature = "serde")] #[test] + #[rustfmt::skip] fn nonce_serde() { use serde_test::{assert_tokens, Configure, Token}; @@ -418,9 +391,10 @@ mod tests { #[cfg(feature = "serde")] #[test] fn bf_serde() { - use serde_json; use std::str::FromStr; + use serde_json; + let abf_str = "a5b3d111cdaa5fc111e2723df4caf315864f25fb4610cc737f10d5a55cd4096f"; let abf_str_quoted = format!("\"{}\"", abf_str); let abf_from_serde: AssetBlindingFactor = serde_json::from_str(&abf_str_quoted).unwrap(); @@ -450,14 +424,10 @@ mod tests { fn test_value_bincode_le() { use bincode::Options; let value = Value::Explicit(500); - let bytes = bincode::DefaultOptions::default() - .with_little_endian() - .serialize(&value) - .unwrap(); - let decoded: Value = bincode::DefaultOptions::default() - .with_little_endian() - .deserialize(&bytes) - .unwrap(); + let bytes = + bincode::DefaultOptions::default().with_little_endian().serialize(&value).unwrap(); + let decoded: Value = + bincode::DefaultOptions::default().with_little_endian().deserialize(&bytes).unwrap(); assert_eq!(value, decoded); } } diff --git a/src/confidential/nonce.rs b/src/confidential/nonce.rs index f3174997..02df1742 100644 --- a/src/confidential/nonce.rs +++ b/src/confidential/nonce.rs @@ -5,8 +5,8 @@ use core::fmt; use std::io; +use secp256k1_zkp::rand::{CryptoRng, RngCore}; use secp256k1_zkp::{self, PublicKey, Secp256k1, SecretKey, Signing}; -use secp256k1_zkp::rand::{RngCore, CryptoRng}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -43,7 +43,7 @@ impl Nonce { pub fn with_ephemeral_sk( secp: &Secp256k1, ephemeral_sk: SecretKey, - receiver_blinding_pk: &PublicKey + receiver_blinding_pk: &PublicKey, ) -> (Self, SecretKey) { let sender_pk = PublicKey::from_secret_key(secp, &ephemeral_sk); let shared_secret = Self::make_shared_secret(receiver_blinding_pk, &ephemeral_sk); @@ -53,9 +53,8 @@ impl Nonce { /// Calculate the shared secret. pub fn shared_secret(&self, receiver_blinding_sk: &SecretKey) -> Option { match self { - Nonce::Confidential(sender_pk) => { - Some(Self::make_shared_secret(sender_pk, receiver_blinding_sk)) - } + Nonce::Confidential(sender_pk) => + Some(Self::make_shared_secret(sender_pk, receiver_blinding_sk)), _ => None, } } @@ -68,11 +67,7 @@ impl Nonce { // However, this is more by accident then by design, see here: https://github.com/rust-bitcoin/rust-secp256k1/pull/255#issuecomment-744146282 let mut dh_secret = [0u8; 33]; - dh_secret[0] = if xy.last().unwrap() % 2 == 0 { - 0x02 - } else { - 0x03 - }; + dh_secret[0] = if xy.last().unwrap() % 2 == 0 { 0x02 } else { 0x03 }; dh_secret[1..].copy_from_slice(&xy[0..32]); sha256d::Hash::hash(&dh_secret).to_byte_array() @@ -98,19 +93,13 @@ impl Nonce { } /// Check if the object is null. - pub fn is_null(&self) -> bool { - matches!(*self, Nonce::Null) - } + pub fn is_null(&self) -> bool { matches!(*self, Nonce::Null) } /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { - matches!(*self, Nonce::Explicit(_)) - } + pub fn is_explicit(&self) -> bool { matches!(*self, Nonce::Explicit(_)) } /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { - matches!(*self, Nonce::Confidential(_)) - } + pub fn is_confidential(&self) -> bool { matches!(*self, Nonce::Confidential(_)) } /// Returns the explicit inner value. /// Returns [None] if [`Nonce::is_explicit`] returns false. @@ -132,9 +121,7 @@ impl Nonce { } impl From for Nonce { - fn from(from: PublicKey) -> Self { - Nonce::Confidential(from) - } + fn from(from: PublicKey) -> Self { Nonce::Confidential(from) } } impl fmt::Display for Nonce { @@ -196,7 +183,7 @@ impl Serialize for Nonce { let seq_len = match *self { Nonce::Null => 1, - Nonce::Explicit(_) | Nonce::Confidential(_) => 2 + Nonce::Explicit(_) | Nonce::Confidential(_) => 2, }; let mut seq = s.serialize_seq(Some(seq_len))?; @@ -232,19 +219,15 @@ impl<'de> Deserialize<'de> for Nonce { let prefix = access.next_element::()?; match prefix { Some(0) => Ok(Nonce::Null), - Some(1) => { - match access.next_element()? { - Some(x) => Ok(Nonce::Explicit(x)), - None => Err(A::Error::custom("missing explicit nonce")), - } - } - Some(2) => { - match access.next_element()? { - Some(x) => Ok(Nonce::Confidential(x)), - None => Err(A::Error::custom("missing nonce")), - } - } - _ => Err(A::Error::custom("wrong or missing prefix")) + Some(1) => match access.next_element()? { + Some(x) => Ok(Nonce::Explicit(x)), + None => Err(A::Error::custom("missing explicit nonce")), + }, + Some(2) => match access.next_element()? { + Some(x) => Ok(Nonce::Confidential(x)), + None => Err(A::Error::custom("missing nonce")), + }, + _ => Err(A::Error::custom("wrong or missing prefix")), } } } @@ -252,4 +235,3 @@ impl<'de> Deserialize<'de> for Nonce { d.deserialize_seq(CommitVisitor) } } - diff --git a/src/confidential/value.rs b/src/confidential/value.rs index 47483ae4..2705c557 100644 --- a/src/confidential/value.rs +++ b/src/confidential/value.rs @@ -2,16 +2,19 @@ //! Confiential Values -use core::{fmt, ops::{AddAssign, Neg}, str}; +use core::ops::{AddAssign, Neg}; +use core::{fmt, str}; use std::io; -use secp256k1_zkp::{self, CommitmentSecrets, PedersenCommitment, Generator, Secp256k1, SecretKey, Signing, Tweak, ZERO_TWEAK}; -use secp256k1_zkp::compute_adaptive_blinding_factor; use secp256k1_zkp::rand::Rng; +use secp256k1_zkp::{ + self, compute_adaptive_blinding_factor, CommitmentSecrets, Generator, PedersenCommitment, + Secp256k1, SecretKey, Signing, Tweak, ZERO_TWEAK, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::confidential::{AssetBlindingFactor}; +use crate::confidential::AssetBlindingFactor; use crate::encode::{self, Decodable, Encodable}; use crate::issuance::AssetId; @@ -68,19 +71,13 @@ impl Value { } /// Check if the object is null. - pub fn is_null(&self) -> bool { - matches!(*self, Value::Null) - } + pub fn is_null(&self) -> bool { matches!(*self, Value::Null) } /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { - matches!(*self, Value::Explicit(_)) - } + pub fn is_explicit(&self) -> bool { matches!(*self, Value::Explicit(_)) } /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { - matches!(*self, Value::Confidential(_)) - } + pub fn is_confidential(&self) -> bool { matches!(*self, Value::Confidential(_)) } /// Returns the explicit inner value. /// Returns [None] if [`Value::is_explicit`] returns false. @@ -102,9 +99,7 @@ impl Value { } impl From for Value { - fn from(from: PedersenCommitment) -> Self { - Value::Confidential(from) - } + fn from(from: PedersenCommitment) -> Self { Value::Confidential(from) } } impl fmt::Display for Value { @@ -161,7 +156,7 @@ impl Serialize for Value { let seq_len = match *self { Value::Null => 1, - Value::Explicit(_) | Value::Confidential(_) => 2 + Value::Explicit(_) | Value::Confidential(_) => 2, }; let mut seq = s.serialize_seq(Some(seq_len))?; @@ -197,18 +192,14 @@ impl<'de> Deserialize<'de> for Value { let prefix = access.next_element::()?; match prefix { Some(0) => Ok(Value::Null), - Some(1) => { - match access.next_element()? { - Some(x) => Ok(Value::Explicit(u64::swap_bytes(x))), - None => Err(A::Error::custom("missing explicit value")), - } - } - Some(2) => { - match access.next_element()? { - Some(x) => Ok(Value::Confidential(x)), - None => Err(A::Error::custom("missing pedersen commitment")), - } - } + Some(1) => match access.next_element()? { + Some(x) => Ok(Value::Explicit(u64::swap_bytes(x))), + None => Err(A::Error::custom("missing explicit value")), + }, + Some(2) => match access.next_element()? { + Some(x) => Ok(Value::Confidential(x)), + None => Err(A::Error::custom("missing pedersen commitment")), + }, _ => Err(A::Error::custom("wrong or missing prefix")), } } @@ -224,15 +215,11 @@ pub struct ValueBlindingFactor(pub(crate) Tweak); impl ValueBlindingFactor { /// Generate random value blinding factor. - pub fn new(rng: &mut R) -> Self { - ValueBlindingFactor(Tweak::new(rng)) - } + pub fn new(rng: &mut R) -> Self { ValueBlindingFactor(Tweak::new(rng)) } /// Parse a blinding factor from a 64-character hex string. #[deprecated(since = "0.27.0", note = "use s.parse() instead")] - pub fn from_hex(s: &str) -> Result { - s.parse() - } + pub fn from_hex(s: &str) -> Result { s.parse() } /// Create the value blinding factor of the last output of a transaction. pub fn last( @@ -259,9 +246,7 @@ impl ValueBlindingFactor { }) .collect::>(); - ValueBlindingFactor(compute_adaptive_blinding_factor( - secp, value, abf.0, &set_a, &set_b, - )) + ValueBlindingFactor(compute_adaptive_blinding_factor(secp, value, abf.0, &set_a, &set_b)) } /// Create from bytes. @@ -270,14 +255,10 @@ impl ValueBlindingFactor { } /// Returns the inner value. - pub fn into_inner(self) -> Tweak { - self.0 - } + pub fn into_inner(self) -> Tweak { self.0 } /// Get a unblinded/zero `AssetBlinding` factor - pub fn zero() -> Self { - ValueBlindingFactor(ZERO_TWEAK) - } + pub fn zero() -> Self { ValueBlindingFactor(ZERO_TWEAK) } } impl AddAssign for ValueBlindingFactor { @@ -296,8 +277,10 @@ impl AddAssign for ValueBlindingFactor { // is when the keys add up to zero since we have already checked // keys are in valid secret keys match sk.add_tweak(&sk2.into()) { - Ok(sk_tweaked) => *self = ValueBlindingFactor::from_slice(sk_tweaked.as_ref()).expect("Valid Tweak"), - Err(_) => *self = Self::zero(), + Ok(sk_tweaked) => + *self = + ValueBlindingFactor::from_slice(sk_tweaked.as_ref()).expect("Valid Tweak"), + Err(_) => *self = Self::zero(), } } } @@ -413,4 +396,3 @@ impl<'de> Deserialize<'de> for ValueBlindingFactor { } } } - From 2a4ce2db52572b06ee536455dec738a2541f2c22 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 14:35:39 +0000 Subject: [PATCH 19/41] confidential: enable use_self lint This greatly reduces the diff between the different confidential commitments. --- src/confidential/asset.rs | 82 +++++++++++++-------------- src/confidential/mod.rs | 18 +++--- src/confidential/nonce.rs | 64 ++++++++++----------- src/confidential/range_proof.rs | 2 +- src/confidential/surjection_proof.rs | 2 +- src/confidential/value.rs | 85 ++++++++++++++-------------- 6 files changed, 127 insertions(+), 126 deletions(-) diff --git a/src/confidential/asset.rs b/src/confidential/asset.rs index 7098bf76..dd86ff1c 100644 --- a/src/confidential/asset.rs +++ b/src/confidential/asset.rs @@ -32,53 +32,53 @@ impl Asset { asset: AssetId, bf: AssetBlindingFactor, ) -> Self { - Asset::Confidential(Generator::new_blinded(secp, asset.into_tag(), bf.into_inner())) + Self::Confidential(Generator::new_blinded(secp, asset.into_tag(), bf.into_inner())) } /// Serialized length, in bytes pub fn encoded_length(&self) -> usize { match *self { - Asset::Null => 1, - Asset::Explicit(..) => 33, - Asset::Confidential(..) => 33, + Self::Null => 1, + Self::Explicit(..) => 33, + Self::Confidential(..) => 33, } } /// Create from commitment. pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Asset::Confidential(Generator::from_slice(bytes)?)) + Ok(Self::Confidential(Generator::from_slice(bytes)?)) } /// Check if the object is null. - pub fn is_null(&self) -> bool { matches!(*self, Asset::Null) } + pub fn is_null(&self) -> bool { matches!(*self, Self::Null) } /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { matches!(*self, Asset::Explicit(_)) } + pub fn is_explicit(&self) -> bool { matches!(*self, Self::Explicit(_)) } /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { matches!(*self, Asset::Confidential(_)) } + pub fn is_confidential(&self) -> bool { matches!(*self, Self::Confidential(_)) } /// Returns the explicit inner value. - /// Returns [None] if [`Asset::is_explicit`] returns false. + /// Returns [None] if [`Self::is_explicit`] returns false. pub fn explicit(&self) -> Option { match *self { - Asset::Explicit(i) => Some(i), + Self::Explicit(i) => Some(i), _ => None, } } /// Returns the confidential commitment in case of a confidential value. - /// Returns [None] if [`Asset::is_confidential`] returns false. + /// Returns [None] if [`Self::is_confidential`] returns false. pub fn commitment(&self) -> Option { match *self { - Asset::Confidential(i) => Some(i), + Self::Confidential(i) => Some(i), _ => None, } } /// Internally used function for getting the generator from asset /// Used in the amount verification check - /// Returns [`None`] is the asset is [`Asset::Null`] + /// Returns [`None`] is the asset is [`Self::Null`] /// Converts a explicit asset into a generator and returns the confidential /// generator as is. pub fn into_asset_gen( @@ -88,23 +88,23 @@ impl Asset { match self { // Only error is Null error which is dealt with later // when we have more context information about it. - Asset::Null => None, - Asset::Explicit(x) => Some(Generator::new_unblinded(secp, x.into_tag())), - Asset::Confidential(gen) => Some(gen), + Self::Null => None, + Self::Explicit(x) => Some(Generator::new_unblinded(secp, x.into_tag())), + Self::Confidential(gen) => Some(gen), } } } impl From for Asset { - fn from(from: Generator) -> Self { Asset::Confidential(from) } + fn from(from: Generator) -> Self { Self::Confidential(from) } } impl fmt::Display for Asset { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Asset::Null => f.write_str("null"), - Asset::Explicit(n) => write!(f, "{}", n), - Asset::Confidential(generator) => write!(f, "{:02x}", generator), + Self::Null => f.write_str("null"), + Self::Explicit(n) => write!(f, "{}", n), + Self::Confidential(generator) => write!(f, "{:02x}", generator), } } } @@ -112,12 +112,12 @@ impl fmt::Display for Asset { impl Encodable for Asset { fn consensus_encode(&self, mut s: S) -> Result { match *self { - Asset::Null => 0u8.consensus_encode(s), - Asset::Explicit(n) => { + Self::Null => 0u8.consensus_encode(s), + Self::Explicit(n) => { 1u8.consensus_encode(&mut s)?; Ok(1 + n.consensus_encode(&mut s)?) } - Asset::Confidential(generator) => { + Self::Confidential(generator) => { s.write_all(&generator.serialize())?; Ok(33) } @@ -130,16 +130,16 @@ impl Decodable for Asset { let prefix = u8::consensus_decode(&mut d)?; match prefix { - 0 => Ok(Asset::Null), + 0 => Ok(Self::Null), 1 => { let explicit = Decodable::consensus_decode(&mut d)?; - Ok(Asset::Explicit(explicit)) + Ok(Self::Explicit(explicit)) } p if p == 0x0a || p == 0x0b => { let mut comm = [0u8; 33]; comm[0] = p; d.read_exact(&mut comm[1..])?; - Ok(Asset::Confidential(Generator::from_slice(&comm[..])?)) + Ok(Self::Confidential(Generator::from_slice(&comm[..])?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } @@ -152,18 +152,18 @@ impl Serialize for Asset { use serde::ser::SerializeSeq; let seq_len = match *self { - Asset::Null => 1, - Asset::Explicit(_) | Asset::Confidential(_) => 2, + Self::Null => 1, + Self::Explicit(_) | Self::Confidential(_) => 2, }; let mut seq = s.serialize_seq(Some(seq_len))?; match *self { - Asset::Null => seq.serialize_element(&0u8)?, - Asset::Explicit(n) => { + Self::Null => seq.serialize_element(&0u8)?, + Self::Explicit(n) => { seq.serialize_element(&1u8)?; seq.serialize_element(&n)?; } - Asset::Confidential(commitment) => { + Self::Confidential(commitment) => { seq.serialize_element(&2u8)?; seq.serialize_element(&commitment)?; } @@ -185,16 +185,16 @@ impl<'de> Deserialize<'de> for Asset { f.write_str("a committed value") } - fn visit_seq>(self, mut access: A) -> Result { + fn visit_seq>(self, mut access: A) -> Result { let prefix = access.next_element::()?; match prefix { - Some(0) => Ok(Asset::Null), + Some(0) => Ok(Self::Value::Null), Some(1) => match access.next_element()? { - Some(x) => Ok(Asset::Explicit(x)), + Some(x) => Ok(Self::Value::Explicit(x)), None => Err(A::Error::custom("missing explicit asset")), }, Some(2) => match access.next_element()? { - Some(x) => Ok(Asset::Confidential(x)), + Some(x) => Ok(Self::Value::Confidential(x)), None => Err(A::Error::custom("missing generator")), }, _ => Err(A::Error::custom("wrong or missing prefix")), @@ -212,7 +212,7 @@ pub struct AssetBlindingFactor(pub(crate) Tweak); impl AssetBlindingFactor { /// Generate random asset blinding factor. - pub fn new(rng: &mut R) -> Self { AssetBlindingFactor(Tweak::new(rng)) } + pub fn new(rng: &mut R) -> Self { Self(Tweak::new(rng)) } /// Parse a blinding factor from a 64-character hex string. #[deprecated(since = "0.27.0", note = "use s.parse() instead")] @@ -220,19 +220,19 @@ impl AssetBlindingFactor { /// Create from bytes. pub fn from_byte_array(bytes: [u8; 32]) -> Result { - Ok(AssetBlindingFactor(Tweak::from_inner(bytes)?)) + Ok(Self(Tweak::from_inner(bytes)?)) } /// Create from bytes. pub fn from_slice(bytes: &[u8]) -> Result { - Ok(AssetBlindingFactor(Tweak::from_slice(bytes)?)) + Ok(Self(Tweak::from_slice(bytes)?)) } /// Returns the inner value. pub fn into_inner(self) -> Tweak { self.0 } /// Get a unblinded/zero `AssetBlinding` factor - pub fn zero() -> Self { AssetBlindingFactor(ZERO_TWEAK) } + pub fn zero() -> Self { Self(ZERO_TWEAK) } } impl core::borrow::Borrow<[u8]> for AssetBlindingFactor { @@ -254,7 +254,7 @@ impl str::FromStr for AssetBlindingFactor { slice.reverse(); let inner = Tweak::from_inner(slice)?; - Ok(AssetBlindingFactor(inner)) + Ok(Self(inner)) } } @@ -271,7 +271,7 @@ impl Serialize for AssetBlindingFactor { #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for AssetBlindingFactor { - fn deserialize>(d: D) -> Result { + fn deserialize>(d: D) -> Result { if d.is_human_readable() { struct HexVisitor; diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 0c9556a6..67cf4662 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -17,6 +17,8 @@ //! Structures representing Pedersen commitments of various types //! +#![warn(clippy::use_self)] + mod asset; mod nonce; mod range_proof; @@ -47,10 +49,10 @@ pub enum TweakHexDecodeError { impl fmt::Display for TweakHexDecodeError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - TweakHexDecodeError::InvalidHex(err) => { + Self::InvalidHex(err) => { write!(f, "Invalid hex: {}", err) } - TweakHexDecodeError::InvalidTweak(err) => { + Self::InvalidTweak(err) => { write!(f, "Invalid tweak: {}", err) } } @@ -59,19 +61,19 @@ impl fmt::Display for TweakHexDecodeError { #[doc(hidden)] impl From for TweakHexDecodeError { - fn from(err: hex::DecodeFixedLengthBytesError) -> Self { TweakHexDecodeError::InvalidHex(err) } + fn from(err: hex::DecodeFixedLengthBytesError) -> Self { Self::InvalidHex(err) } } #[doc(hidden)] impl From for TweakHexDecodeError { - fn from(err: secp256k1_zkp::Error) -> Self { TweakHexDecodeError::InvalidTweak(err) } + fn from(err: secp256k1_zkp::Error) -> Self { Self::InvalidTweak(err) } } impl From for encode::Error { fn from(value: TweakHexDecodeError) -> Self { match value { - TweakHexDecodeError::InvalidHex(err) => encode::Error::HexFixedError(err), - TweakHexDecodeError::InvalidTweak(err) => encode::Error::Secp256k1zkp(err), + TweakHexDecodeError::InvalidHex(err) => Self::HexFixedError(err), + TweakHexDecodeError::InvalidTweak(err) => Self::Secp256k1zkp(err), } } } @@ -79,8 +81,8 @@ impl From for encode::Error { impl std::error::Error for TweakHexDecodeError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - TweakHexDecodeError::InvalidHex(err) => Some(err), - TweakHexDecodeError::InvalidTweak(err) => Some(err), + Self::InvalidHex(err) => Some(err), + Self::InvalidTweak(err) => Some(err), } } } diff --git a/src/confidential/nonce.rs b/src/confidential/nonce.rs index 02df1742..41725422 100644 --- a/src/confidential/nonce.rs +++ b/src/confidential/nonce.rs @@ -38,7 +38,7 @@ impl Nonce { Self::with_ephemeral_sk(secp, ephemeral_sk, receiver_blinding_pk) } - /// Similar to [`Nonce::new_confidential`], but with a given `ephemeral_sk` + /// Similar to [`Self::new_confidential`], but with a given `ephemeral_sk` /// instead of sampling it from rng. pub fn with_ephemeral_sk( secp: &Secp256k1, @@ -47,13 +47,13 @@ impl Nonce { ) -> (Self, SecretKey) { let sender_pk = PublicKey::from_secret_key(secp, &ephemeral_sk); let shared_secret = Self::make_shared_secret(receiver_blinding_pk, &ephemeral_sk); - (Nonce::Confidential(sender_pk), shared_secret) + (Self::Confidential(sender_pk), shared_secret) } /// Calculate the shared secret. pub fn shared_secret(&self, receiver_blinding_sk: &SecretKey) -> Option { match self { - Nonce::Confidential(sender_pk) => + Self::Confidential(sender_pk) => Some(Self::make_shared_secret(sender_pk, receiver_blinding_sk)), _ => None, } @@ -79,62 +79,62 @@ impl Nonce { /// Serialized length, in bytes pub fn encoded_length(&self) -> usize { match *self { - Nonce::Null => 1, - Nonce::Explicit(..) => 33, - Nonce::Confidential(..) => 33, + Self::Null => 1, + Self::Explicit(..) => 33, + Self::Confidential(..) => 33, } } /// Create from commitment. pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Nonce::Confidential( + Ok(Self::Confidential( PublicKey::from_slice(bytes).map_err(secp256k1_zkp::Error::Upstream)?, )) } /// Check if the object is null. - pub fn is_null(&self) -> bool { matches!(*self, Nonce::Null) } + pub fn is_null(&self) -> bool { matches!(*self, Self::Null) } /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { matches!(*self, Nonce::Explicit(_)) } + pub fn is_explicit(&self) -> bool { matches!(*self, Self::Explicit(_)) } /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { matches!(*self, Nonce::Confidential(_)) } + pub fn is_confidential(&self) -> bool { matches!(*self, Self::Confidential(_)) } /// Returns the explicit inner value. - /// Returns [None] if [`Nonce::is_explicit`] returns false. + /// Returns [None] if [`Self::is_explicit`] returns false. pub fn explicit(&self) -> Option<[u8; 32]> { match *self { - Nonce::Explicit(i) => Some(i), + Self::Explicit(i) => Some(i), _ => None, } } /// Returns the confidential commitment in case of a confidential value. - /// Returns [None] if [`Nonce::is_confidential`] returns false. + /// Returns [None] if [`Self::is_confidential`] returns false. pub fn commitment(&self) -> Option { match *self { - Nonce::Confidential(i) => Some(i), + Self::Confidential(i) => Some(i), _ => None, } } } impl From for Nonce { - fn from(from: PublicKey) -> Self { Nonce::Confidential(from) } + fn from(from: PublicKey) -> Self { Self::Confidential(from) } } impl fmt::Display for Nonce { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Nonce::Null => f.write_str("null"), - Nonce::Explicit(n) => { + Self::Null => f.write_str("null"), + Self::Explicit(n) => { for b in &n { write!(f, "{:02x}", b)?; } Ok(()) } - Nonce::Confidential(pk) => write!(f, "{:02x}", pk), + Self::Confidential(pk) => write!(f, "{:02x}", pk), } } } @@ -142,12 +142,12 @@ impl fmt::Display for Nonce { impl Encodable for Nonce { fn consensus_encode(&self, mut s: S) -> Result { match *self { - Nonce::Null => 0u8.consensus_encode(s), - Nonce::Explicit(n) => { + Self::Null => 0u8.consensus_encode(s), + Self::Explicit(n) => { 1u8.consensus_encode(&mut s)?; Ok(1 + n.consensus_encode(&mut s)?) } - Nonce::Confidential(commitment) => { + Self::Confidential(commitment) => { s.write_all(&commitment.serialize())?; Ok(33) } @@ -160,16 +160,16 @@ impl Decodable for Nonce { let prefix = u8::consensus_decode(&mut d)?; match prefix { - 0 => Ok(Nonce::Null), + 0 => Ok(Self::Null), 1 => { let explicit = Decodable::consensus_decode(&mut d)?; - Ok(Nonce::Explicit(explicit)) + Ok(Self::Explicit(explicit)) } p if p == 0x02 || p == 0x03 => { let mut comm = [0u8; 33]; comm[0] = p; d.read_exact(&mut comm[1..])?; - Ok(Nonce::Confidential(PublicKey::from_slice(&comm)?)) + Ok(Self::Confidential(PublicKey::from_slice(&comm)?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } @@ -182,18 +182,18 @@ impl Serialize for Nonce { use serde::ser::SerializeSeq; let seq_len = match *self { - Nonce::Null => 1, - Nonce::Explicit(_) | Nonce::Confidential(_) => 2, + Self::Null => 1, + Self::Explicit(_) | Self::Confidential(_) => 2, }; let mut seq = s.serialize_seq(Some(seq_len))?; match *self { - Nonce::Null => seq.serialize_element(&0u8)?, - Nonce::Explicit(n) => { + Self::Null => seq.serialize_element(&0u8)?, + Self::Explicit(n) => { seq.serialize_element(&1u8)?; seq.serialize_element(&n)?; } - Nonce::Confidential(commitment) => { + Self::Confidential(commitment) => { seq.serialize_element(&2u8)?; seq.serialize_element(&commitment)?; } @@ -218,13 +218,13 @@ impl<'de> Deserialize<'de> for Nonce { fn visit_seq>(self, mut access: A) -> Result { let prefix = access.next_element::()?; match prefix { - Some(0) => Ok(Nonce::Null), + Some(0) => Ok(Self::Value::Null), Some(1) => match access.next_element()? { - Some(x) => Ok(Nonce::Explicit(x)), + Some(x) => Ok(Self::Value::Explicit(x)), None => Err(A::Error::custom("missing explicit nonce")), }, Some(2) => match access.next_element()? { - Some(x) => Ok(Nonce::Confidential(x)), + Some(x) => Ok(Self::Value::Confidential(x)), None => Err(A::Error::custom("missing nonce")), }, _ => Err(A::Error::custom("wrong or missing prefix")), diff --git a/src/confidential/range_proof.rs b/src/confidential/range_proof.rs index e9a6c6ab..89894333 100644 --- a/src/confidential/range_proof.rs +++ b/src/confidential/range_proof.rs @@ -38,7 +38,7 @@ impl RangeProof { exp: i32, min_bits: u8, additional_generator: Generator, - ) -> Result { + ) -> Result { secp256k1_zkp::RangeProof::new( secp, min_value, diff --git a/src/confidential/surjection_proof.rs b/src/confidential/surjection_proof.rs index ad3d3e3f..6fb6f8ea 100644 --- a/src/confidential/surjection_proof.rs +++ b/src/confidential/surjection_proof.rs @@ -72,7 +72,7 @@ impl SurjectionProof { abf: AssetBlindingFactor, ) -> Result { let gen = Generator::new_unblinded(secp, asset.into_tag()); - SurjectionProof::new(secp, rng, asset, abf, [(gen, asset.into_tag(), ZERO_TWEAK)]) + Self::new(secp, rng, asset, abf, [(gen, asset.into_tag(), ZERO_TWEAK)]) } /// Verifies a [`SurjectionProof`] proving that an asset matches an exact asset ID. diff --git a/src/confidential/value.rs b/src/confidential/value.rs index 2705c557..2f128281 100644 --- a/src/confidential/value.rs +++ b/src/confidential/value.rs @@ -38,7 +38,7 @@ impl Value { asset: Generator, bf: ValueBlindingFactor, ) -> Self { - Value::Confidential(PedersenCommitment::new(secp, value, bf.0, asset)) + Self::Confidential(PedersenCommitment::new(secp, value, bf.0, asset)) } /// Create value commitment from assetID, asset blinding factor, @@ -53,61 +53,61 @@ impl Value { let generator = Generator::new_blinded(secp, asset.into_tag(), a_bf.0); let comm = PedersenCommitment::new(secp, value, v_bf.0, generator); - Value::Confidential(comm) + Self::Confidential(comm) } /// Serialized length, in bytes pub fn encoded_length(&self) -> usize { match *self { - Value::Null => 1, - Value::Explicit(..) => 9, - Value::Confidential(..) => 33, + Self::Null => 1, + Self::Explicit(..) => 9, + Self::Confidential(..) => 33, } } /// Create from commitment. pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Value::Confidential(PedersenCommitment::from_slice(bytes)?)) + Ok(Self::Confidential(PedersenCommitment::from_slice(bytes)?)) } /// Check if the object is null. - pub fn is_null(&self) -> bool { matches!(*self, Value::Null) } + pub fn is_null(&self) -> bool { matches!(*self, Self::Null) } /// Check if the object is explicit. - pub fn is_explicit(&self) -> bool { matches!(*self, Value::Explicit(_)) } + pub fn is_explicit(&self) -> bool { matches!(*self, Self::Explicit(_)) } /// Check if the object is confidential. - pub fn is_confidential(&self) -> bool { matches!(*self, Value::Confidential(_)) } + pub fn is_confidential(&self) -> bool { matches!(*self, Self::Confidential(_)) } /// Returns the explicit inner value. - /// Returns [None] if [`Value::is_explicit`] returns false. + /// Returns [None] if [`Self::is_explicit`] returns false. pub fn explicit(&self) -> Option { match *self { - Value::Explicit(i) => Some(i), + Self::Explicit(i) => Some(i), _ => None, } } /// Returns the confidential commitment in case of a confidential value. - /// Returns [None] if [`Value::is_confidential`] returns false. + /// Returns [None] if [`Self::is_confidential`] returns false. pub fn commitment(&self) -> Option { match *self { - Value::Confidential(i) => Some(i), + Self::Confidential(i) => Some(i), _ => None, } } } impl From for Value { - fn from(from: PedersenCommitment) -> Self { Value::Confidential(from) } + fn from(from: PedersenCommitment) -> Self { Self::Confidential(from) } } impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Value::Null => f.write_str("null"), - Value::Explicit(n) => write!(f, "{}", n), - Value::Confidential(commitment) => write!(f, "{:02x}", commitment), + Self::Null => f.write_str("null"), + Self::Explicit(n) => write!(f, "{}", n), + Self::Confidential(commitment) => write!(f, "{:02x}", commitment), } } } @@ -115,12 +115,12 @@ impl fmt::Display for Value { impl Encodable for Value { fn consensus_encode(&self, mut s: S) -> Result { match *self { - Value::Null => 0u8.consensus_encode(s), - Value::Explicit(n) => { + Self::Null => 0u8.consensus_encode(s), + Self::Explicit(n) => { 1u8.consensus_encode(&mut s)?; Ok(1 + u64::swap_bytes(n).consensus_encode(&mut s)?) } - Value::Confidential(commitment) => { + Self::Confidential(commitment) => { s.write_all(&commitment.serialize())?; Ok(33) } @@ -129,20 +129,20 @@ impl Encodable for Value { } impl Decodable for Value { - fn consensus_decode(mut d: D) -> Result { + fn consensus_decode(mut d: D) -> Result { let prefix = u8::consensus_decode(&mut d)?; match prefix { - 0 => Ok(Value::Null), + 0 => Ok(Self::Null), 1 => { let explicit = u64::swap_bytes(Decodable::consensus_decode(&mut d)?); - Ok(Value::Explicit(explicit)) + Ok(Self::Explicit(explicit)) } p if p == 0x08 || p == 0x09 => { let mut comm = [0u8; 33]; comm[0] = p; d.read_exact(&mut comm[1..])?; - Ok(Value::Confidential(PedersenCommitment::from_slice(&comm)?)) + Ok(Self::Confidential(PedersenCommitment::from_slice(&comm)?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } @@ -155,18 +155,18 @@ impl Serialize for Value { use serde::ser::SerializeSeq; let seq_len = match *self { - Value::Null => 1, - Value::Explicit(_) | Value::Confidential(_) => 2, + Self::Null => 1, + Self::Explicit(_) | Self::Confidential(_) => 2, }; let mut seq = s.serialize_seq(Some(seq_len))?; match *self { - Value::Null => seq.serialize_element(&0u8)?, - Value::Explicit(n) => { + Self::Null => seq.serialize_element(&0u8)?, + Self::Explicit(n) => { seq.serialize_element(&1u8)?; seq.serialize_element(&u64::swap_bytes(n))?; } - Value::Confidential(commitment) => { + Self::Confidential(commitment) => { seq.serialize_element(&2u8)?; seq.serialize_element(&commitment)?; } @@ -191,13 +191,13 @@ impl<'de> Deserialize<'de> for Value { fn visit_seq>(self, mut access: A) -> Result { let prefix = access.next_element::()?; match prefix { - Some(0) => Ok(Value::Null), + Some(0) => Ok(Self::Value::Null), Some(1) => match access.next_element()? { - Some(x) => Ok(Value::Explicit(u64::swap_bytes(x))), + Some(x) => Ok(Self::Value::Explicit(u64::swap_bytes(x))), None => Err(A::Error::custom("missing explicit value")), }, Some(2) => match access.next_element()? { - Some(x) => Ok(Value::Confidential(x)), + Some(x) => Ok(Self::Value::Confidential(x)), None => Err(A::Error::custom("missing pedersen commitment")), }, _ => Err(A::Error::custom("wrong or missing prefix")), @@ -215,7 +215,7 @@ pub struct ValueBlindingFactor(pub(crate) Tweak); impl ValueBlindingFactor { /// Generate random value blinding factor. - pub fn new(rng: &mut R) -> Self { ValueBlindingFactor(Tweak::new(rng)) } + pub fn new(rng: &mut R) -> Self { Self(Tweak::new(rng)) } /// Parse a blinding factor from a 64-character hex string. #[deprecated(since = "0.27.0", note = "use s.parse() instead")] @@ -226,8 +226,8 @@ impl ValueBlindingFactor { secp: &Secp256k1, value: u64, abf: AssetBlindingFactor, - inputs: &[(u64, AssetBlindingFactor, ValueBlindingFactor)], - outputs: &[(u64, AssetBlindingFactor, ValueBlindingFactor)], + inputs: &[(u64, AssetBlindingFactor, Self)], + outputs: &[(u64, AssetBlindingFactor, Self)], ) -> Self { let set_a = inputs .iter() @@ -246,19 +246,19 @@ impl ValueBlindingFactor { }) .collect::>(); - ValueBlindingFactor(compute_adaptive_blinding_factor(secp, value, abf.0, &set_a, &set_b)) + Self(compute_adaptive_blinding_factor(secp, value, abf.0, &set_a, &set_b)) } /// Create from bytes. pub fn from_slice(bytes: &[u8]) -> Result { - Ok(ValueBlindingFactor(Tweak::from_slice(bytes)?)) + Ok(Self(Tweak::from_slice(bytes)?)) } /// Returns the inner value. pub fn into_inner(self) -> Tweak { self.0 } /// Get a unblinded/zero `AssetBlinding` factor - pub fn zero() -> Self { ValueBlindingFactor(ZERO_TWEAK) } + pub fn zero() -> Self { Self(ZERO_TWEAK) } } impl AddAssign for ValueBlindingFactor { @@ -278,8 +278,7 @@ impl AddAssign for ValueBlindingFactor { // keys are in valid secret keys match sk.add_tweak(&sk2.into()) { Ok(sk_tweaked) => - *self = - ValueBlindingFactor::from_slice(sk_tweaked.as_ref()).expect("Valid Tweak"), + *self = Self::from_slice(sk_tweaked.as_ref()).expect("Valid Tweak"), Err(_) => *self = Self::zero(), } } @@ -294,7 +293,7 @@ impl Neg for ValueBlindingFactor { self } else { let sk = SecretKey::from_slice(self.into_inner().as_ref()).expect("Valid key").negate(); - ValueBlindingFactor::from_slice(sk.as_ref()).expect("Valid Tweak") + Self::from_slice(sk.as_ref()).expect("Valid Tweak") } } } @@ -318,7 +317,7 @@ impl str::FromStr for ValueBlindingFactor { slice.reverse(); let inner = Tweak::from_inner(slice)?; - Ok(ValueBlindingFactor(inner)) + Ok(Self(inner)) } } @@ -335,7 +334,7 @@ impl Serialize for ValueBlindingFactor { #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for ValueBlindingFactor { - fn deserialize>(d: D) -> Result { + fn deserialize>(d: D) -> Result { if d.is_human_readable() { struct HexVisitor; From c92694b53d9694a244ec1c25c2b5e71e14e1c04a Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 14:40:13 +0000 Subject: [PATCH 20/41] confidential: add a bunch of consts and type aliases More work to make the different confidential types more uniform. --- src/confidential/asset.rs | 62 ++++++++++++++++++++---------------- src/confidential/mod.rs | 4 +-- src/confidential/nonce.rs | 46 ++++++++++++++++----------- src/confidential/value.rs | 66 ++++++++++++++++++++++----------------- 4 files changed, 101 insertions(+), 77 deletions(-) diff --git a/src/confidential/asset.rs b/src/confidential/asset.rs index dd86ff1c..3d5d8082 100644 --- a/src/confidential/asset.rs +++ b/src/confidential/asset.rs @@ -13,6 +13,14 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::encode::{self, Decodable, Encodable}; use crate::issuance::AssetId; +type ExplicitInner = AssetId; +type ConfInner = Generator; + +const EXPLICIT_LEN: usize = 32; +const CONFIDENTIAL_LEN: usize = 33; +const CONF_PREFIX_1: u8 = 0x0a; +const CONF_PREFIX_2: u8 = 0x0b; + /// A CT commitment to an asset #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] pub enum Asset { @@ -20,9 +28,9 @@ pub enum Asset { #[default] Null, /// Asset entropy is explicitly encoded - Explicit(AssetId), + Explicit(ExplicitInner), /// Asset is committed - Confidential(Generator), + Confidential(ConfInner), } impl Asset { @@ -30,23 +38,23 @@ impl Asset { pub fn new_confidential( secp: &Secp256k1, asset: AssetId, - bf: AssetBlindingFactor, + bf: BlindingFactor, ) -> Self { - Self::Confidential(Generator::new_blinded(secp, asset.into_tag(), bf.into_inner())) + Self::Confidential(ConfInner::new_blinded(secp, asset.into_tag(), bf.into_inner())) } /// Serialized length, in bytes pub fn encoded_length(&self) -> usize { match *self { Self::Null => 1, - Self::Explicit(..) => 33, - Self::Confidential(..) => 33, + Self::Explicit(..) => 1 + EXPLICIT_LEN, + Self::Confidential(..) => CONFIDENTIAL_LEN, } } /// Create from commitment. pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Self::Confidential(Generator::from_slice(bytes)?)) + Ok(Self::Confidential(ConfInner::from_slice(bytes)?)) } /// Check if the object is null. @@ -60,7 +68,7 @@ impl Asset { /// Returns the explicit inner value. /// Returns [None] if [`Self::is_explicit`] returns false. - pub fn explicit(&self) -> Option { + pub fn explicit(&self) -> Option { match *self { Self::Explicit(i) => Some(i), _ => None, @@ -69,7 +77,7 @@ impl Asset { /// Returns the confidential commitment in case of a confidential value. /// Returns [None] if [`Self::is_confidential`] returns false. - pub fn commitment(&self) -> Option { + pub fn commitment(&self) -> Option { match *self { Self::Confidential(i) => Some(i), _ => None, @@ -84,19 +92,19 @@ impl Asset { pub fn into_asset_gen( self, secp: &Secp256k1, - ) -> Option { + ) -> Option { match self { // Only error is Null error which is dealt with later // when we have more context information about it. Self::Null => None, - Self::Explicit(x) => Some(Generator::new_unblinded(secp, x.into_tag())), + Self::Explicit(x) => Some(ConfInner::new_unblinded(secp, x.into_tag())), Self::Confidential(gen) => Some(gen), } } } -impl From for Asset { - fn from(from: Generator) -> Self { Self::Confidential(from) } +impl From for Asset { + fn from(from: ConfInner) -> Self { Self::Confidential(from) } } impl fmt::Display for Asset { @@ -119,7 +127,7 @@ impl Encodable for Asset { } Self::Confidential(generator) => { s.write_all(&generator.serialize())?; - Ok(33) + Ok(CONFIDENTIAL_LEN) } } } @@ -135,11 +143,11 @@ impl Decodable for Asset { let explicit = Decodable::consensus_decode(&mut d)?; Ok(Self::Explicit(explicit)) } - p if p == 0x0a || p == 0x0b => { - let mut comm = [0u8; 33]; + p if p == CONF_PREFIX_1 || p == CONF_PREFIX_2 => { + let mut comm = [0u8; CONFIDENTIAL_LEN]; comm[0] = p; d.read_exact(&mut comm[1..])?; - Ok(Self::Confidential(Generator::from_slice(&comm[..])?)) + Ok(Self::Confidential(ConfInner::from_slice(&comm[..])?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } @@ -208,9 +216,9 @@ impl<'de> Deserialize<'de> for Asset { /// Blinding factor used for asset commitments. #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] -pub struct AssetBlindingFactor(pub(crate) Tweak); +pub struct BlindingFactor(pub(crate) Tweak); -impl AssetBlindingFactor { +impl BlindingFactor { /// Generate random asset blinding factor. pub fn new(rng: &mut R) -> Self { Self(Tweak::new(rng)) } @@ -235,18 +243,18 @@ impl AssetBlindingFactor { pub fn zero() -> Self { Self(ZERO_TWEAK) } } -impl core::borrow::Borrow<[u8]> for AssetBlindingFactor { +impl core::borrow::Borrow<[u8]> for BlindingFactor { fn borrow(&self) -> &[u8] { &self.0[..] } } hex::impl_fmt_traits! { #[display_backward(true)] - impl fmt_traits for AssetBlindingFactor { + impl fmt_traits for BlindingFactor { const LENGTH: usize = 32; } } -impl str::FromStr for AssetBlindingFactor { +impl str::FromStr for BlindingFactor { type Err = encode::Error; fn from_str(s: &str) -> Result { @@ -259,7 +267,7 @@ impl str::FromStr for AssetBlindingFactor { } #[cfg(feature = "serde")] -impl Serialize for AssetBlindingFactor { +impl Serialize for BlindingFactor { fn serialize(&self, s: S) -> Result { if s.is_human_readable() { s.collect_str(&self) @@ -270,13 +278,13 @@ impl Serialize for AssetBlindingFactor { } #[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for AssetBlindingFactor { +impl<'de> Deserialize<'de> for BlindingFactor { fn deserialize>(d: D) -> Result { if d.is_human_readable() { struct HexVisitor; impl ::serde::de::Visitor<'_> for HexVisitor { - type Value = AssetBlindingFactor; + type Value = BlindingFactor; fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { formatter.write_str("an ASCII hex string") @@ -306,7 +314,7 @@ impl<'de> Deserialize<'de> for AssetBlindingFactor { struct BytesVisitor; impl ::serde::de::Visitor<'_> for BytesVisitor { - type Value = AssetBlindingFactor; + type Value = BlindingFactor; fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { formatter.write_str("a bytestring") @@ -321,7 +329,7 @@ impl<'de> Deserialize<'de> for AssetBlindingFactor { match <[u8; 32]>::try_from(v) { Ok(ret) => { let inner = Tweak::from_inner(ret).map_err(E::custom)?; - Ok(AssetBlindingFactor(inner)) + Ok(BlindingFactor(inner)) } Err(_) => Err(E::invalid_length(v.len(), &stringify!($len))), } diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 67cf4662..b6dbe807 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -29,11 +29,11 @@ use core::fmt; use secp256k1_zkp; -pub use self::asset::{Asset, AssetBlindingFactor}; +pub use self::asset::{Asset, BlindingFactor as AssetBlindingFactor}; pub use self::nonce::Nonce; pub use self::range_proof::RangeProof; pub use self::surjection_proof::SurjectionProof; -pub use self::value::{Value, ValueBlindingFactor}; +pub use self::value::{BlindingFactor as ValueBlindingFactor, Value}; use crate::encode; use crate::issuance::AssetId; diff --git a/src/confidential/nonce.rs b/src/confidential/nonce.rs index 41725422..67f4c09a 100644 --- a/src/confidential/nonce.rs +++ b/src/confidential/nonce.rs @@ -13,6 +13,14 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::encode::{self, Decodable, Encodable}; use crate::hashes::sha256d; +type ExplicitInner = [u8; 32]; +type ConfInner = PublicKey; + +const EXPLICIT_LEN: usize = 32; +const CONFIDENTIAL_LEN: usize = 33; +const CONF_PREFIX_1: u8 = 0x02; +const CONF_PREFIX_2: u8 = 0x03; + /// A CT commitment to an output nonce (i.e. a public key) #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] pub enum Nonce { @@ -22,9 +30,9 @@ pub enum Nonce { /// There should be no such thing as an "explicit nonce", but Elements will deserialize /// such a thing (and insists that its size be 32 bytes). So we stick a 32-byte type here /// that implements all the traits we need. - Explicit([u8; 32]), + Explicit(ExplicitInner), /// Nonce is committed - Confidential(PublicKey), + Confidential(ConfInner), } impl Nonce { @@ -32,7 +40,7 @@ impl Nonce { pub fn new_confidential( rng: &mut R, secp: &Secp256k1, - receiver_blinding_pk: &PublicKey, + receiver_blinding_pk: &ConfInner, ) -> (Self, SecretKey) { let ephemeral_sk = SecretKey::new(rng); Self::with_ephemeral_sk(secp, ephemeral_sk, receiver_blinding_pk) @@ -43,9 +51,9 @@ impl Nonce { pub fn with_ephemeral_sk( secp: &Secp256k1, ephemeral_sk: SecretKey, - receiver_blinding_pk: &PublicKey, + receiver_blinding_pk: &ConfInner, ) -> (Self, SecretKey) { - let sender_pk = PublicKey::from_secret_key(secp, &ephemeral_sk); + let sender_pk = ConfInner::from_secret_key(secp, &ephemeral_sk); let shared_secret = Self::make_shared_secret(receiver_blinding_pk, &ephemeral_sk); (Self::Confidential(sender_pk), shared_secret) } @@ -60,14 +68,14 @@ impl Nonce { } /// Create the shared secret. - fn make_shared_secret(pk: &PublicKey, sk: &SecretKey) -> SecretKey { + fn make_shared_secret(pk: &ConfInner, sk: &SecretKey) -> SecretKey { let xy = secp256k1_zkp::ecdh::shared_secret_point(pk, sk); let shared_secret = { // Yes, what follows is the compressed representation of a Bitcoin public key. // However, this is more by accident then by design, see here: https://github.com/rust-bitcoin/rust-secp256k1/pull/255#issuecomment-744146282 - let mut dh_secret = [0u8; 33]; - dh_secret[0] = if xy.last().unwrap() % 2 == 0 { 0x02 } else { 0x03 }; + let mut dh_secret = [0u8; CONFIDENTIAL_LEN]; + dh_secret[0] = if xy.last().unwrap() % 2 == 0 { CONF_PREFIX_1 } else { CONF_PREFIX_2 }; dh_secret[1..].copy_from_slice(&xy[0..32]); sha256d::Hash::hash(&dh_secret).to_byte_array() @@ -80,15 +88,15 @@ impl Nonce { pub fn encoded_length(&self) -> usize { match *self { Self::Null => 1, - Self::Explicit(..) => 33, - Self::Confidential(..) => 33, + Self::Explicit(..) => 1 + EXPLICIT_LEN, + Self::Confidential(..) => CONFIDENTIAL_LEN, } } /// Create from commitment. pub fn from_commitment(bytes: &[u8]) -> Result { Ok(Self::Confidential( - PublicKey::from_slice(bytes).map_err(secp256k1_zkp::Error::Upstream)?, + ConfInner::from_slice(bytes).map_err(secp256k1_zkp::Error::Upstream)?, )) } @@ -103,7 +111,7 @@ impl Nonce { /// Returns the explicit inner value. /// Returns [None] if [`Self::is_explicit`] returns false. - pub fn explicit(&self) -> Option<[u8; 32]> { + pub fn explicit(&self) -> Option { match *self { Self::Explicit(i) => Some(i), _ => None, @@ -112,7 +120,7 @@ impl Nonce { /// Returns the confidential commitment in case of a confidential value. /// Returns [None] if [`Self::is_confidential`] returns false. - pub fn commitment(&self) -> Option { + pub fn commitment(&self) -> Option { match *self { Self::Confidential(i) => Some(i), _ => None, @@ -120,8 +128,8 @@ impl Nonce { } } -impl From for Nonce { - fn from(from: PublicKey) -> Self { Self::Confidential(from) } +impl From for Nonce { + fn from(from: ConfInner) -> Self { Self::Confidential(from) } } impl fmt::Display for Nonce { @@ -149,7 +157,7 @@ impl Encodable for Nonce { } Self::Confidential(commitment) => { s.write_all(&commitment.serialize())?; - Ok(33) + Ok(CONFIDENTIAL_LEN) } } } @@ -165,11 +173,11 @@ impl Decodable for Nonce { let explicit = Decodable::consensus_decode(&mut d)?; Ok(Self::Explicit(explicit)) } - p if p == 0x02 || p == 0x03 => { - let mut comm = [0u8; 33]; + p if p == CONF_PREFIX_1 || p == CONF_PREFIX_2 => { + let mut comm = [0u8; CONFIDENTIAL_LEN]; comm[0] = p; d.read_exact(&mut comm[1..])?; - Ok(Self::Confidential(PublicKey::from_slice(&comm)?)) + Ok(Self::Confidential(ConfInner::from_slice(&comm)?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } diff --git a/src/confidential/value.rs b/src/confidential/value.rs index 2f128281..9c5313cb 100644 --- a/src/confidential/value.rs +++ b/src/confidential/value.rs @@ -18,6 +18,14 @@ use crate::confidential::AssetBlindingFactor; use crate::encode::{self, Decodable, Encodable}; use crate::issuance::AssetId; +type ExplicitInner = u64; +type ConfInner = PedersenCommitment; + +const EXPLICIT_LEN: usize = 8; +const CONFIDENTIAL_LEN: usize = 33; +const CONF_PREFIX_1: u8 = 0x08; +const CONF_PREFIX_2: u8 = 0x09; + /// A CT commitment to an amount #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] pub enum Value { @@ -25,9 +33,9 @@ pub enum Value { #[default] Null, /// Value is explicitly encoded - Explicit(u64), + Explicit(ExplicitInner), /// Value is committed - Confidential(PedersenCommitment), + Confidential(ConfInner), } impl Value { @@ -36,9 +44,9 @@ impl Value { secp: &Secp256k1, value: u64, asset: Generator, - bf: ValueBlindingFactor, + bf: BlindingFactor, ) -> Self { - Self::Confidential(PedersenCommitment::new(secp, value, bf.0, asset)) + Self::Confidential(ConfInner::new(secp, value, bf.0, asset)) } /// Create value commitment from assetID, asset blinding factor, @@ -47,11 +55,11 @@ impl Value { secp: &Secp256k1, value: u64, asset: AssetId, - v_bf: ValueBlindingFactor, + v_bf: BlindingFactor, a_bf: AssetBlindingFactor, ) -> Self { let generator = Generator::new_blinded(secp, asset.into_tag(), a_bf.0); - let comm = PedersenCommitment::new(secp, value, v_bf.0, generator); + let comm = ConfInner::new(secp, value, v_bf.0, generator); Self::Confidential(comm) } @@ -60,14 +68,14 @@ impl Value { pub fn encoded_length(&self) -> usize { match *self { Self::Null => 1, - Self::Explicit(..) => 9, - Self::Confidential(..) => 33, + Self::Explicit(..) => 1 + EXPLICIT_LEN, + Self::Confidential(..) => CONFIDENTIAL_LEN, } } /// Create from commitment. pub fn from_commitment(bytes: &[u8]) -> Result { - Ok(Self::Confidential(PedersenCommitment::from_slice(bytes)?)) + Ok(Self::Confidential(ConfInner::from_slice(bytes)?)) } /// Check if the object is null. @@ -81,7 +89,7 @@ impl Value { /// Returns the explicit inner value. /// Returns [None] if [`Self::is_explicit`] returns false. - pub fn explicit(&self) -> Option { + pub fn explicit(&self) -> Option { match *self { Self::Explicit(i) => Some(i), _ => None, @@ -90,7 +98,7 @@ impl Value { /// Returns the confidential commitment in case of a confidential value. /// Returns [None] if [`Self::is_confidential`] returns false. - pub fn commitment(&self) -> Option { + pub fn commitment(&self) -> Option { match *self { Self::Confidential(i) => Some(i), _ => None, @@ -98,8 +106,8 @@ impl Value { } } -impl From for Value { - fn from(from: PedersenCommitment) -> Self { Self::Confidential(from) } +impl From for Value { + fn from(from: ConfInner) -> Self { Self::Confidential(from) } } impl fmt::Display for Value { @@ -122,7 +130,7 @@ impl Encodable for Value { } Self::Confidential(commitment) => { s.write_all(&commitment.serialize())?; - Ok(33) + Ok(CONFIDENTIAL_LEN) } } } @@ -138,11 +146,11 @@ impl Decodable for Value { let explicit = u64::swap_bytes(Decodable::consensus_decode(&mut d)?); Ok(Self::Explicit(explicit)) } - p if p == 0x08 || p == 0x09 => { - let mut comm = [0u8; 33]; + p if p == CONF_PREFIX_1 || p == CONF_PREFIX_2 => { + let mut comm = [0u8; CONFIDENTIAL_LEN]; comm[0] = p; d.read_exact(&mut comm[1..])?; - Ok(Self::Confidential(PedersenCommitment::from_slice(&comm)?)) + Ok(Self::Confidential(ConfInner::from_slice(&comm)?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } @@ -211,9 +219,9 @@ impl<'de> Deserialize<'de> for Value { /// Blinding factor used for value commitments. #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] -pub struct ValueBlindingFactor(pub(crate) Tweak); +pub struct BlindingFactor(pub(crate) Tweak); -impl ValueBlindingFactor { +impl BlindingFactor { /// Generate random value blinding factor. pub fn new(rng: &mut R) -> Self { Self(Tweak::new(rng)) } @@ -261,7 +269,7 @@ impl ValueBlindingFactor { pub fn zero() -> Self { Self(ZERO_TWEAK) } } -impl AddAssign for ValueBlindingFactor { +impl AddAssign for BlindingFactor { fn add_assign(&mut self, other: Self) { if self.0.as_ref() == &[0u8; 32] { *self = other; @@ -285,7 +293,7 @@ impl AddAssign for ValueBlindingFactor { } } -impl Neg for ValueBlindingFactor { +impl Neg for BlindingFactor { type Output = Self; fn neg(self) -> Self::Output { @@ -298,18 +306,18 @@ impl Neg for ValueBlindingFactor { } } -impl core::borrow::Borrow<[u8]> for ValueBlindingFactor { +impl core::borrow::Borrow<[u8]> for BlindingFactor { fn borrow(&self) -> &[u8] { &self.0[..] } } hex::impl_fmt_traits! { #[display_backward(true)] - impl fmt_traits for ValueBlindingFactor { + impl fmt_traits for BlindingFactor { const LENGTH: usize = 32; } } -impl str::FromStr for ValueBlindingFactor { +impl str::FromStr for BlindingFactor { type Err = encode::Error; fn from_str(s: &str) -> Result { @@ -322,7 +330,7 @@ impl str::FromStr for ValueBlindingFactor { } #[cfg(feature = "serde")] -impl Serialize for ValueBlindingFactor { +impl Serialize for BlindingFactor { fn serialize(&self, s: S) -> Result { if s.is_human_readable() { s.collect_str(&self) @@ -333,13 +341,13 @@ impl Serialize for ValueBlindingFactor { } #[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for ValueBlindingFactor { +impl<'de> Deserialize<'de> for BlindingFactor { fn deserialize>(d: D) -> Result { if d.is_human_readable() { struct HexVisitor; impl ::serde::de::Visitor<'_> for HexVisitor { - type Value = ValueBlindingFactor; + type Value = BlindingFactor; fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { formatter.write_str("an ASCII hex string") @@ -369,7 +377,7 @@ impl<'de> Deserialize<'de> for ValueBlindingFactor { struct BytesVisitor; impl ::serde::de::Visitor<'_> for BytesVisitor { - type Value = ValueBlindingFactor; + type Value = BlindingFactor; fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { formatter.write_str("a bytestring") @@ -384,7 +392,7 @@ impl<'de> Deserialize<'de> for ValueBlindingFactor { match <[u8; 32]>::try_from(v) { Ok(ret) => { let inner = Tweak::from_inner(ret).map_err(E::custom)?; - Ok(ValueBlindingFactor(inner)) + Ok(BlindingFactor(inner)) } Err(_) => Err(E::invalid_length(v.len(), &stringify!($len))), } From e9ffc698a2d966b24bd82d95957024f4b576b5e5 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 27 Jun 2026 15:10:15 +0000 Subject: [PATCH 21/41] confidential: directly use Read/Write in Encodable/Decodable We eventually want to remove Encodable and Decodable. To help with that, start by cleaning up a few things: * don't use the traits on u8 or u64 * especially don't use the trait on u64 then call `swap_bytes` to deal with the fact that we want a BE encoding --- src/confidential/asset.rs | 26 +++++++++++++++----------- src/confidential/nonce.rs | 26 +++++++++++++++----------- src/confidential/value.rs | 26 +++++++++++++++----------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/confidential/asset.rs b/src/confidential/asset.rs index 3d5d8082..d419ebde 100644 --- a/src/confidential/asset.rs +++ b/src/confidential/asset.rs @@ -120,10 +120,14 @@ impl fmt::Display for Asset { impl Encodable for Asset { fn consensus_encode(&self, mut s: S) -> Result { match *self { - Self::Null => 0u8.consensus_encode(s), + Self::Null => { + s.write_all(&[0u8])?; + Ok(1) + } Self::Explicit(n) => { - 1u8.consensus_encode(&mut s)?; - Ok(1 + n.consensus_encode(&mut s)?) + s.write_all(&[1u8])?; + s.write_all(n.as_byte_array())?; + Ok(1 + EXPLICIT_LEN) } Self::Confidential(generator) => { s.write_all(&generator.serialize())?; @@ -135,19 +139,19 @@ impl Encodable for Asset { impl Decodable for Asset { fn consensus_decode(mut d: D) -> Result { - let prefix = u8::consensus_decode(&mut d)?; + let mut buf = [0u8; CONFIDENTIAL_LEN]; + d.read_exact(&mut buf[0..1])?; - match prefix { + match buf[0] { 0 => Ok(Self::Null), 1 => { - let explicit = Decodable::consensus_decode(&mut d)?; - Ok(Self::Explicit(explicit)) + let mut buf = [0; EXPLICIT_LEN]; + d.read_exact(&mut buf)?; + Ok(Self::Explicit(AssetId::from_byte_array(buf))) } p if p == CONF_PREFIX_1 || p == CONF_PREFIX_2 => { - let mut comm = [0u8; CONFIDENTIAL_LEN]; - comm[0] = p; - d.read_exact(&mut comm[1..])?; - Ok(Self::Confidential(ConfInner::from_slice(&comm[..])?)) + d.read_exact(&mut buf[1..])?; + Ok(Self::Confidential(ConfInner::from_slice(&buf[..])?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } diff --git a/src/confidential/nonce.rs b/src/confidential/nonce.rs index 67f4c09a..0d9ca000 100644 --- a/src/confidential/nonce.rs +++ b/src/confidential/nonce.rs @@ -150,10 +150,14 @@ impl fmt::Display for Nonce { impl Encodable for Nonce { fn consensus_encode(&self, mut s: S) -> Result { match *self { - Self::Null => 0u8.consensus_encode(s), + Self::Null => { + s.write_all(&[0u8])?; + Ok(1) + } Self::Explicit(n) => { - 1u8.consensus_encode(&mut s)?; - Ok(1 + n.consensus_encode(&mut s)?) + s.write_all(&[1u8])?; + s.write_all(&n)?; + Ok(1 + EXPLICIT_LEN) } Self::Confidential(commitment) => { s.write_all(&commitment.serialize())?; @@ -165,19 +169,19 @@ impl Encodable for Nonce { impl Decodable for Nonce { fn consensus_decode(mut d: D) -> Result { - let prefix = u8::consensus_decode(&mut d)?; + let mut buf = [0u8; CONFIDENTIAL_LEN]; + d.read_exact(&mut buf[0..1])?; - match prefix { + match buf[0] { 0 => Ok(Self::Null), 1 => { - let explicit = Decodable::consensus_decode(&mut d)?; - Ok(Self::Explicit(explicit)) + let mut buf = [0; EXPLICIT_LEN]; + d.read_exact(&mut buf)?; + Ok(Self::Explicit(buf)) } p if p == CONF_PREFIX_1 || p == CONF_PREFIX_2 => { - let mut comm = [0u8; CONFIDENTIAL_LEN]; - comm[0] = p; - d.read_exact(&mut comm[1..])?; - Ok(Self::Confidential(ConfInner::from_slice(&comm)?)) + d.read_exact(&mut buf[1..])?; + Ok(Self::Confidential(ConfInner::from_slice(&buf)?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } diff --git a/src/confidential/value.rs b/src/confidential/value.rs index 9c5313cb..a8267148 100644 --- a/src/confidential/value.rs +++ b/src/confidential/value.rs @@ -123,10 +123,14 @@ impl fmt::Display for Value { impl Encodable for Value { fn consensus_encode(&self, mut s: S) -> Result { match *self { - Self::Null => 0u8.consensus_encode(s), + Self::Null => { + s.write_all(&[0u8])?; + Ok(1) + } Self::Explicit(n) => { - 1u8.consensus_encode(&mut s)?; - Ok(1 + u64::swap_bytes(n).consensus_encode(&mut s)?) + s.write_all(&[1u8])?; + s.write_all(&n.to_be_bytes())?; + Ok(1 + EXPLICIT_LEN) } Self::Confidential(commitment) => { s.write_all(&commitment.serialize())?; @@ -138,19 +142,19 @@ impl Encodable for Value { impl Decodable for Value { fn consensus_decode(mut d: D) -> Result { - let prefix = u8::consensus_decode(&mut d)?; + let mut buf = [0u8; CONFIDENTIAL_LEN]; + d.read_exact(&mut buf[0..1])?; - match prefix { + match buf[0] { 0 => Ok(Self::Null), 1 => { - let explicit = u64::swap_bytes(Decodable::consensus_decode(&mut d)?); - Ok(Self::Explicit(explicit)) + let mut buf = [0; EXPLICIT_LEN]; + d.read_exact(&mut buf)?; + Ok(Self::Explicit(u64::from_be_bytes(buf))) } p if p == CONF_PREFIX_1 || p == CONF_PREFIX_2 => { - let mut comm = [0u8; CONFIDENTIAL_LEN]; - comm[0] = p; - d.read_exact(&mut comm[1..])?; - Ok(Self::Confidential(ConfInner::from_slice(&comm)?)) + d.read_exact(&mut buf[1..])?; + Ok(Self::Confidential(ConfInner::from_slice(&buf)?)) } p => Err(encode::Error::InvalidConfidentialPrefix(p)), } From dca9bb001e824649259af0acd4dcff3ff9f02a06 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 24 Jun 2026 15:18:01 +0000 Subject: [PATCH 22/41] update bitcoin dependency to 0.32.102, hashes to 1.1 The 0.32.100 series has a higher MSRV (but still lower than ours) and introduces the bitcoin-consensus-encoding crate. Using this, we can eliminate all the bitcoin-io junk from this crate, eliminate our own Encoder trait, eliminate its passthrough impls on bitcoin types, etc. Since bitcoin depends on `encoding 1.0` but we will need 1.1, also add a separate explict encoding dependency. By updating bitcoin and hashes together, we can eliminate the duplicate `bitcoin-internals` dependency; everything is on 0.6 now. --- Cargo-recent.lock | 72 ++++++++++++++++++++++------------------------- Cargo.toml | 8 +++--- src/lib.rs | 2 ++ 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/Cargo-recent.lock b/Cargo-recent.lock index f83c2120..54c72aa2 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -22,12 +22,11 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "base58ck" -version = "0.1.0" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" dependencies = [ - "bitcoin-internals 0.3.0", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.14.101", ] [[package]] @@ -59,18 +58,18 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.6" +version = "0.32.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8929a18b8e33ea6b3c09297b687baaa71fb1b97353243a3f1029fad5c59c5b" +checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" dependencies = [ "base58ck", "base64 0.21.7", "bech32", - "bitcoin-internals 0.3.0", + "bitcoin-consensus-encoding", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes 0.14.0", - "hex-conservative 0.2.1", + "bitcoin_hashes 0.14.101", + "hex-conservative 0.2.2", "hex_lit", "secp256k1", "serde", @@ -78,33 +77,29 @@ dependencies = [ [[package]] name = "bitcoin-consensus-encoding" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" -dependencies = [ - "bitcoin-internals 0.5.0", -] - -[[package]] -name = "bitcoin-internals" -version = "0.3.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" dependencies = [ + "bitcoin-internals", + "hex-conservative 1.1.0", "serde", ] [[package]] name = "bitcoin-internals" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] [[package]] name = "bitcoin-private" @@ -114,33 +109,33 @@ checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" [[package]] name = "bitcoin-units" -version = "0.1.2" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" dependencies = [ - "bitcoin-internals 0.3.0", + "bitcoin-consensus-encoding", "serde", ] [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", - "hex-conservative 0.2.1", + "hex-conservative 0.2.2", "serde", ] [[package]] name = "bitcoin_hashes" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f70c29ac06e7effa19682e91318deae86bdb46c4fd1bbd0f12fd196ff427ab0" +checksum = "a67800fcf7f3ca52f7796d4466948a424326b225950c79d1e76d0458760bb25d" dependencies = [ "bitcoin-consensus-encoding", - "bitcoin-internals 0.5.0", + "bitcoin-internals", "hex-conservative 1.1.0", "serde", ] @@ -230,8 +225,9 @@ dependencies = [ "bech32", "bincode", "bitcoin", - "bitcoin-internals 0.5.0", - "bitcoin_hashes 1.0.0", + "bitcoin-consensus-encoding", + "bitcoin-internals", + "bitcoin_hashes 1.1.0", "getrandom 0.2.16", "hex-conservative 1.1.0", "rand", @@ -313,9 +309,9 @@ dependencies = [ [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] @@ -512,7 +508,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.14.101", "rand", "secp256k1-sys", "serde", diff --git a/Cargo.toml b/Cargo.toml index 256c2c16..cfe143ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,9 +29,10 @@ base64 = ["bitcoin/base64"] [dependencies] bech32 = "0.11.0" -bitcoin = "0.32.2" -hashes = { package = "bitcoin_hashes", version = "1.0", features = [ "hex" ] } -internals = { package = "bitcoin-internals", version = "0.5" } +bitcoin = { version = "0.32.102", default-features = false, features = [ "encoding", "std" ] } +encoding = { package = "bitcoin-consensus-encoding", version = "1.1.0", default-features = false, features = [ "hex", "std" ] } +hashes = { package = "bitcoin_hashes", version = "1.1", features = [ "hex" ] } +internals = { package = "bitcoin-internals", version = "0.6" } secp256k1-zkp = { version = "0.11.0", features = ["global-context", "hashes"] } # Used for ContractHash::from_json_contract. @@ -191,7 +192,6 @@ zero_sized_map_values = "warn" [package.metadata.rbmt.lint] allowed_duplicates = [ "bitcoin_hashes", - "bitcoin-internals", "hex-conservative", ] diff --git a/src/lib.rs b/src/lib.rs index 18c92cea..21543b1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ /// Re-export of bitcoin crate pub extern crate bitcoin; +/// Re-export of bitcoin-consensus-encoding crate +pub extern crate encoding; /// Re-export of `bitcoin_hashes` crate pub extern crate hashes; /// Re-export of hex crate From 6baad08d736f15aff308340e76425e828bdc1ead Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 16 Jul 2026 13:25:20 +0000 Subject: [PATCH 23/41] hashes: remove deprecated macro This macro provided very little savings in lines of code, and wasn't worth the obfuscation. --- src/taproot.rs | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/taproot.rs b/src/taproot.rs index e6496cca..4bffd3fd 100644 --- a/src/taproot.rs +++ b/src/taproot.rs @@ -20,21 +20,41 @@ use crate::hashes::HashEngine as _; use crate::schnorr::{UntweakedPublicKey, TweakedPublicKey, TapTweak}; use crate::Script; use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; -use hashes::sha256t; +use hashes::{sha256, sha256t}; use secp256k1_zkp::{self, Secp256k1, Scalar}; use crate::encode::Encodable; -hashes::sha256t_tag! { - pub struct TapLeafTag = hash_str("TapLeaf/elements"); + +/// The tag for tapleaves. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapLeafTag; + +/// The tag for tapbranches. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapBranchTag; + +/// The tag for taptweaks. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapTweakTag; + +/// The tag for Taproot sighashes. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapSighashTag; + +impl sha256t::Tag for TapLeafTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapLeaf/elements"); } -hashes::sha256t_tag! { - pub struct TapBranchTag = hash_str("TapBranch/elements"); + +impl sha256t::Tag for TapBranchTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapBranch/elements"); } -hashes::sha256t_tag! { - pub struct TapTweakTag = hash_str("TapTweak/elements"); + +impl sha256t::Tag for TapTweakTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapTweak/elements"); } -hashes::sha256t_tag! { - pub struct TapSighashTag = hash_str("TapSighash/elements"); + +impl sha256t::Tag for TapSighashTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapSighash/elements"); } // Taproot test vectors from BIP-341 state the hashes without any reversing From aacd08149308ed946a7c64790d39a5511d30187e Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 24 Jun 2026 23:25:13 +0000 Subject: [PATCH 24/41] confidential: implement Encode/Decode for confidential commitments This commit introduces the `decoder_state_machine` macro which generates (a lot of) the boilerplate needed to implement the encoding::Decoder trait. This includes internal enums, wrapper types to hide the internal enums, the state machine accounting, impls of std::error::Error, etc etc. I got Claude 4 to write a detailed doccomment, from which I deleted a bunch of fluff. But all the code in the macro was hand-written based on my implementing this decoder state machine a dozen times across the codebase. (My original implementations are thrown away and not included in this PR.) --- src/confidential/asset.rs | 112 +++++++++++++++++ src/confidential/mod.rs | 74 ++++++++++- src/confidential/nonce.rs | 112 +++++++++++++++++ src/confidential/value.rs | 112 +++++++++++++++++ src/internal_macros.rs | 258 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 663 insertions(+), 5 deletions(-) diff --git a/src/confidential/asset.rs b/src/confidential/asset.rs index d419ebde..5636b4bf 100644 --- a/src/confidential/asset.rs +++ b/src/confidential/asset.rs @@ -10,7 +10,9 @@ use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use super::CommitmentEncoder; use crate::encode::{self, Decodable, Encodable}; +use crate::encoding; use crate::issuance::AssetId; type ExplicitInner = AssetId; @@ -18,6 +20,7 @@ type ConfInner = Generator; const EXPLICIT_LEN: usize = 32; const CONFIDENTIAL_LEN: usize = 33; +const CONFIDENTIAL_LEN_LESS_PREFIX: usize = CONFIDENTIAL_LEN - 1; const CONF_PREFIX_1: u8 = 0x0a; const CONF_PREFIX_2: u8 = 0x0b; @@ -344,3 +347,112 @@ impl<'de> Deserialize<'de> for BlindingFactor { } } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`Asset`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(CommitmentEncoder<'e>); +} + +impl encoding::Encode for Asset { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(match *self { + Self::Null => CommitmentEncoder::Null(0), + Self::Explicit(ref id) => CommitmentEncoder::Explicit32(Some(1), id.as_byte_array()), + Self::Confidential(ref gen) => CommitmentEncoder::Explicit33(gen.serialize()), + }) + } +} + +decoder_state_machine! { + /// A decoder for the [`Asset`] type. + pub struct Decoder(enum DecoderInner { + Done(Asset), + Errored, + DecodePrefix { + decoder: encoding::ArrayDecoder<1>, + => transition_decode_prefix(prefix, ...) -> Result { + match prefix { + [0] => Ok(DecoderInner::Done(Asset::Null)), + [1] => { + Ok(DecoderInner::DecodeExplicit { decoder: encoding::ArrayDecoder::default() }) + }, + [prefix @ (CONF_PREFIX_1 | CONF_PREFIX_2)] => { + Ok(DecoderInner::DecodeConfidential { decoder: encoding::ArrayDecoder::default(), prefix }) + }, + [prefix] => Err(DecoderErrorInner::InvalidConfidentialPrefix { prefix }) + } + } + }, + DecodeExplicit { + decoder: encoding::ArrayDecoder + => transition_decode_explicit(bytes, ...) -> Result { + Ok(DecoderInner::Done(Asset::Explicit(AssetId::from_byte_array(bytes)))) + } + }, + DecodeConfidential { + decoder: encoding::ArrayDecoder, + prefix: u8 + => transition_decode_confidential(x_coord, ...) -> Result { + let mut bytes = [0; CONFIDENTIAL_LEN]; + bytes[0] = prefix; + bytes[1..].copy_from_slice(&x_coord); + let gen = ConfInner::from_slice(&bytes) + .map_err(DecoderErrorInner::InvalidCommitment)?; + Ok(DecoderInner::Done(Asset::Confidential(gen))) + } + }, + }); + + /// A decoder error for the [`Asset`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + [macro-inserted decoder variants] + /// Confidential prefix was not one of the two allowable values. + InvalidConfidentialPrefix { + prefix: u8, + }, + /// Malformed confidential commitment. + InvalidCommitment(secp256k1_zkp::Error), + }); +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(_) => f.write_str("failed to decode prefix"), + Inner::DecodeExplicit(_) => f.write_str("failed to decode explicit value"), + Inner::DecodeConfidential(_) => f.write_str("failed to decode confidential value"), + Inner::InvalidConfidentialPrefix { prefix, .. } => { + write!( + f, + "confidential prefix 0x{:02x} was not one of 0, 1, 0x{:02x} or 0x{:02x}", + prefix, CONF_PREFIX_1, CONF_PREFIX_2, + ) + } + Inner::InvalidCommitment(_) => f.write_str("failed to parse confidential commitment"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(ref e) => Some(e), + Inner::DecodeExplicit(ref e) => Some(e), + Inner::DecodeConfidential(ref e) => Some(e), + Inner::InvalidConfidentialPrefix { .. } => None, + Inner::InvalidCommitment(ref e) => Some(e), + } + } +} + +impl Default for Decoder { + fn default() -> Self { + Self(DecoderInner::DecodePrefix { decoder: encoding::ArrayDecoder::default() }) + } +} diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index b6dbe807..335a179d 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -25,17 +25,68 @@ mod range_proof; mod surjection_proof; mod value; -use core::fmt; +use core::{fmt, slice}; use secp256k1_zkp; -pub use self::asset::{Asset, BlindingFactor as AssetBlindingFactor}; -pub use self::nonce::Nonce; +pub use self::asset::{ + Asset, BlindingFactor as AssetBlindingFactor, Decoder as AssetDecoder, + DecoderError as AssetDecoderError, Encoder as AssetEncoder, +}; +pub use self::nonce::{ + Decoder as NonceDecoder, DecoderError as NonceDecoderError, Encoder as NonceEncoder, Nonce, +}; pub use self::range_proof::RangeProof; pub use self::surjection_proof::SurjectionProof; -pub use self::value::{BlindingFactor as ValueBlindingFactor, Value}; -use crate::encode; +pub use self::value::{ + BlindingFactor as ValueBlindingFactor, Decoder as ValueDecoder, + DecoderError as ValueDecoderError, Encoder as ValueEncoder, Value, +}; use crate::issuance::AssetId; +use crate::{encode, encoding}; + +#[derive(Clone, Debug)] +enum CommitmentEncoder<'e> { + Null(u8), + Explicit8(Option, [u8; 8]), + Explicit32(Option, &'e [u8; 32]), + Explicit33([u8; 33]), +} + +impl encoding::Encoder for CommitmentEncoder<'_> { + fn current_chunk(&self) -> &[u8] { + match *self { + Self::Null(ref prefix) => slice::from_ref(prefix), + Self::Explicit8(ref prefix, ref arr) => prefix.as_ref().map_or(arr, slice::from_ref), + Self::Explicit32(ref prefix, arr) => prefix.as_ref().map_or(arr, slice::from_ref), + Self::Explicit33(ref arr) => arr, + } + } + + fn advance(&mut self) -> encoding::EncoderStatus { + match *self { + Self::Explicit8(ref mut prefix @ Some(_), _) + | Self::Explicit32(ref mut prefix @ Some(_), _) => { + *prefix = None; + encoding::EncoderStatus::HasMore + } + _ => encoding::EncoderStatus::Finished, + } + } +} + +impl encoding::ExactSizeEncoder for CommitmentEncoder<'_> { + fn len(&self) -> usize { + match *self { + Self::Null(_) => 1, + Self::Explicit8(Some(_), _) => 9, + Self::Explicit8(None, _) => 8, + Self::Explicit32(Some(_), _) => 33, + Self::Explicit32(None, _) => 32, + Self::Explicit33(_) => 33, + } + } +} /// Error decoding hexadecimal string into tweak-like value. #[derive(Debug, Clone, PartialEq, Eq)] @@ -96,6 +147,7 @@ mod tests { use super::*; use crate::encode::Encodable as _; + use crate::encoding; const VALUE_EXPLICIT: [u8; 9] = [1, 0, 0, 0, 0, 0, 0, 3, 232]; @@ -158,6 +210,9 @@ mod tests { assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); assert_eq!(x, *enc); + + assert_eq!(encoding::encode_to_vec(v), *enc); + assert_eq!(encoding::decode_from_slice(enc), Ok(*v)); } let nonce_encodings = [ @@ -177,6 +232,9 @@ mod tests { assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); assert_eq!(x, *enc); + + assert_eq!(encoding::encode_to_vec(v), *enc); + assert_eq!(encoding::decode_from_slice(enc), Ok(*v)); } let asset_encodings = [ @@ -196,6 +254,9 @@ mod tests { assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); assert_eq!(x, *enc); + + assert_eq!(encoding::encode_to_vec(v), *enc); + assert_eq!(encoding::decode_from_slice(enc), Ok(*v)); } } @@ -207,6 +268,7 @@ mod tests { assert_eq!(x, Value::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Value::from_commitment(&commitment[..]).is_err()); + assert_eq!(encoding::encode_to_vec(&x), VALUE_COMMITMENT1); let x = Asset::from_commitment(&ASSET_COMMITMENT1).unwrap(); let commitment = x.commitment().unwrap(); @@ -214,6 +276,7 @@ mod tests { assert_eq!(x, Asset::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Asset::from_commitment(&commitment[..]).is_err()); + assert_eq!(encoding::encode_to_vec(&x), ASSET_COMMITMENT1); let x = Nonce::from_commitment(&NONCE_COMMITMENT1).unwrap(); let commitment = x.commitment().unwrap(); @@ -221,6 +284,7 @@ mod tests { assert_eq!(x, Nonce::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Nonce::from_commitment(&commitment[..]).is_err()); + assert_eq!(encoding::encode_to_vec(&x), NONCE_COMMITMENT1); } #[cfg(feature = "serde")] diff --git a/src/confidential/nonce.rs b/src/confidential/nonce.rs index 0d9ca000..12310e0c 100644 --- a/src/confidential/nonce.rs +++ b/src/confidential/nonce.rs @@ -10,7 +10,9 @@ use secp256k1_zkp::{self, PublicKey, Secp256k1, SecretKey, Signing}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use super::CommitmentEncoder; use crate::encode::{self, Decodable, Encodable}; +use crate::encoding; use crate::hashes::sha256d; type ExplicitInner = [u8; 32]; @@ -18,6 +20,7 @@ type ConfInner = PublicKey; const EXPLICIT_LEN: usize = 32; const CONFIDENTIAL_LEN: usize = 33; +const CONFIDENTIAL_LEN_LESS_PREFIX: usize = CONFIDENTIAL_LEN - 1; const CONF_PREFIX_1: u8 = 0x02; const CONF_PREFIX_2: u8 = 0x03; @@ -247,3 +250,112 @@ impl<'de> Deserialize<'de> for Nonce { d.deserialize_seq(CommitVisitor) } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`Nonce`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(CommitmentEncoder<'e>); +} + +impl encoding::Encode for Nonce { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(match *self { + Self::Null => CommitmentEncoder::Null(0), + Self::Explicit(ref id) => CommitmentEncoder::Explicit32(Some(1), id), + Self::Confidential(ref gen) => CommitmentEncoder::Explicit33(gen.serialize()), + }) + } +} + +decoder_state_machine! { + /// A decoder for the [`Nonce`] type. + pub struct Decoder(enum DecoderInner { + Done(Nonce), + Errored, + DecodePrefix { + decoder: encoding::ArrayDecoder<1>, + => transition_decode_prefix(prefix, ...) -> Result { + match prefix { + [0] => Ok(DecoderInner::Done(Nonce::Null)), + [1] => { + Ok(DecoderInner::DecodeExplicit { decoder: encoding::ArrayDecoder::default() }) + }, + [prefix @ (CONF_PREFIX_1 | CONF_PREFIX_2)] => { + Ok(DecoderInner::DecodeConfidential { decoder: encoding::ArrayDecoder::default(), prefix }) + }, + [prefix] => Err(DecoderErrorInner::InvalidConfidentialPrefix { prefix }) + } + } + }, + DecodeExplicit { + decoder: encoding::ArrayDecoder + => transition_decode_explicit(bytes, ...) -> Result { + Ok(DecoderInner::Done(Nonce::Explicit(bytes))) + } + }, + DecodeConfidential { + decoder: encoding::ArrayDecoder, + prefix: u8 + => transition_decode_confidential(x_coord, ...) -> Result { + let mut bytes = [0; CONFIDENTIAL_LEN]; + bytes[0] = prefix; + bytes[1..].copy_from_slice(&x_coord); + let gen = ConfInner::from_slice(&bytes) + .map_err(DecoderErrorInner::InvalidCommitment)?; + Ok(DecoderInner::Done(Nonce::Confidential(gen))) + } + }, + }); + + /// A decoder error for the [`Nonce`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + [macro-inserted decoder variants] + /// Confidential prefix was not one of the two allowable values. + InvalidConfidentialPrefix { + prefix: u8, + }, + /// Malformed confidential commitment. + InvalidCommitment(bitcoin::secp256k1::Error), + }); +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(_) => f.write_str("failed to decode prefix"), + Inner::DecodeExplicit(_) => f.write_str("failed to decode explicit value"), + Inner::DecodeConfidential(_) => f.write_str("failed to decode confidential value"), + Inner::InvalidConfidentialPrefix { prefix, .. } => { + write!( + f, + "confidential prefix 0x{:02x} was not one of 0, 1, 0x{:02x} or 0x{:02x}", + prefix, CONF_PREFIX_1, CONF_PREFIX_2, + ) + } + Inner::InvalidCommitment(_) => f.write_str("failed to parse confidential commitment"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(ref e) => Some(e), + Inner::DecodeExplicit(ref e) => Some(e), + Inner::DecodeConfidential(ref e) => Some(e), + Inner::InvalidConfidentialPrefix { .. } => None, + Inner::InvalidCommitment(ref e) => Some(e), + } + } +} + +impl Default for Decoder { + fn default() -> Self { + Self(DecoderInner::DecodePrefix { decoder: encoding::ArrayDecoder::default() }) + } +} diff --git a/src/confidential/value.rs b/src/confidential/value.rs index a8267148..930e20c3 100644 --- a/src/confidential/value.rs +++ b/src/confidential/value.rs @@ -14,8 +14,10 @@ use secp256k1_zkp::{ #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use super::CommitmentEncoder; use crate::confidential::AssetBlindingFactor; use crate::encode::{self, Decodable, Encodable}; +use crate::encoding; use crate::issuance::AssetId; type ExplicitInner = u64; @@ -23,6 +25,7 @@ type ConfInner = PedersenCommitment; const EXPLICIT_LEN: usize = 8; const CONFIDENTIAL_LEN: usize = 33; +const CONFIDENTIAL_LEN_LESS_PREFIX: usize = CONFIDENTIAL_LEN - 1; const CONF_PREFIX_1: u8 = 0x08; const CONF_PREFIX_2: u8 = 0x09; @@ -407,3 +410,112 @@ impl<'de> Deserialize<'de> for BlindingFactor { } } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`Asset`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(CommitmentEncoder<'e>); +} + +impl encoding::Encode for Value { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(match *self { + Self::Null => CommitmentEncoder::Null(0), + Self::Explicit(ref id) => CommitmentEncoder::Explicit8(Some(1), id.to_be_bytes()), + Self::Confidential(ref gen) => CommitmentEncoder::Explicit33(gen.serialize()), + }) + } +} + +decoder_state_machine! { + /// A decoder for the [`Value`] type. + pub struct Decoder(enum DecoderInner { + Done(Value), + Errored, + DecodePrefix { + decoder: encoding::ArrayDecoder<1>, + => transition_decode_prefix(prefix, ...) -> Result { + match prefix { + [0] => Ok(DecoderInner::Done(Value::Null)), + [1] => { + Ok(DecoderInner::DecodeExplicit { decoder: encoding::ArrayDecoder::default() }) + }, + [prefix @ (CONF_PREFIX_1 | CONF_PREFIX_2)] => { + Ok(DecoderInner::DecodeConfidential { decoder: encoding::ArrayDecoder::default(), prefix }) + }, + [prefix] => Err(DecoderErrorInner::InvalidConfidentialPrefix { prefix }) + } + } + }, + DecodeExplicit { + decoder: encoding::ArrayDecoder + => transition_decode_explicit(bytes, ...) -> Result { + Ok(DecoderInner::Done(Value::Explicit(u64::from_be_bytes(bytes)))) + } + }, + DecodeConfidential { + decoder: encoding::ArrayDecoder, + prefix: u8 + => transition_decode_confidential(x_coord, ...) -> Result { + let mut bytes = [0; CONFIDENTIAL_LEN]; + bytes[0] = prefix; + bytes[1..].copy_from_slice(&x_coord); + let gen = ConfInner::from_slice(&bytes) + .map_err(DecoderErrorInner::InvalidCommitment)?; + Ok(DecoderInner::Done(Value::Confidential(gen))) + } + }, + }); + + /// A decoder error for the [`Value`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + [macro-inserted decoder variants] + /// Confidential prefix was not one of the two allowable values. + InvalidConfidentialPrefix { + prefix: u8, + }, + /// Malformed confidential commitment. + InvalidCommitment(secp256k1_zkp::Error), + }); +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(_) => f.write_str("failed to decode prefix"), + Inner::DecodeExplicit(_) => f.write_str("failed to decode explicit value"), + Inner::DecodeConfidential(_) => f.write_str("failed to decode confidential value"), + Inner::InvalidConfidentialPrefix { prefix, .. } => { + write!( + f, + "confidential prefix 0x{:02x} was not one of 0, 1, 0x{:02x} or 0x{:02x}", + prefix, CONF_PREFIX_1, CONF_PREFIX_2, + ) + } + Inner::InvalidCommitment(_) => f.write_str("failed to parse confidential commitment"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(ref e) => Some(e), + Inner::DecodeExplicit(ref e) => Some(e), + Inner::DecodeConfidential(ref e) => Some(e), + Inner::InvalidConfidentialPrefix { .. } => None, + Inner::InvalidCommitment(ref e) => Some(e), + } + } +} + +impl Default for Decoder { + fn default() -> Self { + Self(DecoderInner::DecodePrefix { decoder: encoding::ArrayDecoder::default() }) + } +} diff --git a/src/internal_macros.rs b/src/internal_macros.rs index 3e2b7431..b9cf0c19 100644 --- a/src/internal_macros.rs +++ b/src/internal_macros.rs @@ -527,3 +527,261 @@ macro_rules! hex_deserialize( macro_rules! hex_script( ($e:expr) => (crate::Script::from_hex_no_prefix($e).expect("hex decoding")) ); + +/// Generates a state machine decoder satisfying the [`crate::encoding::Decoder`] trait. +/// +/// This macro creates a decoder that processes input bytes through multiple states, +/// transitioning between states based on decoded data. It automatically generates +/// both the decoder struct and associated error types. +/// +/// # Syntax +/// +/// ```ignore +/// decoder_state_machine! { +/// /// Documentation for the decoder struct +/// pub struct DecoderName(enum InnerEnumName { +/// Done(TargetType), +/// Errored, +/// StateName { +/// decoder: DecoderType, +/// field1: FieldType1, +/// field2: FieldType2, +/// => transition_function_name(output, ...) -> Result { +/// // Transition logic that returns Ok(NextState) or Err(error) +/// } +/// }, +/// // ... more states +/// }); +/// +/// /// Documentation for the error struct +/// pub struct ErrorName(enum InnerErrorName { +/// [macro-inserted decoder variants] +/// CustomError1(ErrorType1), +/// CustomError2 { field: ErrorType2 }, +/// // ... custom error variants +/// }); +/// } +/// ``` +/// +/// # Generated Code +/// +/// The macro generates: +/// - A public decoder struct wrapping a private enum +/// - A private enum with states, Done, and Errored variants +/// - Transition functions for each state +/// - A public error struct wrapping a private error enum +/// - A private error enum with decoder errors and custom variants +/// - `Decoder` trait implementation with `push_bytes`, `end`, and `read_limit` methods +/// - `Decode` impl for the target type +/// +/// The macro does **not** generate a `fmt::Display` or `std::error::Error` impl +/// for `ErrorName`, so the user must implement these outside of the macro. +/// +/// The variants that the macro inserts into `ErrorName` have the same names +/// as the variants on `InnerEnumName`, and they contain a single value of +/// type `::Error`. +/// +/// # State Structure +/// +/// Each state must have: +/// - `decoder: SomeDecoderType` - The decoder for this state's data +/// - Optional additional fields to carry state between transitions +/// - A transition function that processes the decoder's output +/// +/// # Transition Functions +/// +/// Transition functions: +/// - Take the decoder's output as first parameter +/// - Take any additional state fields as subsequent parameters +/// - Return `Result` +/// - Are called automatically when the decoder completes +/// +/// # Error Handling +/// +/// The macro automatically: +/// - Creates error variants for each decoder type +/// - Maps decoder errors to the appropriate variant +/// +/// # Example Usage +/// +/// ```ignore +/// decoder_state_machine! { +/// /// Decodes a length-prefixed string +/// pub struct StringDecoder(enum StringDecoderInner { +/// Done(String), +/// Errored, +/// ReadLength { +/// decoder: encoding::ArrayDecoder<4> +/// => transition_read_length(length_bytes, ...) -> Result { +/// let length = u32::from_be_bytes(length_bytes) as usize; +/// if length > MAX_STRING_LENGTH { +/// return Err(StringDecoderErrorInner::StringTooLong { length }); +/// } +/// Ok(StringDecoderInner::ReadData { +/// decoder: encoding::VecDecoder::new(length), +/// expected_length: length +/// }) +/// } +/// }, +/// ReadData { +/// decoder: encoding::VecDecoder, +/// expected_length: usize +/// => transition_read_data(data, expected_length, ...) -> Result { +/// if data.len() != expected_length { +/// return Err(StringDecoderErrorInner::LengthMismatch); +/// } +/// let string = String::from_utf8(data) +/// .map_err(StringDecoderErrorInner::InvalidUtf8)?; +/// Ok(StringDecoderInner::Done(string)) +/// } +/// }, +/// }); +/// +/// /// Errors that can occur during string decoding +/// #[derive(Debug)] +/// pub struct StringDecoderError(enum StringDecoderErrorInner { +/// [macro-inserted decoder variants] +/// StringTooLong { length: usize }, +/// LengthMismatch, +/// InvalidUtf8(std::string::FromUtf8Error), +/// }); +/// } +/// ``` +macro_rules! decoder_state_machine { + ( + $(#[$($struct_attr:tt)*])* + pub struct $outer_ty:ident(enum $inner_ty:ident { + Done($target_ty:ty), + Errored, + $( + $variant:ident { + decoder: $decoder_ty:ty$(,)? + $(, $field:ident: $field_ty:ty)* + => $transition_fn:ident($output:ident, ...) -> Result { + $($transition_fn_inner:tt)* + } + }, + )* + }); + + $(#[$($error_struct_attr:tt)*])* + pub struct $error_ty:ident(enum $inner_error_ty:ident { + [macro-inserted decoder variants] + $($extra_variants:tt)* + }); + ) => { + $(#[$($struct_attr)*])* + pub struct $outer_ty($inner_ty); + + #[allow(clippy::large_enum_variant)] + enum $inner_ty { + $($variant { + decoder: $decoder_ty + $(, $field: $field_ty)* + },)* + Done($target_ty), + Errored, + } + + impl $inner_ty { + $( + #[inline] + #[allow(clippy::unnecessary_wraps)] // returns a Result even if it never returns Err + fn $transition_fn( + $output: <$decoder_ty as $crate::encoding::Decoder>::Output + $(, $field: $field_ty)* + ) -> Result { + $($transition_fn_inner)* + } + )* + } + + $(#[$($error_struct_attr)*])* + pub struct $error_ty($inner_error_ty); + + $(#[$($error_struct_attr)*])* + enum $inner_error_ty { + $( + $variant(<$decoder_ty as $crate::encoding::Decoder>::Error), + )* + $($extra_variants)* + } + + impl $crate::encoding::Decoder for $outer_ty { + type Output = $target_ty; + type Error = $error_ty; + + fn push_bytes( + &mut self, + bytes: &mut &[u8], + ) -> Result<$crate::encoding::DecoderStatus, Self::Error> { + use $inner_ty as Inner; + loop { + match core::mem::replace(&mut self.0, Inner::Errored) { + $( + Inner::$variant { mut decoder $(, $field)* } => { + if decoder + .push_bytes(bytes) + .map_err($inner_error_ty::$variant) + .map_err($error_ty)? + .needs_more() + { + self.0 = Inner::$variant { decoder $(, $field)* }; + return Ok($crate::encoding::DecoderStatus::NeedsMore); + } + + let output = decoder.end() + .map_err($inner_error_ty::$variant) + .map_err($error_ty)?; + self.0 = $inner_ty::$transition_fn(output $(, $field)*) + .map_err($error_ty)?; + } + )* + Inner::Done(out) => { + self.0 = Inner::Done(out); + return Ok($crate::encoding::DecoderStatus::Ready); + } + Inner::Errored => panic!("called push_bytes() on an errored decoder"), + + } + } + } + + fn end(self) -> Result { + use $inner_ty as Inner; + match self.0 { + $( + Inner::$variant { decoder, .. } => { + decoder.end() + .map_err($inner_error_ty::$variant) + .map_err($error_ty)?; + // This unreachable! can be hit by badly behaved decoders where push_bytes() + // returns DecoderStatus::NeedMore, but end() returns Ok. In general, end() + // should return Ok only if push_bytes has returned DecoderStatus::Ready. + unreachable!( + "end() succeeded on decoder for {} state, but we did not leave that state", + stringify!($variant), + ) + } + )* + Inner::Done(out) => Ok(out), + Inner::Errored => panic!("called end() on an errored decoder"), + } + } + + fn read_limit(&self) -> usize { + use $inner_ty as Inner; + match self.0 { + $( + Inner::$variant { ref decoder, .. } => decoder.read_limit(), + )* + Inner::Done(_) | Inner::Errored => 0, + } + } + } + + impl $crate::encoding::Decode for $target_ty { + type Decoder = $outer_ty; + } + }; +} From c7e16cdc69eee344d37d3cdf29a5497baec343ab Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 15:45:36 +0000 Subject: [PATCH 25/41] confidential: implement Encode and Decode for rangeproof/surjectionproof Introduces the decoder_newtype! macro. As with the other macro, this one was hand-written based on multiple examples of hand-written code, but I let Claude write the doccomment (which I made some minor touchups to). There is a similarly-named macro in rust-bitcoin `primitives` but this is *not* that macro. A later PR will elaborate this macro, but this is all we need for now. --- src/confidential/mod.rs | 65 ++++++++++- src/confidential/range_proof.rs | 56 +++++++++- src/confidential/surjection_proof.rs | 56 +++++++++- src/internal_macros.rs | 155 +++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 4 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 335a179d..22fe9542 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -36,8 +36,14 @@ pub use self::asset::{ pub use self::nonce::{ Decoder as NonceDecoder, DecoderError as NonceDecoderError, Encoder as NonceEncoder, Nonce, }; -pub use self::range_proof::RangeProof; -pub use self::surjection_proof::SurjectionProof; +pub use self::range_proof::{ + Decoder as RangeProofDecoder, DecoderError as RangeProofDecoderError, + Encoder as RangeProofEncoder, RangeProof, +}; +pub use self::surjection_proof::{ + Decoder as SurjectionProofDecoder, DecoderError as SurjectionProofDecoderError, + Encoder as SurjectionProofEncoder, SurjectionProof, +}; pub use self::value::{ BlindingFactor as ValueBlindingFactor, Decoder as ValueDecoder, DecoderError as ValueDecoderError, Encoder as ValueEncoder, Value, @@ -88,6 +94,52 @@ impl encoding::ExactSizeEncoder for CommitmentEncoder<'_> { } } +/// Because the rust-secp256k1-zkp proof types have no `as_bytes()` method, we need +/// to serialize them to a byte vector before encoding them. +/// +/// This encoder accomplishes that -- this situation never happens in rust-bitcoin +/// so there is no "owned bytes encoder" shipped with bitcoin-consensus-encoding. +#[derive(Clone, Debug)] +struct PrefixedByteVecEncoder { + prefix_encoder: Option, + data: Vec, +} + +impl PrefixedByteVecEncoder { + pub fn new(data: Vec) -> Self { + Self { prefix_encoder: Some(encoding::CompactSizeEncoder::new(data.len())), data } + } +} + +impl encoding::Encoder for PrefixedByteVecEncoder { + fn current_chunk(&self) -> &[u8] { + if let Some(ref enc) = self.prefix_encoder { + return enc.current_chunk(); + } + &self.data + } + + fn advance(&mut self) -> encoding::EncoderStatus { + if let Some(ref mut enc) = self.prefix_encoder { + if enc.advance().has_finished() { + self.prefix_encoder = None; + if self.data.is_empty() { + return encoding::EncoderStatus::Finished; + } + } + encoding::EncoderStatus::HasMore + } else { + encoding::EncoderStatus::Finished + } + } +} + +impl encoding::ExactSizeEncoder for PrefixedByteVecEncoder { + fn len(&self) -> usize { + self.prefix_encoder.as_ref().map_or(0, encoding::CompactSizeEncoder::len) + self.data.len() + } +} + /// Error decoding hexadecimal string into tweak-like value. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TweakHexDecodeError { @@ -191,6 +243,15 @@ mod tests { 1, 1, 1, 1, ]; + #[test] + fn prefixed_byte_encoder() { + assert_eq!(encoding::drain_to_vec(&mut PrefixedByteVecEncoder::new(vec![])), [0]); + assert_eq!( + encoding::drain_to_vec(&mut PrefixedByteVecEncoder::new(vec![1, 2, 3])), + [3, 1, 2, 3] + ); + } + #[test] fn encode_length() { let val_encodings = [ diff --git a/src/confidential/range_proof.rs b/src/confidential/range_proof.rs index 89894333..441b8c4f 100644 --- a/src/confidential/range_proof.rs +++ b/src/confidential/range_proof.rs @@ -3,6 +3,7 @@ //! Range Proofs use core::convert::TryInto; +use core::fmt; use std::io; use secp256k1_zkp::rand::{CryptoRng, RngCore}; @@ -11,7 +12,7 @@ use secp256k1_zkp::{self, Generator, PedersenCommitment, Secp256k1, SecretKey, S use serde::{Deserializer, Serializer}; use crate::confidential::ValueBlindingFactor; -use crate::encode; +use crate::{encode, encoding}; /// A range proof, which represents a proof that a confidential value lies within /// some range (typically `[0, 2^64)`). @@ -190,3 +191,56 @@ impl<'de> serde::Deserialize<'de> for RangeProof { .map(|inner| Self { inner: inner.map(Box::new) }) } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`RangeProof`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(super::PrefixedByteVecEncoder); +} + +impl encoding::Encode for RangeProof { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(super::PrefixedByteVecEncoder::new(self.to_vec())) + } +} + +decoder_newtype! { + /// Decoder for the [`RangeProof`] type. + #[derive(Default)] + pub struct Decoder(encoding::ByteVecDecoder); + + /// Decoder error for the [`RangeProof`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + Decode(encoding::ByteVecDecoderError), + RangeProof(secp256k1_zkp::Error), + }); + + impl Decode for RangeProof { + fn convert_inner(v) -> Result<_, DecoderErrorInner> { + Self::Output::from_slice(&v).map_err(DecoderErrorInner::RangeProof) + } + } +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(..) => f.write_str("error decoding byte vector"), + Inner::RangeProof(..) => f.write_str("error decoding range proof"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(ref e) => Some(e), + Inner::RangeProof(ref e) => Some(e), + } + } +} diff --git a/src/confidential/surjection_proof.rs b/src/confidential/surjection_proof.rs index 6fb6f8ea..9a9ceed5 100644 --- a/src/confidential/surjection_proof.rs +++ b/src/confidential/surjection_proof.rs @@ -2,6 +2,7 @@ //! Surjection Proofs +use core::fmt; use std::io; use secp256k1_zkp::rand::{CryptoRng, RngCore}; @@ -10,7 +11,7 @@ use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; use serde::{Deserializer, Serializer}; use crate::confidential::{AssetBlindingFactor, AssetId}; -use crate::encode; +use crate::{encode, encoding}; /// A surjection proof, proving that an asset commitment commits to the same asset ID /// as a commitment from a given set. @@ -143,3 +144,56 @@ impl<'de> serde::Deserialize<'de> for SurjectionProof { .map(|inner| Self { inner: inner.map(Box::new) }) } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`SurjectionProof`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(super::PrefixedByteVecEncoder); +} + +impl encoding::Encode for SurjectionProof { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(super::PrefixedByteVecEncoder::new(self.to_vec())) + } +} + +decoder_newtype! { + /// Decoder for the [`SurjectionProof`] type. + #[derive(Default)] + pub struct Decoder(encoding::ByteVecDecoder); + + /// Decoder error for the [`SurjectionProof`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + Decode(encoding::ByteVecDecoderError), + SurjectionProof(secp256k1_zkp::Error), + }); + + impl Decode for SurjectionProof { + fn convert_inner(v) -> Result<_, DecoderErrorInner> { + Self::Output::from_slice(&v).map_err(DecoderErrorInner::SurjectionProof) + } + } +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(..) => f.write_str("error decoding byte vector"), + Inner::SurjectionProof(..) => f.write_str("error decoding surjection proof"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(ref e) => Some(e), + Inner::SurjectionProof(ref e) => Some(e), + } + } +} diff --git a/src/internal_macros.rs b/src/internal_macros.rs index b9cf0c19..a20a8193 100644 --- a/src/internal_macros.rs +++ b/src/internal_macros.rs @@ -785,3 +785,158 @@ macro_rules! decoder_state_machine { } }; } + +/// Generates a simple decoder wrapper that converts output from an inner decoder. +/// +/// This macro creates a decoder that wraps an existing decoder and applies a conversion +/// function to transform the inner decoder's output into the target type. It's simpler +/// than `decoder_state_machine!` as it only wraps a single decoder without state transitions. +/// +/// # Syntax +/// +/// ```ignore +/// decoder_newtype! { +/// /// Documentation for the decoder struct +/// pub struct DecoderName(InnerDecoderType); +/// +/// /// Documentation for the error struct +/// pub struct ErrorName(enum InnerErrorName { +/// // The first variant must be called Decode and hold the inner decoder type. +/// Decode(InnerDecoderErrorType), +/// // All other variants are free-form. +/// CustomError1(ErrorType1), +/// CustomError2 { field: ErrorType2 }, +/// // ... custom error variants +/// }); +/// +/// impl Decode for TargetType { +/// fn convert_inner(output_var) -> Result<_, ErrorName> { +/// // Conversion logic +/// } +/// } +/// } +/// ``` +/// +/// # Generated Code +/// +/// The macro generates: +/// - A public decoder struct wrapping the inner decoder +/// - A public error struct wrapping a private error enum +/// - A private error enum with `Decode` variant and custom variants +/// - `Decoder` trait implementation with `push_bytes`, `end`, and `read_limit` methods +/// - `Decode` trait implementation for the target type +/// +/// The macro does **not** generate a `fmt::Display` or `std::error::Error` impl +/// for `ErrorName`, so the user must implement these outside of the macro. +/// +/// # Parameters +/// +/// - `DecoderName` - The wrapper decoder struct name +/// - `InnerDecoderType` - The existing decoder type to wrap +/// - `ErrorName` - The error struct name +/// - `InnerErrorName` - The private error enum name +/// - `InnerDecoderErrorType` - The error type from the inner decoder +/// - `TargetType` - The final output type after conversion +/// - `convert_inner` - Function that converts inner output to target type +/// +/// # Conversion Function +/// +/// The conversion function: +/// - Takes the inner decoder's output as its parameter +/// - Returns `Result` +/// - Is called automatically when the inner decoder completes +/// - Can return custom errors defined in the error enum +/// +/// # Error Handling +/// +/// The macro automatically: +/// - Creates a `Decode` variant containing the inner decoder's error +/// - Maps inner decoder errors to the `Decode` variant +/// - Allows custom error variants for conversion failures +/// +/// # Example Usage +/// +/// ```ignore +/// decoder_newtype! { +/// /// Decoder for range proofs +/// #[derive(Default)] +/// pub struct Decoder(encoding::ByteVecDecoder); +/// +/// /// Decoder error for range proofs +/// #[derive(Clone, PartialEq, Eq, Debug)] +/// pub struct DecoderError(enum DecoderErrorInner { +/// Decode(encoding::ByteVecDecoderError), +/// RangeProof(secp256k1_zkp::Error), +/// }); +/// +/// impl Decode for RangeProof { +/// fn convert_inner(v) -> Result<_, DecoderError> { +/// RangeProof::from_slice(&v).map_err(DecoderError::RangeProof) +/// } +/// } +/// } +/// ``` +macro_rules! decoder_newtype { + ( + $(#[$($struct_attr:tt)*])* + pub struct $outer_ty:ident($inner_ty:ty); + + $(#[$($error_struct_attr:tt)*])* + pub struct $error_ty:ident(enum $inner_error_ty:ident { + Decode($inner_ty_error:ty), + $($extra_variants:tt)* + }); + + impl Decode for $target_ty:ty { + fn convert_inner($output:ident) -> Result<_, $error_inner1:ty> { + $($output_fn_inner:tt)* + } + } + ) => { + $(#[$($struct_attr)*])* + pub struct $outer_ty($inner_ty); + + $(#[$($error_struct_attr)*])* + pub struct $error_ty($inner_error_ty); + + $(#[$($error_struct_attr)*])* + enum $inner_error_ty { + Decode($inner_ty_error), + $($extra_variants)* + } + + impl $crate::encoding::Decoder for $outer_ty { + type Output = $target_ty; + type Error = $error_ty; + + fn push_bytes( + &mut self, + bytes: &mut &[u8], + ) -> Result<$crate::encoding::DecoderStatus, Self::Error> { + self.0 + .push_bytes(bytes) + .map_err($inner_error_ty::Decode) + .map_err($error_ty) + } + + fn end(self) -> Result { + let $output = self.0 + .end() + .map_err($inner_error_ty::Decode) + .map_err($error_ty)?; + let converted = { + $($output_fn_inner)* + }; + converted.map_err($error_ty) + } + + fn read_limit(&self) -> usize { + self.0.read_limit() + } + } + + impl $crate::encoding::Decode for $target_ty{ + type Decoder = $outer_ty; + } + }; +} From c13d392a530ce7a0356f8bcf64c1bf292b1b0500 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 19:06:38 +0000 Subject: [PATCH 26/41] rename transaction to transaction/mod.rs --- src/{transaction.rs => transaction/mod.rs} | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) rename src/{transaction.rs => transaction/mod.rs} (99%) diff --git a/src/transaction.rs b/src/transaction/mod.rs similarity index 99% rename from src/transaction.rs rename to src/transaction/mod.rs index 694c43ac..f68af9ba 100644 --- a/src/transaction.rs +++ b/src/transaction/mod.rs @@ -2406,7 +2406,7 @@ mod tests { #[test] fn discount_vsize() { - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in2out_pegin.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in2out_pegin.hex")); assert_eq!(tx.input.len(), 1); assert!(tx.input[0].is_pegin()); assert_eq!(tx.output.len(), 2); @@ -2415,7 +2415,7 @@ mod tests { assert_eq!(tx.discount_weight(), 2403); assert_eq!(tx.discount_vsize(), 601); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in2out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in2out_tx.hex")); assert_eq!(tx.input.len(), 1); assert_eq!(tx.output.len(), 2); assert_eq!(tx.weight(), 5330); @@ -2423,7 +2423,7 @@ mod tests { assert_eq!(tx.discount_weight(), 863); assert_eq!(tx.discount_vsize(), 216); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in3out_tx.hex")); assert_eq!(tx.input.len(), 1); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10107); @@ -2431,7 +2431,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1173); assert_eq!(tx.discount_vsize(), 294); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_exp.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_exp.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 1302); @@ -2439,7 +2439,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1302); assert_eq!(tx.discount_vsize(), 326); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10300); @@ -2447,7 +2447,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1302); assert_eq!(tx.discount_vsize(), 326); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10300); @@ -2455,7 +2455,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1302); assert_eq!(tx.discount_vsize(), 326); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_tx2.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_tx2.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10536); @@ -2463,7 +2463,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1538); assert_eq!(tx.discount_vsize(), 385); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/3in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/3in3out_tx.hex")); assert_eq!(tx.input.len(), 3); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10922); @@ -2471,7 +2471,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1860); assert_eq!(tx.discount_vsize(), 465); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/4in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/4in3out_tx.hex")); assert_eq!(tx.input.len(), 4); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 11192); @@ -2479,7 +2479,7 @@ mod tests { assert_eq!(tx.discount_weight(), 2130); assert_eq!(tx.discount_vsize(), 533); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in4out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in4out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 4); assert_eq!(tx.weight(), 15261); @@ -2487,7 +2487,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1764); assert_eq!(tx.discount_vsize(), 441); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in5out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in5out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 5); assert_eq!(tx.weight(), 20030); @@ -2501,8 +2501,8 @@ mod tests { // Check that rust-elements can deserialize tests vector from ELIP203 // from // https://github.com/ElementsProject/ELIPs/blob/main/elip-0203.mediawiki - let tx3: Transaction = hex_deserialize!(include_str!("../tests/data/elip203_3.hex")); - let tx4: Transaction = hex_deserialize!(include_str!("../tests/data/elip203_4.hex")); + let tx3: Transaction = hex_deserialize!(include_str!("../../tests/data/elip203_3.hex")); + let tx4: Transaction = hex_deserialize!(include_str!("../../tests/data/elip203_4.hex")); let max_money = 2_100_000_000_000_000; assert!(tx3.input[0].asset_issuance.amount.explicit().unwrap() > max_money); assert!(tx4.input[0].asset_issuance.inflation_keys.explicit().unwrap() > max_money); @@ -2513,7 +2513,7 @@ mod tests { use crate::encode::{serialize, deserialize}; // Start with a transaction that has a pegin. - let base_tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in2out_pegin.hex")); + let base_tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in2out_pegin.hex")); // Test case (a): input witnesses but no output witnesses let mut tx_input_only = base_tx.clone(); From 9f82b4b32707e2193ebd54c894cab5bb3f330b77 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Fri, 26 Jun 2026 13:55:14 +0000 Subject: [PATCH 27/41] serde: remove impls for PSET and Transaction Leave the impl for BlockHeader (which is used by the functionary in a couple of places) (though I'd also like to delete this), the impls for the hash types (which are manually implemented and have regression tests), and the confidential stuff (which are also manually implemented and fairly straightforward). But for Transaction and PSET these impls are untenable. They are: * based on serde-derives (though 'manually' through the serde_struct_impl macro) of extremely complicated and fragile types, exposing implementation details and probably bypassing invariants on deserialization * not specified or documented anywhere * have very poor error messages on deserialization * not tested anywhere (not a single test broke with this change!!) If people are depending on these, we should provide some sort of compat crate/module. We can wait until they are trying to upgrade rust-elements and file complaints to do this. I'm skeptical that anybody *is*, given the above problems, at least not on purpose. If people want serde impls but don't need backward compatibility, we can provide that via the bitcoin-consensus-encoding crate, which will have general-purpose "serde-encode consensus objects in hex/bytes" adaptors in an upcoming version. --- src/block.rs | 1 - src/lib.rs | 2 - src/locktime.rs | 3 - src/opcodes.rs | 12 -- src/pset/map/global.rs | 11 -- src/pset/map/input.rs | 33 ----- src/pset/map/output.rs | 12 -- src/pset/mod.rs | 1 - src/pset/raw.rs | 16 --- src/serde_utils.rs | 276 ----------------------------------------- src/transaction/mod.rs | 7 -- 11 files changed, 374 deletions(-) delete mode 100644 src/serde_utils.rs diff --git a/src/block.rs b/src/block.rs index f022f1ae..c2153b33 100644 --- a/src/block.rs +++ b/src/block.rs @@ -360,7 +360,6 @@ pub struct Block { /// Complete list of transaction in the block pub txdata: Vec, } -serde_struct_impl!(Block, header, txdata); impl_consensus_encoding!(Block, header, txdata); impl Block { diff --git a/src/lib.rs b/src/lib.rs index 21543b1f..b36491e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,8 +69,6 @@ mod parse; pub mod pset; pub mod schnorr; pub mod script; -#[cfg(feature = "serde")] -mod serde_utils; pub mod sighash; pub mod taproot; mod transaction; diff --git a/src/locktime.rs b/src/locktime.rs index 0fd8c043..51c22f71 100644 --- a/src/locktime.rs +++ b/src/locktime.rs @@ -66,7 +66,6 @@ pub const LOCK_TIME_THRESHOLD: u32 = 500_000_000; /// ``` #[allow(clippy::derive_ord_xor_partial_ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LockTime { /// A block height lock time value. /// @@ -292,7 +291,6 @@ impl Decodable for LockTime { /// An absolute block height, guaranteed to always contain a valid height value. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Height(u32); impl Height { @@ -377,7 +375,6 @@ impl TryFrom for Height { /// `to_consensus_u32()`. Said another way, `Time(x)` means 'x seconds since epoch' _not_ '(x - /// threshold) seconds since epoch'. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Time(u32); impl Time { diff --git a/src/opcodes.rs b/src/opcodes.rs index fb8648c4..d9cfec5b 100644 --- a/src/opcodes.rs +++ b/src/opcodes.rs @@ -20,8 +20,6 @@ #![allow(non_camel_case_types)] -#[cfg(feature = "serde")] use serde; - use std::fmt; // Note: I am deliberately not implementing PartialOrd or Ord on the @@ -862,16 +860,6 @@ impl fmt::Display for All { } } -#[cfg(feature = "serde")] -impl serde::Serialize for All { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - /// Empty stack is also FALSE pub static OP_FALSE: All = all::OP_PUSHBYTES_0; /// Number 1 is also TRUE diff --git a/src/pset/map/global.rs b/src/pset/map/global.rs index 8051cc39..4ed267e3 100644 --- a/src/pset/map/global.rs +++ b/src/pset/map/global.rs @@ -58,7 +58,6 @@ const PSBT_ELEMENTS_GLOBAL_TX_MODIFIABLE: u8 = 0x01; /// Global transaction data #[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TxData { /// Transaction version. Must be 2. pub version: u32, @@ -93,10 +92,8 @@ impl Default for TxData { /// A key-value map for global data. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Global { /// Global transaction data - #[cfg_attr(feature = "serde", serde(flatten))] pub tx_data: TxData, /// The version number of this PSET. Must be present. pub version: u32, @@ -109,16 +106,8 @@ pub struct Global { /// Elements tx modifiable flag pub elements_tx_modifiable_flag: Option, /// Other Proprietary fields - #[cfg_attr( - feature = "serde", - serde(with = "crate::serde_utils::btreemap_as_seq_byte_values") - )] pub proprietary: BTreeMap>, /// Unknown global key-value pairs. - #[cfg_attr( - feature = "serde", - serde(with = "crate::serde_utils::btreemap_as_seq_byte_values") - )] pub unknown: BTreeMap>, } diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index 53438a1a..b146041f 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -172,7 +172,6 @@ const PSBT_ELEMENTS_IN_BLINDED_ISSUANCE: u8 = 0x15; /// A key-value map for an input of the corresponding index in the unsigned /// transaction. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Input { /// The non-witness transaction this input spends from. Should only be /// [`std::option::Option::Some`] for inputs which spend non-segwit outputs or @@ -184,10 +183,6 @@ pub struct Input { pub witness_utxo: Option, /// A map from public keys to their corresponding signature as would be /// pushed to the stack from a scriptSig or witness. - #[cfg_attr( - feature = "serde", - serde(with = "crate::serde_utils::btreemap_byte_values") - )] pub partial_sigs: BTreeMap>, /// The sighash type to be used for this input. Signatures for this input /// must use the sighash type. @@ -198,7 +193,6 @@ pub struct Input { pub witness_script: Option