Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,6 @@ private fun ZcashPcztSimpleContent(req: SignRequest.ZcashPczt) {
// Fee
val feeZec = "%.8f".format(inspection.netValue / 100_000_000.0)
DetailRow(label = "Fee", value = "$feeZec ZEC")

// Warnings
if (!inspection.anchorMatches) {
WarningCard("Anchor mismatch — the transaction may reference an outdated state.")
}
}

// =============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,12 @@ fun ZcashPcztScreen(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Anchor verification
AnchorCard(insp)
// Verified-notes context (known-spends ratio + local balance).
// Anchor is intentionally not displayed or validated —
// Keystone doesn't and equality-checking against our local
// anchor was the rotko-invented gate behind the constant
// "re-sync first" friction.
VerificationCard(insp)

// Spends
if (insp.spends.isNotEmpty()) {
Expand Down Expand Up @@ -177,9 +181,6 @@ fun ZcashPcztScreen(
}

// Warnings
if (!insp.anchorMatches) {
WarningCard("Anchor does not match verified state. Transaction may reference a different chain state.")
}
if (insp.knownSpends < insp.actionCount) {
WarningCard("${insp.actionCount - insp.knownSpends} spend(s) not in verified notes. These may be unknown or dummy actions.")
}
Expand Down Expand Up @@ -271,7 +272,7 @@ fun ZcashPcztScreen(
}

@Composable
private fun AnchorCard(insp: ZcashPcztInspection) {
private fun VerificationCard(insp: ZcashPcztInspection) {
Column(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -280,17 +281,6 @@ private fun AnchorCard(insp: ZcashPcztInspection) {
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Anchor", style = SignerTypeface.LabelM, color = MaterialTheme.colors.textTertiary)
Text(
text = if (insp.anchorMatches) "matches" else "MISMATCH",
style = SignerTypeface.LabelM,
color = if (insp.anchorMatches) MaterialTheme.colors.primary else MaterialTheme.colors.red500
)
}
Text(
text = "${insp.knownSpends}/${insp.actionCount} spends verified",
style = SignerTypeface.CaptionM,
Expand Down
18 changes: 3 additions & 15 deletions ios/PolkadotVault/Screens/Scan/Zcash/ZcashPcztView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct ZcashPcztView: View {
ScrollView {
VStack(alignment: .leading, spacing: Spacing.small) {
// Anchor status
anchorCard(inspection: inspection)
verificationCard(inspection: inspection)

// Spends
if !inspection.spends.isEmpty {
Expand Down Expand Up @@ -108,9 +108,6 @@ struct ZcashPcztView: View {
}

// Warnings
if !inspection.anchorMatches {
warningCard("Anchor does not match verified state. Transaction may reference a different chain state.")
}
if inspection.knownSpends < inspection.actionCount {
warningCard("\(inspection.actionCount - inspection.knownSpends) spend(s) not in verified notes.")
}
Expand Down Expand Up @@ -188,25 +185,16 @@ struct ZcashPcztView: View {

// MARK: - Card Components

private func anchorCard(inspection: ZcashPcztInspection) -> some View {
private func verificationCard(inspection: ZcashPcztInspection) -> some View {
VStack(alignment: .leading, spacing: Spacing.extraSmall) {
HStack {
Text("Anchor")
.font(PrimaryFont.labelM.font)
.foregroundColor(.textAndIconsTertiary)
Spacer()
Text(inspection.anchorMatches ? "matches" : "MISMATCH")
.font(PrimaryFont.labelM.font)
.foregroundColor(inspection.anchorMatches ? .textAndIconsPrimary : .accentRed300)
}
Text("\(inspection.knownSpends)/\(inspection.actionCount) spends verified")
.font(PrimaryFont.captionM.font)
.foregroundColor(inspection.knownSpends == inspection.actionCount ? .textAndIconsSecondary : .accentRed300)
let balZec = Double(inspection.verifiedBalance) / 100_000_000.0
Text(String(format: "Verified balance: %.8f ZEC", balZec))
.font(PrimaryFont.captionM.font)
.foregroundColor(.textAndIconsSecondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(Spacing.medium)
.containerBackground()
Expand Down
2 changes: 0 additions & 2 deletions rust/definitions/src/navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1084,10 +1084,8 @@ pub struct ZcashPcztInspection {
pub spends: Vec<ZcashPcztSpend>,
pub outputs: Vec<ZcashPcztOutput>,
pub net_value: i64,
pub anchor_matches: bool,
pub verified_balance: u64,
pub known_spends: u32,
pub anchor_hex: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down
71 changes: 33 additions & 38 deletions rust/signer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1970,17 +1970,28 @@ fn inspect_zcash_pczt(pczt_bytes: Vec<u8>) -> Result<ZcashPcztInspection, ErrorD

let orchard = pczt.orchard();
let action_count = orchard.actions().len() as u32;
let pczt_anchor = orchard.anchor();
let pczt_anchor_hex = hex::encode(pczt_anchor);

// Load verified notes for cross-reference
let (verified_balance, verified_anchor, verified_nullifiers, verified_notes_values) = {
// Load verified notes for cross-reference. Used by the known_spends warning
// (zigner's distinguishing safety guard over a Keystone-equivalent signer —
// we can warn when the PCZT spends notes we haven't verified) and to derive
// the network for recipient-address encoding.
//
// The verified_anchor was previously used for byte-equality matching against
// the PCZT's anchor; that check was rotko-invented and doesn't exist in the
// canonical Zashi → Keystone flow, so it's gone. The DB still stores the
// anchor for the note-sync UI on a separate screen; the PCZT path doesn't
// consult it for signing decisions.
let (verified_balance, verified_nullifiers, verified_notes_values, is_mainnet) = {
let db_guard = DB.read().map_err(|_| ErrorDisplayed::MutexPoisoned)?;
if let Some(database) = db_guard.as_ref() {
let balance = db_handling::zcash::get_verified_balance(database).unwrap_or(0);
let anchor = db_handling::zcash::get_verified_anchor(database)
// Anchor is read only to recover the network flag; the bytes are
// discarded. Defaults to mainnet when no notes have been synced.
let mainnet = db_handling::zcash::get_verified_anchor(database)
.ok()
.flatten();
.flatten()
.map(|(_, _, m, _)| m)
.unwrap_or(true);
let notes = db_handling::zcash::get_verified_notes(database).unwrap_or_default();
let nullifiers: std::collections::HashSet<String> = notes
.iter()
Expand All @@ -1991,23 +2002,17 @@ fn inspect_zcash_pczt(pczt_bytes: Vec<u8>) -> Result<ZcashPcztInspection, ErrorD
.iter()
.map(|(val, nf_hex, _, _, _)| (nf_hex.clone(), *val))
.collect();
(balance, anchor, nullifiers, values)
(balance, nullifiers, values, mainnet)
} else {
(
0,
None,
std::collections::HashSet::new(),
std::collections::HashMap::new(),
true,
)
}
};

// Check anchor match
let anchor_matches = verified_anchor
.as_ref()
.map(|(a, _, _, _)| hex::encode(a) == pczt_anchor_hex)
.unwrap_or(false);

// Extract spend details (value may be redacted in PCZT, use nullifier for cross-ref)
let mut spends = Vec::new();
let mut known_spends = 0u32;
Expand All @@ -2032,12 +2037,6 @@ fn inspect_zcash_pczt(pczt_bytes: Vec<u8>) -> Result<ZcashPcztInspection, ErrorD
});
}

// Determine network for address encoding
let is_mainnet = verified_anchor
.as_ref()
.map(|(_, _, mainnet, _)| *mainnet)
.unwrap_or(true);

// Extract output details with human-readable addresses
let mut outputs = Vec::new();
for action in orchard.actions() {
Expand Down Expand Up @@ -2069,10 +2068,8 @@ fn inspect_zcash_pczt(pczt_bytes: Vec<u8>) -> Result<ZcashPcztInspection, ErrorD
spends,
outputs,
net_value,
anchor_matches,
verified_balance,
known_spends,
anchor_hex: pczt_anchor_hex,
})
}

Expand All @@ -2089,24 +2086,22 @@ fn sign_zcash_pczt(
use pczt::Pczt;
use transaction_signing::zcash::OrchardSpendingKey;

// Run inspection and enforce verification gates before signing
// Run inspection and enforce verification gates before signing.
//
// The previous anchor-equality and "no verified notes" gates were
// rotko-invented and don't appear in the canonical Zashi → Keystone PCZT
// flow — Keystone's firmware signs whatever passes user review of
// recipient + amount + fee, without inspecting the PCZT anchor at all
// (verified at /steam/rotko/keystone3-firmware/rust/apps/zcash/src/pczt/
// {sign,check}.rs). We mirror that.
//
// The only zigner-specific guard kept: refuse to sign when zero PCZT
// spend nullifiers match the locally verified note set, since the
// verified-notes store is zigner's distinguishing value over a
// canonical Keystone-shape signer. A blind-signing footgun that
// Keystone cannot detect.
let inspection = inspect_zcash_pczt(pczt_bytes.clone())?;

if inspection.verified_balance == 0 && !inspection.anchor_matches {
return Err(ErrorDisplayed::Str {
s: "No verified notes. Sync notes from zcli before signing (zcli export-notes → scan QR).".to_string(),
});
}

if !inspection.anchor_matches {
return Err(ErrorDisplayed::Str {
s: format!(
"PCZT anchor does not match verified anchor. Sync notes to current state first. PCZT anchor: {}",
inspection.anchor_hex
),
});
}

if inspection.known_spends == 0 && inspection.action_count > 0 {
return Err(ErrorDisplayed::Str {
s: "No PCZT spend nullifiers match verified notes. This transaction may spend notes you don't recognize.".to_string(),
Expand Down
8 changes: 4 additions & 4 deletions rust/signer/src/signer.udl
Original file line number Diff line number Diff line change
Expand Up @@ -1496,7 +1496,10 @@ dictionary ZcashSignContext {
boolean has_notes;
};

// PCZT inspection result for display before signing
// PCZT inspection result for display before signing.
// Anchor is intentionally not validated — matches the Keystone signing model
// (PCZT is ground truth; signature happens after user review of
// recipient + amount + fee).
dictionary ZcashPcztInspection {
u32 action_count;
// spends: list of (value_zatoshis, nullifier_is_known) pairs
Expand All @@ -1505,13 +1508,10 @@ dictionary ZcashPcztInspection {
sequence<ZcashPcztOutput> outputs;
// net value (spends - outputs) in zatoshis = fee
i64 net_value;
// anchor matches our verified anchor?
boolean anchor_matches;
// our verified balance (0 if no notes stored)
u64 verified_balance;
// how many spend nullifiers match our verified notes
u32 known_spends;
string anchor_hex;
};

dictionary ZcashPcztSpend {
Expand Down
Loading