///|
/// Failure reason produced by policy-level validation.
pub(all) enum PolicyFailureKind {
PolicyFailureNone
PolicyFailureInvalidBech32(String)
PolicyFailureNotCanonical
PolicyFailureVariantNotAllowed(String)
PolicyFailureHrpNotAllowed(String)
PolicyFailurePayloadTooShort(Int, Int)
PolicyFailurePayloadTooLong(Int, Int)
PolicyFailureSegwitRequired
PolicyFailureNetworkNotAllowed(String)
PolicyFailureProgramKindNotAllowed(String)
} derive(Eq, Debug)
///|
/// Higher-level acceptance policy built on top of Bech32 and SegWit validation.
pub(all) struct Bech32Policy {
name : String
allow_any_hrp : Bool
allowed_hrps : Array[String]
allow_bech32 : Bool
allow_bech32m : Bool
require_canonical : Bool
require_segwit : Bool
allow_any_network : Bool
allowed_networks : Array[String]
allow_any_program_kind : Bool
allowed_program_kinds : Array[String]
min_payload_words : Int
max_payload_words : Int
} derive(Eq, Debug)
///|
/// Result of applying a high-level policy to one input.
pub(all) struct PolicyCheck {
input : String
policy_name : String
accepted : Bool
valid : Bool
segwit_valid : Bool
canonical : Bool
variant : String
hrp : String
network : String
program_kind : String
payload_words : Int
failure : PolicyFailureKind
failure_code : String
failure_message : String
recommendation : String
} derive(Eq, Debug)
///|
/// Aggregated policy-check result for a batch.
pub(all) struct PolicySummary {
policy_name : String
total : Int
accepted : Int
rejected : Int
rejected_invalid : Int
rejected_not_canonical : Int
rejected_variant : Int
rejected_hrp : Int
rejected_payload_too_short : Int
rejected_payload_too_long : Int
rejected_segwit_required : Int
rejected_network : Int
rejected_program_kind : Int
} derive(Eq, Debug)
///|
pub fn any_bech32_policy() -> Bech32Policy {
{
name: "any_bech32",
allow_any_hrp: true,
allowed_hrps: [],
allow_bech32: true,
allow_bech32m: true,
require_canonical: false,
require_segwit: false,
allow_any_network: true,
allowed_networks: [],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 0,
max_payload_words: 83,
}
}
///|
pub fn canonical_bech32_policy() -> Bech32Policy {
{
name: "canonical_bech32",
allow_any_hrp: true,
allowed_hrps: [],
allow_bech32: true,
allow_bech32m: true,
require_canonical: true,
require_segwit: false,
allow_any_network: true,
allowed_networks: [],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 0,
max_payload_words: 83,
}
}
///|
pub fn bech32_only_policy() -> Bech32Policy {
{
name: "bech32_only",
allow_any_hrp: true,
allowed_hrps: [],
allow_bech32: true,
allow_bech32m: false,
require_canonical: false,
require_segwit: false,
allow_any_network: true,
allowed_networks: [],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 0,
max_payload_words: 83,
}
}
///|
pub fn bech32m_only_policy() -> Bech32Policy {
{
name: "bech32m_only",
allow_any_hrp: true,
allowed_hrps: [],
allow_bech32: false,
allow_bech32m: true,
require_canonical: false,
require_segwit: false,
allow_any_network: true,
allowed_networks: [],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 0,
max_payload_words: 83,
}
}
///|
pub fn bitcoin_segwit_policy() -> Bech32Policy {
{
name: "bitcoin_segwit",
allow_any_hrp: false,
allowed_hrps: ["bc", "tb", "bcrt"],
allow_bech32: true,
allow_bech32m: true,
require_canonical: true,
require_segwit: true,
allow_any_network: false,
allowed_networks: ["bitcoin_mainnet", "bitcoin_testnet", "bitcoin_regtest"],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 1,
max_payload_words: 65,
}
}
///|
pub fn bitcoin_mainnet_segwit_policy() -> Bech32Policy {
{
name: "bitcoin_mainnet_segwit",
allow_any_hrp: false,
allowed_hrps: ["bc"],
allow_bech32: true,
allow_bech32m: true,
require_canonical: true,
require_segwit: true,
allow_any_network: false,
allowed_networks: ["bitcoin_mainnet"],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 1,
max_payload_words: 65,
}
}
///|
pub fn bitcoin_testnet_segwit_policy() -> Bech32Policy {
{
name: "bitcoin_testnet_segwit",
allow_any_hrp: false,
allowed_hrps: ["tb"],
allow_bech32: true,
allow_bech32m: true,
require_canonical: true,
require_segwit: true,
allow_any_network: false,
allowed_networks: ["bitcoin_testnet"],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 1,
max_payload_words: 65,
}
}
///|
pub fn bitcoin_regtest_segwit_policy() -> Bech32Policy {
{
name: "bitcoin_regtest_segwit",
allow_any_hrp: false,
allowed_hrps: ["bcrt"],
allow_bech32: true,
allow_bech32m: true,
require_canonical: true,
require_segwit: true,
allow_any_network: false,
allowed_networks: ["bitcoin_regtest"],
allow_any_program_kind: true,
allowed_program_kinds: [],
min_payload_words: 1,
max_payload_words: 65,
}
}
///|
pub fn taproot_mainnet_policy() -> Bech32Policy {
{
name: "taproot_mainnet",
allow_any_hrp: false,
allowed_hrps: ["bc"],
allow_bech32: false,
allow_bech32m: true,
require_canonical: true,
require_segwit: true,
allow_any_network: false,
allowed_networks: ["bitcoin_mainnet"],
allow_any_program_kind: false,
allowed_program_kinds: ["taproot"],
min_payload_words: 1,
max_payload_words: 65,
}
}
///|
pub fn policy_with_name(policy : Bech32Policy, name : String) -> Bech32Policy {
{
name,
allow_any_hrp: policy.allow_any_hrp,
allowed_hrps: policy.allowed_hrps,
allow_bech32: policy.allow_bech32,
allow_bech32m: policy.allow_bech32m,
require_canonical: policy.require_canonical,
require_segwit: policy.require_segwit,
allow_any_network: policy.allow_any_network,
allowed_networks: policy.allowed_networks,
allow_any_program_kind: policy.allow_any_program_kind,
allowed_program_kinds: policy.allowed_program_kinds,
min_payload_words: policy.min_payload_words,
max_payload_words: policy.max_payload_words,
}
}
///|
pub fn policy_with_hrps(
policy : Bech32Policy,
hrps : Array[String],
) -> Bech32Policy {
{
name: policy.name,
allow_any_hrp: false,
allowed_hrps: normalize_string_list(hrps),
allow_bech32: policy.allow_bech32,
allow_bech32m: policy.allow_bech32m,
require_canonical: policy.require_canonical,
require_segwit: policy.require_segwit,
allow_any_network: policy.allow_any_network,
allowed_networks: policy.allowed_networks,
allow_any_program_kind: policy.allow_any_program_kind,
allowed_program_kinds: policy.allowed_program_kinds,
min_payload_words: policy.min_payload_words,
max_payload_words: policy.max_payload_words,
}
}
///|
pub fn policy_with_payload_bounds(
policy : Bech32Policy,
min_payload_words : Int,
max_payload_words : Int,
) -> Bech32Policy {
{
name: policy.name,
allow_any_hrp: policy.allow_any_hrp,
allowed_hrps: policy.allowed_hrps,
allow_bech32: policy.allow_bech32,
allow_bech32m: policy.allow_bech32m,
require_canonical: policy.require_canonical,
require_segwit: policy.require_segwit,
allow_any_network: policy.allow_any_network,
allowed_networks: policy.allowed_networks,
allow_any_program_kind: policy.allow_any_program_kind,
allowed_program_kinds: policy.allowed_program_kinds,
min_payload_words,
max_payload_words,
}
}
///|
pub fn policy_requiring_canonical(
policy : Bech32Policy,
required : Bool,
) -> Bech32Policy {
{
name: policy.name,
allow_any_hrp: policy.allow_any_hrp,
allowed_hrps: policy.allowed_hrps,
allow_bech32: policy.allow_bech32,
allow_bech32m: policy.allow_bech32m,
require_canonical: required,
require_segwit: policy.require_segwit,
allow_any_network: policy.allow_any_network,
allowed_networks: policy.allowed_networks,
allow_any_program_kind: policy.allow_any_program_kind,
allowed_program_kinds: policy.allowed_program_kinds,
min_payload_words: policy.min_payload_words,
max_payload_words: policy.max_payload_words,
}
}
///|
pub fn check_with_policy(input : String, policy : Bech32Policy) -> PolicyCheck {
let diag = diagnose(input)
if !diag.valid {
return policy_reject(
diag,
policy,
PolicyFailureInvalidBech32(diag.error_code),
)
}
if policy.require_canonical && !diag.canonical {
return policy_reject(diag, policy, PolicyFailureNotCanonical)
}
if !variant_allowed(policy, diag.variant) {
return policy_reject(
diag,
policy,
PolicyFailureVariantNotAllowed(diag.variant),
)
}
if !hrp_allowed(policy, diag.hrp) {
return policy_reject(diag, policy, PolicyFailureHrpNotAllowed(diag.hrp))
}
let payload_words = match inspect(input) {
Ok(info) => info.data_length
Err(_) => 0
}
if payload_words < policy.min_payload_words {
return policy_reject(
diag,
policy,
PolicyFailurePayloadTooShort(policy.min_payload_words, payload_words),
)
}
if payload_words > policy.max_payload_words {
return policy_reject(
diag,
policy,
PolicyFailurePayloadTooLong(policy.max_payload_words, payload_words),
)
}
if policy.require_segwit && !diag.segwit_valid {
return policy_reject(diag, policy, PolicyFailureSegwitRequired)
}
if diag.segwit_valid && !network_allowed(policy, diag.network) {
return policy_reject(
diag,
policy,
PolicyFailureNetworkNotAllowed(diag.network),
)
}
if diag.segwit_valid && !program_kind_allowed(policy, diag.program_kind) {
return policy_reject(
diag,
policy,
PolicyFailureProgramKindNotAllowed(diag.program_kind),
)
}
policy_accept(diag, policy, payload_words)
}
///|
pub fn check_many_with_policy(
inputs : Array[String],
policy : Bech32Policy,
) -> Array[PolicyCheck] {
let out = Array::new(capacity=inputs.length())
for input in inputs {
out.push(check_with_policy(input, policy))
}
out
}
///|
pub fn summarize_policy_checks(checks : Array[PolicyCheck]) -> PolicySummary {
let mut policy_name = ""
let mut total = 0
let mut accepted = 0
let mut rejected = 0
let mut rejected_invalid = 0
let mut rejected_not_canonical = 0
let mut rejected_variant = 0
let mut rejected_hrp = 0
let mut rejected_payload_too_short = 0
let mut rejected_payload_too_long = 0
let mut rejected_segwit_required = 0
let mut rejected_network = 0
let mut rejected_program_kind = 0
for check in checks {
if policy_name.is_empty() {
policy_name = check.policy_name
}
total += 1
if check.accepted {
accepted += 1
} else {
rejected += 1
match check.failure {
PolicyFailureNone => ()
PolicyFailureInvalidBech32(_) => rejected_invalid += 1
PolicyFailureNotCanonical => rejected_not_canonical += 1
PolicyFailureVariantNotAllowed(_) => rejected_variant += 1
PolicyFailureHrpNotAllowed(_) => rejected_hrp += 1
PolicyFailurePayloadTooShort(_, _) => rejected_payload_too_short += 1
PolicyFailurePayloadTooLong(_, _) => rejected_payload_too_long += 1
PolicyFailureSegwitRequired => rejected_segwit_required += 1
PolicyFailureNetworkNotAllowed(_) => rejected_network += 1
PolicyFailureProgramKindNotAllowed(_) => rejected_program_kind += 1
}
}
}
{
policy_name,
total,
accepted,
rejected,
rejected_invalid,
rejected_not_canonical,
rejected_variant,
rejected_hrp,
rejected_payload_too_short,
rejected_payload_too_long,
rejected_segwit_required,
rejected_network,
rejected_program_kind,
}
}
///|
pub fn summarize_policy_inputs(
inputs : Array[String],
policy : Bech32Policy,
) -> PolicySummary {
summarize_policy_checks(check_many_with_policy(inputs, policy))
}
///|
pub fn policy_accepts(input : String, policy : Bech32Policy) -> Bool {
check_with_policy(input, policy).accepted
}
///|
pub fn policy_rejects(input : String, policy : Bech32Policy) -> Bool {
!policy_accepts(input, policy)
}
///|
pub fn accepted_inputs(
inputs : Array[String],
policy : Bech32Policy,
) -> Array[String] {
let out = Array::new()
for input in inputs {
if policy_accepts(input, policy) {
out.push(input)
}
}
out
}
///|
pub fn rejected_inputs(
inputs : Array[String],
policy : Bech32Policy,
) -> Array[String] {
let out = Array::new()
for input in inputs {
if policy_rejects(input, policy) {
out.push(input)
}
}
out
}
///|
pub fn first_policy_failure(
inputs : Array[String],
policy : Bech32Policy,
) -> PolicyCheck? {
for input in inputs {
let check = check_with_policy(input, policy)
if !check.accepted {
return Some(check)
}
}
None
}
///|
pub fn policy_failure_code(failure : PolicyFailureKind) -> String {
match failure {
PolicyFailureNone => ""
PolicyFailureInvalidBech32(code) => "invalid_bech32." + code
PolicyFailureNotCanonical => "not_canonical"
PolicyFailureVariantNotAllowed(_) => "variant_not_allowed"
PolicyFailureHrpNotAllowed(_) => "hrp_not_allowed"
PolicyFailurePayloadTooShort(_, _) => "payload_too_short"
PolicyFailurePayloadTooLong(_, _) => "payload_too_long"
PolicyFailureSegwitRequired => "segwit_required"
PolicyFailureNetworkNotAllowed(_) => "network_not_allowed"
PolicyFailureProgramKindNotAllowed(_) => "program_kind_not_allowed"
}
}
///|
pub fn policy_failure_message(failure : PolicyFailureKind) -> String {
match failure {
PolicyFailureNone => "input satisfies the selected policy"
PolicyFailureInvalidBech32(code) => "input is not valid Bech32: " + code
PolicyFailureNotCanonical => "input is valid but not canonical lowercase"
PolicyFailureVariantNotAllowed(variant) =>
"checksum variant is not allowed: " + variant
PolicyFailureHrpNotAllowed(hrp) =>
"human-readable part is not allowed: " + hrp
PolicyFailurePayloadTooShort(expected, actual) =>
"payload is shorter than the policy minimum: expected at least " +
policy_int_to_string(expected) +
", got " +
policy_int_to_string(actual)
PolicyFailurePayloadTooLong(expected, actual) =>
"payload is longer than the policy maximum: expected at most " +
policy_int_to_string(expected) +
", got " +
policy_int_to_string(actual)
PolicyFailureSegwitRequired => "policy requires a valid SegWit address"
PolicyFailureNetworkNotAllowed(network) =>
"SegWit network is not allowed: " + network
PolicyFailureProgramKindNotAllowed(kind) =>
"witness program kind is not allowed: " + kind
}
}
///|
pub fn policy_failure_recommendation(failure : PolicyFailureKind) -> String {
match failure {
PolicyFailureNone => "no action required"
PolicyFailureInvalidBech32(_) =>
"fix the underlying Bech32 validation error"
PolicyFailureNotCanonical =>
"normalize the input before applying the policy"
PolicyFailureVariantNotAllowed(_) =>
"use an allowed Bech32 checksum variant"
PolicyFailureHrpNotAllowed(_) => "use one of the policy-approved HRPs"
PolicyFailurePayloadTooShort(_, _) => "increase the payload data length"
PolicyFailurePayloadTooLong(_, _) => "reduce the payload data length"
PolicyFailureSegwitRequired =>
"provide a BIP-0173 or BIP-0350 SegWit address"
PolicyFailureNetworkNotAllowed(_) => "switch to the expected SegWit network"
PolicyFailureProgramKindNotAllowed(_) =>
"use a witness program type accepted by the policy"
}
}
///|
pub fn render_policy_check(check : PolicyCheck) -> String {
let out = StringBuilder(size_hint=512)
policy_write_line(out, "policy check")
policy_write_kv(out, "policy", check.policy_name)
policy_write_kv(out, "input", check.input)
policy_write_kv(out, "accepted", policy_bool_text(check.accepted))
policy_write_kv(out, "valid", policy_bool_text(check.valid))
policy_write_kv(out, "segwit_valid", policy_bool_text(check.segwit_valid))
policy_write_kv(out, "canonical", policy_bool_text(check.canonical))
policy_write_kv(out, "variant", check.variant)
policy_write_kv(out, "hrp", check.hrp)
policy_write_kv(out, "network", check.network)
policy_write_kv(out, "program_kind", check.program_kind)
policy_write_kv(
out,
"payload_words",
policy_int_to_string(check.payload_words),
)
policy_write_kv(out, "failure_code", check.failure_code)
policy_write_kv(out, "failure_message", check.failure_message)
policy_write_kv(out, "recommendation", check.recommendation)
out.to_string()
}
///|
pub fn render_policy_summary(summary : PolicySummary) -> String {
let out = StringBuilder(size_hint=512)
policy_write_line(out, "policy summary")
policy_write_kv(out, "policy", summary.policy_name)
policy_write_kv(out, "total", policy_int_to_string(summary.total))
policy_write_kv(out, "accepted", policy_int_to_string(summary.accepted))
policy_write_kv(out, "rejected", policy_int_to_string(summary.rejected))
policy_write_kv(
out,
"invalid",
policy_int_to_string(summary.rejected_invalid),
)
policy_write_kv(
out,
"not_canonical",
policy_int_to_string(summary.rejected_not_canonical),
)
policy_write_kv(
out,
"variant",
policy_int_to_string(summary.rejected_variant),
)
policy_write_kv(out, "hrp", policy_int_to_string(summary.rejected_hrp))
policy_write_kv(
out,
"payload_too_short",
policy_int_to_string(summary.rejected_payload_too_short),
)
policy_write_kv(
out,
"payload_too_long",
policy_int_to_string(summary.rejected_payload_too_long),
)
policy_write_kv(
out,
"segwit_required",
policy_int_to_string(summary.rejected_segwit_required),
)
policy_write_kv(
out,
"network",
policy_int_to_string(summary.rejected_network),
)
policy_write_kv(
out,
"program_kind",
policy_int_to_string(summary.rejected_program_kind),
)
out.to_string()
}
///|
fn policy_accept(
diag : Diagnostic,
policy : Bech32Policy,
payload_words : Int,
) -> PolicyCheck {
{
input: diag.input,
policy_name: policy.name,
accepted: true,
valid: diag.valid,
segwit_valid: diag.segwit_valid,
canonical: diag.canonical,
variant: diag.variant,
hrp: diag.hrp,
network: diag.network,
program_kind: diag.program_kind,
payload_words,
failure: PolicyFailureNone,
failure_code: "",
failure_message: policy_failure_message(PolicyFailureNone),
recommendation: policy_failure_recommendation(PolicyFailureNone),
}
}
///|
fn policy_reject(
diag : Diagnostic,
policy : Bech32Policy,
failure : PolicyFailureKind,
) -> PolicyCheck {
let payload_words = if diag.valid {
match inspect(diag.input) {
Ok(info) => info.data_length
Err(_) => 0
}
} else {
0
}
{
input: diag.input,
policy_name: policy.name,
accepted: false,
valid: diag.valid,
segwit_valid: diag.segwit_valid,
canonical: diag.canonical,
variant: diag.variant,
hrp: diag.hrp,
network: diag.network,
program_kind: diag.program_kind,
payload_words,
failure,
failure_code: policy_failure_code(failure),
failure_message: policy_failure_message(failure),
recommendation: policy_failure_recommendation(failure),
}
}
///|
fn variant_allowed(policy : Bech32Policy, variant : String) -> Bool {
if variant == "bech32" {
policy.allow_bech32
} else if variant == "bech32m" {
policy.allow_bech32m
} else {
false
}
}
///|
fn hrp_allowed(policy : Bech32Policy, hrp : String) -> Bool {
policy.allow_any_hrp ||
string_array_contains(policy.allowed_hrps, hrp.to_lower())
}
///|
fn network_allowed(policy : Bech32Policy, network : String) -> Bool {
policy.allow_any_network ||
string_array_contains(policy.allowed_networks, network)
}
///|
fn program_kind_allowed(policy : Bech32Policy, kind : String) -> Bool {
policy.allow_any_program_kind ||
string_array_contains(policy.allowed_program_kinds, kind)
}
///|
fn normalize_string_list(values : Array[String]) -> Array[String] {
let out = Array::new(capacity=values.length())
for value in values {
out.push(value.to_lower())
}
out
}
///|
fn string_array_contains(values : Array[String], expected : String) -> Bool {
for value in values {
if value == expected {
return true
}
}
false
}
///|
fn policy_write_kv(out : StringBuilder, key : String, value : String) -> Unit {
out.write_string(key)
out.write_string(": ")
out.write_string(value)
out.write_char('\n')
}
///|
fn policy_write_line(out : StringBuilder, value : String) -> Unit {
out.write_string(value)
out.write_char('\n')
}
///|
fn policy_bool_text(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}
///|
fn policy_int_to_string(value : Int) -> String {
if value == 0 {
return "0"
}
let mut n = value
let mut negative = false
if n < 0 {
negative = true
n = -n
}
let digits = Array::new()
while n > 0 {
let d = n % 10
digits.push(char_at("0123456789", d))
n = n / 10
}
let out = StringBuilder(size_hint=12)
if negative {
out.write_char('-')
}
let mut i = digits.length()
while i > 0 {
i -= 1
out.write_char(digits[i])
}
out.to_string()
}