Skip to content

Commit

Permalink
Intern BLS lazy public keys
Browse files Browse the repository at this point in the history
This ensures that for each compressed public key there's at most one
lazy public key, which means everyone profits when it is uncompressed.

This drops the (unused: never read from) BLS public key cache.
  • Loading branch information
hrxi committed Nov 23, 2024
1 parent 107b362 commit f245572
Show file tree
Hide file tree
Showing 31 changed files with 389 additions and 368 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions blockchain/tests/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use nimiq_block::{
};
use nimiq_blockchain::{Blockchain, BlockchainConfig};
use nimiq_blockchain_interface::AbstractBlockchain;
use nimiq_bls::{lazy::LazyPublicKey, AggregateSignature, KeyPair};
use nimiq_bls::{AggregateSignature, KeyPair, LazyPublicKey as BlsLazyPublicKey};
use nimiq_collections::bitset::BitSet;
use nimiq_database::mdbx::MdbxDatabase;
use nimiq_keys::{Address, Ed25519PublicKey};
Expand Down Expand Up @@ -56,7 +56,7 @@ fn test_skip_block_single_signature() {
// verify skip block proof
let validators = Validators::new(vec![Validator::new(
Address::default(),
LazyPublicKey::from(key_pair.public_key),
BlsLazyPublicKey::from(key_pair.public_key),
Ed25519PublicKey::from([0u8; 32]),
0..Policy::SLOTS,
)]);
Expand Down
4 changes: 1 addition & 3 deletions bls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,5 @@ nimiq-test-log = { workspace = true }
nimiq-test-utils = { workspace = true }

[features]
cache = ["lazy"]
default = ["lazy", "serde-derive"]
lazy = ["parking_lot"]
default = ["serde-derive"]
serde-derive = ["nimiq-serde", "serde"]
75 changes: 0 additions & 75 deletions bls/src/cache.rs

This file was deleted.

137 changes: 68 additions & 69 deletions bls/src/lazy.rs
Original file line number Diff line number Diff line change
@@ -1,117 +1,116 @@
use std::{cmp::Ordering, fmt};
use std::{
cmp::Ordering,
fmt,
hash::Hasher,
sync::{Arc, OnceLock},
};

use nimiq_hash::Hash;
use parking_lot::{
MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard,
};
use nimiq_utils::interner::{Interned, Interner, InternerKey};

use crate::{CompressedPublicKey, PublicKey, SigHash, Signature};

pub struct LazyPublicKey {
pub(crate) compressed: CompressedPublicKey,
pub(crate) cache: RwLock<Option<PublicKey>>,
static CACHE: OnceLock<Interner<LazyPublicKey>> = OnceLock::new();

impl Interned for LazyPublicKey {
type Key = CompressedPublicKey;
fn from_interner_key(compressed: InternerKey<LazyPublicKey>) -> LazyPublicKey {
LazyPublicKey(Arc::new(LazyPublicKeyInner {
compressed,
uncompressed: OnceLock::new(),
}))
}
fn as_interner_key(&self) -> &InternerKey<LazyPublicKey> {
&self.0.compressed
}
fn interned_strong_count(&self) -> usize {
Arc::strong_count(&self.0)
}
}

impl Drop for LazyPublicKey {
fn drop(&mut self) {
InternerKey::notify_deletion(&self.0.compressed, Arc::strong_count(&self.0))
}
}

#[derive(Clone)]
pub struct LazyPublicKey(Arc<LazyPublicKeyInner>);

impl fmt::Debug for LazyPublicKey {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "LazyPublicKey({})", self.compressed)
f.debug_tuple("LazyPublicKey")
.field(self.compressed())
.finish()
}
}

impl fmt::Display for LazyPublicKey {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
fmt::Display::fmt(&self.compressed, f)
fmt::Display::fmt(self.compressed(), f)
}
}

