From 603a3f8c9f582081813e1596edd40390c1945c20 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 21 Aug 2022 06:36:53 -0400 Subject: [PATCH] Generate Bulletproofs(+) generators at compile time Creates a new monero-generators crate so the monero crate can run the code in question at build time. Saves several seconds from running the tests. Closes https://github.com/serai-dex/serai/issues/101. --- Cargo.lock | 14 +++- Cargo.toml | 1 + coins/monero/.gitignore | 1 + coins/monero/Cargo.toml | 7 +- coins/monero/build.rs | 67 +++++++++++++++++++ coins/monero/generators/Cargo.toml | 19 ++++++ coins/monero/generators/LICENSE | 21 ++++++ coins/monero/generators/src/hash_to_point.rs | 51 ++++++++++++++ coins/monero/generators/src/lib.rs | 61 +++++++++++++++++ coins/monero/generators/src/varint.rs | 16 +++++ coins/monero/src/lib.rs | 11 ++- coins/monero/src/ringct/bulletproofs/core.rs | 36 +--------- coins/monero/src/ringct/bulletproofs/mod.rs | 8 --- .../src/ringct/bulletproofs/original.rs | 10 +-- coins/monero/src/ringct/bulletproofs/plus.rs | 9 +-- coins/monero/src/ringct/hash_to_point.rs | 52 +------------- coins/monero/src/tests/bulletproofs.rs | 11 +-- processor/src/coin/monero.rs | 10 +-- processor/src/wallet.rs | 2 +- 19 files changed, 274 insertions(+), 133 deletions(-) create mode 100644 coins/monero/.gitignore create mode 100644 coins/monero/build.rs create mode 100644 coins/monero/generators/Cargo.toml create mode 100644 coins/monero/generators/LICENSE create mode 100644 coins/monero/generators/src/hash_to_point.rs create mode 100644 coins/monero/generators/src/lib.rs create mode 100644 coins/monero/generators/src/varint.rs diff --git a/Cargo.lock b/Cargo.lock index 2643e49d..2fe0a615 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4583,6 +4583,18 @@ dependencies = [ "serde", ] +[[package]] +name = "monero-generators" +version = "0.1.0" +dependencies = [ + "curve25519-dalek 3.2.0", + "dalek-ff-group", + "group", + "lazy_static", + "subtle", + "tiny-keccak", +] + [[package]] name = "monero-serai" version = "0.1.0" @@ -4600,6 +4612,7 @@ dependencies = [ "modular-frost", "monero", "monero-epee-bin-serde", + "monero-generators", "multiexp", "rand 0.8.5", "rand_chacha 0.3.1", @@ -4608,7 +4621,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2 0.10.2", "subtle", "thiserror", "tiny-keccak", diff --git a/Cargo.toml b/Cargo.toml index 3b7eb7d0..7d5384bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crypto/frost", "coins/ethereum", + "coins/monero/generators", "coins/monero", "processor", diff --git a/coins/monero/.gitignore b/coins/monero/.gitignore new file mode 100644 index 00000000..15320da4 --- /dev/null +++ b/coins/monero/.gitignore @@ -0,0 +1 @@ +.generators diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index b6ebd028..df93ca59 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -32,6 +32,8 @@ transcript = { package = "flexible-transcript", path = "../../crypto/transcript" frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true } dleq = { path = "../../crypto/dleq", features = ["serialize"], optional = true } +monero-generators = { path = "generators" } + hex = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -45,6 +47,9 @@ reqwest = { version = "0.11", features = ["json"] } [features] multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"] +[build-dependencies] +dalek-ff-group = { path = "../../crypto/dalek-ff-group" } +monero-generators = { path = "generators" } + [dev-dependencies] -sha2 = "0.10" tokio = { version = "1", features = ["full"] } diff --git a/coins/monero/build.rs b/coins/monero/build.rs new file mode 100644 index 00000000..e8afbaec --- /dev/null +++ b/coins/monero/build.rs @@ -0,0 +1,67 @@ +use std::{ + io::Write, + path::Path, + fs::{File, remove_file}, +}; + +use dalek_ff_group::EdwardsPoint; + +use monero_generators::bulletproofs_generators; + +fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) { + for generator in points { + generators_string.extend( + format!( + " + dalek_ff_group::EdwardsPoint( + curve25519_dalek::edwards::CompressedEdwardsY({:?}).decompress().unwrap() + ), + ", + generator.compress().to_bytes() + ) + .chars(), + ); + } +} + +fn generators(prefix: &'static str, path: &str) { + let generators = bulletproofs_generators(prefix.as_bytes()); + #[allow(non_snake_case)] + let mut G_str = "".to_string(); + serialize(&mut G_str, &generators.G); + #[allow(non_snake_case)] + let mut H_str = "".to_string(); + serialize(&mut H_str, &generators.H); + + let path = Path::new(".generators").join(path); + let _ = remove_file(&path); + File::create(&path) + .unwrap() + .write_all( + format!( + " + lazy_static! {{ + static ref GENERATORS: Generators = Generators {{ + G: [ + {} + ], + H: [ + {} + ], + }}; + }} + ", + G_str, H_str, + ) + .as_bytes(), + ) + .unwrap(); +} + +fn main() { + // For some reason, filtering off .generators does not work. This prevents re-building overall + println!("cargo:rerun-if-changed=build.rs"); + + generators("bulletproof", "generators.rs"); + generators("bulletproof_plus", "generators_plus.rs"); +} diff --git a/coins/monero/generators/Cargo.toml b/coins/monero/generators/Cargo.toml new file mode 100644 index 00000000..b46c574e --- /dev/null +++ b/coins/monero/generators/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "monero-generators" +version = "0.1.0" +description = "Monero's hash_to_point and generators" +license = "MIT" +authors = ["Luke Parker "] +edition = "2021" + +[dependencies] +lazy_static = "1" + +subtle = "2.4" + +tiny-keccak = { version = "2", features = ["keccak"] } + +curve25519-dalek = { version = "3", features = ["std"] } + +group = { version = "0.12" } +dalek-ff-group = { path = "../../../crypto/dalek-ff-group" } diff --git a/coins/monero/generators/LICENSE b/coins/monero/generators/LICENSE new file mode 100644 index 00000000..f05b748b --- /dev/null +++ b/coins/monero/generators/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/coins/monero/generators/src/hash_to_point.rs b/coins/monero/generators/src/hash_to_point.rs new file mode 100644 index 00000000..9c13f849 --- /dev/null +++ b/coins/monero/generators/src/hash_to_point.rs @@ -0,0 +1,51 @@ +use subtle::ConditionallySelectable; + +use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; + +use group::ff::{Field, PrimeField}; +use dalek_ff_group::field::FieldElement; + +use crate::hash; + +#[allow(dead_code)] +pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint { + #[allow(non_snake_case)] + let A = FieldElement::from(486662u64); + + let v = FieldElement::from_square(hash(&bytes)).double(); + let w = v + FieldElement::one(); + let x = w.square() + (-A.square() * v); + + // This isn't the complete X, yet its initial value + // We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X + // While inefficient, it solves API boundaries and reduces the amount of work done here + #[allow(non_snake_case)] + let X = { + let u = w; + let v = x; + let v3 = v * v * v; + let uv3 = u * v3; + let v7 = v3 * v3 * v; + let uv7 = u * v7; + uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap()) + }; + let x = X.square() * x; + + let y = w - x; + let non_zero_0 = !y.is_zero(); + let y_if_non_zero_0 = w + x; + let sign = non_zero_0 & (!y_if_non_zero_0.is_zero()); + + let mut z = -A; + z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign); + #[allow(non_snake_case)] + let Z = z + w; + #[allow(non_snake_case)] + let mut Y = z - w; + + Y *= Z.invert().unwrap(); + let mut bytes = Y.to_repr(); + bytes[31] |= sign.unwrap_u8() << 7; + + CompressedEdwardsY(bytes).decompress().unwrap().mul_by_cofactor() +} diff --git a/coins/monero/generators/src/lib.rs b/coins/monero/generators/src/lib.rs new file mode 100644 index 00000000..6fdf98f1 --- /dev/null +++ b/coins/monero/generators/src/lib.rs @@ -0,0 +1,61 @@ +use lazy_static::lazy_static; + +use tiny_keccak::{Hasher, Keccak}; + +use curve25519_dalek::{ + constants::ED25519_BASEPOINT_POINT, + edwards::{EdwardsPoint as DalekPoint, CompressedEdwardsY}, +}; + +use group::Group; +use dalek_ff_group::EdwardsPoint; + +mod varint; +use varint::write_varint; + +mod hash_to_point; +pub use hash_to_point::hash_to_point; + +fn hash(data: &[u8]) -> [u8; 32] { + let mut keccak = Keccak::v256(); + keccak.update(data); + let mut res = [0; 32]; + keccak.finalize(&mut res); + res +} + +lazy_static! { + pub static ref H: DalekPoint = + CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes())) + .decompress() + .unwrap() + .mul_by_cofactor(); +} + +const MAX_M: usize = 16; +const N: usize = 64; +const MAX_MN: usize = MAX_M * N; + +#[allow(non_snake_case)] +pub struct Generators { + pub G: [EdwardsPoint; MAX_MN], + pub H: [EdwardsPoint; MAX_MN], +} + +pub fn bulletproofs_generators(prefix: &'static [u8]) -> Generators { + let mut res = + Generators { G: [EdwardsPoint::identity(); MAX_MN], H: [EdwardsPoint::identity(); MAX_MN] }; + for i in 0 .. MAX_MN { + let i = 2 * i; + + let mut even = H.compress().to_bytes().to_vec(); + even.extend(prefix); + let mut odd = even.clone(); + + write_varint(&i.try_into().unwrap(), &mut even).unwrap(); + write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap(); + res.H[i / 2] = EdwardsPoint(hash_to_point(hash(&even))); + res.G[i / 2] = EdwardsPoint(hash_to_point(hash(&odd))); + } + res +} diff --git a/coins/monero/generators/src/varint.rs b/coins/monero/generators/src/varint.rs new file mode 100644 index 00000000..e0aa6a3b --- /dev/null +++ b/coins/monero/generators/src/varint.rs @@ -0,0 +1,16 @@ +use std::io; + +const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000; +pub(crate) fn write_varint(varint: &u64, w: &mut W) -> io::Result<()> { + let mut varint = *varint; + while { + let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap(); + varint >>= 7; + if varint != 0 { + b |= VARINT_CONTINUATION_MASK; + } + w.write_all(&[b])?; + varint != 0 + } {} + Ok(()) +} diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 0b22db0c..364566be 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -6,11 +6,13 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; use tiny_keccak::{Hasher, Keccak}; use curve25519_dalek::{ - constants::{ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_TABLE}, + constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, - edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY}, + edwards::{EdwardsPoint, EdwardsBasepointTable}, }; +pub use monero_generators::H; + #[cfg(feature = "multisig")] pub mod frost; @@ -54,11 +56,6 @@ impl Protocol { } lazy_static! { - static ref H: EdwardsPoint = - CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes())) - .decompress() - .unwrap() - .mul_by_cofactor(); static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&H); } diff --git a/coins/monero/src/ringct/bulletproofs/core.rs b/coins/monero/src/ringct/bulletproofs/core.rs index e1a85d5d..bf94d470 100644 --- a/coins/monero/src/ringct/bulletproofs/core.rs +++ b/coins/monero/src/ringct/bulletproofs/core.rs @@ -13,10 +13,9 @@ use dalek_ff_group::{Scalar, EdwardsPoint}; use multiexp::multiexp as multiexp_const; -use crate::{ - H as DALEK_H, Commitment, hash, hash_to_scalar as dalek_hash, - ringct::hash_to_point::raw_hash_to_point, serialize::write_varint, -}; +pub(crate) use monero_generators::Generators; + +use crate::{H as DALEK_H, Commitment, hash_to_scalar as dalek_hash}; pub(crate) use crate::ringct::bulletproofs::scalar_vector::*; // Bring things into ff/group @@ -33,29 +32,6 @@ pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar { pub(crate) const MAX_M: usize = 16; pub(crate) const LOG_N: usize = 6; // 2 << 6 == N pub(crate) const N: usize = 64; -pub(crate) const MAX_MN: usize = MAX_M * N; - -pub(crate) struct Generators { - pub(crate) G: Vec, - pub(crate) H: Vec, -} - -pub(crate) fn generators_core(prefix: &'static [u8]) -> Generators { - let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) }; - for i in 0 .. MAX_MN { - let i = 2 * i; - - let mut even = (*H).compress().to_bytes().to_vec(); - even.extend(prefix); - let mut odd = even.clone(); - - write_varint(&i.try_into().unwrap(), &mut even).unwrap(); - write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap(); - res.H.push(EdwardsPoint(raw_hash_to_point(hash(&even)))); - res.G.push(EdwardsPoint(raw_hash_to_point(hash(&odd)))); - } - res -} pub(crate) fn prove_multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint { multiexp_const(pairs) * *INV_EIGHT @@ -153,12 +129,6 @@ lazy_static! { pub(crate) static ref TWO_N: ScalarVector = ScalarVector::powers(Scalar::from(2u8), N); } -pub(crate) fn init() { - let _ = &*INV_EIGHT; - let _ = &*H; - let _ = &*TWO_N; -} - pub(crate) fn challenge_products(w: &[Scalar], winv: &[Scalar]) -> Vec { let mut products = vec![Scalar::zero(); 1 << w.len()]; products[0] = winv[0]; diff --git a/coins/monero/src/ringct/bulletproofs/mod.rs b/coins/monero/src/ringct/bulletproofs/mod.rs index e877175a..2d2ff9e8 100644 --- a/coins/monero/src/ringct/bulletproofs/mod.rs +++ b/coins/monero/src/ringct/bulletproofs/mod.rs @@ -43,14 +43,6 @@ impl Bulletproofs { len + clawback } - pub fn init(plus: bool) { - if !plus { - OriginalStruct::init(); - } else { - PlusStruct::init(); - } - } - pub fn prove( rng: &mut R, outputs: &[Commitment], diff --git a/coins/monero/src/ringct/bulletproofs/original.rs b/coins/monero/src/ringct/bulletproofs/original.rs index bf235d21..57f78849 100644 --- a/coins/monero/src/ringct/bulletproofs/original.rs +++ b/coins/monero/src/ringct/bulletproofs/original.rs @@ -12,8 +12,9 @@ use multiexp::BatchVerifier; use crate::{Commitment, ringct::bulletproofs::core::*}; +include!("../../../.generators/generators.rs"); + lazy_static! { - static ref GENERATORS: Generators = generators_core(b"bulletproof"); static ref ONE_N: ScalarVector = ScalarVector(vec![Scalar::one(); N]); static ref IP12: Scalar = inner_product(&ONE_N, &TWO_N); } @@ -34,13 +35,6 @@ pub struct OriginalStruct { } impl OriginalStruct { - pub(crate) fn init() { - init(); - let _ = &*GENERATORS; - let _ = &*ONE_N; - let _ = &*IP12; - } - pub(crate) fn prove( rng: &mut R, commitments: &[Commitment], diff --git a/coins/monero/src/ringct/bulletproofs/plus.rs b/coins/monero/src/ringct/bulletproofs/plus.rs index 7c5b8b33..e44095af 100644 --- a/coins/monero/src/ringct/bulletproofs/plus.rs +++ b/coins/monero/src/ringct/bulletproofs/plus.rs @@ -15,8 +15,9 @@ use crate::{ ringct::{hash_to_point::raw_hash_to_point, bulletproofs::core::*}, }; +include!("../../../.generators/generators_plus.rs"); + lazy_static! { - static ref GENERATORS: Generators = generators_core(b"bulletproof_plus"); static ref TRANSCRIPT: [u8; 32] = EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))).compress().to_bytes(); } @@ -52,12 +53,6 @@ pub struct PlusStruct { } impl PlusStruct { - pub(crate) fn init() { - init(); - let _ = &*GENERATORS; - let _ = &*TRANSCRIPT; - } - pub(crate) fn prove( rng: &mut R, commitments: &[Commitment], diff --git a/coins/monero/src/ringct/hash_to_point.rs b/coins/monero/src/ringct/hash_to_point.rs index 779030de..5c19b9bd 100644 --- a/coins/monero/src/ringct/hash_to_point.rs +++ b/coins/monero/src/ringct/hash_to_point.rs @@ -1,54 +1,6 @@ -use subtle::ConditionallySelectable; +use curve25519_dalek::edwards::EdwardsPoint; -use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint}; - -use group::ff::{Field, PrimeField}; -use dalek_ff_group::field::FieldElement; - -use crate::hash; - -#[allow(dead_code)] -pub(crate) fn raw_hash_to_point(bytes: [u8; 32]) -> EdwardsPoint { - #[allow(non_snake_case)] - let A = FieldElement::from(486662u64); - - let v = FieldElement::from_square(hash(&bytes)).double(); - let w = v + FieldElement::one(); - let x = w.square() + (-A.square() * v); - - // This isn't the complete X, yet its initial value - // We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X - // While inefficient, it solves API boundaries and reduces the amount of work done here - #[allow(non_snake_case)] - let X = { - let u = w; - let v = x; - let v3 = v * v * v; - let uv3 = u * v3; - let v7 = v3 * v3 * v; - let uv7 = u * v7; - uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap()) - }; - let x = X.square() * x; - - let y = w - x; - let non_zero_0 = !y.is_zero(); - let y_if_non_zero_0 = w + x; - let sign = non_zero_0 & (!y_if_non_zero_0.is_zero()); - - let mut z = -A; - z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign); - #[allow(non_snake_case)] - let Z = z + w; - #[allow(non_snake_case)] - let mut Y = z - w; - - Y *= Z.invert().unwrap(); - let mut bytes = Y.to_repr(); - bytes[31] |= sign.unwrap_u8() << 7; - - CompressedEdwardsY(bytes).decompress().unwrap().mul_by_cofactor() -} +pub(crate) use monero_generators::{hash_to_point as raw_hash_to_point}; pub fn hash_to_point(key: EdwardsPoint) -> EdwardsPoint { raw_hash_to_point(key.compress().to_bytes()) diff --git a/coins/monero/src/tests/bulletproofs.rs b/coins/monero/src/tests/bulletproofs.rs index 3bb88921..efa97fae 100644 --- a/coins/monero/src/tests/bulletproofs.rs +++ b/coins/monero/src/tests/bulletproofs.rs @@ -57,12 +57,7 @@ fn bulletproofs_vector() { } macro_rules! bulletproofs_tests { - ($init: ident, $name: ident, $max: ident, $plus: literal) => { - #[test] - fn $init() { - Bulletproofs::init($plus); - } - + ($name: ident, $max: ident, $plus: literal) => { #[test] fn $name() { // Create Bulletproofs for all possible output quantities @@ -93,5 +88,5 @@ macro_rules! bulletproofs_tests { }; } -bulletproofs_tests!(bulletproofs_init, bulletproofs, bulletproofs_max, false); -bulletproofs_tests!(bulletproofs_plus_init, bulletproofs_plus, bulletproofs_plus_max, true); +bulletproofs_tests!(bulletproofs, bulletproofs_max, false); +bulletproofs_tests!(bulletproofs_plus, bulletproofs_plus_max, true); diff --git a/processor/src/coin/monero.rs b/processor/src/coin/monero.rs index 2a40c572..27990ef5 100644 --- a/processor/src/coin/monero.rs +++ b/processor/src/coin/monero.rs @@ -7,7 +7,6 @@ use transcript::RecommendedTranscript; use frost::{curve::Ed25519, FrostKeys}; use monero_serai::{ - ringct::bulletproofs::Bulletproofs, transaction::Transaction, rpc::Rpc, wallet::{ @@ -69,14 +68,7 @@ pub struct Monero { impl Monero { pub async fn new(url: String) -> Monero { - let view = view_key::(0).0; - let res = Monero { rpc: Rpc::new(url), view }; - - // Initialize Bulletproofs now to prevent the first call from taking several seconds - // TODO: Do this for both, unless we're sure we're only working on a single protocol - Bulletproofs::init(res.rpc.get_protocol().await.unwrap().bp_plus()); - - res + Monero { rpc: Rpc::new(url), view: view_key::(0).0 } } fn view_pair(&self, spend: dfg::EdwardsPoint) -> ViewPair { diff --git a/processor/src/wallet.rs b/processor/src/wallet.rs index 0fdb06c7..5cb5949d 100644 --- a/processor/src/wallet.rs +++ b/processor/src/wallet.rs @@ -86,7 +86,7 @@ impl CoinDb for MemCoinDb { fn add_output(&mut self, output: &O) -> bool { // This would be insecure as we're indexing by ID and this will replace the output as a whole // Multiple outputs may have the same ID in edge cases such as Monero, where outputs are ID'd - // by key image, not by hash + index + // by output key, not by hash + index // self.outputs.insert(output.id(), output).is_some() let id = output.id().as_ref().to_vec(); if self.outputs.contains_key(&id) {