open-standards/openpay/crates/op-bin/src/network_ranges.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

399 lines
9.3 KiB
Rust

//! Static tables of published BIN prefix ranges per network.
//!
//! These tables encode the **public, structural** prefix
//! assignments documented in ISO/IEC 7812 and each network's
//! developer documentation. They are NOT a commercial BIN
//! database — they do not resolve individual issuing banks, and
//! they do not differentiate every credit / debit / prepaid
//! sub-range. Operators who need that resolution wire a
//! commercial feed (e.g. BinList, BIN Codes, FreeBINChecker) on
//! top of this scaffolding.
//!
//! ## Encoding
//!
//! Each entry is a half-open prefix interval `[low, high)` in
//! **8-digit prefix form**, with default annotations (network,
//! card type, country, Durbin). Use [`build_default_tree`] to
//! load the whole catalog into a [`RangeTree`].
//!
//! ## Adding a range
//!
//! Append a `R(low, high, network, type, country, durbin)` entry
//! to the appropriate const slice. The build function inserts in
//! order; overlap is rejected at runtime — keep entries disjoint.
use crate::bin::BinRange;
use crate::card_type::CardType;
use crate::error::Result;
use crate::issuer_country::IssuerCountry;
use crate::network::CardNetwork;
use crate::range_tree::RangeTree;
/// Convenience tuple for declaring a range in a const slice.
type Row = (u32, u32, CardNetwork, CardType, Option<IssuerCountry>, bool);
const US: Option<IssuerCountry> = Some(IssuerCountry::from_ascii(b'U', b'S'));
const CN: Option<IssuerCountry> = Some(IssuerCountry::from_ascii(b'C', b'N'));
const JP: Option<IssuerCountry> = Some(IssuerCountry::from_ascii(b'J', b'P'));
const IN_: Option<IssuerCountry> = Some(IssuerCountry::from_ascii(b'I', b'N'));
const BR: Option<IssuerCountry> = Some(IssuerCountry::from_ascii(b'B', b'R'));
const RU: Option<IssuerCountry> = Some(IssuerCountry::from_ascii(b'R', b'U'));
const TR: Option<IssuerCountry> = Some(IssuerCountry::from_ascii(b'T', b'R'));
const ANY: Option<IssuerCountry> = None;
/// Visa: leading digit `4`. Single contiguous range over the
/// entire `[4_000_000_0, 5_000_000_0)` 8-digit space.
pub const VISA: &[Row] = &[(
40_000_000,
50_000_000,
CardNetwork::Visa,
CardType::Credit,
ANY,
false,
)];
/// Mastercard: classic 51-55 + 2017-expansion 2-series 2221-2720.
pub const MASTERCARD: &[Row] = &[
// 2-series (the "2BIN" range, opened 2017).
(
22_210_000,
27_210_000,
CardNetwork::Mastercard,
CardType::Credit,
ANY,
false,
),
// Classic 51-55.
(
51_000_000,
56_000_000,
CardNetwork::Mastercard,
CardType::Credit,
ANY,
false,
),
];
/// American Express: 34 / 37 prefixes, charge cards.
pub const AMEX: &[Row] = &[
(
34_000_000,
35_000_000,
CardNetwork::Amex,
CardType::Charge,
ANY,
false,
),
(
37_000_000,
38_000_000,
CardNetwork::Amex,
CardType::Charge,
ANY,
false,
),
];
/// Discover: 6011, 622126-622925, 644-649, 65.
///
/// Note: 65 overlaps RuPay at the 2-digit prefix; the 8-digit
/// sub-range chosen here (`65_000_000..66_000_000`) is the
/// Discover-assigned sub-block per Discover's public range list.
pub const DISCOVER: &[Row] = &[
(
60_110_000,
60_120_000,
CardNetwork::Discover,
CardType::Credit,
US,
false,
),
(
62_212_600,
62_292_600,
CardNetwork::Discover,
CardType::Credit,
US,
false,
),
(
64_400_000,
65_000_000,
CardNetwork::Discover,
CardType::Credit,
US,
false,
),
(
65_000_000,
66_000_000,
CardNetwork::Discover,
CardType::Credit,
US,
false,
),
];
/// JCB: 3528-3589.
pub const JCB: &[Row] = &[(
35_280_000,
35_900_000,
CardNetwork::Jcb,
CardType::Credit,
JP,
false,
)];
/// Diners Club: 300-305, 309, 36, 38-39.
///
/// 38 / 39 historically split with Amex (37) and Diners. The
/// 38-39 prefixes are Diners; Amex sits at 34/37.
pub const DINERS_CLUB: &[Row] = &[
(
30_000_000,
30_600_000,
CardNetwork::DinersClub,
CardType::Charge,
ANY,
false,
),
(
30_900_000,
31_000_000,
CardNetwork::DinersClub,
CardType::Charge,
ANY,
false,
),
(
36_000_000,
37_000_000,
CardNetwork::DinersClub,
CardType::Charge,
ANY,
false,
),
(
38_000_000,
40_000_000,
CardNetwork::DinersClub,
CardType::Charge,
ANY,
false,
),
];
/// UnionPay: 62 (note: overlaps Discover's `622126-622925`
/// sub-range, which we already carved out above; we encode the
/// non-overlapping remainder here in two slices).
pub const UNIONPAY: &[Row] = &[
(
62_000_000,
62_212_600,
CardNetwork::UnionPay,
CardType::Credit,
CN,
false,
),
(
62_292_600,
63_000_000,
CardNetwork::UnionPay,
CardType::Credit,
CN,
false,
),
];
/// RuPay: 60 (excluding Discover's 6011), 81, 82.
pub const RUPAY: &[Row] = &[
(
60_000_000,
60_110_000,
CardNetwork::RuPay,
CardType::Debit,
IN_,
false,
),
(
60_120_000,
61_000_000,
CardNetwork::RuPay,
CardType::Debit,
IN_,
false,
),
(
81_000_000,
83_000_000,
CardNetwork::RuPay,
CardType::Debit,
IN_,
false,
),
];
/// Maestro (Mastercard international debit): selected sub-ranges.
pub const MAESTRO: &[Row] = &[
(
50_180_000,
50_190_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
(
50_200_000,
50_210_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
(
50_380_000,
50_390_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
(
56_120_000,
56_130_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
(
58_930_000,
58_940_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
(
63_040_000,
63_050_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
(
67_590_000,
67_600_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
(
67_610_000,
67_640_000,
CardNetwork::Maestro,
CardType::Debit,
ANY,
false,
),
];
/// Elo (Brazil): published 4-digit prefixes.
///
/// Elo is a co-brand BIN sponsor in Brazil — many of its public
/// prefixes sit *inside* ranges otherwise issued by Visa (4011,
/// 4312, 4389, 4514), Mastercard (5041, 5066, 5067, 5090), or
/// Discover (6504, 6505). To keep the range tree disjoint we
/// store **only** Elo prefixes that do not overlap a primary
/// network's catalog range; the co-branded prefixes are still
/// resolvable via the [`crate::network::classify`] structural
/// classifier, which special-cases each known Elo 4-digit
/// prefix. The two non-overlapping prefixes encoded here are
/// 6362-6363 and 6277, which sit in unassigned Discover/UnionPay
/// carve-outs.
pub const ELO: &[Row] = &[
(
63_620_000,
63_640_000,
CardNetwork::Elo,
CardType::Credit,
BR,
false,
),
];
/// Mir (Russia): 2200-2204.
pub const MIR: &[Row] = &[(
22_000_000,
22_050_000,
CardNetwork::Mir,
CardType::Debit,
RU,
false,
)];
/// Troy (Türkiye): 9792.
pub const TROY: &[Row] = &[(
97_920_000,
97_930_000,
CardNetwork::Troy,
CardType::Debit,
TR,
false,
)];
/// Build a [`RangeTree`] populated with every published range
/// from this module. The catalog is intentionally **disjoint**:
/// where two networks share a 2-digit prefix (Discover/UnionPay
/// on `62`, Discover/RuPay on `60` and `65`), the conflict is
/// resolved by sub-range carve-out above.
///
/// # Errors
///
/// - [`crate::error::Error::InvalidRange`] if any pair overlaps —
/// indicates a bug in the static tables that the unit test in
/// `tests/range_tree.rs` should catch.
pub fn build_default_tree() -> Result<RangeTree> {
let mut tree = RangeTree::new();
for slice in [
VISA,
MASTERCARD,
AMEX,
DISCOVER,
JCB,
DINERS_CLUB,
UNIONPAY,
RUPAY,
MAESTRO,
ELO,
MIR,
TROY,
] {
for &(low, high, net, ct, country, durbin) in slice {
tree.insert(BinRange::new(low, high, net, ct, country, durbin)?)?;
}
}
Ok(tree)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_tree_builds() {
let tree = build_default_tree().expect("disjoint");
assert!(tree.len() > 20);
}
#[test]
fn default_tree_visa_lookup() {
let tree = build_default_tree().expect("disjoint");
let bin = crate::bin::Bin::parse("411111").expect("ok");
let r = tree.lookup(&bin).expect("Visa range");
assert_eq!(r.network, CardNetwork::Visa);
}
}