impl Clone for LazyPublicKey {
fn clone(&self) -> Self {
LazyPublicKey {
compressed: self.compressed.clone(),
cache: RwLock::new(*self.cache.read()),
}
}
struct LazyPublicKeyInner {
compressed: InternerKey<LazyPublicKey>, // Essentially a `CompressedPublicKey`
uncompressed: OnceLock<Option<PublicKey>>,
}

impl PartialEq for LazyPublicKey {
fn eq(&self, other: &LazyPublicKey) -> bool {
self.compressed.eq(&other.compressed)
fn eq(&self, other: &Self) -> bool {
self.compressed().eq(&other.compressed())

Check warning on line 61 in bls/src/lazy.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

this expression creates a reference which is immediately dereferenced by the compiler

warning: this expression creates a reference which is immediately dereferenced by the compiler --> bls/src/lazy.rs:61:30 | 61 | self.compressed().eq(&other.compressed()) | ^^^^^^^^^^^^^^^^^^^ help: change this to: `other.compressed()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `#[warn(clippy::needless_borrow)]` on by default

Check warning on line 61 in bls/src/lazy.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

this expression creates a reference which is immediately dereferenced by the compiler

warning: this expression creates a reference which is immediately dereferenced by the compiler --> bls/src/lazy.rs:61:30 | 61 | self.compressed().eq(&other.compressed()) | ^^^^^^^^^^^^^^^^^^^ help: change this to: `other.compressed()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `#[warn(clippy::needless_borrow)]` on by default
}
}

impl Eq for LazyPublicKey {}

impl PartialOrd<LazyPublicKey> for LazyPublicKey {
fn partial_cmp(&self, other: &LazyPublicKey) -> Option<Ordering> {
Some(self.cmp(other))
impl std::hash::Hash for LazyPublicKey {
fn hash<H: Hasher>(&self, state: &mut H) {
std::hash::Hash::hash(self.compressed(), state)
}
}

impl Ord for LazyPublicKey {
fn cmp(&self, other: &Self) -> Ordering {
self.compressed.cmp(&other.compressed)
impl PartialOrd for LazyPublicKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.compressed().partial_cmp(other.compressed())
}
}

Check warning on line 77 in bls/src/lazy.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

non-canonical implementation of `partial_cmp` on an `Ord` type

warning: non-canonical implementation of `partial_cmp` on an `Ord` type --> bls/src/lazy.rs:73:1 | 73 | / impl PartialOrd for LazyPublicKey { 74 | | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | | _____________________________________________________________- 75 | || self.compressed().partial_cmp(other.compressed()) 76 | || } | ||_____- help: change this to: `{ Some(self.cmp(other)) }` 77 | | } | |__^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#non_canonical_partial_ord_impl = note: `#[warn(clippy::non_canonical_partial_ord_impl)]` on by default

Check warning on line 77 in bls/src/lazy.rs

View workflow job for this annotation

GitHub Actions / Clippy Report

non-canonical implementation of `partial_cmp` on an `Ord` type

warning: non-canonical implementation of `partial_cmp` on an `Ord` type --> bls/src/lazy.rs:73:1 | 73 | / impl PartialOrd for LazyPublicKey { 74 | | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | | _____________________________________________________________- 75 | || self.compressed().partial_cmp(other.compressed()) 76 | || } | ||_____- help: change this to: `{ Some(self.cmp(other)) }` 77 | | } | |__^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#non_canonical_partial_ord_impl = note: `#[warn(clippy::non_canonical_partial_ord_impl)]` on by default

impl AsRef<[u8]> for LazyPublicKey {
fn as_ref(&self) -> &[u8] {
self.compressed.as_ref()
impl Ord for LazyPublicKey {
fn cmp(&self, other: &Self) -> Ordering {
self.compressed().cmp(other.compressed())
}
}

impl LazyPublicKey {
pub fn from_compressed(compressed: &CompressedPublicKey) -> Self {
LazyPublicKey {
compressed: compressed.clone(),
cache: RwLock::new(None),
}
CACHE.get_or_init(Default::default).intern(compressed)
}

pub fn uncompress(&self) -> Option<MappedRwLockReadGuard<PublicKey>> {
let read_guard: RwLockReadGuard<Option<PublicKey>>;

let upgradable = self.cache.upgradable_read();
if upgradable.is_some() {
// Fast path, downgrade and return
read_guard = RwLockUpgradableReadGuard::downgrade(upgradable);
} else {
// Slow path, upgrade, write, downgrade and return
let mut upgraded = RwLockUpgradableReadGuard::upgrade(upgradable);
*upgraded = Some(match self.compressed.uncompress() {
Ok(p) => p,
_ => return None,
});
read_guard = RwLockWriteGuard::downgrade(upgraded);
}

Some(RwLockReadGuard::map(read_guard, |opt| {
opt.as_ref().unwrap()
}))
}

pub fn uncompress_unchecked(&self) -> MappedRwLockReadGuard<PublicKey> {
self.uncompress().expect("Invalid public key")
pub fn uncompress(&self) -> Option<&PublicKey> {
self.0
.uncompressed
.get_or_init(|| self.0.compressed.uncompress().ok())
.as_ref()
}

pub fn compressed(&self) -> &CompressedPublicKey {
&self.compressed
&self.0.compressed
}

pub fn has_uncompressed(&self) -> bool {
self.cache.read().is_some()
self.0.uncompressed.get().is_some()
}

pub fn verify<M: Hash>(&self, msg: &M, signature: &Signature) -> bool {
let cached = self.uncompress();
if let Some(public_key) = cached.as_ref() {
if let Some(public_key) = self.uncompress() {
return public_key.verify(msg, signature);
}
false
}

pub fn verify_hash(&self, hash: SigHash, signature: &Signature) -> bool {
let cached = self.uncompress();
if let Some(public_key) = cached.as_ref() {
if let Some(public_key) = self.uncompress() {
return public_key.verify_hash(hash, signature);
}
false
Expand All @@ -120,22 +119,22 @@ impl LazyPublicKey {

impl From<PublicKey> for LazyPublicKey {
fn from(key: PublicKey) -> Self {
LazyPublicKey {
compressed: key.compress(),
cache: RwLock::new(Some(key)),
}
let result = LazyPublicKey::from_compressed(&key.compress());
// TODO: this might block
let _ = result.0.uncompressed.set(Some(key));
result
}
}

impl From<CompressedPublicKey> for LazyPublicKey {
fn from(key: CompressedPublicKey) -> Self {
LazyPublicKey::from_compressed(&key)
fn from(compressed: CompressedPublicKey) -> Self {
LazyPublicKey::from_compressed(&compressed)
}
}

impl From<LazyPublicKey> for CompressedPublicKey {
fn from(key: LazyPublicKey) -> Self {
key.compressed
key.0.compressed.as_ref().clone()
}
}

Expand All @@ -155,7 +154,7 @@ mod serialization {
where
S: serde::Serializer,
{
Serialize::serialize(&self.compressed, serializer)
Serialize::serialize(&self.compressed(), serializer)
}
}

Expand Down
8 changes: 2 additions & 6 deletions bls/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
pub use lazy::LazyPublicKey;
use nimiq_hash::Blake2sHash;
pub use types::*;

// Implements the LazyPublicKey type. Which is a faster, cached version of PublicKey.
#[cfg(feature = "lazy")]
pub mod lazy;
mod lazy;

// Implements all of the types needed to do BLS signatures.
mod types;

// Specifies the hash algorithm used for signatures
pub type SigHash = Blake2sHash;

// A simple cache implementation for the (un)compressed keys.
#[cfg(feature = "cache")]
pub mod cache;

// Implements the tagged-signing traits
#[cfg(feature = "serde-derive")]
mod tagged_signing;
Loading

0 comments on commit f245572

Please sign in to comment.