fix(shielded): use RwLock for note commitment tree root caches (#3809)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
teor 2022-03-10 09:26:49 +10:00 committed by GitHub
parent 03d123b428
commit 3291db35c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 46 deletions

View File

@ -10,16 +10,14 @@
//! //!
//! A root of a note commitment tree is associated with each treestate. //! A root of a note commitment tree is associated with each treestate.
#![allow(clippy::unit_arg)]
#![allow(clippy::derive_hash_xor_eq)] #![allow(clippy::derive_hash_xor_eq)]
#![allow(dead_code)] #![allow(dead_code)]
use std::{ use std::{
cell::Cell,
convert::TryFrom,
fmt, fmt,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
io, io,
ops::Deref,
}; };
use bitvec::prelude::*; use bitvec::prelude::*;
@ -213,7 +211,7 @@ pub enum NoteCommitmentTreeError {
} }
/// Orchard Incremental Note Commitment Tree /// Orchard Incremental Note Commitment Tree
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct NoteCommitmentTree { pub struct NoteCommitmentTree {
/// The tree represented as a Frontier. /// The tree represented as a Frontier.
/// ///
@ -244,12 +242,9 @@ pub struct NoteCommitmentTree {
/// serialized with the tree). This is particularly important since we decided /// serialized with the tree). This is particularly important since we decided
/// to instantiate the trees from the genesis block, for simplicity. /// to instantiate the trees from the genesis block, for simplicity.
/// ///
/// [`Cell`] offers interior mutability (it works even with a non-mutable /// We use a [`RwLock`] for this cache, because it is only written once per tree update.
/// reference to the tree) but it prevents the tree (and anything that uses it) /// Each tree has its own cached root, a new lock is created for each clone.
/// from being shared between threads. If this ever becomes an issue we can cached_root: std::sync::RwLock<Option<Root>>,
/// leave caching to the callers (which requires much more code), or replace
/// `Cell` with `Arc<Mutex<_>>` (and be careful of deadlocks and async code.)
cached_root: Cell<Option<Root>>,
} }
impl NoteCommitmentTree { impl NoteCommitmentTree {
@ -263,7 +258,13 @@ impl NoteCommitmentTree {
pub fn append(&mut self, cm_x: pallas::Base) -> Result<(), NoteCommitmentTreeError> { pub fn append(&mut self, cm_x: pallas::Base) -> Result<(), NoteCommitmentTreeError> {
if self.inner.append(&cm_x.into()) { if self.inner.append(&cm_x.into()) {
// Invalidate cached root // Invalidate cached root
self.cached_root.replace(None); let cached_root = self
.cached_root
.get_mut()
.expect("a thread that previously held exclusive lock access panicked");
*cached_root = None;
Ok(()) Ok(())
} else { } else {
Err(NoteCommitmentTreeError::FullTree) Err(NoteCommitmentTreeError::FullTree)
@ -273,13 +274,28 @@ impl NoteCommitmentTree {
/// Returns the current root of the tree, used as an anchor in Orchard /// Returns the current root of the tree, used as an anchor in Orchard
/// shielded transactions. /// shielded transactions.
pub fn root(&self) -> Root { pub fn root(&self) -> Root {
match self.cached_root.get() { if let Some(root) = self
// Return cached root .cached_root
Some(root) => root, .read()
.expect("a thread that previously held exclusive lock access panicked")
.deref()
{
// Return cached root.
return *root;
}
// Get exclusive access, compute the root, and cache it.
let mut write_root = self
.cached_root
.write()
.expect("a thread that previously held exclusive lock access panicked");
match write_root.deref() {
// Another thread got write access first, return cached root.
Some(root) => *root,
None => { None => {
// Compute root and cache it // Compute root and cache it.
let root = Root(self.inner.root().0); let root = Root(self.inner.root().0);
self.cached_root.replace(Some(root)); *write_root = Some(root);
root root
} }
} }
@ -308,6 +324,21 @@ impl NoteCommitmentTree {
} }
} }
impl Clone for NoteCommitmentTree {
/// Clones the inner tree, and creates a new `RwLock` with the cloned root data.
fn clone(&self) -> Self {
let cached_root = *self
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked");
Self {
inner: self.inner.clone(),
cached_root: std::sync::RwLock::new(cached_root),
}
}
}
impl Default for NoteCommitmentTree { impl Default for NoteCommitmentTree {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@ -10,14 +10,13 @@
//! //!
//! A root of a note commitment tree is associated with each treestate. //! A root of a note commitment tree is associated with each treestate.
#![allow(clippy::unit_arg)]
#![allow(dead_code)] #![allow(dead_code)]
use std::{ use std::{
cell::Cell,
fmt, fmt,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
io, io,
ops::Deref,
}; };
use bitvec::prelude::*; use bitvec::prelude::*;
@ -216,7 +215,7 @@ pub enum NoteCommitmentTreeError {
} }
/// Sapling Incremental Note Commitment Tree. /// Sapling Incremental Note Commitment Tree.
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct NoteCommitmentTree { pub struct NoteCommitmentTree {
/// The tree represented as a Frontier. /// The tree represented as a Frontier.
/// ///
@ -247,12 +246,9 @@ pub struct NoteCommitmentTree {
/// serialized with the tree). This is particularly important since we decided /// serialized with the tree). This is particularly important since we decided
/// to instantiate the trees from the genesis block, for simplicity. /// to instantiate the trees from the genesis block, for simplicity.
/// ///
/// [`Cell`] offers interior mutability (it works even with a non-mutable /// We use a [`RwLock`] for this cache, because it is only written once per tree update.
/// reference to the tree) but it prevents the tree (and anything that uses it) /// Each tree has its own cached root, a new lock is created for each clone.
/// from being shared between threads. If this ever becomes an issue we can cached_root: std::sync::RwLock<Option<Root>>,
/// leave caching to the callers (which requires much more code), or replace
/// `Cell` with `Arc<Mutex<_>>` (and be careful of deadlocks and async code.)
cached_root: Cell<Option<Root>>,
} }
impl NoteCommitmentTree { impl NoteCommitmentTree {
@ -266,7 +262,13 @@ impl NoteCommitmentTree {
pub fn append(&mut self, cm_u: jubjub::Fq) -> Result<(), NoteCommitmentTreeError> { pub fn append(&mut self, cm_u: jubjub::Fq) -> Result<(), NoteCommitmentTreeError> {
if self.inner.append(&cm_u.into()) { if self.inner.append(&cm_u.into()) {
// Invalidate cached root // Invalidate cached root
self.cached_root.replace(None); let cached_root = self
.cached_root
.get_mut()
.expect("a thread that previously held exclusive lock access panicked");
*cached_root = None;
Ok(()) Ok(())
} else { } else {
Err(NoteCommitmentTreeError::FullTree) Err(NoteCommitmentTreeError::FullTree)
@ -276,13 +278,28 @@ impl NoteCommitmentTree {
/// Returns the current root of the tree, used as an anchor in Sapling /// Returns the current root of the tree, used as an anchor in Sapling
/// shielded transactions. /// shielded transactions.
pub fn root(&self) -> Root { pub fn root(&self) -> Root {
match self.cached_root.get() { if let Some(root) = self
// Return cached root .cached_root
Some(root) => root, .read()
.expect("a thread that previously held exclusive lock access panicked")
.deref()
{
// Return cached root.
return *root;
}
// Get exclusive access, compute the root, and cache it.
let mut write_root = self
.cached_root
.write()
.expect("a thread that previously held exclusive lock access panicked");
match write_root.deref() {
// Another thread got write access first, return cached root.
Some(root) => *root,
None => { None => {
// Compute root and cache it // Compute root and cache it.
let root = Root::try_from(self.inner.root().0).unwrap(); let root = Root::try_from(self.inner.root().0).unwrap();
self.cached_root.replace(Some(root)); *write_root = Some(root);
root root
} }
} }
@ -311,6 +328,21 @@ impl NoteCommitmentTree {
} }
} }
impl Clone for NoteCommitmentTree {
/// Clones the inner tree, and creates a new `RwLock` with the cloned root data.
fn clone(&self) -> Self {
let cached_root = *self
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked");
Self {
inner: self.inner.clone(),
cached_root: std::sync::RwLock::new(cached_root),
}
}
}
impl Default for NoteCommitmentTree { impl Default for NoteCommitmentTree {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@ -10,9 +10,7 @@
//! //!
//! A root of a note commitment tree is associated with each treestate. //! A root of a note commitment tree is associated with each treestate.
#![allow(clippy::unit_arg)] use std::{fmt, ops::Deref};
use std::{cell::Cell, fmt};
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use incrementalmerkletree::{bridgetree, Frontier}; use incrementalmerkletree::{bridgetree, Frontier};
@ -201,7 +199,7 @@ pub enum NoteCommitmentTreeError {
/// ///
/// [Sprout Note Commitment Tree]: https://zips.z.cash/protocol/protocol.pdf#merkletree /// [Sprout Note Commitment Tree]: https://zips.z.cash/protocol/protocol.pdf#merkletree
/// [nullifier set]: https://zips.z.cash/protocol/protocol.pdf#nullifierset /// [nullifier set]: https://zips.z.cash/protocol/protocol.pdf#nullifierset
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct NoteCommitmentTree { pub struct NoteCommitmentTree {
/// The tree represented as a [`incrementalmerkletree::bridgetree::Frontier`]. /// The tree represented as a [`incrementalmerkletree::bridgetree::Frontier`].
/// ///
@ -232,13 +230,9 @@ pub struct NoteCommitmentTree {
/// the tree). This is particularly important since we decided to /// the tree). This is particularly important since we decided to
/// instantiate the trees from the genesis block, for simplicity. /// instantiate the trees from the genesis block, for simplicity.
/// ///
/// [`Cell`] offers interior mutability (it works even with a non-mutable /// We use a [`RwLock`] for this cache, because it is only written once per tree update.
/// reference to the tree) but it prevents the tree (and anything that uses /// Each tree has its own cached root, a new lock is created for each clone.
/// it) from being shared between threads. If this ever becomes an issue we cached_root: std::sync::RwLock<Option<Root>>,
/// can leave caching to the callers (which requires much more code), or
/// replace `Cell` with `Arc<Mutex<_>>` (and be careful of deadlocks and
/// async code.)
cached_root: Cell<Option<Root>>,
} }
impl NoteCommitmentTree { impl NoteCommitmentTree {
@ -248,7 +242,13 @@ impl NoteCommitmentTree {
pub fn append(&mut self, cm: NoteCommitment) -> Result<(), NoteCommitmentTreeError> { pub fn append(&mut self, cm: NoteCommitment) -> Result<(), NoteCommitmentTreeError> {
if self.inner.append(&cm.into()) { if self.inner.append(&cm.into()) {
// Invalidate cached root // Invalidate cached root
self.cached_root.replace(None); let cached_root = self
.cached_root
.get_mut()
.expect("a thread that previously held exclusive lock access panicked");
*cached_root = None;
Ok(()) Ok(())
} else { } else {
Err(NoteCommitmentTreeError::FullTree) Err(NoteCommitmentTreeError::FullTree)
@ -258,13 +258,28 @@ impl NoteCommitmentTree {
/// Returns the current root of the tree; used as an anchor in Sprout /// Returns the current root of the tree; used as an anchor in Sprout
/// shielded transactions. /// shielded transactions.
pub fn root(&self) -> Root { pub fn root(&self) -> Root {
match self.cached_root.get() { if let Some(root) = self
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked")
.deref()
{
// Return cached root. // Return cached root.
Some(root) => root, return *root;
}
// Get exclusive access, compute the root, and cache it.
let mut write_root = self
.cached_root
.write()
.expect("a thread that previously held exclusive lock access panicked");
match write_root.deref() {
// Another thread got write access first, return cached root.
Some(root) => *root,
None => { None => {
// Compute root and cache it. // Compute root and cache it.
let root = Root(self.inner.root().0); let root = Root(self.inner.root().0);
self.cached_root.replace(Some(root)); *write_root = Some(root);
root root
} }
} }
@ -294,6 +309,21 @@ impl NoteCommitmentTree {
} }
} }
impl Clone for NoteCommitmentTree {
/// Clones the inner tree, and creates a new `RwLock` with the cloned root data.
fn clone(&self) -> Self {
let cached_root = *self
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked");
Self {
inner: self.inner.clone(),
cached_root: std::sync::RwLock::new(cached_root),
}
}
}
impl Default for NoteCommitmentTree { impl Default for NoteCommitmentTree {
fn default() -> Self { fn default() -> Self {
Self { Self {