use sha2::digest::{FixedOutput, HashMarker, Output, OutputSizeUser, Reset, Update};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
static CAPTURES: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug)]
pub struct HashObservation(());
impl HashObservation {
#[must_use]
pub fn start() -> Self {
CAPTURES.fetch_add(1, Ordering::Relaxed);
Self(())
}
}
impl Drop for HashObservation {
fn drop(&mut self) {
CAPTURES.fetch_sub(1, Ordering::Relaxed);
}
}
fn active() -> bool {
CAPTURES.load(Ordering::Relaxed) != 0
}
use std::time::Instant;
static BYTES: AtomicU64 = AtomicU64::new(0);
static NANOS: AtomicU64 = AtomicU64::new(0);
#[derive(Clone)]
pub struct ObservedSha256(
sha2::Sha256,
#[cfg(any(test, feature = "test-support"))] HashDomain,
);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HashDomain {
ArtifactPayload,
ContractIdentity,
ControlAuthentication,
PortableAuthentication,
OptionalEvidence,
Unclassified,
}
impl Default for ObservedSha256 {
fn default() -> Self {
Self::for_domain(HashDomain::Unclassified)
}
}
impl ObservedSha256 {
#[must_use]
pub fn for_domain(domain: HashDomain) -> Self {
#[cfg(not(any(test, feature = "test-support")))]
let _ = domain;
Self(
sha2::Sha256::default(),
#[cfg(any(test, feature = "test-support"))]
domain,
)
}
}
#[derive(Clone)]
pub struct DomainSha256<const DOMAIN: u8>(ObservedSha256);
pub type ArtifactSha256 = DomainSha256<0>;
pub type ContractSha256 = DomainSha256<1>;
pub type ControlSha256 = DomainSha256<2>;
pub type EvidenceSha256 = DomainSha256<3>;
pub type PortableSha256 = DomainSha256<4>;
impl<const DOMAIN: u8> Default for DomainSha256<DOMAIN> {
fn default() -> Self {
let domain = match DOMAIN {
0 => HashDomain::ArtifactPayload,
1 => HashDomain::ContractIdentity,
2 => HashDomain::ControlAuthentication,
3 => HashDomain::OptionalEvidence,
4 => HashDomain::PortableAuthentication,
_ => HashDomain::Unclassified,
};
Self(ObservedSha256::for_domain(domain))
}
}
impl<const DOMAIN: u8> OutputSizeUser for DomainSha256<DOMAIN> {
type OutputSize = <sha2::Sha256 as OutputSizeUser>::OutputSize;
}
impl<const DOMAIN: u8> HashMarker for DomainSha256<DOMAIN> {}
impl<const DOMAIN: u8> Update for DomainSha256<DOMAIN> {
fn update(&mut self, data: &[u8]) {
Update::update(&mut self.0, data);
}
}
impl<const DOMAIN: u8> FixedOutput for DomainSha256<DOMAIN> {
fn finalize_into(self, out: &mut Output<Self>) {
FixedOutput::finalize_into(self.0, out);
}
}
impl<const DOMAIN: u8> Reset for DomainSha256<DOMAIN> {
fn reset(&mut self) {
Reset::reset(&mut self.0);
}
}
impl OutputSizeUser for ObservedSha256 {
type OutputSize = <sha2::Sha256 as OutputSizeUser>::OutputSize;
}
impl HashMarker for ObservedSha256 {}
impl Update for ObservedSha256 {
fn update(&mut self, data: &[u8]) {
#[cfg(any(test, feature = "test-support"))]
operation::record_hash(self.1, data.len() as u64);
if !active() {
Update::update(&mut self.0, data);
return;
}
let started = Instant::now();
Update::update(&mut self.0, data);
NANOS.fetch_add(
u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX),
Ordering::Relaxed,
);
BYTES.fetch_add(data.len() as u64, Ordering::Relaxed);
}
}
impl FixedOutput for ObservedSha256 {
fn finalize_into(self, out: &mut Output<Self>) {
if !active() {
FixedOutput::finalize_into(self.0, out);
return;
}
let started = Instant::now();
FixedOutput::finalize_into(self.0, out);
NANOS.fetch_add(
u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX),
Ordering::Relaxed,
);
}
}
impl Reset for ObservedSha256 {
fn reset(&mut self) {
Reset::reset(&mut self.0);
}
}
#[must_use]
pub fn totals() -> (u64, u64) {
(BYTES.load(Ordering::Relaxed), NANOS.load(Ordering::Relaxed))
}
#[derive(Clone, Default)]
pub struct OperationContext(#[cfg(any(test, feature = "test-support"))] operation::Context);
impl OperationContext {
#[must_use]
pub fn capture() -> Self {
Self(
#[cfg(any(test, feature = "test-support"))]
operation::Context::capture(),
)
}
#[must_use]
pub fn attach(&self) -> OperationGuard {
OperationGuard {
#[cfg(any(test, feature = "test-support"))]
_guard: self.0.attach(),
}
}
}
pub struct OperationGuard {
#[cfg(any(test, feature = "test-support"))]
_guard: operation::Guard,
}
#[inline]
pub fn record_topology_projection() {
#[cfg(any(test, feature = "test-support"))]
operation::record_topology_projection();
}
#[inline]
pub fn record_composite_request_fingerprint() {
#[cfg(any(test, feature = "test-support"))]
operation::record_composite_request_fingerprint();
}
#[cfg(any(test, feature = "test-support"))]
pub mod operation {
use super::HashDomain;
use std::cell::RefCell;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Default)]
struct Counters {
hash: [AtomicU64; 6],
checksum: AtomicU64,
topology_projections: AtomicU64,
composite_request_fingerprints: AtomicU64,
}
thread_local! {
static CURRENT: RefCell<Option<Arc<Counters>>> = const { RefCell::new(None) };
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Snapshot {
pub artifact_payload_sha256_bytes: u64,
pub contract_identity_sha256_bytes: u64,
pub control_authentication_sha256_bytes: u64,
pub portable_authentication_sha256_bytes: u64,
pub optional_evidence_sha256_bytes: u64,
pub unclassified_sha256_bytes: u64,
pub checksum_bytes: u64,
pub topology_projections: u64,
pub composite_request_fingerprints: u64,
}
#[derive(Clone, Default)]
pub struct Context(Option<Arc<Counters>>);
impl Context {
#[must_use]
pub fn capture() -> Self {
CURRENT.with(|current| Self(current.borrow().clone()))
}
#[must_use]
pub fn attach(&self) -> Guard {
let previous = CURRENT.with(|current| current.replace(self.0.clone()));
Guard {
previous,
_thread: std::marker::PhantomData,
}
}
}
pub struct Guard {
previous: Option<Arc<Counters>>,
_thread: std::marker::PhantomData<std::rc::Rc<()>>,
}
impl Drop for Guard {
fn drop(&mut self) {
CURRENT.with(|current| {
current.replace(self.previous.take());
});
}
}
pub struct Capture {
counters: Arc<Counters>,
_guard: Guard,
}
impl Capture {
#[must_use]
pub fn start() -> Self {
let counters = Arc::new(Counters::default());
let context = Context(Some(Arc::clone(&counters)));
Self {
counters,
_guard: context.attach(),
}
}
#[must_use]
pub fn context(&self) -> Context {
Context(Some(Arc::clone(&self.counters)))
}
#[must_use]
pub fn snapshot(&self) -> Snapshot {
let load = |index: usize| self.counters.hash[index].load(Ordering::Relaxed);
Snapshot {
artifact_payload_sha256_bytes: load(0),
contract_identity_sha256_bytes: load(1),
control_authentication_sha256_bytes: load(2),
optional_evidence_sha256_bytes: load(3),
portable_authentication_sha256_bytes: load(4),
unclassified_sha256_bytes: load(5),
checksum_bytes: self.counters.checksum.load(Ordering::Relaxed),
topology_projections: self.counters.topology_projections.load(Ordering::Relaxed),
composite_request_fingerprints: self
.counters
.composite_request_fingerprints
.load(Ordering::Relaxed),
}
}
}
pub(super) fn record_hash(domain: HashDomain, bytes: u64) {
let index = match domain {
HashDomain::ArtifactPayload => 0,
HashDomain::ContractIdentity => 1,
HashDomain::ControlAuthentication => 2,
HashDomain::OptionalEvidence => 3,
HashDomain::PortableAuthentication => 4,
HashDomain::Unclassified => 5,
};
CURRENT.with(|current| {
if let Some(counters) = current.borrow().as_ref() {
counters.hash[index].fetch_add(bytes, Ordering::Relaxed);
}
});
}
pub fn record_checksum(bytes: u64) {
CURRENT.with(|current| {
if let Some(counters) = current.borrow().as_ref() {
counters.checksum.fetch_add(bytes, Ordering::Relaxed);
}
});
}
pub(super) fn record_topology_projection() {
CURRENT.with(|current| {
if let Some(counters) = current.borrow().as_ref() {
counters
.topology_projections
.fetch_add(1, Ordering::Relaxed);
}
});
}
pub(super) fn record_composite_request_fingerprint() {
CURRENT.with(|current| {
if let Some(counters) = current.borrow().as_ref() {
counters
.composite_request_fingerprints
.fetch_add(1, Ordering::Relaxed);
}
});
}
}
#[cfg(test)]
mod tests {
use super::{
HashDomain, ObservedSha256, OperationContext, PortableSha256,
operation::{Capture, Snapshot},
};
use crate::canonical::{CanonicalDomain, fingerprint};
use sha2::Digest;
#[test]
fn portable_authentication_keeps_all_operation_domains_disjoint() {
let capture = Capture::start();
for (domain, bytes) in [
(HashDomain::ArtifactPayload, 2),
(HashDomain::ContractIdentity, 3),
(HashDomain::ControlAuthentication, 5),
(HashDomain::OptionalEvidence, 7),
(HashDomain::PortableAuthentication, 11),
(HashDomain::Unclassified, 13),
] {
let mut hash = ObservedSha256::for_domain(domain);
hash.update(vec![0_u8; bytes]);
hash.finalize();
}
assert_eq!(
capture.snapshot(),
Snapshot {
artifact_payload_sha256_bytes: 2,
contract_identity_sha256_bytes: 3,
control_authentication_sha256_bytes: 5,
portable_authentication_sha256_bytes: 11,
optional_evidence_sha256_bytes: 7,
unclassified_sha256_bytes: 13,
checksum_bytes: 0,
topology_projections: 0,
composite_request_fingerprints: 0,
}
);
}
#[test]
fn portable_sha256_preserves_digest_and_counts_clone_and_reset_inputs() {
let capture = Capture::start();
let mut hash = PortableSha256::new();
hash.update(b"abc");
let mut cloned = hash.clone();
cloned.update(b"!");
assert_eq!(cloned.finalize(), sha2::Sha256::digest(b"abc!"));
sha2::digest::Reset::reset(&mut hash);
hash.update(b"portable");
assert_eq!(hash.finalize(), sha2::Sha256::digest(b"portable"));
assert_eq!(
capture.snapshot(),
Snapshot {
portable_authentication_sha256_bytes: 12,
..Snapshot::default()
}
);
}
#[test]
fn portable_captures_isolate_workers_and_restore_nested_and_detached_scopes() {
let outer = Capture::start();
let context = OperationContext::capture();
let independent = std::thread::spawn(|| {
let capture = Capture::start();
PortableSha256::digest([0_u8; 29]);
let context = OperationContext::capture();
std::thread::spawn(move || {
let _attached = context.attach();
PortableSha256::digest([0_u8; 31]);
})
.join()
.unwrap();
capture.snapshot()
});
std::thread::spawn(move || {
{
let _attached = context.attach();
PortableSha256::digest([0_u8; 17]);
{
let nested = Capture::start();
PortableSha256::digest([0_u8; 11]);
assert_eq!(
nested.snapshot(),
Snapshot {
portable_authentication_sha256_bytes: 11,
..Snapshot::default()
}
);
}
PortableSha256::digest([0_u8; 13]);
}
PortableSha256::digest([0_u8; 7]);
})
.join()
.unwrap();
assert_eq!(
outer.snapshot(),
Snapshot {
portable_authentication_sha256_bytes: 30,
..Snapshot::default()
}
);
assert_eq!(
independent.join().unwrap(),
Snapshot {
portable_authentication_sha256_bytes: 60,
..Snapshot::default()
}
);
}
#[test]
fn operation_captures_isolate_parallel_workers_and_restore_nested_scopes() {
let outer = Capture::start();
let first_context = outer.context();
let second = std::thread::spawn(|| {
let capture = Capture::start();
let context = capture.context();
std::thread::spawn(move || {
let _attached = context.attach();
let mut hash = ObservedSha256::for_domain(HashDomain::ArtifactPayload);
hash.update([0_u8; 29]);
hash.finalize();
fingerprint(CanonicalDomain::CompositeRequest, 1, b"second worker").unwrap();
super::record_composite_request_fingerprint();
})
.join()
.unwrap();
capture.snapshot()
});
std::thread::spawn(move || {
let _attached = first_context.attach();
let mut hash = ObservedSha256::for_domain(HashDomain::ArtifactPayload);
hash.update([0_u8; 17]);
hash.finalize();
fingerprint(CanonicalDomain::CompositeRequest, 1, b"first worker").unwrap();
super::record_composite_request_fingerprint();
{
let nested = Capture::start();
let mut hash = ObservedSha256::for_domain(HashDomain::ControlAuthentication);
hash.update([0_u8; 11]);
hash.finalize();
fingerprint(CanonicalDomain::CompositeRequest, 1, b"nested").unwrap();
assert_eq!(nested.snapshot().composite_request_fingerprints, 0);
super::record_composite_request_fingerprint();
fingerprint(
CanonicalDomain::CompositeGraphMutationContent,
1,
b"another domain",
)
.unwrap();
assert_eq!(nested.snapshot().control_authentication_sha256_bytes, 11);
assert_eq!(nested.snapshot().composite_request_fingerprints, 1);
}
super::operation::record_checksum(7);
})
.join()
.unwrap();
assert_eq!(outer.snapshot().artifact_payload_sha256_bytes, 17);
assert_eq!(outer.snapshot().control_authentication_sha256_bytes, 0);
assert_eq!(outer.snapshot().checksum_bytes, 7);
assert_eq!(outer.snapshot().composite_request_fingerprints, 1);
let second = second.join().unwrap();
assert_eq!(second.artifact_payload_sha256_bytes, 29);
assert_eq!(second.composite_request_fingerprints, 1);
}
}