open-standards/openpay/crates/op-bin/src/luhn.rs
Transaction Science 5ae7e4ef86 ingest wave E: 3 crates · op-bin + smart-byte-tendermint + eoc-safety
OpenPay (1 crate, 1801 LOC, 57 tests)
  op-bin              BIN/IIN database + range matching + card-network
                      classification. 8-digit normalized prefix Bin +
                      BinRange. Sorted-vec interval tree with O(log N) point
                      lookup + overlap rejection. CardNetwork classify()
                      with structural rules (Visa 4, MC 51-55/2221-2720,
                      Amex 34/37, Discover, JCB, Diners, UnionPay, RuPay,
                      Mir, Troy, Maestro, Elo). Static prefix tables +
                      build_default_tree(). ISO/IEC 7812-1 Annex B Luhn
                      (validate / is_valid / check_digit). Card-type +
                      issuer-country + Durbin regulated-debit. PCI-safe:
                      8-digit cap, no full-PAN APIs.

Smart Byte (1 crate, ~1500 LOC, 20 tests)
  smart-byte-tendermint  BFT consensus. Step enum + Round + propose/prevote/
                         precommit/commit FSM with voting-power tallies.
                         ValidatorSet with 2/3+1 quorum, 1/3+1 trust
                         threshold, round-robin proposer. Block / Header /
                         Commit / CommitSig with Blake3 hashing. Light client
                         (sequential + skipping). ABCI Application trait +
                         EchoApp. DuplicateVoteEvidence + LightClientAttackEvidence.
                         IBC ClientState + ConsensusState + light-client
                         update.

EOC (1 crate, 2084 LOC, 23 tests)
  eoc-safety          AI safety primitives. RegexSet over 16 prompt-injection
                      signatures. 13-family jailbreak detector (DAN/DUDE/
                      Grandma/STAN/AIM/dev-mode/etc.). Presidio-style PII
                      redactor with Luhn-validated CC. Toxicity / bias /
                      NSFW classifier traits + heuristic baselines (5-axis
                      bias, 4-category NSFW with hard-reject for minors).
                      Constitutional AI critique→revise loop. Composable
                      InputGuard / OutputGuard pipelines. JSON-Schema-subset
                      output validator. Token-bucket rate limiter +
                      abuse-window detection. Red-team replay harness.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 16:52:07 -04:00

144 lines
3.9 KiB
Rust

//! Luhn (ISO/IEC 7812-1 Annex B) check-digit algorithm.
//!
//! The Luhn formula is a mod-10 checksum used by every major card
//! network on the full PAN. This module operates on **ASCII-digit
//! strings** so the caller controls allocation and we never copy
//! into a `String`. It is the only place in `op-bin` that accepts
//! more than 8 digits — calculation requires the whole PAN.
//!
//! ## Algorithm
//!
//! 1. From the **rightmost** digit (the check digit), double every
//! second digit (positions 2, 4, 6, … counting from the right
//! starting at 1).
//! 2. If doubling produces a two-digit number, sum its digits
//! (equivalently, subtract 9).
//! 3. Sum all (possibly transformed) digits.
//! 4. Valid iff `sum mod 10 == 0`.
use crate::error::{Error, Result};
/// Validate a PAN-shaped ASCII-digit string under the Luhn
/// algorithm. Accepts any length `>= 2`; rejects empty strings,
/// single digits, and any non-digit character.
///
/// # Errors
///
/// - [`Error::InvalidBinCharacter`] for non-`'0'..='9'`.
/// - [`Error::LuhnFailed`] on checksum mismatch.
pub fn validate(pan: &str) -> Result<()> {
if pan.len() < 2 {
return Err(Error::LuhnFailed);
}
let mut sum: u32 = 0;
let mut alt = false;
for c in pan.chars().rev() {
let d = c.to_digit(10).ok_or(Error::InvalidBinCharacter(c))?;
let v = if alt {
let doubled = d * 2;
if doubled > 9 {
doubled - 9
} else {
doubled
}
} else {
d
};
sum += v;
alt = !alt;
}
if sum % 10 == 0 {
Ok(())
} else {
Err(Error::LuhnFailed)
}
}
/// Boolean form of [`validate`]. Returns `false` on any error
/// (non-digit input *or* checksum mismatch).
pub fn is_valid(pan: &str) -> bool {
validate(pan).is_ok()
}
/// Compute the check digit that would make `partial` Luhn-valid.
/// `partial` is the PAN **without** its final check digit.
///
/// # Errors
///
/// - [`Error::InvalidBinCharacter`] for non-`'0'..='9'`.
pub fn check_digit(partial: &str) -> Result<u8> {
let mut sum: u32 = 0;
// Position counting from the right of the **completed** PAN:
// partial.len() rightmost positions of the completed string
// start at index 2 (the check digit itself is index 1, which
// we are computing). So the rightmost digit of `partial`
// sits at the "doubled" alt position.
let mut alt = true;
for c in partial.chars().rev() {
let d = c.to_digit(10).ok_or(Error::InvalidBinCharacter(c))?;
let v = if alt {
let doubled = d * 2;
if doubled > 9 {
doubled - 9
} else {
doubled
}
} else {
d
};
sum += v;
alt = !alt;
}
let cd = (10 - (sum % 10)) % 10;
Ok(cd as u8)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_valid_visa() {
// Industry test PAN published in Visa developer docs.
assert!(is_valid("4111111111111111"));
}
#[test]
fn known_valid_mastercard() {
assert!(is_valid("5555555555554444"));
}
#[test]
fn known_valid_amex() {
assert!(is_valid("378282246310005"));
}
#[test]
fn invalid_checksum() {
assert!(!is_valid("4111111111111112"));
}
#[test]
fn empty_rejected() {
assert!(!is_valid(""));
}
#[test]
fn non_digit_rejected() {
assert!(!is_valid("4111-1111-1111-1111"));
}
#[test]
fn check_digit_visa() {
let cd = check_digit("411111111111111").expect("ok");
assert_eq!(cd, 1);
}
#[test]
fn check_digit_round_trip() {
let partial = "53799347395198";
let cd = check_digit(partial).expect("ok");
let full = format!("{partial}{cd}");
assert!(is_valid(&full), "{full} should be Luhn-valid");
}
}