perf(zcash): reuse checked batch signability
What changed, and why it matters
This commit is a performance improvement for Zcash batch signing on the Keystone 3 hardware wallet. It avoids re-checking the same transaction data twice by caching a 'signability decision' from the review step and reusing it during signing. The change also adds integrity checks so the cached decision can only be used with the exact normalized transaction bytes and wallet context it was created for. The CHANGELOG frames it as an improvement and a bug fix for stalled signing when QR generation fails, not as a security fix.
No immediate action required. Treat as a hardening/performance change. Reviewers may want to confirm that the SHA-256 digest comparison and seed fingerprint/account index binding are sufficient to prevent reuse of cached signability decisions across different transactions or wallet contexts, and that the canonical round-trip serialization in the C bridge does not introduce malleability.
Security signals we found
Adds SHA-256 digest binding of cached signability decision to normalized PCZT bytes
Adds seed fingerprint and account index binding for cached batch signability decisions
Reuses previously validated signable shielded action list at signing time instead of recomputing it
Low-level signer still independently verifies each action's derivation and rk against the seed-derived key
CHANGELOG describes change as performance improvement and bug fix for stalled signing, not as a security vulnerability fix
Evidence from the diff
The patch refactors Zcash batch PCZT (Partially Created Zcash Transaction) handling so that check_batch_pczt_with_display returns a new CheckedBatchPcztSignability object alongside normalized bytes, parsed display rows, and migration summary. This object contains a SHA-256 digest of the normalized PCZT and a list of SignableShieldedActions. A new signing function sign_checked_batch_pczt_with_cached_signability accepts only the cached signability object and verifies the digest matches the provided PCZT before signing. The C bridge stores the per-PCZT signability decisions in BatchDisplayCache, bound to seed_fingerprint and account_index, and validates those match at signing time. The existing low-level signer still independently matches each action’s derivation and rk to the seed-derived key.
Changed components
rust/apps/zcash/src/lib.rsrust/rust_c/src/zcash/mod.rsrust/rust_c/src/zcash/structs.rsZcash batch PCZT signing flow (cypherpunk feature)Inspect captured patch +230 / −42
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 886c206..d50eb32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@
1. Added support for Zcash batch PCZT signing
+### Improvements
+
+1. Reduced repeated processing during Zcash batch signing
+
### Bug Fixes
1. Fixed stalled Zcash signing when response QR generation fails
diff --git a/rust/apps/zcash/src/lib.rs b/rust/apps/zcash/src/lib.rs
index ca53ba8..bd6f9e0 100644
--- a/rust/apps/zcash/src/lib.rs
+++ b/rust/apps/zcash/src/lib.rs
@@ -30,6 +30,8 @@ use zcash_vendor::{
use zcash_vendor::pczt::roles::signer::SpendAuthSignature;
#[cfg(any(test, feature = "multi_coins", feature = "cypherpunk"))]
use zcash_vendor::pczt::Pczt;
+#[cfg(feature = "cypherpunk")]
+use zcash_vendor::sha2::{Digest, Sha256};
#[cfg(feature = "cypherpunk")]
use zcash_vendor::zcash_protocol::consensus::NetworkConstants;
@@ -322,7 +324,7 @@ fn check_and_parse_batch_pczt_internal<P: consensus::Parameters>(
ctx: &BatchCheckContext<'_>,
seed_fingerprint: &[u8; 32],
account_index: u32,
-) -> Result<(Pczt, ParsedPczt)> {
+) -> Result<(Pczt, ParsedPczt, Vec<SignableShieldedAction>)> {
let mut pczt = pczt::parse_pczt(pczt_bytes)?;
// Resolve compact field representations before the single-pass validation.
pczt.resolve_fields().map_err(|e| {
@@ -388,13 +390,14 @@ fn check_and_parse_batch_pczt_internal<P: consensus::Parameters>(
checked_orchard,
checked_ironwood,
)?;
- Ok((pczt, parsed))
+ Ok((pczt, parsed, signable_actions))
}
-/// Checks one batch PCZT and returns normalized bytes, display rows, and an
-/// optional compact migration classification. Validation and display share one
-/// shielded action pass, while the context reuses viewing keys across the batch.
-/// `None` means the caller must retain the ordinary display for this PCZT.
+/// Checks one batch PCZT and returns normalized bytes, display rows, an optional
+/// compact migration classification, and an opaque signability decision bound
+/// to the normalized bytes and check context. Validation, display, and
+/// signability share one shielded action pass; signing reuses that decision
+/// instead of rebuilding the shielded bundles.
#[cfg(feature = "cypherpunk")]
pub fn check_batch_pczt_with_display<P: consensus::Parameters>(
params: &P,
@@ -402,23 +405,27 @@ pub fn check_batch_pczt_with_display<P: consensus::Parameters>(
ctx: &BatchCheckContext<'_>,
seed_fingerprint: &[u8; 32],
account_index: u32,
-) -> Result<(Vec<u8>, ParsedPczt, Option<BatchMigrationTransferSummary>)> {
- let (pczt, parsed) = check_and_parse_batch_pczt_internal(
+) -> Result<(
+ Vec<u8>,
+ ParsedPczt,
+ Option<BatchMigrationTransferSummary>,
+ CheckedBatchPcztSignability,
+)> {
+ let (pczt, parsed, actions) = check_and_parse_batch_pczt_internal(
params,
pczt_bytes,
ctx,
seed_fingerprint,
account_index,
)?;
-
// Classify from the complete display model produced by this check pass.
// The check already bound every funded spend to the selected account.
let migration_summary = migration_transfer_summary(&parsed);
-
let normalized = pczt
.serialize()
.map_err(|e| ZcashError::InvalidPczt(alloc::format!("serialize normalized PCZT: {e:?}")))?;
- Ok((normalized, parsed, migration_summary))
+ let signability = CheckedBatchPcztSignability::new(&normalized, actions);
+ Ok((normalized, parsed, migration_summary, signability))
}
/// Checks and parses one batch PCZT using the shared check-time display path.
@@ -430,7 +437,7 @@ pub fn check_and_parse_batch_pczt_cypherpunk<P: consensus::Parameters>(
seed_fingerprint: &[u8; 32],
account_index: u32,
) -> Result<ParsedPczt> {
- let (_, parsed, _) = check_batch_pczt_with_display(
+ let (_, parsed, _, _) = check_batch_pczt_with_display(
params,
pczt_bytes,
&BatchCheckContext::new(ufvk_text),
@@ -853,6 +860,38 @@ struct SignableShieldedAction {
index: usize,
}
+/// Opaque signability result produced by the full batch check and bound to the
+/// exact normalized PCZT bytes that produced it.
+#[cfg(feature = "cypherpunk")]
+#[derive(Debug)]
+pub struct CheckedBatchPcztSignability {
+ pczt_digest: [u8; 32],
+ required_actions: Vec<SignableShieldedAction>,
+}
+
+#[cfg(feature = "cypherpunk")]
+impl CheckedBatchPcztSignability {
+ fn new(checked_pczt: &[u8], required_actions: Vec<SignableShieldedAction>) -> Self {
+ Self {
+ pczt_digest: Sha256::digest(checked_pczt).into(),
+ required_actions,
+ }
+ }
+
+ fn required_actions(&self, checked_pczt: &[u8]) -> Result<&[SignableShieldedAction]> {
+ let pczt_digest: [u8; 32] = Sha256::digest(checked_pczt).into();
+ if self.pczt_digest != pczt_digest {
+ return Err(ZcashError::InvalidDataError(
+ "checked batch signability does not match the PCZT".to_string(),
+ ));
+ }
+ if self.required_actions.is_empty() {
+ return Err(ZcashError::PcztNoMyInputs);
+ }
+ Ok(&self.required_actions)
+ }
+}
+
#[cfg(feature = "cypherpunk")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ShieldedActionPolicy {
@@ -1189,6 +1228,23 @@ pub fn sign_checked_batch_pczt_with_cache<P: consensus::Parameters>(
)
}
+/// Signs one firmware-owned checked batch PCZT using the signability decision
+/// retained by [`check_batch_pczt_with_display`]. The decision is accepted only
+/// for those exact normalized bytes. The low-level signer still independently
+/// matches each action's derivation and `rk` to the seed-derived signing key.
+#[cfg(feature = "cypherpunk")]
+pub fn sign_checked_batch_pczt_with_cached_signability(
+ checked_pczt: &[u8],
+ checked_signability: &CheckedBatchPcztSignability,
+ seed: &[u8],
+ ask_cache: &SpendAuthCache,
+) -> Result<Vec<u8>> {
+ let required_actions = checked_signability.required_actions(checked_pczt)?;
+ let pczt = pczt::parse_pczt(checked_pczt)?;
+ reject_unsupported_batch_pczt(&pczt)?;
+ sign_pczt_with_required_actions(pczt, seed, required_actions, ask_cache)
+}
+
#[cfg(feature = "cypherpunk")]
#[allow(clippy::too_many_arguments)]
fn sign_checked_pczt_with_policy<P: consensus::Parameters>(
@@ -1208,11 +1264,21 @@ fn sign_checked_pczt_with_policy<P: consensus::Parameters>(
if policy == ShieldedActionPolicy::Batch && signable_actions.is_empty() {
return Err(ZcashError::PcztNoMyInputs);
}
+ sign_pczt_with_required_actions(pczt, seed, &signable_actions, ask_cache)
+}
+
+#[cfg(feature = "cypherpunk")]
+fn sign_pczt_with_required_actions(
+ pczt: Pczt,
+ seed: &[u8],
+ required_actions: &[SignableShieldedAction],
+ ask_cache: &SpendAuthCache,
+) -> Result<Vec<u8>> {
let signed = pczt::sign::sign_and_redact_pczt_with_cache(pczt, seed, ask_cache)?;
- let signed = if signable_actions.is_empty() {
+ let signed = if required_actions.is_empty() {
signed
} else {
- ensure_shielded_actions_are_signed(signed, &signable_actions)?
+ ensure_shielded_actions_are_signed(signed, required_actions)?
};
signed
.serialize()
@@ -1914,6 +1980,48 @@ mod tests {
.any(|sig| sig.value_pool() == zcash_vendor::orchard::ValuePool::Ironwood));
}
+ #[test]
+ fn test_cached_batch_signability_signs_canonical_checked_pczt() {
+ let sample = pczt::test_support::sample_orchard_change_pczt();
+ let (normalized, _, _, signability) = check_batch_pczt_with_display(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &BatchCheckContext::new(&sample.ufvk_text),
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ // The C bridge performs this canonical round trip when rebuilding the
+ // checked batch envelope.
+ let canonical = Pczt::parse(&normalized).unwrap().serialize().unwrap();
+ let signed = sign_checked_batch_pczt_with_cached_signability(
+ &canonical,
+ &signability,
+ &sample.seed,
+ &SpendAuthCache::new(),
+ )
+ .unwrap();
+ assert_eq!(
+ extract_compact_sigs_from_signed_pczt(&signed)
+ .unwrap()
+ .len(),
+ 2,
+ "the signer must still sign the wallet-controlled zero-value action"
+ );
+
+ let mut different_pczt = canonical;
+ different_pczt[0] ^= 1;
+ assert!(matches!(
+ sign_checked_batch_pczt_with_cached_signability(
+ &different_pczt,
+ &signability,
+ &sample.seed,
+ &SpendAuthCache::new(),
+ ),
+ Err(ZcashError::InvalidDataError(_))
+ ));
+ }
+
#[test]
fn test_sign_checked_pczt_signs_owned_orchard_actions() {
let sample = pczt::test_support::sample_orchard_change_pczt();
@@ -2045,7 +2153,7 @@ mod tests {
fn test_batch_check_and_parse_accepts_ironwood_spend() {
let sample = pczt::test_support::sample_ironwood_pczt();
- let (_, parsed, _) = check_batch_pczt_with_display(
+ let (_, parsed, _, _) = check_batch_pczt_with_display(
&pczt::test_support::Nu6_3Network,
&sample.bytes,
&BatchCheckContext::new(&sample.ufvk_text),
@@ -2072,7 +2180,7 @@ mod tests {
fn test_batch_check_and_parse_accepts_orchard_to_ironwood_migration() {
let sample = pczt::test_support::sample_migration_pczt();
- let (_, parsed, _) = check_batch_pczt_with_display(
+ let (_, parsed, _, _) = check_batch_pczt_with_display(
&pczt::test_support::Nu6_3Network,
&sample.bytes,
&BatchCheckContext::new(&sample.ufvk_text),
@@ -2868,7 +2976,7 @@ mod tests {
0,
)
.unwrap();
- let (bytes, _parsed, _summary) = check_batch_pczt_with_display(
+ let (bytes, _parsed, _summary, _) = check_batch_pczt_with_display(
&pczt::test_support::Nu6_3Network,
&sample.bytes,
&ctx,
@@ -2894,7 +3002,7 @@ mod tests {
pczt::test_support::sample_ironwood_pczt(),
pczt::test_support::sample_migration_pczt(),
] {
- let (bytes, parsed, _summary) = check_batch_pczt_with_display(
+ let (bytes, parsed, _summary, _) = check_batch_pczt_with_display(
&pczt::test_support::Nu6_3Network,
&sample.bytes,
&BatchCheckContext::new(&sample.ufvk_text),
@@ -2921,7 +3029,7 @@ mod tests {
#[test]
fn test_check_batch_with_display_caches_migration_classification() {
let sample = pczt::test_support::sample_migration_pczt();
- let (_bytes, _parsed, summary) = check_batch_pczt_with_display(
+ let (_bytes, _parsed, summary, _) = check_batch_pczt_with_display(
&pczt::test_support::Nu6_3Network,
&sample.bytes,
&BatchCheckContext::new(&sample.ufvk_text),
@@ -2946,7 +3054,7 @@ mod tests {
let sample = pczt::test_support::sample_migration_pczt_with_output_memo(
MemoBytes::from_bytes(b"covert note").expect("memo text fits"),
);
- let (_bytes, parsed, summary) = check_batch_pczt_with_display(
+ let (_bytes, parsed, summary, _) = check_batch_pczt_with_display(
&pczt::test_support::Nu6_3Network,
&sample.bytes,
&BatchCheckContext::new(&sample.ufvk_text),
diff --git a/rust/rust_c/src/zcash/mod.rs b/rust/rust_c/src/zcash/mod.rs
index 3105909..c5263a1 100644
--- a/rust/rust_c/src/zcash/mod.rs
+++ b/rust/rust_c/src/zcash/mod.rs
@@ -393,12 +393,13 @@ pub unsafe extern "C" fn check_zcash_batch_tx_cypherpunk(
Err(e) => return TransactionCheckResult::from(e).c_ptr(),
};
- // Each check returns normalized bytes, display rows, and an optional compact
- // migration classification from one trial-decrypt pass. Parse later converts
- // the cached rows.
+ // Each check returns normalized bytes, display rows, an optional compact
+ // migration classification, and the bound signability decision from the
+ // same shielded action pass.
let mut checked_pczts = Vec::with_capacity(payloads.len());
let mut rows: Vec<ParsedPczt> = Vec::with_capacity(payloads.len());
let mut migration_summaries = Vec::with_capacity(payloads.len());
+ let mut signability = Vec::with_capacity(payloads.len());
// One check context for the whole batch: the UFVK decode and the wallet
// Orchard key derivation depend only on the device UFVK, so they run once
// here instead of once per PCZT (see BatchCheckContext).
@@ -411,7 +412,7 @@ pub unsafe extern "C" fn check_zcash_batch_tx_cypherpunk(
seed_fingerprint,
account_index,
) {
- Ok((normalized, parsed, migration_summary)) => {
+ Ok((normalized, parsed, migration_summary, checked_signability)) => {
let pczt = match Pczt::parse(&normalized) {
Ok(pczt) => pczt,
Err(e) => {
@@ -424,6 +425,7 @@ pub unsafe extern "C" fn check_zcash_batch_tx_cypherpunk(
checked_pczts.push(pczt);
rows.push(parsed);
migration_summaries.push(migration_summary);
+ signability.push(checked_signability);
}
Err(e) => return TransactionCheckResult::from(e).c_ptr(),
}
@@ -434,7 +436,8 @@ pub unsafe extern "C" fn check_zcash_batch_tx_cypherpunk(
let display_rows = app_zcash::compact_checked_batch_migration_review(
rows.into_iter().zip(migration_summaries),
);
- let display = BatchDisplayCache::new(display_rows);
+ let display =
+ BatchDisplayCache::new(display_rows, signability, *seed_fingerprint, account_index);
// Rebuild the Postcard request around the normalized PCZTs so parse/sign
// consume exactly what was checked, then preserve the outer request id for
@@ -531,8 +534,24 @@ unsafe fn sign_zcash_batch_tx_cypherpunk_dynamic(
.c_ptr();
}
let checked = extract_ptr_with_type!(checked_batch, ZcashCheckedPczt);
+ if checked.display.is_null() {
+ return UREncodeResult::from(RustCError::InvalidData(
+ "no checked Zcash batch signability available".to_string(),
+ ))
+ .c_ptr();
+ }
let expected_seed_fingerprint = extract_array!(seed_fingerprint, u8, 32);
let expected_seed_fingerprint: &[u8; 32] = expected_seed_fingerprint.try_into().unwrap();
+ let checked_signability =
+ match (&*checked.display).signability(expected_seed_fingerprint, account_index) {
+ Some(signability) => signability,
+ None => {
+ return UREncodeResult::from(RustCError::InvalidData(
+ "checked Zcash batch signing context mismatch".to_string(),
+ ))
+ .c_ptr();
+ }
+ };
let mut seed = extract_array_mut!(seed, u8, seed_len as usize);
let result = match checked.checked_bytes() {
@@ -543,6 +562,13 @@ unsafe fn sign_zcash_batch_tx_cypherpunk_dynamic(
seed.zeroize();
return UREncodeResult::from(RustCError::MasterFingerprintMismatch).c_ptr();
}
+ if checked_signability.len() != batch.pczts().len() {
+ seed.zeroize();
+ return UREncodeResult::from(RustCError::InvalidData(
+ "checked Zcash batch signability count mismatch".to_string(),
+ ))
+ .c_ptr();
+ }
let mut results = Vec::new();
let mut error = None;
@@ -550,7 +576,8 @@ unsafe fn sign_zcash_batch_tx_cypherpunk_dynamic(
// selected account key stays cached across every batch PCZT.
let ask_cache = SpendAuthCache::new();
// Preserve request order and emit nothing unless every PCZT signs.
- for pczt in batch.pczts() {
+ for (pczt, checked_signability) in batch.pczts().iter().zip(checked_signability)
+ {
let payload = match serialize_batch_pczt(pczt) {
Ok(payload) => payload,
Err(e) => {
@@ -558,12 +585,10 @@ unsafe fn sign_zcash_batch_tx_cypherpunk_dynamic(
break;
}
};
- match app_zcash::sign_checked_batch_pczt_with_cache(
- &MainNetwork,
+ match app_zcash::sign_checked_batch_pczt_with_cached_signability(
&payload,
+ checked_signability,
seed,
- &seed_fingerprint,
- account_index,
&ask_cache,
) {
Ok(payload) => {
@@ -1199,11 +1224,16 @@ mod tests {
#[cfg(feature = "cypherpunk")]
#[test]
fn test_parse_zcash_batch_reads_display_cache() {
- let items = batch_display_items(&BatchDisplayCache::new(vec![
- sample_parsed_pczt(),
- sample_parsed_pczt(),
- sample_parsed_pczt(),
- ]));
+ let items = batch_display_items(&BatchDisplayCache::new(
+ vec![
+ sample_parsed_pczt(),
+ sample_parsed_pczt(),
+ sample_parsed_pczt(),
+ ],
+ Vec::new(),
+ [0; 32],
+ 0,
+ ));
assert_eq!(
items.len(),
3,
@@ -1213,7 +1243,12 @@ mod tests {
unsafe { item.free() };
}
- let cache = BatchDisplayCache::new(vec![sample_parsed_pczt(), sample_parsed_pczt()]);
+ let cache = BatchDisplayCache::new(
+ vec![sample_parsed_pczt(), sample_parsed_pczt()],
+ Vec::new(),
+ [0; 32],
+ 0,
+ );
let checked_ptr =
ZcashCheckedPczt::new_with_display(b"normalized-batch-bytes".to_vec(), cache).c_ptr();
let result = unsafe {
@@ -1240,7 +1275,12 @@ mod tests {
#[cfg(feature = "cypherpunk")]
#[test]
fn test_free_cache_bearing_container_is_clean() {
- let cache = BatchDisplayCache::new(vec![sample_parsed_pczt(), sample_parsed_pczt()]);
+ let cache = BatchDisplayCache::new(
+ vec![sample_parsed_pczt(), sample_parsed_pczt()],
+ Vec::new(),
+ [0; 32],
+ 0,
+ );
let checked_ptr =
ZcashCheckedPczt::new_with_display(b"normalized-batch-bytes".to_vec(), cache).c_ptr();
unsafe { free_zcash_checked_pczt(checked_ptr) };
diff --git a/rust/rust_c/src/zcash/structs.rs b/rust/rust_c/src/zcash/structs.rs
index cd133e6..73319ce 100644
--- a/rust/rust_c/src/zcash/structs.rs
+++ b/rust/rust_c/src/zcash/structs.rs
@@ -12,6 +12,8 @@ use alloc::{string::ToString, vec::Vec};
use app_zcash::pczt::structs::{
ParsedFrom, ParsedOrchard, ParsedPczt, ParsedTo, ParsedTransparent,
};
+#[cfg(feature = "cypherpunk")]
+use app_zcash::CheckedBatchPcztSignability;
use cstr_core;
#[repr(C)]
@@ -216,8 +218,9 @@ impl_c_ptrs!(
DisplayOrchard
);
-/// The batch display rows produced by the check pass and converted to FFI
-/// structs by the batch parse FFI, so parse no longer re-decrypts every output.
+/// The batch display rows and per-PCZT signability decisions produced by the
+/// check pass. Parse converts the rows without re-decrypting outputs; signing
+/// reuses the bound decisions without reclassifying shielded actions.
///
/// Opaque to C: this is a plain Rust struct (deliberately not `#[repr(C)]`), and C
/// only ever holds it behind the [`ZcashCheckedPczt::display`] pointer without
@@ -226,19 +229,43 @@ impl_c_ptrs!(
#[cfg(feature = "cypherpunk")]
pub struct BatchDisplayCache {
rows: Vec<ParsedPczt>,
+ signability: Vec<CheckedBatchPcztSignability>,
+ seed_fingerprint: [u8; 32],
+ account_index: u32,
}
#[cfg(feature = "cypherpunk")]
impl BatchDisplayCache {
- /// Stores the final review rows after any migration compaction.
- pub fn new(rows: Vec<ParsedPczt>) -> Self {
- Self { rows }
+ /// Stores final review rows and one bound signability decision for every
+ /// checked PCZT, in request order.
+ pub fn new(
+ rows: Vec<ParsedPczt>,
+ signability: Vec<CheckedBatchPcztSignability>,
+ seed_fingerprint: [u8; 32],
+ account_index: u32,
+ ) -> Self {
+ Self {
+ rows,
+ signability,
+ seed_fingerprint,
+ account_index,
+ }
}
/// Returns the final review rows that batch parse converts for the C UI.
pub fn rows(&self) -> &[ParsedPczt] {
&self.rows
}
+
+ /// Returns the decisions only for the wallet context used at check.
+ pub fn signability(
+ &self,
+ seed_fingerprint: &[u8; 32],
+ account_index: u32,
+ ) -> Option<&[CheckedBatchPcztSignability]> {
+ (self.seed_fingerprint == *seed_fingerprint && self.account_index == account_index)
+ .then_some(&self.signability)
+ }
}
/// Normalized transaction bytes verified during check and retained by C between
@@ -322,4 +349,13 @@ mod tests {
assert_eq!(bytes, b"normalized-bytes");
unsafe { checked.free() };
}
+
+ #[cfg(feature = "cypherpunk")]
+ #[test]
+ fn test_batch_cache_binds_signing_context() {
+ let cache = BatchDisplayCache::new(Vec::new(), Vec::new(), [7; 32], 3);
+ assert!(cache.signability(&[7; 32], 3).is_some());
+ assert!(cache.signability(&[8; 32], 3).is_none());
+ assert!(cache.signability(&[7; 32], 4).is_none());
+ }
}
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.