diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d27ea09..9dc54fb9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,16 @@ { "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.rulers": [80, 120], }, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.rulers": [80] }, + "[ql]": { + "editor.rulers": [100] + }, "editor.formatOnSave": true, "editor.minimap.enabled": true, "editor.rulers": [120], diff --git a/Cargo.lock b/Cargo.lock index effcffd5..fb2d1e2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,7 @@ name = "dash-pkc" version = "0.0.0" dependencies = [ "bitcoin-consensus-encoding 0.2.0", + "bitcoin_hashes", "blst", "cfg-if", "dash-dev", @@ -438,6 +439,7 @@ dependencies = [ "rstest", "serde", "sha2", + "subtle", "zeroize", ] @@ -484,7 +486,9 @@ dependencies = [ "base58ck", "bitcoin-consensus-encoding 0.2.0", "bitcoin_hashes", + "dash-types", "hex-literal", + "rstest", "serde", ] @@ -496,7 +500,9 @@ dependencies = [ "cfg-if", "dash-types-marker", "hex-conservative", + "rstest", "serde", + "zeroize", ] [[package]] diff --git a/contrib/codeql/lib/files.qll b/contrib/codeql/lib/files.qll index 2425d787..c06c1461 100644 --- a/contrib/codeql/lib/files.qll +++ b/contrib/codeql/lib/files.qll @@ -49,6 +49,9 @@ predicate fileRelPath(File f, string relPath) { relPath = f.getAbsolutePath().regexpCapture(".*/(pkgs/.*)", 1) } +/** Holds if `f` belongs to a crate in this workspace. */ +predicate isWorkspaceFile(File f) { fileRelPath(f, _) } + /** Holds if module `m` is not nested inside another module. */ predicate isRootModule(Module m) { not exists(Module enclosing | m.getParentNode() = enclosing.getItemList()) diff --git a/contrib/codeql/lib/filters.qll b/contrib/codeql/lib/filters.qll index 39553728..bbb8a57f 100644 --- a/contrib/codeql/lib/filters.qll +++ b/contrib/codeql/lib/filters.qll @@ -7,6 +7,7 @@ */ import lib.files +import lib.types import rust /** Materialises function spans per file for containment checks. */ @@ -40,9 +41,9 @@ private predicate testModuleSpan(File file, int mStart, int mEnd) { ) } -/** Holds if `t` is inside a test module or test file. */ -predicate isTestCode(TypeItem t) { - fileOf(t).getAbsolutePath().matches("%/tests/%") +/** Holds if `t` is inside a test module, test file, or benchmark. */ +predicate isTestCode(Locatable t) { + fileOf(t).getAbsolutePath().matches(["%/tests/%", "%/bench/%"]) or exists(File file, int mStart, int mEnd | testModuleSpan(file, mStart, mEnd) and @@ -93,32 +94,5 @@ string cratePrefix(TypeItem t) { ) } -/** Gets the type name of a field in struct `s`. */ -string structFieldTypeName(Struct s) { - exists(PathTypeRepr tr | - tr = s.getFieldList().(StructFieldList).getAField().getTypeRepr() or - tr = s.getFieldList().(TupleFieldList).getField(_).getTypeRepr() - | - result = tr.getPath().getSegment().getIdentifier().getText() - ) -} - -/** Gets the type name of a field in enum variant of `e`. */ -string enumFieldTypeName(Enum e) { - exists(Variant v, PathTypeRepr tr | - v = e.getVariantList().getAVariant() and - ( - tr = v.getFieldList().(StructFieldList).getAField().getTypeRepr() or - tr = v.getFieldList().(TupleFieldList).getField(_).getTypeRepr() - ) - | - result = tr.getPath().getSegment().getIdentifier().getText() - ) -} - -/** Gets the type name of a field in type item `t` (struct or enum). */ -string typeFieldName(TypeItem t) { - result = structFieldTypeName(t) - or - result = enumFieldTypeName(t) -} +/** Gets the type name of a field in type item `t` (struct, enum, or union). */ +string typeFieldName(TypeItem t) { result = typeHead(fieldTypeRepr(t)) } diff --git a/contrib/codeql/lib/policy.qll b/contrib/codeql/lib/policy.qll index e2466fb3..a2782b1a 100644 --- a/contrib/codeql/lib/policy.qll +++ b/contrib/codeql/lib/policy.qll @@ -10,6 +10,7 @@ import lib.files import lib.filters import lib.source_lines import lib.traits +import lib.types import rust /** Holds if `t` carries `#[derive(...name...)]` detected via source-line scanning. */ @@ -39,13 +40,31 @@ predicate isNotEncodable(TypeItem t) { /** Holds if `t` holds secret or security-sensitive material. */ predicate isSecretType(TypeItem t) { - t.getName().getText().regexpMatch(".*(Secret|Private|Seed|Password|Mnemonic|Share).*") and - // Exclude types whose name contains "Shared" (e.g. SharedState), - // which match the Share substring but are not secret holders. - not t.getName().getText().regexpMatch(".*Shared.*") - or - // Scalar field wrapper holding secret key material - t.getName().getText() = "Fr" + ( + t.getName().getText().regexpMatch(".*(Secret|Private|Seed|Password|Mnemonic|SkBytes).*") + or + // "Share" is the one keyword that "Shared" (e.g. SharedState) matches without holding a secret, + // so the guard applies to it alone, exceptions to this rule are explicitly enumerated. + t.getName().getText().regexpMatch(".*Share.*") and + not t.getName().getText().regexpMatch(".*Shared.*") + or + // Scalar field wrapper holding secret key material + t.getName().getText() = "Fr" + ) and + // A share *of a signature* is published, so it holds nothing to protect. Excluded by + // exact name because `SecretKeyShare` and `RawShare` match the same Share substring + // and do carry secret scalars. + not t.getName().getText() = "SignatureShare" +} + +/** + * Holds if `tr` names a heap-growable container. + * + * A `Vec` or `String` can reallocate while being filled, stranding a copy at + * the old allocation that drop-time wiping cannot reach. + */ +predicate isGrowableType(TypeRepr tr) { + typeHead(tr) = ["Vec", "String", "VecDeque", "BTreeMap", "BTreeSet", "BinaryHeap"] } /** Holds if `t` is an iterator type (name ends with Iterator or Iter). */ @@ -252,12 +271,18 @@ predicate isEnforcedCrate(File f) { f.getAbsolutePath().matches("%/pkgs/primitives/%") or f.getAbsolutePath().matches("%/pkgs/p2p_core/%") + or + f.getAbsolutePath().matches("%/pkgs/pkc/%") + or + f.getAbsolutePath().matches("%/pkgs/script/%") } /** Holds if file `f` is in a crate that can derive `Unencodable`. */ predicate isUnencodableCrate(File f) { f.getAbsolutePath().matches("%/pkgs/primitives/%") or - f.getAbsolutePath().matches("%/pkgs/p2p_core/%") + f.getAbsolutePath().matches("%/pkgs/p2p_core/%") or + f.getAbsolutePath().matches("%/pkgs/pkc/%") or + f.getAbsolutePath().matches("%/pkgs/script/%") } /** Declaration slots that define the required source ordering. */ diff --git a/contrib/codeql/lib/traits.qll b/contrib/codeql/lib/traits.qll index 7b958cc4..b2fd873d 100644 --- a/contrib/codeql/lib/traits.qll +++ b/contrib/codeql/lib/traits.qll @@ -24,8 +24,19 @@ private predicate implTraitHasCrate(Impl i, string traitName, string crate) { ) } -/** Gets the trait name from an impl block's trait reference. */ -string implTraitName(Impl i) { result = implTraitPath(i).getSegment().getIdentifier().getText() } +/** + * Gets the trait name from an impl block's trait reference. + * + * Prefers the resolved `Trait` item so an aliased or fully qualified path + * still reports the trait's own name, falling back to the written path when + * the trait lives in a crate the extractor did not resolve. + */ +string implTraitName(Impl i) { + result = i.getTrait().getName().getText() + or + not exists(i.getTrait()) and + result = implTraitPath(i).getSegment().getIdentifier().getText() +} /** Gets the type name from an impl block's self type. */ string implSelfName(Impl i) { @@ -70,13 +81,6 @@ private predicate manualImplInfo(Impl i, File f, string selfName, string traitNa scope = i.(AstNode).getParentNode() } -/** Holds if `t` has a manual impl for `traitName`. */ -predicate hasManualImpl(TypeItem t, string traitName) { - exists(Impl i | - manualImplInfo(i, fileOf(t), t.getName().getText(), traitName, t.(AstNode).getParentNode()) - ) -} - /** Materialises macro impl metadata for join efficiency. */ pragma[nomagic] private predicate macroImplInfo(MacroItems m, Impl i, File f, string selfName, string traitName) { @@ -86,19 +90,16 @@ private predicate macroImplInfo(MacroItems m, Impl i, File f, string selfName, s traitName = implTraitName(i) } -/** Holds if `t` has a macro-generated (non-derive) impl for `traitName`. */ -predicate hasMacroImpl(TypeItem t, string traitName) { - exists(MacroItems m, Impl i | - macroImplInfo(m, i, fileOf(t), t.getName().getText(), traitName) and - not m = t.getADeriveMacroExpansion() - ) -} - -/** Holds if `t` implements `traitName` via derive, manual impl, or macro. */ +/** + * Holds if `t` implements `traitName`. + * + * Resolves the impl's self type rather than matching it by name and enclosing + * scope, so generic impls (`impl Zeroize for Bag`), impls written + * in another module, and `macro_rules!`-generated impls are all covered without + * enumerating where they may appear. + */ predicate implementsTrait(TypeItem t, string traitName) { - hasDerivedImpl(t, traitName) or - hasManualImpl(t, traitName) or - hasMacroImpl(t, traitName) + exists(Impl i | i.getSelf() = t and implTraitName(i) = traitName) } /** diff --git a/contrib/codeql/lib/types.qll b/contrib/codeql/lib/types.qll new file mode 100644 index 00000000..b0084c18 --- /dev/null +++ b/contrib/codeql/lib/types.qll @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026-present, The Dash Core developers + * SPDX-License-Identifier: MIT + * See the accompanying file LICENSE or https://opensource.org/license/MIT + * + * @description Helpers for reading and resolving written type positions. + */ + +import rust +private import codeql.rust.internal.typeinference.Type as T +private import codeql.rust.internal.typeinference.TypeMention + +/** Gets the head identifier of `tr`, e.g. `Vec` for `Vec`. */ +string typeHead(TypeRepr tr) { + result = tr.(PathTypeRepr).getPath().getSegment().getIdentifier().getText() +} + +/** Gets the type item `tr` names, resolved through the type layer. */ +TypeItem namedTypeItem(TypeRepr tr) { + result = tr.(TypeMention).getType().(T::DataType).getTypeItem() +} + +/** Gets the declared type of a field of `t`, including enum variant fields. */ +TypeRepr fieldTypeRepr(TypeItem t) { + result = t.(Struct).getFieldList().(StructFieldList).getAField().getTypeRepr() + or + result = t.(Struct).getFieldList().(TupleFieldList).getField(_).getTypeRepr() + or + result = t.(Union).getStructFieldList().getAField().getTypeRepr() + or + exists(Variant v | + v = t.(Enum).getVariantList().getAVariant() and + ( + result = v.getFieldList().(StructFieldList).getAField().getTypeRepr() or + result = v.getFieldList().(TupleFieldList).getField(_).getTypeRepr() + ) + ) +} diff --git a/contrib/codeql/zeroize.ql b/contrib/codeql/zeroize.ql new file mode 100644 index 00000000..84af8f23 --- /dev/null +++ b/contrib/codeql/zeroize.ql @@ -0,0 +1,350 @@ +/** + * Copyright (c) 2026-present, The Dash Core developers + * SPDX-License-Identifier: MIT + * See the accompanying file LICENSE or https://opensource.org/license/MIT + * + * @id base-sdk/zeroize-rules + * @name Secret material handling rules + * @description Secret types must wipe, redact, and stay off growable buffers. + * @kind problem + * @precision high + * @problem.severity error + * @tags security + */ + +import lib.files +import lib.filters +import lib.fmt +import lib.policy +import lib.traits +import lib.types +import rust + +/** + * Holds if `f` erases something, as a method call or a qualified path call. + * + * Matched on the `zeroize` prefix: `::zeroize(self)` is the + * spelling a manual `Drop` needs to reach the trait method, and a backend + * wipes through its own helper. + */ +predicate callsZeroize(Function f) { + exists(MethodCallExpr mc | + mc.getEnclosingCallable() = f and + mc.getIdentifier().getText().matches("zeroize%") + ) + or + exists(PathExpr pe | + pe.getEnclosingCallable() = f and + pe.getPath().getSegment().getIdentifier().getText().matches("zeroize%") + ) +} + +/** + * Holds if `t` wipes its own storage. + * + * A bare `Drop` impl proves nothing on its own, so the body has to be seen + * erasing something before the type counts as wiped. + */ +predicate wipesSelf(TypeItem t) { + isWorkspaceFile(fileOf(t)) and + ( + implementsTrait(t, ["Zeroize", "ZeroizeOnDrop"]) or + hasDerive(t, ["Zeroize", "ZeroizeOnDrop"]) + ) + or + exists(Impl i, Function d | + i.getSelf() = t and + implTraitName(i) = "Drop" and + isWorkspaceFile(fileOf(i)) and + d = i.getAssocItemList().getAnAssocItem() and + d.getName().getText() = "drop" and + callsZeroize(d) + ) +} + +/** + * Holds if the dependency type `t` erases itself on drop. + * + * Enumerated because the extractor keeps dependency function bodies out of + * the database. + */ +predicate externalWiper(TypeItem t) { + not isWorkspaceFile(fileOf(t)) and + ( + // `k256::ecdsa::SigningKey` derives `ZeroizeOnDrop`. + t.getName().getText() = "SigningKey" and + fileOf(t).getAbsolutePath().matches("%/ecdsa-%/src/signing.rs") + or + // `blst::{min_pk,min_sig}::SecretKey` are declared `#[zeroize(drop)]`. + t.getName().getText() = "SecretKey" and + fileOf(t).getAbsolutePath().matches("%/blst-%/src/lib.rs") + ) +} + +/** Holds if `t` erases its own storage, without delegating to a field. */ +predicate wipesDirectly(TypeItem t) { + wipesSelf(t) + or + externalWiper(t) +} + +/** + * Holds if a field written as `tr` may be holding secret material. + */ +predicate fieldMayHoldSecret(TypeRepr tr) { + isSecretType(namedTypeItem(tr)) + or + isGrowableType(tr) + or + tr instanceof ArrayTypeRepr +} + +/** + * Holds if a field written as `tr` keeps secret material nothing erases. + * + * Recursive, so a field that neither wipes itself nor sits inside `Zeroizing` + * is cleared only when everything it is in turn built from is cleared. A field + * whose type has no fields to descend into has nowhere left to delegate, so it + * stays reported. + */ +predicate fieldNotWiped(TypeRepr tr) { + fieldMayHoldSecret(tr) and + not typeHead(tr) = "Zeroizing" and + not wipesDirectly(namedTypeItem(tr)) and + ( + not exists(fieldTypeRepr(namedTypeItem(tr))) + or + fieldNotWiped(fieldTypeRepr(namedTypeItem(tr))) + ) +} + +/** + * Holds if every field of `t` is wiped or holds no secret, at any depth. + * + * Checked per field: one wrapped field says nothing about its siblings. + */ +predicate fieldsWipe(TypeItem t) { + exists(fieldTypeRepr(t)) and + not fieldNotWiped(fieldTypeRepr(t)) +} + +/** + * Holds if the secret material in `t` is wiped by something. + * + * Either `t` erases itself, or every secret-bearing field it holds is wiped, + * recursively. + */ +predicate zeroizeSatisfied(TypeItem t) { + wipesDirectly(t) + or + fieldsWipe(t) +} + +/** + * Holds if `t` reaches the wire through the wiping encoder pair. + * + * `impl_stype!`/`impl_sbyte!` emit `type Encoder = ArrEncoder`; the plain + * `impl_type!`/`impl_bytes!` emit `type Encoder = VecEncoder`. + */ +predicate usesSecretBridge(TypeItem t) { + exists(Impl i, TypeAlias ta | + i.getSelf() = t and + implTraitName(i) = "Encodable" and + ta = i.getAssocItemList().getAnAssocItem() and + ta.getName().getText() = "Encoder" and + typeHead(ta.getTypeRepr()) = "ArrEncoder" + ) +} + +/** Holds if `f` stages secret material through a wiping wrapper. */ +predicate wipesInBody(Function f) { + callsZeroize(f) + or + exists(PathExpr pe, Path p | + pe.getEnclosingCallable() = f and + p = pe.getPath() and + p.getSegment().getIdentifier().getText() = "new" and + p.getQualifier().getSegment().getIdentifier().getText() = "Zeroizing" + ) +} + +/** + * Holds if `f` hands back a bare byte container. + * + * A `Zeroizing<..>` return is the wanted shape and a reference borrows rather + * than copies, so neither is reported. + */ +predicate returnsBareBytes(Function f, string retType) { + exists(TypeRepr tr | + tr = f.getRetType().getTypeRepr() and + ( + tr instanceof ArrayTypeRepr and + typeHead(tr.(ArrayTypeRepr).getElementTypeRepr()) = "u8" and + retType = "[u8; N]" + or + typeHead(tr) = ["Vec", "String"] and retType = typeHead(tr) + ) + ) +} + +/** + * Holds if `t` decides equality with `subtle`'s constant-time comparison. + * + * A derived `PartialEq` compares field by field and returns at the first + * mismatch, so how long a comparison runs reveals how much of the secret the + * caller already guessed. + */ +predicate constantTimeEq(TypeItem t) { + exists(Impl i, Function eq | + i.getSelf() = t and + implTraitName(i) = "PartialEq" and + eq = i.getAssocItemList().getAnAssocItem() and + eq.getName().getText() = "eq" and + callsCtEq(eq) + ) +} + +/** Holds if `f` compares through `subtle`. */ +predicate callsCtEq(Function f) { + exists(MethodCallExpr mc | + mc.getEnclosingCallable() = f and + mc.getIdentifier().getText() = "ct_eq" + ) +} + +/** + * Holds if `f` decides something by a comparison that stops early, described + * by `how`. + * + * The short-circuiting adapters walk only as far as the first byte that settles + * the answer, and `==` on a byte container lowers to `memcmp`, which does the + * same. + */ +predicate stopsEarly(Function f, string how) { + exists(MethodCallExpr mc, string name | + mc.getEnclosingCallable() = f and + name = mc.getIdentifier().getText() and + name = ["all", "any", "position", "find", "contains", "starts_with", "ends_with"] and + how = name + "()" + ) + or + exists(BinaryExpr be | + be.getEnclosingCallable() = f and + be.getOperatorName() = ["==", "!="] and + how = be.getOperatorName() + ) +} + +/** + * Holds if a method of a secret type answers a yes/no question about its own + * bytes in variable time, because it uses `how`. + * + * `PartialEq` has its own rule, so `eq` is left to it. This covers the + * predicates that sit beside it, e.g. an `is_null` that returns at the first + * non-zero byte and so leaks how long the leading run of zeroes is. + */ +predicate variableTimeSecretTest(Function f, string how) { + exists(TypeItem t, Impl i | + enforcedSecretType(t) and + i.getSelf() = t and + f = i.getAssocItemList().getAnAssocItem() and + not isTestCode(f) and + not f.getName().getText() = "eq" and + typeHead(f.getRetType().getTypeRepr()) = "bool" and + stopsEarly(f, how) and + not callsCtEq(f) + ) +} + +/** + * Holds if `t` is a secret-bearing type. + * + * Unlike `isSourceType` this preserves macro-generated items: `make_bytes!` and + * friends can mint secret bags wholesale, and dropping them would leave the types + * this query exists to examine unchecked. + */ +predicate secretType(TypeItem t) { + isSecretType(t) and + fileOf(t).fromSource() and + not isTestCode(t) +} + +/** Holds if `t` is a secret-bearing type in a crate the policy covers. */ +predicate enforcedSecretType(TypeItem t) { + secretType(t) and + isEnforcedCrate(fileOf(t)) +} + +/** Holds if the secret material in `t` is never erased. */ +predicate unwipedSecret(TypeItem t) { + enforcedSecretType(t) and + not zeroizeSatisfied(t) +} + +/** Holds if `t` can be formatted without redacting its contents, because `cause`. */ +predicate unredactedSecret(TypeItem t, string cause) { + enforcedSecretType(t) and + ( + hasDerivedImpl(t, "Debug") and cause = "derives Debug" + or + hasDerivedImpl(t, "Display") and cause = "derives Display" + or + not implementsTrait(t, "Debug") and cause = "has no manual Debug" + ) +} + +/** + * Holds if `f` wipes internally but hands the caller a bare `retType`, leaving + * the erasure of that copy to them. + */ +predicate leakedWipedBytes(Function f, string retType) { + isEnforcedCrate(fileOf(f)) and + not isTestCode(f) and + wipesInBody(f) and + returnsBareBytes(f, retType) +} + +/** + * Holds if `t` is an encodable secret on growable storage, which can + * reallocate mid-write and strand a copy the wipe never reaches. + */ +predicate growableSecret(TypeItem t) { + enforcedSecretType(t) and + not isNotEncodable(t) and + isGrowableType(fieldTypeRepr(t)) +} + +/** Holds if `t` reaches the wire through an encoder that does not wipe. */ +predicate leakySecretBridge(TypeItem t) { + enforcedSecretType(t) and + implementsTrait(t, "Encodable") and + not usesSecretBridge(t) +} + +/** Holds if `t` can be compared in a time that depends on its contents. */ +predicate variableTimeSecretEq(TypeItem t) { + enforcedSecretType(t) and + implementsTrait(t, "PartialEq") and + not constantTimeEq(t) +} + +from Locatable e, string message +where + unwipedSecret(e) and message = "secret type is never wiped" + or + exists(string cause | unredactedSecret(e, cause) | message = fmt("secret type {0}", cause)) + or + exists(string retType | leakedWipedBytes(e, retType) | + message = fmt("wiping function returns bare {0}", retType) + ) + or + growableSecret(e) and message = "encodable secret type is backed by a growable buffer" + or + leakySecretBridge(e) and message = "secret wire type stages through a non-wiping encoder" + or + variableTimeSecretEq(e) and message = "secret type compares in variable time" + or + exists(string how | variableTimeSecretTest(e, how) | + message = fmt("secret type test uses {0}, which stops early", how) + ) +select e, message diff --git a/contrib/semgrep/workspace.yml b/contrib/semgrep/workspace.yml index e5d08936..e4bebb58 100644 --- a/contrib/semgrep/workspace.yml +++ b/contrib/semgrep/workspace.yml @@ -69,6 +69,8 @@ rules: include: - /pkgs/primitives/src/**/*.rs - /pkgs/p2p_core/src/**/*.rs + - /pkgs/pkc/src/**/*.rs + - /pkgs/script/src/**/*.rs patterns: - pattern-regex: '\.extend_from_slice\([^)]*\)' - pattern-not-regex: "to_be_bytes" @@ -76,21 +78,6 @@ rules: - pattern-not-regex: '&self\.0' - pattern-not-regex: "encode_to_vec" - - id: derive-no-debug-secret - message: "secret type must not derive Debug; impl manually to redact contents" - severity: ERROR - languages: [rust] - paths: - include: [/pkgs/**/*.rs, /contrib/samples/**/*.rs] - pattern-regex: | - (?sx) - \#\[derive\( [^)]* \bDebug\b [^)]* \)\] - \s* - (?: \#\[ [^\]]* \] \s* )* # skip intermediate attributes - (?: pub (?: \( [^)]* \) )? \s+ )? # optional visibility - (?:struct|enum) \s+ - \w* (?:Secret|Private|Seed|Password|Mnemonic|Share(?!d)) \w* - - id: derive-serde-disambiguate message: "use ::serde:: instead of serde:: in derive attributes" severity: ERROR diff --git a/pkgs/p2p_core/src/prelude.rs b/pkgs/p2p_core/src/prelude.rs index 3e4af7bb..b0509665 100644 --- a/pkgs/p2p_core/src/prelude.rs +++ b/pkgs/p2p_core/src/prelude.rs @@ -8,4 +8,5 @@ pub(crate) use alloc::format; pub(crate) use alloc::string::String; +pub(crate) use alloc::vec; pub(crate) use alloc::vec::Vec; diff --git a/pkgs/p2p_core/src/primitives/compressed_header.rs b/pkgs/p2p_core/src/primitives/compressed_header.rs index a9788e74..d5842e6a 100644 --- a/pkgs/p2p_core/src/primitives/compressed_header.rs +++ b/pkgs/p2p_core/src/primitives/compressed_header.rs @@ -93,7 +93,7 @@ impl CompressionState { let pos = (version_offset - 1) as usize; if pos >= self.version_cache.len() { return Err(DecodeError::InvalidValue { - expected: self.version_cache.len() as u64, + expected: (0..self.version_cache.len() as u64).collect(), actual: pos as u64, }); } diff --git a/pkgs/p2p_core/src/primitives/inventory.rs b/pkgs/p2p_core/src/primitives/inventory.rs index ecb3bd17..92300308 100644 --- a/pkgs/p2p_core/src/primitives/inventory.rs +++ b/pkgs/p2p_core/src/primitives/inventory.rs @@ -10,57 +10,28 @@ use crate::codec::codec_p2p; use dash_num::Hash256; use dash_primitives::hash_impl; -use dash_types::codec::NumCodec; -use dash_types::{impl_num, TypeId}; +use dash_types::{enum_map, impl_num, TypeId}; use core::fmt; -/// Inventory object type. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum InvType { - /// Error / not used. - Error, - /// Transaction. - Tx, - /// Block. - Block, - /// Filtered block (BIP37). - FilteredBlock, - /// Compact block (BIP152). - CompactBlock, - /// Governance object. - GovernanceObject, - /// Governance object vote. - GovernanceObjectVote, - /// Unknown or unhandled type. - Unknown(u32), -} - -impl NumCodec for InvType { - fn from_base(v: u32) -> Self { - match v { - 0 => Self::Error, - 1 => Self::Tx, - 2 => Self::Block, - 3 => Self::FilteredBlock, - 4 => Self::CompactBlock, - 17 => Self::GovernanceObject, - 18 => Self::GovernanceObjectVote, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u32 { - match self { - Self::Error => 0, - Self::Tx => 1, - Self::Block => 2, - Self::FilteredBlock => 3, - Self::CompactBlock => 4, - Self::GovernanceObject => 17, - Self::GovernanceObjectVote => 18, - Self::Unknown(v) => *v, - } +enum_map! { + /// Inventory object type. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum InvType, u32, Unknown { + /// Error / not used. + Error = 0 => "error", + /// Transaction. + Tx = 1 => "tx", + /// Block. + Block = 2 => "block", + /// Filtered block (BIP37). + FilteredBlock = 3 => "filtered_block", + /// Compact block (BIP152). + CompactBlock = 4 => "compact_block", + /// Governance object. + GovernanceObject = 17 => "governance_object", + /// Governance object vote. + GovernanceObjectVote = 18 => "governance_object_vote", } } @@ -68,21 +39,6 @@ impl_num!(InvType, u32); hash_impl!(InvType); -impl fmt::Display for InvType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Error => f.write_str("error"), - Self::Tx => f.write_str("tx"), - Self::Block => f.write_str("block"), - Self::FilteredBlock => f.write_str("filtered_block"), - Self::CompactBlock => f.write_str("compact_block"), - Self::GovernanceObject => f.write_str("governance_object"), - Self::GovernanceObjectVote => f.write_str("governance_object_vote"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - /// An inventory vector: a typed 32-byte hash. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] diff --git a/pkgs/p2p_core/src/primitives/mn_list.rs b/pkgs/p2p_core/src/primitives/mn_list.rs index 45d65766..2d820a61 100644 --- a/pkgs/p2p_core/src/primitives/mn_list.rs +++ b/pkgs/p2p_core/src/primitives/mn_list.rs @@ -9,7 +9,7 @@ use crate::codec::{codec_p2p, impl_p2p}; use crate::prelude::*; -use dash_pkc::{BlsPublicKeyBytes, BlsSignatureBytes}; +use dash_pkc::bls::{BlsPkBytes, BlsScIetf, BlsSigBytes}; use dash_primitives::{ hash_impl, BlockHash, Commitment, KeyId, LlmqType, MnType, PlatformNodeId, ServiceV1, Transaction, TxHash, }; @@ -32,7 +32,7 @@ pub struct SimplifiedMnListEntry { /// Network service address. pub service: ServiceV1, /// BLS operator public key. - pub operator_key: BlsPublicKeyBytes, + pub operator_key: BlsPkBytes, /// Voting key hash (HASH160). pub voting_key_id: KeyId, /// Whether this masternode is currently valid. @@ -54,7 +54,7 @@ impl BaseCodec for SimplifiedMnListEntry { let pro_reg_tx_hash = TxHash::decode(data)?; let confirmed_hash = BlockHash::decode(data)?; let service = ServiceV1::decode(data)?; - let operator_key = BlsPublicKeyBytes::decode(data)?; + let operator_key = BlsPkBytes::::decode(data)?; let voting_key_id = KeyId::decode(data)?; let is_valid = bool::decode(data)?; @@ -134,7 +134,7 @@ codec_p2p!(DeletedQuorum { llmq_type, hash }); #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct QuorumClSig { /// BLS signature. - pub sig: BlsSignatureBytes, + pub sig: BlsSigBytes, /// Indices into the `new_quorums` vector. pub index_set: Vec, } diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 5eb27e4f..d059ebcc 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -8,10 +8,14 @@ license = "MIT" bitcoin-consensus-encoding = { version = "0.2", default-features = false, features = [ "alloc", ] } +bitcoin_hashes = { version = "0.20", default-features = false, features = [ + "alloc", +] } blst = { version = "0.3", default-features = false, optional = true } cfg-if = "1" dash-num = { version = "0.0.0", path = "../num" } dash-types = { version = "0.0.0", path = "../types", default-features = false } +hex-conservative = { version = "0.3", optional = true } hex-literal = "0.4" k256 = { version = "0.13", default-features = false, features = [ "arithmetic", @@ -26,6 +30,7 @@ serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } sha2 = { version = "0.10", default-features = false, optional = true } +subtle = { version = "2", default-features = false } zeroize = { version = "1", default-features = false, features = ["derive"] } [dev-dependencies] @@ -33,16 +38,22 @@ dash-dev = { version = "0.0.0", path = "../dev", features = ["full"] } divan = "0.1" hex-conservative = "0.3" rand_core = { version = "0.6", features = ["getrandom"] } +rstest = "0.25" serde = { version = "1", features = ["derive"] } [features] default = [] -std = ["dep:rayon", "rand_core/getrandom", "dash-types/std"] +std = [ + "dep:rayon", + "bitcoin_hashes/std", + "dash-types/std", + "rand_core/getrandom", +] bls = ["dep:blst", "dep:sha2"] -k256 = ["dep:k256"] -serde = ["dep:serde", "dash-types/serde"] -full = ["k256", "bls", "serde", "std", "tests"] -tests = ["std", "dep:rstest"] +ecdsa = ["dep:k256"] +serde = ["dep:serde", "dep:hex-conservative", "dash-num/serde", "dash-types/serde"] +full = ["ecdsa", "bls", "serde", "std", "tests"] +tests = ["std", "dep:hex-conservative", "dep:rstest"] [lints] workspace = true @@ -54,6 +65,7 @@ workspace = true name = "pkc" path = "bench/main.rs" harness = false +required-features = ["tests"] [[test]] name = "bls_chia_aggregate" @@ -111,10 +123,3 @@ required-features = ["bls", "tests"] name = "bls_ietf_threshold" required-features = ["bls", "tests"] -[[test]] -name = "k256_keygen" -required-features = ["k256", "tests"] - -[[test]] -name = "k256_sign" -required-features = ["k256", "tests"] diff --git a/pkgs/pkc/bench/k256.rs b/pkgs/pkc/bench/ecdsa.rs similarity index 62% rename from pkgs/pkc/bench/k256.rs rename to pkgs/pkc/bench/ecdsa.rs index 7edafd55..b42858df 100644 --- a/pkgs/pkc/bench/k256.rs +++ b/pkgs/pkc/bench/ecdsa.rs @@ -4,30 +4,20 @@ // See the accompanying file LICENSE or https://opensource.org/license/MIT // -//! Benchmarks for the k256 (secp256k1) feature +//! Benchmarks for the ecdsa (secp256k1) feature -use dash_pkc::k256::{PublicKey, SecretKey}; +use dash_pkc::ecdsa::tests::{message_hash, ALICE_SK}; +use dash_pkc::ecdsa::{EcdsaPublicKey, EcdsaSecretKey}; -fn test_key() -> SecretKey { - let bytes = [ - 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, - 0x98, 0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, - ]; - SecretKey::from_bytes(&bytes).unwrap() -} - -fn test_msg_hash(i: u8) -> [u8; 32] { - let mut h = [0u8; 32]; - h[0] = i; - h[31] = i.wrapping_mul(37); - h +fn test_key() -> EcdsaSecretKey { + EcdsaSecretKey::from_bytes(&ALICE_SK).unwrap() } #[divan::bench] fn sign(bencher: divan::Bencher) { let sk = test_key(); bencher.counter(divan::counter::ItemsCount::new(1u32)).bench(|| { - let msg = test_msg_hash(42); + let msg = message_hash(42); sk.sign(&msg).unwrap() }); } @@ -35,7 +25,7 @@ fn sign(bencher: divan::Bencher) { #[divan::bench] fn verify(bencher: divan::Bencher) { let sk = test_key(); - let msg = test_msg_hash(99); + let msg = message_hash(99); let sig = sk.sign(&msg).unwrap(); let pk = sk.public_key(); bencher @@ -48,17 +38,17 @@ fn sign_recoverable(bencher: divan::Bencher) { let sk = test_key(); bencher .counter(divan::counter::ItemsCount::new(1u32)) - .bench(|| sk.sign_recoverable(&test_msg_hash(7)).unwrap()); + .bench(|| sk.sign_recoverable(&message_hash(7)).unwrap()); } #[divan::bench] fn recover(bencher: divan::Bencher) { let sk = test_key(); - let msg = test_msg_hash(55); + let msg = message_hash(55); let (sig, rid) = sk.sign_recoverable(&msg).unwrap(); bencher .counter(divan::counter::ItemsCount::new(1u32)) - .bench(|| PublicKey::recover(&msg, &sig, rid)); + .bench(|| EcdsaPublicKey::recover(&msg, &sig, rid)); } #[divan::bench] @@ -70,22 +60,21 @@ fn ser_pk(bencher: divan::Bencher) { #[divan::bench] fn deser_pk(bencher: divan::Bencher) { let bytes = test_key().public_key().to_bytes(); - bencher.bench(|| PublicKey::from_bytes(&bytes)); + bencher.bench(|| EcdsaPublicKey::from_bytes(&bytes)); } #[cfg(feature = "std")] mod worker_benches { - use dash_pkc::k256::{PublicKey, SecretKey, Signature}; + use dash_pkc::ecdsa::tests::{message_hash, BOB_SK}; + use dash_pkc::ecdsa::{EcdsaPublicKey, EcdsaSecretKey, EcdsaSignature}; use dash_pkc::worker; - fn setup_sigs(n: usize) -> Vec<(Signature, PublicKey, [u8; 32])> { - let sk = SecretKey::from_bytes(&[0x42u8; 32]).unwrap(); + fn setup_sigs(n: usize) -> Vec<(EcdsaSignature, EcdsaPublicKey, [u8; 32])> { + let sk = EcdsaSecretKey::from_bytes(&BOB_SK).unwrap(); let pk = sk.public_key(); (0..n) .map(|i| { - let mut msg = [0u8; 32]; - msg[0] = i as u8; - msg[31] = (i >> 8) as u8; + let msg = message_hash(i as u16); let sig = sk.sign(&msg).unwrap(); (sig, pk.clone(), msg) }) diff --git a/pkgs/pkc/bench/main.rs b/pkgs/pkc/bench/main.rs index f854c54b..f77636c0 100644 --- a/pkgs/pkc/bench/main.rs +++ b/pkgs/pkc/bench/main.rs @@ -5,7 +5,7 @@ // #![cfg_attr( - any(feature = "bls", feature = "k256"), + any(feature = "bls", feature = "ecdsa"), expect(clippy::unwrap_used, reason = "benchmarks rely on trusted test vectors") )] @@ -13,8 +13,8 @@ mod bls_chia; #[cfg(feature = "bls")] mod bls_ietf; -#[cfg(feature = "k256")] -mod k256; +#[cfg(feature = "ecdsa")] +mod ecdsa; fn main() { divan::main(); diff --git a/pkgs/pkc/corpus/k256_keygen.json5 b/pkgs/pkc/corpus/ecdsa_keygen.json5 similarity index 100% rename from pkgs/pkc/corpus/k256_keygen.json5 rename to pkgs/pkc/corpus/ecdsa_keygen.json5 diff --git a/pkgs/pkc/corpus/k256_sign.json5 b/pkgs/pkc/corpus/ecdsa_sign.json5 similarity index 100% rename from pkgs/pkc/corpus/k256_sign.json5 rename to pkgs/pkc/corpus/ecdsa_sign.json5 diff --git a/pkgs/pkc/src/bls/blst_ffi.rs b/pkgs/pkc/src/bls/blst_ffi.rs index 8ae3e352..411dea95 100644 --- a/pkgs/pkc/src/bls/blst_ffi.rs +++ b/pkgs/pkc/src/bls/blst_ffi.rs @@ -7,9 +7,10 @@ //! Bridging routines for unsafe blst FFI operations. use blst::*; -use dash_types::type_cvrt; +use dash_types::{type_cvrt, Unencodable}; use zeroize::Zeroize; +use core::fmt; use core::ops::{Add, Mul, Neg, Sub}; use core::ptr::null_mut; @@ -80,6 +81,12 @@ impl Fr { } } +impl fmt::Debug for Fr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Fr(..)") + } +} + impl Add for Fr { type Output = Self; @@ -140,7 +147,7 @@ type_cvrt!(From for blst_scalar, |fr| { /// An element of the BLS12-381 base field, i.e. an integer reduced /// modulo the field prime `p`. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] pub(crate) struct Fp(blst_fp); impl Fp { @@ -208,11 +215,9 @@ type_cvrt!(From for blst_fp, |fp| fp.0); type_cvrt!(From for Fp, |raw| Self(*raw)); -type_cvrt!(From for Fp2, |fp| Self::new(*fp, Fp::default())); - /// An element of the quadratic extension field `Fp2 = Fp[u]/(u^2 + 1)`, /// written `c0 + c1*u`. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] pub(crate) struct Fp2(blst_fp2); impl Fp2 { @@ -316,6 +321,8 @@ impl Sub for Fp2 { } } +type_cvrt!(From for Fp2, |fp| Self::new(*fp, Fp::default())); + type_cvrt!(From for blst_fp2, |fp2| fp2.0); type_cvrt!(From for Fp2, |raw| Self(*raw)); @@ -334,7 +341,7 @@ pub(crate) trait Point: Copy + Default + Add { /// A point of the G1 group (over `Fp`) in projective coordinates, /// suitable for accumulation before a single conversion to affine. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, Unencodable)] pub(crate) struct G1(blst_p1); impl G1 { @@ -373,7 +380,7 @@ type_cvrt!(From for G1, |raw| Self(*raw)); /// A point of the G1 group in affine coordinates, the canonical form /// used for serialization and pairing inputs. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] pub(crate) struct G1Affine(blst_p1_affine); impl G1Affine { @@ -418,7 +425,7 @@ type_cvrt!(From for G1Affine, |raw| Self(*raw)); /// A point of the G2 group (over `Fp2`) in projective coordinates, /// suitable for accumulation before a single conversion to affine. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, Unencodable)] pub(crate) struct G2(blst_p2); impl G2 { @@ -479,7 +486,7 @@ type_cvrt!(From for blst_p2, |g| g.0); /// A point of the G2 group in affine coordinates, the canonical form /// used for serialization and pairing inputs. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] pub(crate) struct G2Affine(blst_p2_affine); impl G2Affine { diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index cc72b485..28f62c80 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -7,20 +7,27 @@ //! Unified BLS cryptography module. mod error; +mod public_bytes; +mod schemes; +mod secret_bytes; +mod sig_bytes; +mod sig_id; pub use error::BlsError; +pub use public_bytes::{BlsPkBytes, BLS_PK_LEN}; +pub use schemes::{BlsScChia, BlsScIetf, BlsSchemeId}; +pub use secret_bytes::{BlsSkBytes, BLS_SK_LEN}; +pub use sig_bytes::{BlsSigBytes, BLS_SIG_LEN}; +pub use sig_id::BlsSigId; cfg_if::cfg_if! { if #[cfg(feature = "bls")] { + mod scheme_chia; + mod scheme_ietf; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; pub(crate) mod chia_h2c; pub(crate) mod scheme_ops; - mod scheme_chia; - mod scheme_ietf; - mod schemes; - - pub(crate) use schemes::{BlsScChia, BlsScIetf}; #[cfg(feature = "tests")] #[doc(hidden)] diff --git a/pkgs/pkc/src/bls/public_bytes.rs b/pkgs/pkc/src/bls/public_bytes.rs new file mode 100644 index 00000000..e699539a --- /dev/null +++ b/pkgs/pkc/src/bls/public_bytes.rs @@ -0,0 +1,96 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! BLS public key byte bag. + +use crate::bls::BlsSchemeId; + +use bitcoin_hashes::sha256d::Hash as Sha256d; +use dash_num::Hash256; +use dash_types::codec::{take, BaseCodec, DecodeError, EncodeBuf, Hashable, TypeId}; +use dash_types::{derive_bytes, impl_type}; + +use core::fmt; +use core::marker::PhantomData; + +/// Raw BLS public key length (G1 compressed). +pub const BLS_PK_LEN: usize = 48; + +/// Scheme-tagged BLS public key bytes (48 bytes, unvalidated). +pub struct BlsPkBytes { + inner: [u8; BLS_PK_LEN], + _scheme: PhantomData, +} + +impl BaseCodec for BlsPkBytes { + fn decode(data: &mut &[u8]) -> Result { + take::(data).map(Self::from_bytes) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + buf.extend_from_slice(&self.inner); // nosemgrep: codec-no-raw-extend + } +} + +impl_type!(for[S: BlsSchemeId] BlsPkBytes, BLS_PK_LEN); + +impl Hashable for BlsPkBytes { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + Hash256::from_bytes(Sha256d::hash(&self.inner).to_byte_array()) + } +} + +impl BlsPkBytes { + /// Wraps raw bytes. + pub const fn from_bytes(bytes: [u8; BLS_PK_LEN]) -> Self { + Self { + inner: bytes, + _scheme: PhantomData, + } + } + + /// Borrows the inner byte array. + pub const fn as_bytes(&self) -> &[u8; BLS_PK_LEN] { + &self.inner + } + + /// Returns the inner byte array. + pub const fn into_bytes(self) -> [u8; BLS_PK_LEN] { + self.inner + } + + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { + self.inner.iter().all(|&b| b == 0) + } +} + +impl TypeId for BlsPkBytes { + const TYPE_ID: u32 = S::PK_TYPE_ID; +} + +derive_bytes!(for[S: BlsSchemeId] BlsPkBytes, BLS_PK_LEN); + +impl fmt::Debug for BlsPkBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BlsPkBytes<{}>(", S::LABEL)?; + for byte in &self.inner { + write!(f, "{byte:02x}")?; + } + write!(f, ")") + } +} + +impl fmt::Display for BlsPkBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in &self.inner { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index e3e30d63..1bd440a6 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -8,6 +8,7 @@ use super::blst_ffi::{self, Fr, Point, G1, G2}; use super::error::BlsError; +use super::schemes::BlsSchemeId; use crate::prelude::*; use dash_num::Hash256; @@ -23,7 +24,7 @@ use core::fmt::Debug; const WEIGHT_BITS: usize = 256; /// BLS operations tied to a specific scheme. -pub(crate) trait BlsScheme { +pub(crate) trait BlsScheme: BlsSchemeId { /// Inner secret key representation. type InnerSk: Clone; /// Inner public key representation. diff --git a/pkgs/pkc/src/bls/schemes.rs b/pkgs/pkc/src/bls/schemes.rs index 93feee0f..edee56b7 100644 --- a/pkgs/pkc/src/bls/schemes.rs +++ b/pkgs/pkc/src/bls/schemes.rs @@ -4,12 +4,46 @@ // See the accompanying file LICENSE or https://opensource.org/license/MIT // -//! BLS scheme marker types. +//! BLS scheme trait and marker types. + +use dash_types::Unencodable; + +/// BLS scheme discriminator. +pub trait BlsSchemeId: 'static { + /// `TypeId` constant for `BlsPkBytes`. + const PK_TYPE_ID: u32; + /// `TypeId` constant for `BlsSkBytes`. + const SK_TYPE_ID: u32; + /// `TypeId` constant for `BlsSigBytes`. + const SIG_TYPE_ID: u32; + /// Human-readable scheme label for `Debug`/`Display`. + const LABEL: &'static str; +} /// Legacy (Chia) BLS scheme marker. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub(crate) enum BlsScChia {} +#[derive(Clone, Debug, Eq, Hash, PartialEq, Unencodable)] +pub enum BlsScChia {} + +impl BlsSchemeId for BlsScChia { + // xxh32(b"BlsPkBytesChia", 0) + const PK_TYPE_ID: u32 = 0xE377_6DA7; + // xxh32(b"BlsSkBytesChia", 0) + const SK_TYPE_ID: u32 = 0x3D50_6855; + // xxh32(b"BlsSigBytesChia", 0) + const SIG_TYPE_ID: u32 = 0xEF4A_E265; + const LABEL: &'static str = "Chia"; +} /// IETF-standard BLS scheme marker. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub(crate) enum BlsScIetf {} +#[derive(Clone, Debug, Eq, Hash, PartialEq, Unencodable)] +pub enum BlsScIetf {} + +impl BlsSchemeId for BlsScIetf { + // xxh32(b"BlsPkBytesIetf", 0) + const PK_TYPE_ID: u32 = 0x6D54_3438; + // xxh32(b"BlsSkBytesIetf", 0) + const SK_TYPE_ID: u32 = 0xB5CE_BF45; + // xxh32(b"BlsSigBytesIetf", 0) + const SIG_TYPE_ID: u32 = 0xF57D_EF57; + const LABEL: &'static str = "Ietf"; +} diff --git a/pkgs/pkc/src/bls/secret_bytes.rs b/pkgs/pkc/src/bls/secret_bytes.rs new file mode 100644 index 00000000..24dff237 --- /dev/null +++ b/pkgs/pkc/src/bls/secret_bytes.rs @@ -0,0 +1,126 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! BLS secret key byte bag. + +use crate::bls::BlsSchemeId; + +use bitcoin_hashes::sha256d::Hash as Sha256d; +use dash_num::Hash256; +use dash_types::codec::{take, BaseCodec, DecodeError, EncodeBuf, Hashable, TypeId}; +use dash_types::impl_stype; +use subtle::ConstantTimeEq; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +use core::fmt; +use core::marker::PhantomData; + +/// Raw BLS secret key length (scalar). +pub const BLS_SK_LEN: usize = 32; + +/// Scheme-tagged BLS secret key bytes (32 bytes, zeroized on drop). +pub struct BlsSkBytes { + inner: [u8; BLS_SK_LEN], + _scheme: PhantomData, +} + +impl BaseCodec for BlsSkBytes { + fn decode(data: &mut &[u8]) -> Result { + take::(data).map(Self::from_bytes) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + buf.extend_from_slice(&self.inner); // nosemgrep: codec-no-raw-extend + } +} + +impl_stype!(for[S: BlsSchemeId] BlsSkBytes, BLS_SK_LEN); + +impl Hashable for BlsSkBytes { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + Hash256::from_bytes(Sha256d::hash(&self.inner).to_byte_array()) + } +} + +impl BlsSkBytes { + /// Wraps raw bytes. + pub const fn from_bytes(bytes: [u8; BLS_SK_LEN]) -> Self { + Self { + inner: bytes, + _scheme: PhantomData, + } + } + + /// Borrows the inner byte array. + pub const fn as_bytes(&self) -> &[u8; BLS_SK_LEN] { + &self.inner + } + + /// Copies out the inner bytes in a zeroizing wrapper. + pub fn to_bytes(&self) -> Zeroizing<[u8; BLS_SK_LEN]> { + Zeroizing::new(self.inner) + } + + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { + self.inner.ct_eq(&[0u8; BLS_SK_LEN]).into() + } +} + +impl TypeId for BlsSkBytes { + const TYPE_ID: u32 = S::SK_TYPE_ID; +} + +impl AsRef<[u8; BLS_SK_LEN]> for BlsSkBytes { + fn as_ref(&self) -> &[u8; BLS_SK_LEN] { + &self.inner + } +} + +impl Clone for BlsSkBytes { + fn clone(&self) -> Self { + Self { + inner: self.inner, + _scheme: PhantomData, + } + } +} + +impl Zeroize for BlsSkBytes { + fn zeroize(&mut self) { + self.inner.zeroize(); + } +} + +impl Drop for BlsSkBytes { + fn drop(&mut self) { + ::zeroize(self); + } +} + +impl ZeroizeOnDrop for BlsSkBytes {} + +impl Eq for BlsSkBytes {} + +impl PartialEq for BlsSkBytes { + fn eq(&self, other: &Self) -> bool { + self.inner.ct_eq(&other.inner).into() + } +} + +impl fmt::Debug for BlsSkBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BlsSkBytes<{}>(..)", S::LABEL) + } +} + +impl fmt::Display for BlsSkBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} diff --git a/pkgs/pkc/src/bls/sig_bytes.rs b/pkgs/pkc/src/bls/sig_bytes.rs new file mode 100644 index 00000000..3c0001a3 --- /dev/null +++ b/pkgs/pkc/src/bls/sig_bytes.rs @@ -0,0 +1,96 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! BLS signature byte bag. + +use crate::bls::BlsSchemeId; + +use bitcoin_hashes::sha256d::Hash as Sha256d; +use dash_num::Hash256; +use dash_types::codec::{take, BaseCodec, DecodeError, EncodeBuf, Hashable, TypeId}; +use dash_types::{derive_bytes, impl_type}; + +use core::fmt; +use core::marker::PhantomData; + +/// Raw BLS signature length (G2 compressed). +pub const BLS_SIG_LEN: usize = 96; + +/// Scheme-tagged BLS signature bytes (96 bytes, unvalidated). +pub struct BlsSigBytes { + inner: [u8; BLS_SIG_LEN], + _scheme: PhantomData, +} + +impl BaseCodec for BlsSigBytes { + fn decode(data: &mut &[u8]) -> Result { + take::(data).map(Self::from_bytes) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + buf.extend_from_slice(&self.inner); // nosemgrep: codec-no-raw-extend + } +} + +impl_type!(for[S: BlsSchemeId] BlsSigBytes, BLS_SIG_LEN); + +impl Hashable for BlsSigBytes { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + Hash256::from_bytes(Sha256d::hash(&self.inner).to_byte_array()) + } +} + +impl BlsSigBytes { + /// Wraps raw bytes. + pub const fn from_bytes(bytes: [u8; BLS_SIG_LEN]) -> Self { + Self { + inner: bytes, + _scheme: PhantomData, + } + } + + /// Borrows the inner byte array. + pub const fn as_bytes(&self) -> &[u8; BLS_SIG_LEN] { + &self.inner + } + + /// Returns the inner byte array. + pub const fn into_bytes(self) -> [u8; BLS_SIG_LEN] { + self.inner + } + + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { + self.inner.iter().all(|&b| b == 0) + } +} + +impl TypeId for BlsSigBytes { + const TYPE_ID: u32 = S::SIG_TYPE_ID; +} + +derive_bytes!(for[S: BlsSchemeId] BlsSigBytes, BLS_SIG_LEN); + +impl fmt::Debug for BlsSigBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BlsSigBytes<{}>(", S::LABEL)?; + for byte in &self.inner { + write!(f, "{byte:02x}")?; + } + write!(f, ")") + } +} + +impl fmt::Display for BlsSigBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in &self.inner { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} diff --git a/pkgs/pkc/src/bls/sig_id.rs b/pkgs/pkc/src/bls/sig_id.rs new file mode 100644 index 00000000..7fae85fa --- /dev/null +++ b/pkgs/pkc/src/bls/sig_id.rs @@ -0,0 +1,19 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Signature types. + +use dash_types::Unencodable; + +/// BLS signature variant. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +pub enum BlsSigId { + /// Basic scheme (NUL augmentation). + Basic, + /// Proof of Possession scheme. + ProofOfPossession, +} diff --git a/pkgs/pkc/src/bls_chia/pk.rs b/pkgs/pkc/src/bls_chia/pk.rs index 30713361..a68ce6b3 100644 --- a/pkgs/pkc/src/bls_chia/pk.rs +++ b/pkgs/pkc/src/bls_chia/pk.rs @@ -9,14 +9,16 @@ use super::sk::SecretKey; use crate::bls::blst_ffi::G1Affine; use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScChia}; +use crate::bls::{BlsError, BlsPkBytes, BlsScChia}; + +use dash_types::Unencodable; /// A legacy BLS public key (48-byte G1 point in legacy serialization). -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr( feature = "serde", - serde(into = "crate::BlsPublicKeyBytes", try_from = "crate::BlsPublicKeyBytes",) + serde(into = "BlsPkBytes", try_from = "BlsPkBytes",) )] pub struct PublicKey(pub(super) G1Affine); @@ -53,16 +55,16 @@ impl PublicKey { crate::common::bls::impl_hash_via_bytes!(PublicKey); -impl From for crate::BlsPublicKeyBytes { +impl From for BlsPkBytes { fn from(pk: PublicKey) -> Self { - Self(pk.to_bytes()) + Self::from_bytes(pk.to_bytes()) } } -impl TryFrom for PublicKey { - type Error = crate::bls::BlsError; +impl TryFrom> for PublicKey { + type Error = BlsError; - fn try_from(bytes: crate::BlsPublicKeyBytes) -> Result { - Self::from_bytes(&bytes.0) + fn try_from(bytes: BlsPkBytes) -> Result { + Self::from_bytes(bytes.as_bytes()) } } diff --git a/pkgs/pkc/src/bls_chia/sig.rs b/pkgs/pkc/src/bls_chia/sig.rs index 19c36fed..bc82cb86 100644 --- a/pkgs/pkc/src/bls_chia/sig.rs +++ b/pkgs/pkc/src/bls_chia/sig.rs @@ -9,14 +9,16 @@ use super::pk::PublicKey; use crate::bls::blst_ffi::G2Affine; use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScChia}; +use crate::bls::{BlsError, BlsScChia, BlsSigBytes}; + +use dash_types::Unencodable; /// A legacy BLS signature (96-byte G2 point in legacy serialization). -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr( feature = "serde", - serde(into = "crate::BlsSignatureBytes", try_from = "crate::BlsSignatureBytes",) + serde(into = "BlsSigBytes", try_from = "BlsSigBytes",) )] pub struct Signature(pub(super) G2Affine); @@ -44,16 +46,16 @@ impl Signature { crate::common::bls::impl_hash_via_bytes!(Signature); -impl From for crate::BlsSignatureBytes { +impl From for BlsSigBytes { fn from(sig: Signature) -> Self { - Self(sig.to_bytes()) + Self::from_bytes(sig.to_bytes()) } } -impl TryFrom for Signature { - type Error = crate::bls::BlsError; +impl TryFrom> for Signature { + type Error = BlsError; - fn try_from(bytes: crate::BlsSignatureBytes) -> Result { - Self::from_bytes(&bytes.0) + fn try_from(bytes: BlsSigBytes) -> Result { + Self::from_bytes(bytes.as_bytes()) } } diff --git a/pkgs/pkc/src/bls_chia/threshold.rs b/pkgs/pkc/src/bls_chia/threshold.rs index ebda3f0f..98bc06dd 100644 --- a/pkgs/pkc/src/bls_chia/threshold.rs +++ b/pkgs/pkc/src/bls_chia/threshold.rs @@ -14,6 +14,10 @@ use crate::bls::{BlsError, BlsScChia}; use crate::prelude::*; use dash_num::Hash256; +use dash_types::Unencodable; + +use core::fmt; +use core::hash::{Hash, Hasher}; /// Secret key share for threshold signing. #[derive(Clone)] @@ -47,22 +51,24 @@ impl SecretKeyShare { } } -impl core::fmt::Debug for SecretKeyShare { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl fmt::Debug for SecretKeyShare { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "SecretKeyShare(id={:?})", self.id) } } /// Signature share from one threshold participant. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq, Unencodable)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct SignatureShare { id: Hash256, sig: Signature, } -impl core::fmt::Debug for SignatureShare { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "SignatureShare(id={:?})", self.id) +impl Hash for SignatureShare { + fn hash(&self, state: &mut H) { + self.id.hash(state); + state.write(&self.sig.to_bytes()); } } @@ -83,6 +89,12 @@ impl SignatureShare { } } +impl fmt::Debug for SignatureShare { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SignatureShare(id={:?})", self.id) + } +} + /// Split a secret key into shares for the given participant IDs, requiring /// `threshold` shares to recover. /// diff --git a/pkgs/pkc/src/bls_ietf/agg.rs b/pkgs/pkc/src/bls_ietf/agg.rs index ed97f507..eb348a32 100644 --- a/pkgs/pkc/src/bls_ietf/agg.rs +++ b/pkgs/pkc/src/bls_ietf/agg.rs @@ -14,8 +14,7 @@ use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScIetf}; use crate::prelude::*; -use blst::min_pk; -use blst::BLST_ERROR; +use blst::{min_pk, BLST_ERROR}; /// Aggregate multiple public keys into one. pub fn aggregate_pk(keys: &[&PublicKey]) -> Result { diff --git a/pkgs/pkc/src/bls_ietf/pk.rs b/pkgs/pkc/src/bls_ietf/pk.rs index 952ef586..3516137f 100644 --- a/pkgs/pkc/src/bls_ietf/pk.rs +++ b/pkgs/pkc/src/bls_ietf/pk.rs @@ -10,17 +10,17 @@ use super::sig::Signature; use super::sk::SecretKey; use super::DST_POP_PROVE; use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScIetf}; +use crate::bls::{BlsError, BlsPkBytes, BlsScIetf}; -use blst::min_pk; -use blst::BLST_ERROR; +use blst::{min_pk, BLST_ERROR}; +use dash_types::Unencodable; /// A BLS public key (48-byte compressed G1 point). -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr( feature = "serde", - serde(into = "crate::BlsPublicKeyBytes", try_from = "crate::BlsPublicKeyBytes",) + serde(into = "BlsPkBytes", try_from = "BlsPkBytes",) )] pub struct PublicKey(pub(super) min_pk::PublicKey); @@ -72,16 +72,16 @@ impl PublicKey { crate::common::bls::impl_hash_via_bytes!(PublicKey); -impl From for crate::BlsPublicKeyBytes { +impl From for BlsPkBytes { fn from(pk: PublicKey) -> Self { - Self(pk.to_bytes()) + Self::from_bytes(pk.to_bytes()) } } -impl TryFrom for PublicKey { - type Error = crate::bls::BlsError; +impl TryFrom> for PublicKey { + type Error = BlsError; - fn try_from(bytes: crate::BlsPublicKeyBytes) -> Result { - Self::from_bytes(&bytes.0) + fn try_from(bytes: BlsPkBytes) -> Result { + Self::from_bytes(bytes.as_bytes()) } } diff --git a/pkgs/pkc/src/bls_ietf/sig.rs b/pkgs/pkc/src/bls_ietf/sig.rs index c04581b6..535c9700 100644 --- a/pkgs/pkc/src/bls_ietf/sig.rs +++ b/pkgs/pkc/src/bls_ietf/sig.rs @@ -10,17 +10,17 @@ use super::pk::PublicKey; use super::sk::Scheme; use super::{DST, DST_POP}; use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScIetf}; +use crate::bls::{BlsError, BlsScIetf, BlsSigBytes}; -use blst::min_pk; -use blst::BLST_ERROR; +use blst::{min_pk, BLST_ERROR}; +use dash_types::Unencodable; /// A BLS signature (96-byte compressed G2 point). -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr( feature = "serde", - serde(into = "crate::BlsSignatureBytes", try_from = "crate::BlsSignatureBytes",) + serde(into = "BlsSigBytes", try_from = "BlsSigBytes",) )] pub struct Signature(pub(super) min_pk::Signature); @@ -78,16 +78,16 @@ impl Signature { crate::common::bls::impl_hash_via_bytes!(Signature); -impl From for crate::BlsSignatureBytes { +impl From for BlsSigBytes { fn from(sig: Signature) -> Self { - Self(sig.to_bytes()) + Self::from_bytes(sig.to_bytes()) } } -impl TryFrom for Signature { - type Error = crate::bls::BlsError; +impl TryFrom> for Signature { + type Error = BlsError; - fn try_from(bytes: crate::BlsSignatureBytes) -> Result { - Self::from_bytes(&bytes.0) + fn try_from(bytes: BlsSigBytes) -> Result { + Self::from_bytes(bytes.as_bytes()) } } diff --git a/pkgs/pkc/src/bls_ietf/sk.rs b/pkgs/pkc/src/bls_ietf/sk.rs index 5635aa1f..dd36e808 100644 --- a/pkgs/pkc/src/bls_ietf/sk.rs +++ b/pkgs/pkc/src/bls_ietf/sk.rs @@ -13,11 +13,12 @@ use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScIetf}; use blst::min_pk; +use dash_types::Unencodable; use core::fmt; /// BLS signature scheme (determines the DST). -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub enum Scheme { /// Basic scheme (NUL augmentation). diff --git a/pkgs/pkc/src/bls_ietf/threshold.rs b/pkgs/pkc/src/bls_ietf/threshold.rs index 5116fbfd..cf42038c 100644 --- a/pkgs/pkc/src/bls_ietf/threshold.rs +++ b/pkgs/pkc/src/bls_ietf/threshold.rs @@ -14,6 +14,10 @@ use crate::bls::{BlsError, BlsScIetf}; use crate::prelude::*; use dash_num::Hash256; +use dash_types::Unencodable; + +use core::fmt; +use core::hash::{Hash, Hasher}; /// Secret key share for threshold signing. #[derive(Clone)] @@ -47,22 +51,24 @@ impl SecretKeyShare { } } -impl core::fmt::Debug for SecretKeyShare { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl fmt::Debug for SecretKeyShare { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "SecretKeyShare(id={:?})", self.id) } } /// Signature share from one threshold participant. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq, Unencodable)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct SignatureShare { id: Hash256, sig: Signature, } -impl core::fmt::Debug for SignatureShare { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "SignatureShare(id={:?})", self.id) +impl Hash for SignatureShare { + fn hash(&self, state: &mut H) { + self.id.hash(state); + state.write(&self.sig.to_bytes()); } } @@ -83,6 +89,12 @@ impl SignatureShare { } } +impl fmt::Debug for SignatureShare { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SignatureShare(id={:?})", self.id) + } +} + /// Split a secret key into shares for the given participant IDs, requiring /// `threshold` shares to recover. /// diff --git a/pkgs/pkc/src/k256/error.rs b/pkgs/pkc/src/ecdsa/error.rs similarity index 91% rename from pkgs/pkc/src/k256/error.rs rename to pkgs/pkc/src/ecdsa/error.rs index 81fd3264..0cd1d4b1 100644 --- a/pkgs/pkc/src/k256/error.rs +++ b/pkgs/pkc/src/ecdsa/error.rs @@ -9,49 +9,49 @@ use core::fmt; /// Errors produced by secp256k1 operations. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Error { - /// secret key bytes are not a valid scalar - InvalidSecretKey, +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum EcdsaError { /// public key bytes are not a valid curve point InvalidPublicKey, - /// signature bytes are malformed - InvalidSignature, - /// signature verification failed - VerifyFailed, /// recovery id is out of range (must be 0..4) InvalidRecoveryId, + /// secret key bytes are not a valid scalar + InvalidSecretKey, + /// signature bytes are malformed + InvalidSignature, /// recovery failed; no valid public key for this signature and message RecoveryFailed, /// signing operation failed SigningFailed, + /// signature verification failed + VerifyFailed, } -impl fmt::Display for Error { +impl fmt::Display for EcdsaError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::InvalidSecretKey => { - write!(f, "secret key bytes are not a valid scalar") - } Self::InvalidPublicKey => { write!(f, "public key bytes are not a valid curve point") } - Self::InvalidSignature => { - write!(f, "signature bytes are malformed") - } - Self::VerifyFailed => { - write!(f, "signature verification failed") - } Self::InvalidRecoveryId => { write!(f, "recovery id out of range (must be 0..4)") } + Self::InvalidSecretKey => { + write!(f, "secret key bytes are not a valid scalar") + } + Self::InvalidSignature => { + write!(f, "signature bytes are malformed") + } Self::RecoveryFailed => { write!(f, "recovery failed; no valid public key") } Self::SigningFailed => write!(f, "signing failed"), + Self::VerifyFailed => { + write!(f, "signature verification failed") + } } } } #[cfg(feature = "std")] -impl std::error::Error for Error {} +impl std::error::Error for EcdsaError {} diff --git a/pkgs/pkc/src/ecdsa/mod.rs b/pkgs/pkc/src/ecdsa/mod.rs new file mode 100644 index 00000000..c6489dfa --- /dev/null +++ b/pkgs/pkc/src/ecdsa/mod.rs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! ECDSA types for the secp256k1 curve. + +mod error; +mod public_bytes; +mod secret_bytes; +mod sig_bytes; + +pub use error::EcdsaError; +pub use public_bytes::EcdsaPkBytes; +pub use secret_bytes::EcdsaSkBytes; +pub use sig_bytes::EcdsaSigBytes; + +cfg_if::cfg_if! { + if #[cfg(feature = "ecdsa")] { + mod public_ops; + mod secret_ops; + mod sig_ops; + + #[cfg(any(test, feature = "tests"))] + #[expect(clippy::unwrap_used, reason = "test code")] + pub mod tests; + + pub use public_ops::EcdsaPublicKey; + pub use secret_ops::EcdsaSecretKey; + pub use sig_ops::{EcdsaDerSignature, EcdsaSignature, EcdsaRecoveryId}; + } +} diff --git a/pkgs/pkc/src/ecdsa/public_bytes.rs b/pkgs/pkc/src/ecdsa/public_bytes.rs new file mode 100644 index 00000000..7db92252 --- /dev/null +++ b/pkgs/pkc/src/ecdsa/public_bytes.rs @@ -0,0 +1,14 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! secp256k1 public key byte bag. + +use dash_types::make_bytes; + +make_bytes! { + /// Raw compressed ECDSA public key bytes. + EcdsaPkBytes, 33 +} diff --git a/pkgs/pkc/src/ecdsa/public_ops.rs b/pkgs/pkc/src/ecdsa/public_ops.rs new file mode 100644 index 00000000..d8ce5b32 --- /dev/null +++ b/pkgs/pkc/src/ecdsa/public_ops.rs @@ -0,0 +1,169 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! secp256k1 public key. + +use super::error::EcdsaError; +use super::sig_ops::{EcdsaRecoveryId, EcdsaSignature}; +use super::EcdsaPkBytes; + +use dash_types::{type_cvrt, Unencodable}; +use k256::ecdsa::{signature::hazmat::PrehashVerifier, VerifyingKey}; + +use core::hash::{Hash, Hasher}; + +/// A secp256k1 public key. +#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde(into = "super::EcdsaPkBytes", try_from = "super::EcdsaPkBytes",) +)] +pub struct EcdsaPublicKey(VerifyingKey); + +impl EcdsaPublicKey { + pub(super) fn from_inner(inner: VerifyingKey) -> Self { + Self(inner) + } + + /// Parse from SEC1 (un)compressed bytes. + /// + /// # Errors + /// + /// Returns [`EcdsaError::InvalidPublicKey`] when the bytes are not a SEC1 + /// encoding of a point on the curve. + pub fn from_bytes(bytes: &[u8]) -> Result { + VerifyingKey::from_sec1_bytes(bytes) + .map(Self) + .map_err(|_| EcdsaError::InvalidPublicKey) + } + + /// Serialize as 33-byte compressed SEC1. + pub fn to_bytes(&self) -> [u8; 33] { + let pt = self.0.to_encoded_point(true); + let mut out = [0u8; 33]; + out.copy_from_slice(pt.as_bytes()); + out + } + + /// Serialize as 65-byte uncompressed SEC1. + pub fn to_uncompressed_bytes(&self) -> [u8; 65] { + let pt = self.0.to_encoded_point(false); + let mut out = [0u8; 65]; + out.copy_from_slice(pt.as_bytes()); + out + } + + /// Verify a signature over a 32-byte prehashed message. + /// + /// # Errors + /// + /// Returns [`EcdsaError::VerifyFailed`] when the signature does not verify + /// under this key. + pub fn verify(&self, msg_hash: &[u8; 32], sig: &EcdsaSignature) -> Result<(), EcdsaError> { + self + .0 + .verify_prehash(msg_hash, sig.as_inner()) + .map_err(|_| EcdsaError::VerifyFailed) + } + + /// Recover a public key from a signature, prehashed message, and recovery id. + /// + /// # Errors + /// + /// Returns [`EcdsaError::RecoveryFailed`] when no key recovers from the + /// signature under `rid`. + pub fn recover(msg_hash: &[u8; 32], sig: &EcdsaSignature, rid: EcdsaRecoveryId) -> Result { + VerifyingKey::recover_from_prehash(msg_hash, sig.as_inner(), rid.as_inner()) + .map(Self) + .map_err(|_| EcdsaError::RecoveryFailed) + } +} + +impl Hash for EcdsaPublicKey { + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + +type_cvrt!(From for EcdsaPkBytes, |pk| { + Self(pk.to_bytes()) +}); + +type_cvrt!(TryFrom for EcdsaPublicKey, EcdsaError, |bytes| { + Self::from_bytes(&bytes.0) +}); + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use crate::ecdsa::tests::*; + use crate::ecdsa::{EcdsaPublicKey, EcdsaRecoveryId, EcdsaSignature}; + use crate::prelude::*; + + use dash_dev::{arr_from_hex, assert_json_rt, Corpus}; + use rstest::*; + use serde::Deserialize; + + #[derive(Deserialize)] + struct RecoverVector { + msg: String, + sig: String, + recovery_id: u8, + pk: String, + } + + #[rstest] + fn compressed_roundtrip(alice_pk: EcdsaPublicKey) { + let bytes = alice_pk.to_bytes(); + assert_eq!(bytes.len(), 33); + let restored = EcdsaPublicKey::from_bytes(&bytes).unwrap(); + assert_eq!(restored, alice_pk); + } + + #[rstest] + fn corpus_recover() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "ecdsa_sign"); + for v in corpus.vectors::("recover") { + let sig = EcdsaSignature::from_compact(&arr_from_hex::<64>(&v.sig)).unwrap(); + let rid = EcdsaRecoveryId::try_from(v.recovery_id).unwrap(); + let pk = EcdsaPublicKey::recover(&arr_from_hex::<32>(&v.msg), &sig, rid).unwrap(); + assert_eq!(pk.to_bytes(), arr_from_hex::<33>(&v.pk)); + } + } + + #[rstest] + fn rejects_garbage() { + assert!(EcdsaPublicKey::from_bytes(&[0xff; 33]).is_err()); + } + + #[rstest] + fn recover_roundtrip(alice_pk: EcdsaPublicKey, alice_rec_sig: (EcdsaSignature, EcdsaRecoveryId)) { + let (sig, rid) = alice_rec_sig; + assert_eq!(EcdsaPublicKey::recover(&MSG, &sig, rid).unwrap(), alice_pk); + } + + #[cfg(feature = "serde")] + #[rstest] + fn serde_roundtrip(alice_pk: EcdsaPublicKey) { + assert_json_rt(&alice_pk); + } + + #[rstest] + fn uncompressed_roundtrip(alice_pk: EcdsaPublicKey) { + let bytes = alice_pk.to_uncompressed_bytes(); + assert_eq!(bytes.len(), 65); + let restored = EcdsaPublicKey::from_bytes(&bytes).unwrap(); + assert_eq!(restored, alice_pk); + } + + #[rstest] + fn verify_rejects_wrong_message(alice_pk: EcdsaPublicKey, alice_sig: EcdsaSignature) { + let mut bad = MSG; + bad[0] ^= 0xff; + assert!(alice_pk.verify(&bad, &alice_sig).is_err()); + } +} diff --git a/pkgs/pkc/src/ecdsa/secret_bytes.rs b/pkgs/pkc/src/ecdsa/secret_bytes.rs new file mode 100644 index 00000000..de22ebbc --- /dev/null +++ b/pkgs/pkc/src/ecdsa/secret_bytes.rs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! secp256k1 secret key byte bag. + +use dash_types::{impl_sbyte, TypeId}; +use subtle::ConstantTimeEq; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use core::fmt; + +/// Raw ECDSA secret key bytes. +#[derive(Clone, Default, TypeId, Zeroize, ZeroizeOnDrop)] +pub struct EcdsaSkBytes([u8; 32]); + +impl_sbyte!(32, EcdsaSkBytes); + +impl EcdsaSkBytes { + /// Consumes the bag and returns the inner byte array. + pub fn into_bytes(self) -> [u8; 32] { + self.0 + } + + /// Borrows the inner byte array. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { + self.0.ct_eq(&[0u8; 32]).into() + } +} + +impl Eq for EcdsaSkBytes {} + +impl PartialEq for EcdsaSkBytes { + fn eq(&self, other: &Self) -> bool { + self.0.ct_eq(&other.0).into() + } +} + +impl AsRef<[u8]> for EcdsaSkBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsRef<[u8; 32]> for EcdsaSkBytes { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Debug for EcdsaSkBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "EcdsaSkBytes(..)") + } +} + +impl fmt::Display for EcdsaSkBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl From for [u8; 32] { + fn from(val: EcdsaSkBytes) -> Self { + val.0 + } +} diff --git a/pkgs/pkc/src/ecdsa/secret_ops.rs b/pkgs/pkc/src/ecdsa/secret_ops.rs new file mode 100644 index 00000000..d928bb08 --- /dev/null +++ b/pkgs/pkc/src/ecdsa/secret_ops.rs @@ -0,0 +1,168 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! secp256k1 secret key. + +use super::error::EcdsaError; +use super::public_ops::EcdsaPublicKey; +use super::secret_bytes::EcdsaSkBytes; +use super::sig_ops::{EcdsaRecoveryId, EcdsaSignature}; + +use dash_types::type_cvrt; +use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; + +use core::fmt; + +/// A secp256k1 secret key. +#[derive(Clone)] +pub struct EcdsaSecretKey(SigningKey); + +impl EcdsaSecretKey { + /// Parse a secret key from a 32-byte big-endian scalar. + pub fn from_bytes(bytes: &[u8; 32]) -> Result { + SigningKey::from_bytes(bytes.into()) + .map(Self) + .map_err(|_| EcdsaError::InvalidSecretKey) + } + + /// Serialize to a 32-byte big-endian scalar. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes().into() + } + + /// Derive the corresponding public key. + pub fn public_key(&self) -> EcdsaPublicKey { + EcdsaPublicKey::from_inner(*self.0.verifying_key()) + } + + /// Produce an ECDSA signature over a 32-byte prehashed message (RFC 6979, + /// low-S normalised). + /// + /// # Errors + /// + /// Returns [`EcdsaError::SigningFailed`] if the underlying library rejects + /// the prehash. + pub fn sign(&self, msg_hash: &[u8; 32]) -> Result { + self + .0 + .sign_prehash(msg_hash) + .map(EcdsaSignature::from_inner) + .map_err(|_| EcdsaError::SigningFailed) + } + + /// Sign and return the recovery id needed to recover the public + /// key from the signature. + /// + /// # Errors + /// + /// Returns [`EcdsaError::SigningFailed`] if the underlying library + /// rejects the prehash. + pub fn sign_recoverable(&self, msg_hash: &[u8; 32]) -> Result<(EcdsaSignature, EcdsaRecoveryId), EcdsaError> { + self + .0 + .sign_prehash(msg_hash) + .map(|(sig, rid)| (EcdsaSignature::from_inner(sig), EcdsaRecoveryId::from_inner(rid))) + .map_err(|_| EcdsaError::SigningFailed) + } +} + +impl fmt::Debug for EcdsaSecretKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "EcdsaSecretKey(..)") + } +} + +type_cvrt!(From for EcdsaSkBytes, |sk| { + Self::from(sk.to_bytes()) +}); + +type_cvrt!(TryFrom for EcdsaSecretKey, EcdsaError, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use crate::ecdsa::tests::*; + use crate::ecdsa::{EcdsaPublicKey, EcdsaSecretKey}; + use crate::prelude::*; + + use dash_dev::{arr_from_hex, Corpus}; + use rstest::*; + use serde::Deserialize; + + #[derive(Deserialize)] + struct KeygenVector { + sk: String, + pk_compressed: String, + } + + #[derive(Deserialize)] + struct SignVector { + sk: String, + msg: String, + sig: String, + recovery_id: u8, + } + + #[rstest] + fn corpus_derive_pk() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "ecdsa_keygen"); + for v in corpus.vectors::("derive_pk") { + let sk = EcdsaSecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); + assert_eq!(sk.public_key().to_bytes(), arr_from_hex::<33>(&v.pk_compressed)); + } + } + + #[rstest] + fn corpus_sign_recoverable() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "ecdsa_sign"); + for v in corpus.vectors::("sign_recoverable") { + let sk = EcdsaSecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); + let (sig, rid) = sk.sign_recoverable(&arr_from_hex::<32>(&v.msg)).unwrap(); + assert_eq!(sig.to_compact(), arr_from_hex::<64>(&v.sig)); + assert_eq!(u8::from(rid), v.recovery_id); + } + } + + #[rstest] + fn from_bytes_roundtrip(alice_sk: EcdsaSecretKey) { + let bytes = alice_sk.to_bytes(); + let restored = EcdsaSecretKey::from_bytes(&bytes).unwrap(); + assert_eq!(restored.public_key().to_bytes(), alice_sk.public_key().to_bytes()); + } + + #[rstest] + fn rejects_zero() { + assert!(EcdsaSecretKey::from_bytes(&[0u8; 32]).is_err()); + } + + #[rstest] + fn sign_is_deterministic(alice_sk: EcdsaSecretKey) { + let sig1 = alice_sk.sign(&MSG).unwrap(); + let sig2 = alice_sk.sign(&MSG).unwrap(); + assert_eq!(sig1, sig2); + } + + #[rstest] + fn sign_recoverable_roundtrip(alice_sk: EcdsaSecretKey) { + let (sig, rid) = alice_sk.sign_recoverable(&MSG).unwrap(); + let recovered = EcdsaPublicKey::recover(&MSG, &sig, rid).unwrap(); + assert_eq!(recovered, alice_sk.public_key()); + } + + #[rstest] + fn sign_verify_roundtrip(alice_sk: EcdsaSecretKey) { + let sig = alice_sk.sign(&MSG).unwrap(); + assert!(alice_sk.public_key().verify(&MSG, &sig).is_ok()); + } + + #[rstest] + fn verify_rejects_wrong_key(alice_sk: EcdsaSecretKey, bob_sk: EcdsaSecretKey) { + let sig = alice_sk.sign(&MSG).unwrap(); + assert!(bob_sk.public_key().verify(&MSG, &sig).is_err()); + } +} diff --git a/pkgs/pkc/src/ecdsa/sig_bytes.rs b/pkgs/pkc/src/ecdsa/sig_bytes.rs new file mode 100644 index 00000000..6453c8e6 --- /dev/null +++ b/pkgs/pkc/src/ecdsa/sig_bytes.rs @@ -0,0 +1,14 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! secp256k1 signature byte bag. + +use dash_types::make_bytes; + +make_bytes! { + /// Raw compact ECDSA signature bytes. + EcdsaSigBytes, 64 +} diff --git a/pkgs/pkc/src/ecdsa/sig_ops.rs b/pkgs/pkc/src/ecdsa/sig_ops.rs new file mode 100644 index 00000000..12dbdeb8 --- /dev/null +++ b/pkgs/pkc/src/ecdsa/sig_ops.rs @@ -0,0 +1,218 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! secp256k1 signature. + +use super::error::EcdsaError; +use super::EcdsaSigBytes; + +use dash_types::{type_cvrt, Unencodable}; +use k256::ecdsa::{DerSignature, RecoveryId, Signature}; + +use core::hash::{Hash, Hasher}; + +/// An ECDSA signature (64-byte compact r||s). +#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde(into = "super::EcdsaSigBytes", try_from = "super::EcdsaSigBytes",) +)] +pub struct EcdsaSignature(Signature); + +impl EcdsaSignature { + pub(super) fn from_inner(inner: Signature) -> Self { + Self(inner) + } + + pub(super) fn as_inner(&self) -> &Signature { + &self.0 + } + + /// Parse from 64-byte compact format (r || s). + /// + /// # Errors + /// + /// Returns [`EcdsaError::InvalidSignature`] when `r` or `s` is zero or not + /// less than the group order. + pub fn from_compact(bytes: &[u8; 64]) -> Result { + Signature::from_slice(bytes) + .map(Self) + .map_err(|_| EcdsaError::InvalidSignature) + } + + /// Parse from DER-encoded bytes. + /// + /// # Errors + /// + /// Returns [`EcdsaError::InvalidSignature`] when the DER framing is malformed + /// or the scalars it carries are out of range. + pub fn from_der(bytes: &[u8]) -> Result { + Signature::from_der(bytes) + .map(Self) + .map_err(|_| EcdsaError::InvalidSignature) + } + + /// Serialize as 64-byte compact format (r || s). + pub fn to_compact(&self) -> [u8; 64] { + self.0.to_bytes().into() + } + + /// Encode as DER. + pub fn to_der(&self) -> EcdsaDerSignature { + EcdsaDerSignature(self.0.to_der()) + } +} + +impl Hash for EcdsaSignature { + fn hash(&self, state: &mut H) { + self.to_compact().hash(state); + } +} + +type_cvrt!(From for EcdsaSigBytes, |sig| { + Self(sig.to_compact()) +}); + +type_cvrt!(TryFrom for EcdsaSignature, EcdsaError, |bytes| { + Self::from_compact(&bytes.0) +}); + +/// Recovery id (0..3) used to recover a public key from an ECDSA +/// signature. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Unencodable)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(into = "u8", try_from = "u8"))] +pub struct EcdsaRecoveryId(RecoveryId); + +impl EcdsaRecoveryId { + pub(super) fn from_inner(inner: RecoveryId) -> Self { + Self(inner) + } + + pub(super) fn as_inner(&self) -> RecoveryId { + self.0 + } + + /// Create from a raw byte (0, 1, 2, or 3). + /// + /// # Errors + /// + /// Returns [`EcdsaError::InvalidRecoveryId`] when `id` is greater than 3. + pub fn new(id: u8) -> Result { + RecoveryId::try_from(id) + .map(Self) + .map_err(|_| EcdsaError::InvalidRecoveryId) + } + + /// Return the raw byte value. + pub fn to_byte(self) -> u8 { + self.0.to_byte() + } +} + +impl Hash for EcdsaRecoveryId { + fn hash(&self, state: &mut H) { + self.to_byte().hash(state); + } +} + +type_cvrt!(From for u8, |rid| { + rid.to_byte() +}); + +type_cvrt!(TryFrom for EcdsaRecoveryId, EcdsaError, |byte| { + Self::new(*byte) +}); + +/// DER-encoded ECDSA signature (variable length, typically 70-72 bytes). +#[derive(Clone, Debug, Unencodable)] +pub struct EcdsaDerSignature(DerSignature); + +impl EcdsaDerSignature { + /// Raw DER bytes. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Byte length. + pub fn len(&self) -> usize { + self.0.as_bytes().len() + } + + /// Whether the DER encoding is empty (always false for valid signatures). + pub fn is_empty(&self) -> bool { + self.0.as_bytes().is_empty() + } +} + +impl Eq for EcdsaDerSignature {} + +impl Hash for EcdsaDerSignature { + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +impl PartialEq for EcdsaDerSignature { + fn eq(&self, other: &Self) -> bool { + self.as_bytes() == other.as_bytes() + } +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use crate::ecdsa::tests::*; + use crate::ecdsa::{EcdsaError, EcdsaRecoveryId, EcdsaSignature}; + + use dash_dev::assert_json_rt; + use rstest::*; + + #[rstest] + fn compact_roundtrip(alice_sig: EcdsaSignature) { + let bytes = alice_sig.to_compact(); + let restored = EcdsaSignature::from_compact(&bytes).unwrap(); + assert_eq!(restored, alice_sig); + } + + #[rstest] + fn der_roundtrip(alice_sig: EcdsaSignature) { + let der = alice_sig.to_der(); + let restored = EcdsaSignature::from_der(der.as_bytes()).unwrap(); + assert_eq!(restored, alice_sig); + } + + #[cfg(feature = "serde")] + #[rstest] + fn serde_sig_roundtrip(alice_sig: EcdsaSignature) { + assert_json_rt(&alice_sig); + } + + #[rstest] + #[case(0)] + #[case(1)] + #[case(2)] + #[case(3)] + fn recovery_id_roundtrip(#[case] id: u8) { + let rid = EcdsaRecoveryId::new(id).unwrap(); + assert_eq!(rid.to_byte(), id); + } + + #[rstest] + #[case(4)] + #[case(255)] + fn recovery_id_rejects_out_of_range(#[case] id: u8) { + assert_eq!(EcdsaRecoveryId::new(id), Err(EcdsaError::InvalidRecoveryId)); + } + + #[cfg(feature = "serde")] + #[rstest] + fn serde_recovery_id_roundtrip() { + let rid = EcdsaRecoveryId::new(1).unwrap(); + assert_json_rt(&rid); + } +} diff --git a/pkgs/pkc/src/ecdsa/tests.rs b/pkgs/pkc/src/ecdsa/tests.rs new file mode 100644 index 00000000..4ba16b78 --- /dev/null +++ b/pkgs/pkc/src/ecdsa/tests.rs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Common test definitions. + +use crate::ecdsa::{EcdsaPublicKey, EcdsaRecoveryId, EcdsaSecretKey, EcdsaSignature}; + +use hex_conservative::hex; +use rstest::fixture; + +pub const ALICE_SK: [u8; 32] = hex!("0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"); +pub const BOB_SK: [u8; 32] = hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); +pub const MSG: [u8; 32] = hex!("deadbeefdeadbeefdeadbeefdeadbeefcafebabecafebabecafebabecafebabe"); + +/// Derive a distinct 32-byte message digest from an index. +pub fn message_hash(i: u16) -> [u8; 32] { + let mut h = [0u8; 32]; + h[0] = i as u8; + h[31] = (i >> 8) as u8; + h +} + +#[fixture] +pub fn alice_pk() -> EcdsaPublicKey { + alice_sk().public_key() +} + +#[fixture] +pub fn alice_sk() -> EcdsaSecretKey { + EcdsaSecretKey::from_bytes(&ALICE_SK).unwrap() +} + +#[fixture] +pub fn bob_sk() -> EcdsaSecretKey { + EcdsaSecretKey::from_bytes(&BOB_SK).unwrap() +} + +#[fixture] +pub fn alice_rec_sig() -> (EcdsaSignature, EcdsaRecoveryId) { + alice_sk().sign_recoverable(&MSG).unwrap() +} + +#[fixture] +pub fn alice_sig() -> EcdsaSignature { + alice_sk().sign(&MSG).unwrap() +} diff --git a/pkgs/pkc/src/k256/mod.rs b/pkgs/pkc/src/k256/mod.rs deleted file mode 100644 index b16f8df5..00000000 --- a/pkgs/pkc/src/k256/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! ECDSA signatures using the secp256k1 curve. - -mod error; -mod pk; -mod sig; -mod sk; - -pub use error::Error; -pub use pk::PublicKey; -pub use sig::{DerSignature, RecoveryId, Signature}; -pub use sk::SecretKey; diff --git a/pkgs/pkc/src/k256/pk.rs b/pkgs/pkc/src/k256/pk.rs deleted file mode 100644 index 3926de7f..00000000 --- a/pkgs/pkc/src/k256/pk.rs +++ /dev/null @@ -1,85 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! secp256k1 public key. - -use super::error::Error; -use super::sig::{RecoveryId, Signature}; - -use k256::ecdsa::{self, signature::hazmat::PrehashVerifier}; - -/// A secp256k1 public key. -#[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(into = "crate::EcdsaPublicKeyBytes", try_from = "crate::EcdsaPublicKeyBytes",) -)] -pub struct PublicKey(ecdsa::VerifyingKey); - -impl PublicKey { - pub(super) fn from_inner(inner: ecdsa::VerifyingKey) -> Self { - Self(inner) - } - - /// Parse from SEC1 bytes: 33 (compressed) or 65 (uncompressed). - pub fn from_bytes(bytes: &[u8]) -> Result { - ecdsa::VerifyingKey::from_sec1_bytes(bytes) - .map(Self) - .map_err(|_| Error::InvalidPublicKey) - } - - /// Serialize as 33-byte compressed SEC1. - pub fn to_bytes(&self) -> [u8; 33] { - let pt = self.0.to_encoded_point(true); - let mut out = [0u8; 33]; - out.copy_from_slice(pt.as_bytes()); - out - } - - /// Serialize as 65-byte uncompressed SEC1. - pub fn to_uncompressed_bytes(&self) -> [u8; 65] { - let pt = self.0.to_encoded_point(false); - let mut out = [0u8; 65]; - out.copy_from_slice(pt.as_bytes()); - out - } - - /// Verify an ECDSA signature over a 32-byte prehashed message. - pub fn verify(&self, msg_hash: &[u8; 32], sig: &Signature) -> Result<(), Error> { - self - .0 - .verify_prehash(msg_hash, sig.as_inner()) - .map_err(|_| Error::VerifyFailed) - } - - /// Recover a public key from a signature, prehashed message, and recovery id. - pub fn recover(msg_hash: &[u8; 32], sig: &Signature, rid: RecoveryId) -> Result { - ecdsa::VerifyingKey::recover_from_prehash(msg_hash, sig.as_inner(), rid.as_inner()) - .map(Self) - .map_err(|_| Error::RecoveryFailed) - } -} - -impl core::hash::Hash for PublicKey { - fn hash(&self, state: &mut H) { - self.to_bytes().hash(state); - } -} - -impl From for crate::EcdsaPublicKeyBytes { - fn from(pk: PublicKey) -> Self { - Self(pk.to_bytes()) - } -} - -impl TryFrom for PublicKey { - type Error = super::error::Error; - - fn try_from(bytes: crate::EcdsaPublicKeyBytes) -> Result { - Self::from_bytes(&bytes.0) - } -} diff --git a/pkgs/pkc/src/k256/sig.rs b/pkgs/pkc/src/k256/sig.rs deleted file mode 100644 index 56cfd9cc..00000000 --- a/pkgs/pkc/src/k256/sig.rs +++ /dev/null @@ -1,145 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! ECDSA signature and recovery id. - -use super::error::Error; - -use k256::ecdsa; - -/// An ECDSA signature (64-byte compact r||s). -#[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(into = "crate::EcdsaSignatureBytes", try_from = "crate::EcdsaSignatureBytes",) -)] -pub struct Signature(ecdsa::Signature); - -impl Signature { - pub(super) fn from_inner(inner: ecdsa::Signature) -> Self { - Self(inner) - } - - pub(super) fn as_inner(&self) -> &ecdsa::Signature { - &self.0 - } - - /// Parse from 64-byte compact format (r || s). - pub fn from_compact(bytes: &[u8; 64]) -> Result { - ecdsa::Signature::from_slice(bytes) - .map(Self) - .map_err(|_| Error::InvalidSignature) - } - - /// Parse from DER-encoded bytes. - pub fn from_der(bytes: &[u8]) -> Result { - ecdsa::Signature::from_der(bytes) - .map(Self) - .map_err(|_| Error::InvalidSignature) - } - - /// Serialize as 64-byte compact format (r || s). - pub fn to_compact(&self) -> [u8; 64] { - self.0.to_bytes().into() - } - - /// Encode as DER. - pub fn to_der(&self) -> DerSignature { - DerSignature(self.0.to_der()) - } -} - -impl core::hash::Hash for Signature { - fn hash(&self, state: &mut H) { - self.to_compact().hash(state); - } -} - -impl From for crate::EcdsaSignatureBytes { - fn from(sig: Signature) -> Self { - Self(sig.to_compact()) - } -} - -impl TryFrom for Signature { - type Error = super::error::Error; - - fn try_from(bytes: crate::EcdsaSignatureBytes) -> Result { - Self::from_compact(&bytes.0) - } -} - -/// Recovery id (0..3) used to recover a public key from an ECDSA signature. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(into = "u8", try_from = "u8"))] -pub struct RecoveryId(ecdsa::RecoveryId); - -impl RecoveryId { - pub(super) fn from_inner(inner: ecdsa::RecoveryId) -> Self { - Self(inner) - } - - pub(super) fn as_inner(&self) -> ecdsa::RecoveryId { - self.0 - } - - /// Create from a raw byte (0, 1, 2, or 3). - pub fn new(id: u8) -> Result { - ecdsa::RecoveryId::try_from(id) - .map(Self) - .map_err(|_| Error::InvalidRecoveryId) - } - - /// Return the raw byte value. - pub fn to_byte(self) -> u8 { - self.0.to_byte() - } -} - -/// DER-encoded ECDSA signature (variable length, typically 70-72 bytes). -#[derive(Clone, Debug)] -pub struct DerSignature(ecdsa::DerSignature); - -impl DerSignature { - /// Raw DER bytes. - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } - - /// Byte length. - pub fn len(&self) -> usize { - self.0.as_bytes().len() - } - - /// Whether the DER encoding is empty (always false for valid signatures). - pub fn is_empty(&self) -> bool { - self.0.as_bytes().is_empty() - } -} - -impl PartialEq for DerSignature { - fn eq(&self, other: &Self) -> bool { - self.as_bytes() == other.as_bytes() - } -} - -impl Eq for DerSignature {} - -impl From for u8 { - fn from(rid: RecoveryId) -> Self { - rid.to_byte() - } -} - -impl TryFrom for RecoveryId { - type Error = super::error::Error; - - fn try_from(byte: u8) -> Result { - Self::new(byte) - } -} diff --git a/pkgs/pkc/src/k256/sk.rs b/pkgs/pkc/src/k256/sk.rs deleted file mode 100644 index 16df3555..00000000 --- a/pkgs/pkc/src/k256/sk.rs +++ /dev/null @@ -1,74 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! secp256k1 secret key. - -use super::error::Error; -use super::pk::PublicKey; -use super::sig::{RecoveryId, Signature}; - -use k256::ecdsa::{self, signature::hazmat::PrehashSigner}; - -use core::fmt; - -/// A secp256k1 secret key (32-byte scalar). -#[derive(Clone)] -pub struct SecretKey(ecdsa::SigningKey); - -impl SecretKey { - /// Parse a secret key from a 32-byte big-endian scalar. - pub fn from_bytes(bytes: &[u8; 32]) -> Result { - ecdsa::SigningKey::from_bytes(bytes.into()) - .map(Self) - .map_err(|_| Error::InvalidSecretKey) - } - - /// Serialize to a 32-byte big-endian scalar. - pub fn to_bytes(&self) -> [u8; 32] { - self.0.to_bytes().into() - } - - /// Derive the corresponding public key. - pub fn public_key(&self) -> PublicKey { - PublicKey::from_inner(*self.0.verifying_key()) - } - - /// Produce an ECDSA signature over a 32-byte prehashed message (RFC 6979, - /// low-S normalised). - /// - /// # Errors - /// - /// Returns [`Error::SigningFailed`] if the underlying library rejects the - /// prehash. - pub fn sign(&self, msg_hash: &[u8; 32]) -> Result { - self - .0 - .sign_prehash(msg_hash) - .map(Signature::from_inner) - .map_err(|_| Error::SigningFailed) - } - - /// Sign and return the recovery id needed to recover the public key from - /// the signature. - /// - /// # Errors - /// - /// Returns [`Error::SigningFailed`] if the underlying library rejects the - /// prehash. - pub fn sign_recoverable(&self, msg_hash: &[u8; 32]) -> Result<(Signature, RecoveryId), Error> { - self - .0 - .sign_prehash(msg_hash) - .map(|(sig, rid)| (Signature::from_inner(sig), RecoveryId::from_inner(rid))) - .map_err(|_| Error::SigningFailed) - } -} - -impl fmt::Debug for SecretKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SecretKey(..)") - } -} diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index be3a1e9d..d65252e0 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -15,8 +15,8 @@ extern crate std; #[allow(unused_imports, reason = "ergonomic shim, exports may be unused")] mod prelude; -#[cfg(feature = "k256")] -pub mod k256; +pub mod bls; +pub mod ecdsa; #[cfg(feature = "std")] pub mod worker; @@ -24,28 +24,7 @@ cfg_if::cfg_if! { if #[cfg(feature = "bls")] { mod common; - pub mod bls; pub mod bls_chia; pub mod bls_ietf; } } - -dash_types::make_bytes! { - /// Raw BLS public key bytes (48 bytes, unvalidated). - BlsPublicKeyBytes, 48 -} - -dash_types::make_bytes! { - /// Raw BLS signature bytes (96 bytes, unvalidated). - BlsSignatureBytes, 96 -} - -dash_types::make_bytes! { - /// Raw compressed ECDSA public key bytes (33 bytes, unvalidated). - EcdsaPublicKeyBytes, 33 -} - -dash_types::make_bytes! { - /// Raw compact ECDSA signature bytes (64 bytes, unvalidated). - EcdsaSignatureBytes, 64 -} diff --git a/pkgs/pkc/src/prelude.rs b/pkgs/pkc/src/prelude.rs index 16d40d0b..3e4af7bb 100644 --- a/pkgs/pkc/src/prelude.rs +++ b/pkgs/pkc/src/prelude.rs @@ -6,5 +6,6 @@ //! Re-exports for no_std compatibility. +pub(crate) use alloc::format; pub(crate) use alloc::string::String; pub(crate) use alloc::vec::Vec; diff --git a/pkgs/pkc/tests/k256_keygen.rs b/pkgs/pkc/tests/k256_keygen.rs deleted file mode 100644 index 8e54f9f2..00000000 --- a/pkgs/pkc/tests/k256_keygen.rs +++ /dev/null @@ -1,105 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Key generation and public key serialization tests for k256. - -#![expect(clippy::unwrap_used, reason = "test code")] - -mod common; - -#[cfg(feature = "serde")] -use dash_dev::assert_json_rt; -use dash_pkc::k256::{PublicKey, SecretKey}; -use hex_literal::hex; -use rstest::*; - -/// Shared test keypair. -#[fixture] -fn alice() -> SecretKey { - SecretKey::from_bytes(&hex!( - "0123456789abcdef0123456789abcdef" - "fedcba9876543210fedcba9876543210" - )) - .unwrap() -} - -/// Secret key serialization round-trips. -#[rstest] -fn from_bytes_roundtrip(alice: SecretKey) { - let bytes = alice.to_bytes(); - let restored = SecretKey::from_bytes(&bytes).unwrap(); - assert_eq!(restored.public_key().to_bytes(), alice.public_key().to_bytes()); -} - -/// Zero scalar is rejected. -#[rstest] -fn from_bytes_rejects_zero() { - assert!(SecretKey::from_bytes(&[0u8; 32]).is_err()); -} - -/// Compressed public key round-trips through SEC1. -#[rstest] -fn pubkey_compressed_roundtrip(alice: SecretKey) { - let pk = alice.public_key(); - let bytes = pk.to_bytes(); - assert_eq!(bytes.len(), 33); - let restored = PublicKey::from_bytes(&bytes).unwrap(); - assert_eq!(restored, pk); -} - -/// Uncompressed public key round-trips through SEC1. -#[rstest] -fn pubkey_uncompressed_roundtrip(alice: SecretKey) { - let pk = alice.public_key(); - let bytes = pk.to_uncompressed_bytes(); - assert_eq!(bytes.len(), 65); - let restored = PublicKey::from_bytes(&bytes).unwrap(); - assert_eq!(restored, pk); -} - -/// Garbage bytes are rejected. -#[rstest] -fn pubkey_rejects_garbage() { - assert!(PublicKey::from_bytes(&[0xff; 33]).is_err()); -} - -/// Serde round-trip for PublicKey. -#[cfg(feature = "serde")] -#[rstest] -fn serde_pk_roundtrip(alice: SecretKey) { - let pk = alice.public_key(); - assert_json_rt(&pk); -} - -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct KeygenVector { - sk: String, - pk_compressed: String, - } - - #[test] - fn kat_derive_pk() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "k256_keygen"); - let vecs: Vec = corpus.vectors("derive_pk"); - - for v in &vecs { - let sk_bytes: [u8; 32] = arr_from_hex(&v.sk); - let sk = dash_pkc::k256::SecretKey::from_bytes(&sk_bytes).unwrap(); - let pk = sk.public_key(); - assert_eq!( - pk.to_bytes().to_lower_hex_string(), - v.pk_compressed, - "pk mismatch for sk {}", - v.sk - ); - } - } -} diff --git a/pkgs/pkc/tests/k256_sign.rs b/pkgs/pkc/tests/k256_sign.rs deleted file mode 100644 index 08293765..00000000 --- a/pkgs/pkc/tests/k256_sign.rs +++ /dev/null @@ -1,180 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Signing, verification, and recovery tests for k256. - -#![expect(clippy::unwrap_used, reason = "test code")] - -mod common; - -#[cfg(feature = "serde")] -use dash_dev::assert_json_rt; -use dash_pkc::k256::{PublicKey, RecoveryId, SecretKey, Signature}; -use hex_literal::hex; -use rstest::*; - -/// Shared test keypair. -#[fixture] -fn alice() -> SecretKey { - SecretKey::from_bytes(&hex!( - "0123456789abcdef0123456789abcdef" - "fedcba9876543210fedcba9876543210" - )) - .unwrap() -} - -/// Shared test message digest. -#[fixture] -fn msg_hash() -> [u8; 32] { - common::MSG_DEADBEEF -} - -/// Sign then verify with the same key succeeds. -#[rstest] -fn sign_verify_roundtrip(alice: SecretKey, msg_hash: [u8; 32]) { - let sig = alice.sign(&msg_hash).unwrap(); - let pk = alice.public_key(); - assert!(pk.verify(&msg_hash, &sig).is_ok()); -} - -/// Verification rejects a tampered message. -#[rstest] -fn verify_rejects_wrong_message(alice: SecretKey, msg_hash: [u8; 32]) { - let sig = alice.sign(&msg_hash).unwrap(); - let pk = alice.public_key(); - let mut bad_hash = msg_hash; - bad_hash[0] ^= 0xff; - assert!(pk.verify(&bad_hash, &sig).is_err()); -} - -/// Verification rejects a different signer's key. -#[rstest] -fn verify_rejects_wrong_key(alice: SecretKey, msg_hash: [u8; 32]) { - let sig = alice.sign(&msg_hash).unwrap(); - let bob = SecretKey::from_bytes(&hex!( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - )) - .unwrap(); - assert!(bob.public_key().verify(&msg_hash, &sig).is_err()); -} - -/// Compact signature round-trips. -#[rstest] -fn signature_compact_roundtrip(alice: SecretKey, msg_hash: [u8; 32]) { - let sig = alice.sign(&msg_hash).unwrap(); - let bytes = sig.to_compact(); - let restored = Signature::from_compact(&bytes).unwrap(); - assert_eq!(restored, sig); -} - -/// Recoverable signature yields the original public key. -#[rstest] -fn sign_recoverable_roundtrip(alice: SecretKey, msg_hash: [u8; 32]) { - let (sig, rid) = alice.sign_recoverable(&msg_hash).unwrap(); - let recovered = PublicKey::recover(&msg_hash, &sig, rid).unwrap(); - assert_eq!(recovered, alice.public_key()); -} - -/// Out-of-range recovery ids are rejected. -#[rstest] -fn recovery_id_rejects_out_of_range() { - assert!(RecoveryId::new(4).is_err()); - assert!(RecoveryId::new(255).is_err()); -} - -/// Valid recovery ids round-trip. -#[rstest] -#[case(0)] -#[case(1)] -#[case(2)] -#[case(3)] -fn recovery_id_roundtrip(#[case] id: u8) { - let rid = RecoveryId::new(id).unwrap(); - assert_eq!(rid.to_byte(), id); -} - -/// RFC 6979 signing is deterministic. -#[rstest] -fn sign_is_deterministic(alice: SecretKey, msg_hash: [u8; 32]) { - let sig1 = alice.sign(&msg_hash).unwrap(); - let sig2 = alice.sign(&msg_hash).unwrap(); - assert_eq!(sig1, sig2); -} - -/// Serde round-trip for Signature. -#[cfg(feature = "serde")] -#[rstest] -fn serde_sig_roundtrip(alice: SecretKey, msg_hash: [u8; 32]) { - let sig = alice.sign(&msg_hash).unwrap(); - assert_json_rt(&sig); -} - -/// Serde round-trip for RecoveryId. -#[cfg(feature = "serde")] -#[rstest] -fn serde_recovery_id_roundtrip() { - let rid = RecoveryId::new(1).unwrap(); - assert_json_rt(&rid); -} - -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct SignVector { - sk: String, - msg: String, - sig: String, - recovery_id: u8, - } - - #[derive(Deserialize)] - struct RecoverVector { - msg: String, - sig: String, - recovery_id: u8, - pk: String, - } - - #[test] - fn kat_sign_recoverable() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "k256_sign"); - let vecs: Vec = corpus.vectors("sign_recoverable"); - - for v in &vecs { - let sk_bytes: [u8; 32] = arr_from_hex(&v.sk); - let msg: [u8; 32] = arr_from_hex(&v.msg); - let sk = dash_pkc::k256::SecretKey::from_bytes(&sk_bytes).unwrap(); - let (sig, rid) = sk.sign_recoverable(&msg).unwrap(); - assert_eq!( - sig.to_compact().to_lower_hex_string(), - v.sig, - "sig mismatch for sk={} msg={}", - v.sk, - v.msg - ); - assert_eq!(rid.to_byte(), v.recovery_id, "recovery_id mismatch"); - } - } - - #[test] - fn kat_recover() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "k256_sign"); - let vecs: Vec = corpus.vectors("recover"); - - for v in &vecs { - let msg: [u8; 32] = arr_from_hex(&v.msg); - let sig_bytes: [u8; 64] = arr_from_hex(&v.sig); - let sig = dash_pkc::k256::Signature::from_compact(&sig_bytes).unwrap(); - let rid = dash_pkc::k256::RecoveryId::new(v.recovery_id).unwrap(); - let pk = dash_pkc::k256::PublicKey::recover(&msg, &sig, rid).unwrap(); - assert_eq!(pk.to_bytes().to_lower_hex_string(), v.pk, "recovered pk mismatch"); - } - } -} diff --git a/pkgs/primitives/src/gov.rs b/pkgs/primitives/src/gov.rs index b00f035a..43ae1866 100644 --- a/pkgs/primitives/src/gov.rs +++ b/pkgs/primitives/src/gov.rs @@ -13,8 +13,8 @@ use crate::{codec_base, hash_impl, TxHash}; use bitcoin_hashes::sha256d; use bitcoin_units::Amount; use dash_num::Hash256; -use dash_types::codec::{ArrayBuf, BaseCodec, Checkable, Hashable, NumCodec}; -use dash_types::{impl_num, TypeId, Unencodable}; +use dash_types::codec::{ArrayBuf, BaseCodec, Checkable, Hashable}; +use dash_types::{enum_map, impl_num, TypeId, Unencodable}; use hex_conservative::DisplayHex; use core::fmt; @@ -28,32 +28,14 @@ const MIN_URL_LEN: usize = 4; /// Allowed characters in governance proposal names. const PROPOSAL_NAME_CHARS: &[u8] = b"-_abcdefghijklmnopqrstuvwxyz0123456789"; -/// Governance object type codes. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum GovObjectType { - /// Budget proposal. - Proposal, - /// Superblock trigger. - Trigger, - /// Unknown or unrecognized type. - Unknown(i32), -} - -impl NumCodec for GovObjectType { - fn from_base(v: i32) -> Self { - match v { - 1 => Self::Proposal, - 2 => Self::Trigger, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> i32 { - match self { - Self::Proposal => 1, - Self::Trigger => 2, - Self::Unknown(v) => *v, - } +enum_map! { + /// Governance object type codes. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum GovObjectType, i32, Unknown { + /// Budget proposal. + Proposal = 1 => "proposal", + /// Superblock trigger. + Trigger = 2 => "trigger", } } @@ -61,16 +43,6 @@ impl_num!(GovObjectType, i32); hash_impl!(GovObjectType); -impl fmt::Display for GovObjectType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Proposal => write!(f, "proposal"), - Self::Trigger => write!(f, "trigger"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - /// A governance proposal payload (type 1 JSON). /// /// ```json @@ -222,40 +194,18 @@ impl GovObject { } } -/// Governance vote outcome. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TypeId)] -pub enum VoteOutcome { - /// No vote cast. - None, - /// Vote in favour. - Yes, - /// Vote against. - No, - /// Abstention. - Abstain, - /// Unrecognised outcome. - Unknown(u32), -} - -impl NumCodec for VoteOutcome { - fn from_base(v: u32) -> Self { - match v { - 0 => Self::None, - 1 => Self::Yes, - 2 => Self::No, - 3 => Self::Abstain, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u32 { - match self { - Self::None => 0, - Self::Yes => 1, - Self::No => 2, - Self::Abstain => 3, - Self::Unknown(v) => *v, - } +enum_map! { + /// Governance vote outcome. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum VoteOutcome, u32, Unknown { + /// No vote cast. + None = 0 => "none", + /// Vote in favour. + Yes = 1 => "yes", + /// Vote against. + No = 2 => "no", + /// Abstention. + Abstain = 3 => "abstain", } } @@ -263,56 +213,20 @@ impl_num!(VoteOutcome, u32); hash_impl!(VoteOutcome); -impl fmt::Display for VoteOutcome { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => f.write_str("none"), - Self::Yes => f.write_str("yes"), - Self::No => f.write_str("no"), - Self::Abstain => f.write_str("abstain"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - -/// Governance vote signal type. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TypeId)] -pub enum VoteSignal { - /// No signal. - None, - /// Fund this object. - Funding, - /// Object checks out. - Valid, - /// Object should be deleted. - Delete, - /// Officially endorsed. - Endorsed, - /// Unrecognised signal. - Unknown(u32), -} - -impl NumCodec for VoteSignal { - fn from_base(v: u32) -> Self { - match v { - 0 => Self::None, - 1 => Self::Funding, - 2 => Self::Valid, - 3 => Self::Delete, - 4 => Self::Endorsed, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u32 { - match self { - Self::None => 0, - Self::Funding => 1, - Self::Valid => 2, - Self::Delete => 3, - Self::Endorsed => 4, - Self::Unknown(v) => *v, - } +enum_map! { + /// Governance vote signal type. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum VoteSignal, u32, Unknown { + /// No signal. + None = 0 => "none", + /// Fund this object. + Funding = 1 => "funding", + /// Object checks out. + Valid = 2 => "valid", + /// Object should be deleted. + Delete = 3 => "delete", + /// Officially endorsed. + Endorsed = 4 => "endorsed", } } @@ -320,19 +234,6 @@ impl_num!(VoteSignal, u32); hash_impl!(VoteSignal); -impl fmt::Display for VoteSignal { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => f.write_str("none"), - Self::Funding => f.write_str("funding"), - Self::Valid => f.write_str("valid"), - Self::Delete => f.write_str("delete"), - Self::Endorsed => f.write_str("endorsed"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - /// A governance vote. /// /// ```text diff --git a/pkgs/primitives/src/payload/assetunlock.rs b/pkgs/primitives/src/payload/assetunlock.rs index 14fef65b..1ba89fc8 100644 --- a/pkgs/primitives/src/payload/assetunlock.rs +++ b/pkgs/primitives/src/payload/assetunlock.rs @@ -9,7 +9,7 @@ use super::QuorumHash; use crate::codec::codec_payload; -use dash_pkc::BlsSignatureBytes; +use dash_pkc::bls::{BlsScIetf, BlsSigBytes}; use dash_types::codec::Checkable; use dash_types::{TypeId, Unencodable}; @@ -31,7 +31,7 @@ pub struct AssetUnlock { /// Quorum hash. pub quorum_hash: QuorumHash, /// Quorum BLS authorization signature. - pub quorum_sig: BlsSignatureBytes, + pub quorum_sig: BlsSigBytes, } codec_payload!(AssetUnlock { diff --git a/pkgs/primitives/src/payload/cbtx.rs b/pkgs/primitives/src/payload/cbtx.rs index 66904dce..9f803d62 100644 --- a/pkgs/primitives/src/payload/cbtx.rs +++ b/pkgs/primitives/src/payload/cbtx.rs @@ -10,7 +10,7 @@ use crate::codec::impl_payload; use crate::{hash_impl, MerkleRoot}; use bitcoin_units::BlockHeight; -use dash_pkc::BlsSignatureBytes; +use dash_pkc::bls::{BlsScIetf, BlsSigBytes}; use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf}; use dash_types::{TypeId, Unencodable}; @@ -36,7 +36,7 @@ pub struct CoinbaseCommitment { /// Best ChainLock height difference (v3+, CompactSize). pub best_cl_height_diff: Option, /// Best ChainLock BLS signature (v3+). - pub best_cl_signature: Option, + pub best_cl_signature: Option>, /// Credit pool balance in duffs (v3+). pub credit_pool_balance: Option, } @@ -56,7 +56,7 @@ impl BaseCodec for CoinbaseCommitment { let (best_cl_height_diff, best_cl_signature, credit_pool_balance) = if version >= 3 { ( Some(codec::read_compact_u64(data)?), - Some(BlsSignatureBytes::decode(data)?), + Some(BlsSigBytes::::decode(data)?), Some(i64::decode(data)?), ) } else { diff --git a/pkgs/primitives/src/payload/mnhftx.rs b/pkgs/primitives/src/payload/mnhftx.rs index f8dc28ca..acd2b311 100644 --- a/pkgs/primitives/src/payload/mnhftx.rs +++ b/pkgs/primitives/src/payload/mnhftx.rs @@ -9,7 +9,7 @@ use super::QuorumHash; use crate::codec::codec_payload; -use dash_pkc::BlsSignatureBytes; +use dash_pkc::bls::{BlsScIetf, BlsSigBytes}; use dash_types::codec::Checkable; use dash_types::{TypeId, Unencodable}; @@ -30,7 +30,7 @@ pub struct MnHardFork { /// Quorum hash. pub quorum_hash: QuorumHash, /// Quorum BLS signature. - pub sig: BlsSignatureBytes, + pub sig: BlsSigBytes, } codec_payload!(MnHardFork { diff --git a/pkgs/primitives/src/payload/mod.rs b/pkgs/primitives/src/payload/mod.rs index 1368f2b0..347e7a32 100644 --- a/pkgs/primitives/src/payload/mod.rs +++ b/pkgs/primitives/src/payload/mod.rs @@ -24,8 +24,8 @@ use crate::prelude::*; use crate::types::{NIError, NIPurpose, NITrait, NetInfoV2}; use dash_num::{make_hash, Hash256}; -use dash_types::codec::{Checkable, NumCodec}; -use dash_types::{impl_num, TypeId, Unencodable}; +use dash_types::codec::Checkable; +use dash_types::{enum_map, impl_num, TypeId, Unencodable}; use core::fmt; @@ -58,64 +58,30 @@ make_hash! { hash_impl!(InputsHash); -/// Dash transaction type, encoded in the upper 16 bits of the version field. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum TxType { - /// Spend transaction (includes legacy coinbase). - Spend, - /// Masternode registration (type 1). - ProviderRegister, - /// Masternode service address update (type 2). - ProviderUpdateService, - /// Masternode registrar key update (type 3). - ProviderUpdateRegistrar, - /// Masternode revocation (type 4). - ProviderUpdateRevoke, - /// Coinbase commitment special transaction (type 5). - CoinbaseCommitment, - /// LLMQ final commitment (type 6). - QuorumCommitment, - /// Masternode hard fork signal (type 7). - MnhfSignal, - /// Asset lock: L1 to platform (type 8). - AssetLock, - /// Asset unlock: platform to L1 (type 9). - AssetUnlock, - /// Unknown or future transaction type. - Unknown(u16), -} - -impl NumCodec for TxType { - fn from_base(value: u16) -> Self { - match value { - 0 => Self::Spend, - 1 => Self::ProviderRegister, - 2 => Self::ProviderUpdateService, - 3 => Self::ProviderUpdateRegistrar, - 4 => Self::ProviderUpdateRevoke, - 5 => Self::CoinbaseCommitment, - 6 => Self::QuorumCommitment, - 7 => Self::MnhfSignal, - 8 => Self::AssetLock, - 9 => Self::AssetUnlock, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u16 { - match self { - Self::Spend => 0, - Self::ProviderRegister => 1, - Self::ProviderUpdateService => 2, - Self::ProviderUpdateRegistrar => 3, - Self::ProviderUpdateRevoke => 4, - Self::CoinbaseCommitment => 5, - Self::QuorumCommitment => 6, - Self::MnhfSignal => 7, - Self::AssetLock => 8, - Self::AssetUnlock => 9, - Self::Unknown(v) => *v, - } +enum_map! { + /// Dash transaction type, encoded in the upper 16 bits of the version field. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum TxType, u16, Unknown { + /// Spend transaction (includes legacy coinbase). + Spend = 0 => "spend", + /// Masternode registration (type 1). + ProviderRegister = 1 => "provider_register", + /// Masternode service address update (type 2). + ProviderUpdateService = 2 => "provider_update_service", + /// Masternode registrar key update (type 3). + ProviderUpdateRegistrar = 3 => "provider_update_registrar", + /// Masternode revocation (type 4). + ProviderUpdateRevoke = 4 => "provider_update_revoke", + /// Coinbase commitment special transaction (type 5). + CoinbaseCommitment = 5 => "coinbase_commitment", + /// LLMQ final commitment (type 6). + QuorumCommitment = 6 => "quorum_commitment", + /// Masternode hard fork signal (type 7). + MnhfSignal = 7 => "mnhf_signal", + /// Asset lock: L1 to platform (type 8). + AssetLock = 8 => "asset_lock", + /// Asset unlock: platform to L1 (type 9). + AssetUnlock = 9 => "asset_unlock", } } @@ -123,50 +89,14 @@ impl_num!(TxType, u16); hash_impl!(TxType); -impl fmt::Display for TxType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Spend => write!(f, "spend"), - Self::ProviderRegister => write!(f, "provider_register"), - Self::ProviderUpdateService => write!(f, "provider_update_service"), - Self::ProviderUpdateRegistrar => write!(f, "provider_update_registrar"), - Self::ProviderUpdateRevoke => write!(f, "provider_update_revoke"), - Self::CoinbaseCommitment => write!(f, "coinbase_commitment"), - Self::QuorumCommitment => write!(f, "quorum_commitment"), - Self::MnhfSignal => write!(f, "mnhf_signal"), - Self::AssetLock => write!(f, "asset_lock"), - Self::AssetUnlock => write!(f, "asset_unlock"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - -/// Masternode type, used in provider registration and update transactions. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum MnType { - /// Regular masternode. - Regular, - /// Evolution (Evo) masternode with platform capabilities. - Evo, - /// Unknown or future masternode type. - Unknown(u16), -} - -impl NumCodec for MnType { - fn from_base(value: u16) -> Self { - match value { - 0 => Self::Regular, - 1 => Self::Evo, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u16 { - match self { - Self::Regular => 0, - Self::Evo => 1, - Self::Unknown(v) => *v, - } +enum_map! { + /// Masternode type, used in provider registration and update transactions. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum MnType, u16, Unknown { + /// Regular masternode. + Regular = 0 => "regular", + /// Evolution (Evo) masternode with platform capabilities. + Evo = 1 => "evo", } } @@ -174,16 +104,6 @@ impl_num!(MnType, u16); hash_impl!(MnType); -impl fmt::Display for MnType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Regular => write!(f, "regular"), - Self::Evo => write!(f, "evo"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - /// Provider transaction validation failure. #[derive(Debug, Clone, PartialEq, Eq, Hash, Unencodable)] pub enum ProTxInvalid { diff --git a/pkgs/primitives/src/payload/proregtx.rs b/pkgs/primitives/src/payload/proregtx.rs index fec668aa..eed64b6b 100644 --- a/pkgs/primitives/src/payload/proregtx.rs +++ b/pkgs/primitives/src/payload/proregtx.rs @@ -16,7 +16,7 @@ use crate::script::{KeyId, Script}; use crate::types::{NITrait, NetInfo, NetInfoV1, NetInfoV2, ServiceV1}; use crate::{hash_impl, TxHash}; -use dash_pkc::BlsPublicKeyBytes; +use dash_pkc::bls::{BlsPkBytes, BlsScIetf}; use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; use dash_types::{make_bytes, TypeId}; @@ -46,7 +46,7 @@ pub struct ProRegTx { /// Owner key id (20 bytes). pub key_id_owner: KeyId, /// Operator BLS public key (48 bytes). - pub pub_key_operator: BlsPublicKeyBytes, + pub pub_key_operator: BlsPkBytes, /// Voting key id (20 bytes). pub key_id_voting: KeyId, /// Operator reward in basis points (0-10000). @@ -108,7 +108,7 @@ impl BaseCodec for ProRegTx { NetInfo::Legacy(NetInfoV1(ServiceV1::decode(data)?)) }; let key_id_owner = KeyId::decode(data)?; - let pub_key_operator = BlsPublicKeyBytes::decode(data)?; + let pub_key_operator = BlsPkBytes::::decode(data)?; let key_id_voting = KeyId::decode(data)?; let operator_reward = u16::decode(data)?; let script_payout = Script::decode(data)?; diff --git a/pkgs/primitives/src/payload/proupregtx.rs b/pkgs/primitives/src/payload/proupregtx.rs index 4d69d98b..98fc5943 100644 --- a/pkgs/primitives/src/payload/proupregtx.rs +++ b/pkgs/primitives/src/payload/proupregtx.rs @@ -12,7 +12,7 @@ use crate::prelude::*; use crate::script::{KeyId, Script}; use crate::TxHash; -use dash_pkc::BlsPublicKeyBytes; +use dash_pkc::bls::{BlsPkBytes, BlsScIetf}; use dash_types::codec::Checkable; use dash_types::TypeId; @@ -33,7 +33,7 @@ pub struct ProUpRegTx { /// Reserved, always 0. pub mode: u16, /// Operator BLS public key (48 bytes). - pub pub_key_operator: BlsPublicKeyBytes, + pub pub_key_operator: BlsPkBytes, /// Voting key id (20 bytes). pub key_id_voting: KeyId, /// Payout script. diff --git a/pkgs/primitives/src/payload/prouprevtx.rs b/pkgs/primitives/src/payload/prouprevtx.rs index b9107b66..97d4d3a6 100644 --- a/pkgs/primitives/src/payload/prouprevtx.rs +++ b/pkgs/primitives/src/payload/prouprevtx.rs @@ -11,7 +11,7 @@ use crate::codec::codec_payload; use crate::support::RevocationReason; use crate::TxHash; -use dash_pkc::BlsSignatureBytes; +use dash_pkc::bls::{BlsScIetf, BlsSigBytes}; use dash_types::codec::Checkable; use dash_types::TypeId; @@ -34,7 +34,7 @@ pub struct ProUpRevTx { /// Hash of all inputs. pub inputs_hash: InputsHash, /// Operator BLS signature. - pub sig: BlsSignatureBytes, + pub sig: BlsSigBytes, } codec_payload!(ProUpRevTx { diff --git a/pkgs/primitives/src/payload/proupservtx.rs b/pkgs/primitives/src/payload/proupservtx.rs index 027f8b3f..105e351f 100644 --- a/pkgs/primitives/src/payload/proupservtx.rs +++ b/pkgs/primitives/src/payload/proupservtx.rs @@ -13,7 +13,7 @@ use crate::script::Script; use crate::types::{NITrait, NetInfo, NetInfoV1, NetInfoV2, ServiceV1}; use crate::{hash_impl, TxHash}; -use dash_pkc::BlsSignatureBytes; +use dash_pkc::bls::{BlsScIetf, BlsSigBytes}; use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; use dash_types::TypeId; @@ -49,7 +49,7 @@ pub struct ProUpServTx { #[cfg_attr(feature = "serde", serde(rename = "platformHTTPPort"))] pub platform_http_port: Option, /// Operator BLS signature. - pub sig: BlsSignatureBytes, + pub sig: BlsSigBytes, } impl_payload!(ProUpServTx); @@ -93,7 +93,7 @@ impl BaseCodec for ProUpServTx { platform_node_id, platform_p2p_port, platform_http_port, - sig: BlsSignatureBytes::decode(data)?, + sig: BlsSigBytes::::decode(data)?, }) } diff --git a/pkgs/primitives/src/payload/quorum.rs b/pkgs/primitives/src/payload/quorum.rs index d64e7c82..fa7fe004 100644 --- a/pkgs/primitives/src/payload/quorum.rs +++ b/pkgs/primitives/src/payload/quorum.rs @@ -12,7 +12,7 @@ use crate::hash_impl; use crate::support::{DynBitset, LlmqType}; use dash_num::{make_hash, Hash256}; -use dash_pkc::{BlsPublicKeyBytes, BlsSignatureBytes}; +use dash_pkc::bls::{BlsPkBytes, BlsScIetf, BlsSigBytes}; use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; use dash_types::{TypeId, Unencodable}; @@ -49,13 +49,13 @@ pub struct Commitment { /// Valid members bitset. pub valid_members: DynBitset, /// Quorum BLS public key (48 bytes). - pub quorum_public_key: BlsPublicKeyBytes, + pub quorum_public_key: BlsPkBytes, /// Quorum verification vector hash (32 bytes). pub quorum_vvec_hash: QuorumVvecHash, /// Threshold signature over commitment. - pub quorum_sig: BlsSignatureBytes, + pub quorum_sig: BlsSigBytes, /// Aggregated per-member signature. - pub members_sig: BlsSignatureBytes, + pub members_sig: BlsSigBytes, } impl_payload!(Commitment); @@ -78,10 +78,10 @@ impl BaseCodec for Commitment { quorum_index, signers: DynBitset::decode(data)?, valid_members: DynBitset::decode(data)?, - quorum_public_key: BlsPublicKeyBytes::decode(data)?, + quorum_public_key: BlsPkBytes::::decode(data)?, quorum_vvec_hash: QuorumVvecHash::decode(data)?, - quorum_sig: BlsSignatureBytes::decode(data)?, - members_sig: BlsSignatureBytes::decode(data)?, + quorum_sig: BlsSigBytes::::decode(data)?, + members_sig: BlsSigBytes::::decode(data)?, }) } diff --git a/pkgs/primitives/src/script.rs b/pkgs/primitives/src/script.rs index 67530dfd..cfd76e0b 100644 --- a/pkgs/primitives/src/script.rs +++ b/pkgs/primitives/src/script.rs @@ -9,7 +9,7 @@ use crate::hash_impl; use crate::prelude::*; -use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; +use dash_types::codec::{ArrayBuf, BaseCodec, DecodeError, EncodeBuf}; use dash_types::{impl_type, make_bytes, TypeId}; use core::fmt; @@ -84,10 +84,10 @@ hash_impl!(KeyId); impl KeyId { /// Encode as a Base58Check string with the given version prefix. - pub fn to_base58c(&self, prefix: u8) -> alloc::string::String { - let mut payload = Vec::with_capacity(21); - payload.push(prefix); - self.encode(&mut payload); - base58ck::encode_check(&payload) + pub fn to_base58c(&self, prefix: u8) -> String { + let mut buf = ArrayBuf::<21>::new(); + buf.push(prefix); + self.encode(&mut buf); + base58ck::encode_check(&buf.into_array()) } } diff --git a/pkgs/primitives/src/support.rs b/pkgs/primitives/src/support.rs index db42f8da..0ad9e57c 100644 --- a/pkgs/primitives/src/support.rs +++ b/pkgs/primitives/src/support.rs @@ -9,77 +9,37 @@ use crate::hash_impl; use crate::prelude::*; -use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf, NumCodec}; -use dash_types::{impl_num, impl_type, TypeId, Unencodable}; - -use core::fmt; - -/// LLMQ type (quorum size/threshold configuration). -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum LlmqType { - /// 50 members, 60% threshold. - Llmq50_60, - /// 400 members, 60% threshold. - Llmq400_60, - /// 400 members, 85% threshold. - Llmq400_85, - /// 100 members, 67% threshold. - Llmq100_67, - /// 60 members, 75% threshold. - Llmq60_75, - /// 25 members, 67% threshold. - Llmq25_67, - /// Regtest quorum. - LlmqTest, - /// Devnet quorum. - LlmqDevnet, - /// Test v17-era quorum. - LlmqTestV17, - /// Test InstantSend quorum. - LlmqTestInstantsend, - /// Test Platform quorum. - LlmqTestPlatform, - /// Devnet Platform quorum. - LlmqDevnetPlatform, - /// Unrecognized type code. - Unknown(u8), -} - -impl NumCodec for LlmqType { - fn from_base(val: u8) -> Self { - match val { - 1 => Self::Llmq50_60, - 2 => Self::Llmq400_60, - 3 => Self::Llmq400_85, - 4 => Self::Llmq100_67, - 5 => Self::Llmq60_75, - 6 => Self::Llmq25_67, - 100 => Self::LlmqTest, - 101 => Self::LlmqDevnet, - 102 => Self::LlmqTestV17, - 104 => Self::LlmqTestInstantsend, - 106 => Self::LlmqTestPlatform, - 107 => Self::LlmqDevnetPlatform, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u8 { - match self { - Self::Llmq50_60 => 1, - Self::Llmq400_60 => 2, - Self::Llmq400_85 => 3, - Self::Llmq100_67 => 4, - Self::Llmq60_75 => 5, - Self::Llmq25_67 => 6, - Self::LlmqTest => 100, - Self::LlmqDevnet => 101, - Self::LlmqTestV17 => 102, - Self::LlmqTestInstantsend => 104, - Self::LlmqTestPlatform => 106, - Self::LlmqDevnetPlatform => 107, - Self::Unknown(v) => *v, - } +use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf}; +use dash_types::{enum_map, impl_num, impl_type, TypeId, Unencodable}; + +enum_map! { + /// LLMQ type (quorum size/threshold configuration). + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum LlmqType, u8, Unknown { + /// 50 members, 60% threshold. + Llmq50_60 = 1 => "llmq_50_60", + /// 400 members, 60% threshold. + Llmq400_60 = 2 => "llmq_400_60", + /// 400 members, 85% threshold. + Llmq400_85 = 3 => "llmq_400_85", + /// 100 members, 67% threshold. + Llmq100_67 = 4 => "llmq_100_67", + /// 60 members, 75% threshold. + Llmq60_75 = 5 => "llmq_60_75", + /// 25 members, 67% threshold. + Llmq25_67 = 6 => "llmq_25_67", + /// Regtest quorum. + LlmqTest = 100 => "llmq_test", + /// Devnet quorum. + LlmqDevnet = 101 => "llmq_devnet", + /// Test v17-era quorum. + LlmqTestV17 = 102 => "llmq_test_v17", + /// Test InstantSend quorum. + LlmqTestInstantsend = 104 => "llmq_test_instantsend", + /// Test Platform quorum. + LlmqTestPlatform = 106 => "llmq_test_platform", + /// Devnet Platform quorum. + LlmqDevnetPlatform = 107 => "llmq_devnet_platform", } } @@ -87,60 +47,18 @@ impl_num!(LlmqType, u8); hash_impl!(LlmqType); -impl fmt::Display for LlmqType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Llmq50_60 => write!(f, "llmq_50_60"), - Self::Llmq400_60 => write!(f, "llmq_400_60"), - Self::Llmq400_85 => write!(f, "llmq_400_85"), - Self::Llmq100_67 => write!(f, "llmq_100_67"), - Self::Llmq60_75 => write!(f, "llmq_60_75"), - Self::Llmq25_67 => write!(f, "llmq_25_67"), - Self::LlmqTest => write!(f, "llmq_test"), - Self::LlmqDevnet => write!(f, "llmq_devnet"), - Self::LlmqTestV17 => write!(f, "llmq_test_v17"), - Self::LlmqTestInstantsend => write!(f, "llmq_test_instantsend"), - Self::LlmqTestPlatform => write!(f, "llmq_test_platform"), - Self::LlmqDevnetPlatform => write!(f, "llmq_devnet_platform"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - -/// Revocation reason for provider update revocation. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum RevocationReason { - /// No specific reason. - NotSpecified, - /// Key material has been compromised. - KeyCompromise, - /// Operator is changing keys. - ChangeOfKeys, - /// Service level violation. - ViolationOfService, - /// Unknown reason code. - Unknown(u16), -} - -impl NumCodec for RevocationReason { - fn from_base(val: u16) -> Self { - match val { - 0 => Self::NotSpecified, - 1 => Self::KeyCompromise, - 2 => Self::ChangeOfKeys, - 3 => Self::ViolationOfService, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u16 { - match self { - Self::NotSpecified => 0, - Self::KeyCompromise => 1, - Self::ChangeOfKeys => 2, - Self::ViolationOfService => 3, - Self::Unknown(v) => *v, - } +enum_map! { + /// Revocation reason for provider update revocation. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum RevocationReason, u16, Unknown { + /// No specific reason. + NotSpecified = 0 => "not_specified", + /// Key material has been compromised. + KeyCompromise = 1 => "key_compromise", + /// Operator is changing keys. + ChangeOfKeys = 2 => "change_of_keys", + /// Service level violation. + ViolationOfService = 3 => "violation_of_service", } } @@ -148,18 +66,6 @@ impl_num!(RevocationReason, u16); hash_impl!(RevocationReason); -impl fmt::Display for RevocationReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotSpecified => write!(f, "not_specified"), - Self::KeyCompromise => write!(f, "key_compromise"), - Self::ChangeOfKeys => write!(f, "change_of_keys"), - Self::ViolationOfService => write!(f, "violation_of_service"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - /// LSB-first dynamic bitset. #[derive(Clone, Debug, Eq, Hash, PartialEq, TypeId)] #[cfg_attr(feature = "serde", derive(::serde::Serialize))] @@ -196,7 +102,7 @@ impl BaseCodec for DynBitset { let mask = !((1u8 << remainder) - 1); if raw[byte_len - 1] & mask != 0 { return Err(DecodeError::InvalidValue { - expected: 0, + expected: vec![0], actual: u64::from(raw[byte_len - 1] & mask), }); } diff --git a/pkgs/primitives/src/transaction.rs b/pkgs/primitives/src/transaction.rs index 2c997081..a914e613 100644 --- a/pkgs/primitives/src/transaction.rs +++ b/pkgs/primitives/src/transaction.rs @@ -104,7 +104,7 @@ impl BaseCodec for TxOut { fn decode(data: &mut &[u8]) -> Result { let raw = u64::decode(data)?; let value = Amount::from_sat(raw).map_err(|_| DecodeError::InvalidValue { - expected: Amount::MAX_MONEY.to_sat(), + expected: vec![Amount::MAX_MONEY.to_sat()], actual: raw, })?; Ok(Self { diff --git a/pkgs/primitives/src/types/addrv2.rs b/pkgs/primitives/src/types/addrv2.rs index 6c15c88f..550ebdf0 100644 --- a/pkgs/primitives/src/types/addrv2.rs +++ b/pkgs/primitives/src/types/addrv2.rs @@ -53,9 +53,9 @@ impl BaseCodec for AddrV2 { let len = codec::read_compact_size(data, MAX_ADDR_LEN)?; if let Some(expected) = network.expected_len() { if len != expected { - return Err(DecodeError::InvalidValue { - expected: expected as u64, - actual: len as u64, + return Err(DecodeError::BadLen { + expected: vec![expected], + actual: len, }); } } diff --git a/pkgs/primitives/src/types/netaddr.rs b/pkgs/primitives/src/types/netaddr.rs index 1f9f79ad..ee68a02e 100644 --- a/pkgs/primitives/src/types/netaddr.rs +++ b/pkgs/primitives/src/types/netaddr.rs @@ -8,49 +8,24 @@ use crate::hash_impl; -use dash_types::codec::NumCodec; -use dash_types::{impl_num, TypeId}; +use dash_types::{enum_map, impl_num, TypeId}; use core::fmt; -/// Network address type (BIP155). -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum NetworkType { - /// IPv4. - Ipv4, - /// IPv6. - Ipv6, - /// Tor v3 hidden service. - TorV3, - /// I2P. - I2p, - /// CJDNS. - Cjdns, - /// Unknown network type. - Unknown(u8), -} - -impl NumCodec for NetworkType { - fn from_base(val: u8) -> Self { - match val { - 1 => Self::Ipv4, - 2 => Self::Ipv6, - 4 => Self::TorV3, - 5 => Self::I2p, - 6 => Self::Cjdns, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u8 { - match self { - Self::Ipv4 => 1, - Self::Ipv6 => 2, - Self::TorV3 => 4, - Self::I2p => 5, - Self::Cjdns => 6, - Self::Unknown(v) => *v, - } +enum_map! { + /// Network address type (BIP155). + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum NetworkType, u8, Unknown { + /// IPv4. + Ipv4 = 1 => "ipv4", + /// IPv6. + Ipv6 = 2 => "ipv6", + /// Tor v3 hidden service. + TorV3 = 4 => "torv3", + /// I2P. + I2p = 5 => "i2p", + /// CJDNS. + Cjdns = 6 => "cjdns", } } @@ -73,19 +48,6 @@ impl NetworkType { } } -impl fmt::Display for NetworkType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Ipv4 => f.write_str("ipv4"), - Self::Ipv6 => f.write_str("ipv6"), - Self::TorV3 => f.write_str("torv3"), - Self::I2p => f.write_str("i2p"), - Self::Cjdns => f.write_str("cjdns"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - /// Network address validation error. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum NetAddrError { diff --git a/pkgs/primitives/src/types/netinfo.rs b/pkgs/primitives/src/types/netinfo.rs index d30175f8..8ee5fff7 100644 --- a/pkgs/primitives/src/types/netinfo.rs +++ b/pkgs/primitives/src/types/netinfo.rs @@ -12,7 +12,7 @@ use crate::hash_impl; use crate::prelude::*; use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; -use dash_types::{impl_num, impl_type, TypeId, Unencodable}; +use dash_types::{enum_map, impl_num, impl_type, TypeId, Unencodable}; use core::fmt; @@ -49,36 +49,16 @@ const TLDS_BAD: &[&str] = &[ /// Privacy-network TLDs that must be rejected. const TLDS_PRIVACY: &[&str] = &[".i2p", ".onion"]; -/// Purpose tag for an extended network info entry. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum NIPurpose { - /// Core P2P port. - CoreP2p, - /// Platform P2P port. - PlatformP2p, - /// Platform HTTPS port. - PlatformHttps, - /// Unrecognized purpose code. - Unknown(u8), -} - -impl NumCodec for NIPurpose { - fn from_base(val: u8) -> Self { - match val { - 0 => Self::CoreP2p, - 1 => Self::PlatformP2p, - 2 => Self::PlatformHttps, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u8 { - match self { - Self::CoreP2p => 0, - Self::PlatformP2p => 1, - Self::PlatformHttps => 2, - Self::Unknown(v) => *v, - } +enum_map! { + /// Purpose tag for an extended network info entry. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum NIPurpose, u8, Unknown { + /// Core P2P port. + CoreP2p = 0 => "core_p2p", + /// Platform P2P port. + PlatformP2p = 1 => "platform_p2p", + /// Platform HTTPS port. + PlatformHttps = 2 => "platform_https", } } @@ -86,43 +66,14 @@ impl_num!(NIPurpose, u8); hash_impl!(NIPurpose); -impl fmt::Display for NIPurpose { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::CoreP2p => write!(f, "core_p2p"), - Self::PlatformP2p => write!(f, "platform_p2p"), - Self::PlatformHttps => write!(f, "platform_https"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - -/// Type tag for an extended network info entry. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] -pub enum NIEntryCode { - /// BIP155 address + port. - Service, - /// Domain name + port. - Domain, - /// Unrecognized entry type code. - Unknown(u8), -} - -impl NumCodec for NIEntryCode { - fn from_base(val: u8) -> Self { - match val { - 0x01 => Self::Service, - 0x02 => Self::Domain, - other => Self::Unknown(other), - } - } - - fn to_base(&self) -> u8 { - match self { - Self::Service => 0x01, - Self::Domain => 0x02, - Self::Unknown(v) => *v, - } +enum_map! { + /// Type tag for an extended network info entry. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, TypeId)] + pub enum NIEntryCode, u8, Unknown { + /// BIP155 address + port. + Service = 0x01 => "service", + /// Domain name + port. + Domain = 0x02 => "domain", } } @@ -130,16 +81,6 @@ impl_num!(NIEntryCode, u8); hash_impl!(NIEntryCode); -impl fmt::Display for NIEntryCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Service => write!(f, "service"), - Self::Domain => write!(f, "domain"), - Self::Unknown(v) => write!(f, "unknown({v})"), - } - } -} - /// Network info validation error. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum NIError { @@ -219,7 +160,10 @@ impl BaseCodec for NIEntry { Ok(Self::Domain { name, port }) } NIEntryCode::Unknown(t) => Err(DecodeError::InvalidValue { - expected: NIEntryCode::Service.to_base() as u64, + expected: NIEntryCode::variants() + .iter() + .map(|v| u64::from(NumCodec::::to_base(v))) + .collect(), actual: u64::from(t), }), } diff --git a/pkgs/script/Cargo.toml b/pkgs/script/Cargo.toml index e0bd5984..15f6c5d0 100644 --- a/pkgs/script/Cargo.toml +++ b/pkgs/script/Cargo.toml @@ -10,6 +10,7 @@ std = [ "bitcoin_hashes/std", "base58ck/std", "bitcoin-consensus-encoding/std", + "dash-types/std", ] full = ["std", "serde"] serde = ["dep:serde"] @@ -22,6 +23,7 @@ bitcoin-consensus-encoding = { version = "0.2", default-features = false, featur bitcoin_hashes = { version = "0.20", default-features = false, features = [ "alloc", ] } +dash-types = { version = "0.0.0", path = "../types", default-features = false } serde = { version = "1", default-features = false, features = [ "derive", "alloc", @@ -29,6 +31,7 @@ serde = { version = "1", default-features = false, features = [ [dev-dependencies] hex-literal = "0.4" +rstest = "0.25" [lints] workspace = true diff --git a/pkgs/script/src/lib.rs b/pkgs/script/src/lib.rs index dc504718..b4c50387 100644 --- a/pkgs/script/src/lib.rs +++ b/pkgs/script/src/lib.rs @@ -19,6 +19,8 @@ use crate::opcode::Opcode as Op; use crate::prelude::*; use bitcoin_hashes::{hash160, sha256}; +use dash_types::codec::NumCodec; +use dash_types::Unencodable; pub mod opcode; @@ -46,7 +48,7 @@ const P2PK_COMPRESSED_KEY_LEN: usize = 33; const P2PK_UNCOMPRESSED_KEY_LEN: usize = 65; /// Known output script patterns. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub enum ScriptKind { /// Pay-to-public-key-hash. @@ -82,20 +84,20 @@ pub fn classify(script: &[u8]) -> ScriptKind { /// (`OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG`). pub fn is_p2pkh(script: &[u8]) -> bool { script.len() == P2PKH_SCRIPT_LEN - && script[0] == Op::Dup.to_u8() - && script[1] == Op::Hash160.to_u8() + && script[0] == Op::Dup.to_base() + && script[1] == Op::Hash160.to_base() && script[2] == HASH160_LEN as u8 - && script[23] == Op::EqualVerify.to_u8() - && script[24] == Op::CheckSig.to_u8() + && script[23] == Op::EqualVerify.to_base() + && script[24] == Op::CheckSig.to_base() } /// Returns `true` for P2SH scripts /// (`OP_HASH160 <20> OP_EQUAL`). pub fn is_p2sh(script: &[u8]) -> bool { script.len() == P2SH_SCRIPT_LEN - && script[0] == Op::Hash160.to_u8() + && script[0] == Op::Hash160.to_base() && script[1] == HASH160_LEN as u8 - && script[22] == Op::Equal.to_u8() + && script[22] == Op::Equal.to_base() } /// Returns `true` for P2PK scripts (compressed or uncompressed). @@ -103,15 +105,15 @@ pub fn is_p2pk(script: &[u8]) -> bool { let len = script.len(); (len == P2PK_COMPRESSED_SCRIPT_LEN && script[0] == P2PK_COMPRESSED_KEY_LEN as u8 - && script[P2PK_COMPRESSED_SCRIPT_LEN - 1] == Op::CheckSig.to_u8()) + && script[P2PK_COMPRESSED_SCRIPT_LEN - 1] == Op::CheckSig.to_base()) || (len == P2PK_UNCOMPRESSED_SCRIPT_LEN && script[0] == P2PK_UNCOMPRESSED_KEY_LEN as u8 - && script[P2PK_UNCOMPRESSED_SCRIPT_LEN - 1] == Op::CheckSig.to_u8()) + && script[P2PK_UNCOMPRESSED_SCRIPT_LEN - 1] == Op::CheckSig.to_base()) } /// Returns `true` when the script starts with `OP_RETURN`. pub fn is_op_return(script: &[u8]) -> bool { - script.first() == Some(&Op::Return.to_u8()) + script.first() == Some(&Op::Return.to_base()) } /// Extracts the 20-byte key hash from a P2PKH script. @@ -138,6 +140,7 @@ fn encode_base58_check(prefix: u8, hash: &[u8]) -> Option { } let mut payload = Vec::with_capacity(HASH160_LEN + 1); payload.push(prefix); + // nosemgrep: codec-no-raw-extend payload.extend_from_slice(hash); Some(base58ck::encode_check(&payload)) } @@ -190,8 +193,7 @@ pub fn legacy_sigop_count(script: &[u8]) -> usize { i += 1 + byte as usize; continue; } - let op = Opcode::from_u8(byte); - match op { + match Op::from_base(byte) { Op::CheckSig | Op::CheckSigVerify => count += 1, Op::CheckMultiSig | Op::CheckMultiSigVerify => { count += MAX_PUBKEYS; @@ -344,7 +346,7 @@ mod tests { #[test] fn op_return_bare() { - assert!(is_op_return(&[Op::Return.to_u8()])); + assert!(is_op_return(&[Op::Return.to_base()])); } #[test] @@ -421,25 +423,25 @@ mod tests { #[test] fn sigop_single_checksig() { - let script = [Op::CheckSig.to_u8()]; + let script = [Op::CheckSig.to_base()]; assert_eq!(legacy_sigop_count(&script), 1); } #[test] fn sigop_single_checksigverify() { - let script = [Op::CheckSigVerify.to_u8()]; + let script = [Op::CheckSigVerify.to_base()]; assert_eq!(legacy_sigop_count(&script), 1); } #[test] fn sigop_checkmultisig_counts_as_20() { - let script = [Op::CheckMultiSig.to_u8()]; + let script = [Op::CheckMultiSig.to_base()]; assert_eq!(legacy_sigop_count(&script), 20); } #[test] fn sigop_checkmultisigverify_counts_as_20() { - let script = [Op::CheckMultiSigVerify.to_u8()]; + let script = [Op::CheckMultiSigVerify.to_base()]; assert_eq!(legacy_sigop_count(&script), 20); } diff --git a/pkgs/script/src/opcode.rs b/pkgs/script/src/opcode.rs index 4352422e..b22d5df4 100644 --- a/pkgs/script/src/opcode.rs +++ b/pkgs/script/src/opcode.rs @@ -6,304 +6,264 @@ //! Script opcodes as defined by the consensus rules. -use core::fmt; - -/// Generates the [`Opcode`] enum, `from_u8`, `to_u8`, and `Display` from a -/// single table. -macro_rules! define_opcodes { - ( - $( - $(#[$attr:meta])* - $variant:ident = $byte:literal => $display:literal - ),* - $(,)? - ) => { - /// Script opcode. - /// - /// Every variant corresponds to exactly one byte value on the wire. Bytes `0x01..=0x4b` are - /// direct data pushes (the byte *is* the push length); use [`Opcode::is_direct_push`] to - /// test for them. - #[derive(Clone, Copy, Eq, Hash, PartialEq)] - #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] - #[repr(u8)] - pub enum Opcode { - $( - $(#[$attr])* - $variant = $byte, - )* - } - - impl Opcode { - /// Converts a raw byte to the corresponding opcode. - /// - /// Bytes in the direct-push range `0x01..=0x4b` and any unmapped gaps map to - /// [`InvalidOpcode`](Opcode::InvalidOpcode). Use [`is_direct_push`](Opcode::is_direct_push) - /// to test for the push range before calling this. - pub const fn from_u8(byte: u8) -> Self { - match byte { - $( $byte => Self::$variant, )* - _ => Self::InvalidOpcode, - } - } +use dash_types::{codec::NumCodec, enum_map}; - /// Converts to the raw byte value. - pub const fn to_u8(self) -> u8 { - self as u8 - } - } - - impl fmt::Display for Opcode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - $( Self::$variant => f.write_str($display), )* - } - } - } - }; -} +use core::fmt; -define_opcodes! { - /// Push empty byte array onto the stack. - Op0 = 0x00 => "OP_0", - /// Next byte is the number of bytes to push. - PushData1 = 0x4c => "OP_PUSHDATA1", - /// Next two bytes (LE) are the number of bytes to push. - PushData2 = 0x4d => "OP_PUSHDATA2", - /// Next four bytes (LE) are the number of bytes to push. - PushData4 = 0x4e => "OP_PUSHDATA4", - /// Push the value -1 onto the stack. - Op1Negate = 0x4f => "OP_1NEGATE", - /// Reserved (causes script failure if executed). - Reserved = 0x50 => "OP_RESERVED", - /// Push the value 1 onto the stack. - Op1 = 0x51 => "OP_1", - /// Push the value 2. - Op2 = 0x52 => "OP_2", - /// Push the value 3. - Op3 = 0x53 => "OP_3", - /// Push the value 4. - Op4 = 0x54 => "OP_4", - /// Push the value 5. - Op5 = 0x55 => "OP_5", - /// Push the value 6. - Op6 = 0x56 => "OP_6", - /// Push the value 7. - Op7 = 0x57 => "OP_7", - /// Push the value 8. - Op8 = 0x58 => "OP_8", - /// Push the value 9. - Op9 = 0x59 => "OP_9", - /// Push the value 10. - Op10 = 0x5a => "OP_10", - /// Push the value 11. - Op11 = 0x5b => "OP_11", - /// Push the value 12. - Op12 = 0x5c => "OP_12", - /// Push the value 13. - Op13 = 0x5d => "OP_13", - /// Push the value 14. - Op14 = 0x5e => "OP_14", - /// Push the value 15. - Op15 = 0x5f => "OP_15", - /// Push the value 16. - Op16 = 0x60 => "OP_16", +enum_map! { + /// Script opcode. + /// + /// Every named variant corresponds to exactly one byte value on the wire. + /// Bytes `0x01..=0x4b` are direct data pushes (the byte *is* the push length); + /// use [`Opcode::is_direct_push`] to test for them before mapping a byte. + /// + /// Any byte without a named variant, direct pushes included, decodes to + /// `Unknown(byte)` and re-encodes to that same byte. This differs from + /// `OP_INVALIDOPCODE` (`0xff`), which is a real opcode the consensus rules + /// define; do not treat the two as interchangeable. + #[derive(Clone, Copy, Eq, Hash, PartialEq)] + #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] + pub enum Opcode, u8, Unknown { + /// Push empty byte array onto the stack. + Op0 = 0x00 => "OP_0", + /// Next byte is the number of bytes to push. + PushData1 = 0x4c => "OP_PUSHDATA1", + /// Next two bytes (LE) are the number of bytes to push. + PushData2 = 0x4d => "OP_PUSHDATA2", + /// Next four bytes (LE) are the number of bytes to push. + PushData4 = 0x4e => "OP_PUSHDATA4", + /// Push the value -1 onto the stack. + Op1Negate = 0x4f => "OP_1NEGATE", + /// Reserved (causes script failure if executed). + Reserved = 0x50 => "OP_RESERVED", + /// Push the value 1 onto the stack. + Op1 = 0x51 => "OP_1", + /// Push the value 2. + Op2 = 0x52 => "OP_2", + /// Push the value 3. + Op3 = 0x53 => "OP_3", + /// Push the value 4. + Op4 = 0x54 => "OP_4", + /// Push the value 5. + Op5 = 0x55 => "OP_5", + /// Push the value 6. + Op6 = 0x56 => "OP_6", + /// Push the value 7. + Op7 = 0x57 => "OP_7", + /// Push the value 8. + Op8 = 0x58 => "OP_8", + /// Push the value 9. + Op9 = 0x59 => "OP_9", + /// Push the value 10. + Op10 = 0x5a => "OP_10", + /// Push the value 11. + Op11 = 0x5b => "OP_11", + /// Push the value 12. + Op12 = 0x5c => "OP_12", + /// Push the value 13. + Op13 = 0x5d => "OP_13", + /// Push the value 14. + Op14 = 0x5e => "OP_14", + /// Push the value 15. + Op15 = 0x5f => "OP_15", + /// Push the value 16. + Op16 = 0x60 => "OP_16", - /// Do nothing. - Nop = 0x61 => "OP_NOP", - /// Reserved (causes script failure if executed). - Ver = 0x62 => "OP_VER", - /// Execute following opcodes if top of stack is true. - If = 0x63 => "OP_IF", - /// Execute following opcodes if top of stack is false. - NotIf = 0x64 => "OP_NOTIF", - /// Reserved (causes script failure if executed). - VerIf = 0x65 => "OP_VERIF", - /// Reserved (causes script failure if executed). - VerNotIf = 0x66 => "OP_VERNOTIF", - /// Execute following opcodes if preceding OP_IF was not taken. - Else = 0x67 => "OP_ELSE", - /// End an OP_IF/OP_ELSE block. - EndIf = 0x68 => "OP_ENDIF", - /// Remove top stack item; fail if it is false. - Verify = 0x69 => "OP_VERIFY", - /// Mark transaction output as unspendable. - Return = 0x6a => "OP_RETURN", + /// Do nothing. + Nop = 0x61 => "OP_NOP", + /// Reserved (causes script failure if executed). + Ver = 0x62 => "OP_VER", + /// Execute following opcodes if top of stack is true. + If = 0x63 => "OP_IF", + /// Execute following opcodes if top of stack is false. + NotIf = 0x64 => "OP_NOTIF", + /// Reserved (causes script failure if executed). + VerIf = 0x65 => "OP_VERIF", + /// Reserved (causes script failure if executed). + VerNotIf = 0x66 => "OP_VERNOTIF", + /// Execute following opcodes if preceding OP_IF was not taken. + Else = 0x67 => "OP_ELSE", + /// End an OP_IF/OP_ELSE block. + EndIf = 0x68 => "OP_ENDIF", + /// Remove top stack item; fail if it is false. + Verify = 0x69 => "OP_VERIFY", + /// Mark transaction output as unspendable. + Return = 0x6a => "OP_RETURN", - /// Move top item to the alt stack. - ToAltStack = 0x6b => "OP_TOALTSTACK", - /// Move top item from the alt stack to the main stack. - FromAltStack = 0x6c => "OP_FROMALTSTACK", - /// Remove the top two items. - Drop2 = 0x6d => "OP_2DROP", - /// Duplicate the top two items. - Dup2 = 0x6e => "OP_2DUP", - /// Duplicate the top three items. - Dup3 = 0x6f => "OP_3DUP", - /// Copy items 3 and 4 to the top. - Over2 = 0x70 => "OP_2OVER", - /// Move items 5 and 6 to the top. - Rot2 = 0x71 => "OP_2ROT", - /// Swap the top two pairs. - Swap2 = 0x72 => "OP_2SWAP", - /// Duplicate the top item if it is non-zero. - IfDup = 0x73 => "OP_IFDUP", - /// Push the stack size. - Depth = 0x74 => "OP_DEPTH", - /// Remove the top item. - Drop = 0x75 => "OP_DROP", - /// Duplicate the top item. - Dup = 0x76 => "OP_DUP", - /// Remove the second-to-top item. - Nip = 0x77 => "OP_NIP", - /// Copy the second-to-top item to the top. - Over = 0x78 => "OP_OVER", - /// Copy the n-th item to the top. - Pick = 0x79 => "OP_PICK", - /// Move the n-th item to the top. - Roll = 0x7a => "OP_ROLL", - /// Rotate the top three items. - Rot = 0x7b => "OP_ROT", - /// Swap the top two items. - Swap = 0x7c => "OP_SWAP", - /// Copy the top item below the second item. - Tuck = 0x7d => "OP_TUCK", + /// Move top item to the alt stack. + ToAltStack = 0x6b => "OP_TOALTSTACK", + /// Move top item from the alt stack to the main stack. + FromAltStack = 0x6c => "OP_FROMALTSTACK", + /// Remove the top two items. + Drop2 = 0x6d => "OP_2DROP", + /// Duplicate the top two items. + Dup2 = 0x6e => "OP_2DUP", + /// Duplicate the top three items. + Dup3 = 0x6f => "OP_3DUP", + /// Copy items 3 and 4 to the top. + Over2 = 0x70 => "OP_2OVER", + /// Move items 5 and 6 to the top. + Rot2 = 0x71 => "OP_2ROT", + /// Swap the top two pairs. + Swap2 = 0x72 => "OP_2SWAP", + /// Duplicate the top item if it is non-zero. + IfDup = 0x73 => "OP_IFDUP", + /// Push the stack size. + Depth = 0x74 => "OP_DEPTH", + /// Remove the top item. + Drop = 0x75 => "OP_DROP", + /// Duplicate the top item. + Dup = 0x76 => "OP_DUP", + /// Remove the second-to-top item. + Nip = 0x77 => "OP_NIP", + /// Copy the second-to-top item to the top. + Over = 0x78 => "OP_OVER", + /// Copy the n-th item to the top. + Pick = 0x79 => "OP_PICK", + /// Move the n-th item to the top. + Roll = 0x7a => "OP_ROLL", + /// Rotate the top three items. + Rot = 0x7b => "OP_ROT", + /// Swap the top two items. + Swap = 0x7c => "OP_SWAP", + /// Copy the top item below the second item. + Tuck = 0x7d => "OP_TUCK", - /// Concatenate two byte strings (disabled). - Cat = 0x7e => "OP_CAT", - /// Split a byte string (disabled). - Split = 0x7f => "OP_SPLIT", + /// Concatenate two byte strings (disabled). + Cat = 0x7e => "OP_CAT", + /// Split a byte string (disabled). + Split = 0x7f => "OP_SPLIT", - /// Convert a number to a byte string of given length (disabled). - Num2Bin = 0x80 => "OP_NUM2BIN", - /// Convert a byte string to a number (disabled). - Bin2Num = 0x81 => "OP_BIN2NUM", - /// Push the byte length of the top item. - Size = 0x82 => "OP_SIZE", + /// Convert a number to a byte string of given length (disabled). + Num2Bin = 0x80 => "OP_NUM2BIN", + /// Convert a byte string to a number (disabled). + Bin2Num = 0x81 => "OP_BIN2NUM", + /// Push the byte length of the top item. + Size = 0x82 => "OP_SIZE", - /// Bitwise NOT (disabled). - Invert = 0x83 => "OP_INVERT", - /// Bitwise AND (disabled). - And = 0x84 => "OP_AND", - /// Bitwise OR (disabled). - Or = 0x85 => "OP_OR", - /// Bitwise XOR (disabled). - Xor = 0x86 => "OP_XOR", - /// Push true if the top two items are byte-for-byte equal. - Equal = 0x87 => "OP_EQUAL", - /// Same as OP_EQUAL followed by OP_VERIFY. - EqualVerify = 0x88 => "OP_EQUALVERIFY", - /// Reserved (causes script failure if executed). - Reserved1 = 0x89 => "OP_RESERVED1", - /// Reserved (causes script failure if executed). - Reserved2 = 0x8a => "OP_RESERVED2", + /// Bitwise NOT (disabled). + Invert = 0x83 => "OP_INVERT", + /// Bitwise AND (disabled). + And = 0x84 => "OP_AND", + /// Bitwise OR (disabled). + Or = 0x85 => "OP_OR", + /// Bitwise XOR (disabled). + Xor = 0x86 => "OP_XOR", + /// Push true if the top two items are byte-for-byte equal. + Equal = 0x87 => "OP_EQUAL", + /// Same as OP_EQUAL followed by OP_VERIFY. + EqualVerify = 0x88 => "OP_EQUALVERIFY", + /// Reserved (causes script failure if executed). + Reserved1 = 0x89 => "OP_RESERVED1", + /// Reserved (causes script failure if executed). + Reserved2 = 0x8a => "OP_RESERVED2", - /// Add 1 to the top item. - Add1 = 0x8b => "OP_1ADD", - /// Subtract 1 from the top item. - Sub1 = 0x8c => "OP_1SUB", - /// Multiply by 2 (disabled). - Mul2 = 0x8d => "OP_2MUL", - /// Divide by 2 (disabled). - Div2 = 0x8e => "OP_2DIV", - /// Negate the top item. - Negate = 0x8f => "OP_NEGATE", - /// Absolute value of the top item. - Abs = 0x90 => "OP_ABS", - /// Boolean NOT. - Not = 0x91 => "OP_NOT", - /// Push true if the top item is not zero. - NotEqual0 = 0x92 => "OP_0NOTEQUAL", - /// Add the top two items. - Add = 0x93 => "OP_ADD", - /// Subtract the top item from the second. - Sub = 0x94 => "OP_SUB", - /// Multiply (disabled). - Mul = 0x95 => "OP_MUL", - /// Integer divide (disabled). - Div = 0x96 => "OP_DIV", - /// Modulo (disabled). - Mod = 0x97 => "OP_MOD", - /// Left shift (disabled). - LShift = 0x98 => "OP_LSHIFT", - /// Right shift (disabled). - RShift = 0x99 => "OP_RSHIFT", - /// Boolean AND of the top two items. - BoolAnd = 0x9a => "OP_BOOLAND", - /// Boolean OR of the top two items. - BoolOr = 0x9b => "OP_BOOLOR", - /// Push true if the top two items are numerically equal. - NumEqual = 0x9c => "OP_NUMEQUAL", - /// Same as OP_NUMEQUAL followed by OP_VERIFY. - NumEqualVerify = 0x9d => "OP_NUMEQUALVERIFY", - /// Push true if the top two items are not equal. - NumNotEqual = 0x9e => "OP_NUMNOTEQUAL", - /// Push true if the second item is less than the top. - LessThan = 0x9f => "OP_LESSTHAN", - /// Push true if the second item is greater than the top. - GreaterThan = 0xa0 => "OP_GREATERTHAN", - /// Push true if the second item is <= the top. - LessThanOrEqual = 0xa1 => "OP_LESSTHANOREQUAL", - /// Push true if the second item is >= the top. - GreaterThanOrEqual = 0xa2 => "OP_GREATERTHANOREQUAL", - /// Push the smaller of the top two items. - Min = 0xa3 => "OP_MIN", - /// Push the larger of the top two items. - Max = 0xa4 => "OP_MAX", - /// Push true if x is within the range [min, max). - Within = 0xa5 => "OP_WITHIN", + /// Add 1 to the top item. + Add1 = 0x8b => "OP_1ADD", + /// Subtract 1 from the top item. + Sub1 = 0x8c => "OP_1SUB", + /// Multiply by 2 (disabled). + Mul2 = 0x8d => "OP_2MUL", + /// Divide by 2 (disabled). + Div2 = 0x8e => "OP_2DIV", + /// Negate the top item. + Negate = 0x8f => "OP_NEGATE", + /// Absolute value of the top item. + Abs = 0x90 => "OP_ABS", + /// Boolean NOT. + Not = 0x91 => "OP_NOT", + /// Push true if the top item is not zero. + NotEqual0 = 0x92 => "OP_0NOTEQUAL", + /// Add the top two items. + Add = 0x93 => "OP_ADD", + /// Subtract the top item from the second. + Sub = 0x94 => "OP_SUB", + /// Multiply (disabled). + Mul = 0x95 => "OP_MUL", + /// Integer divide (disabled). + Div = 0x96 => "OP_DIV", + /// Modulo (disabled). + Mod = 0x97 => "OP_MOD", + /// Left shift (disabled). + LShift = 0x98 => "OP_LSHIFT", + /// Right shift (disabled). + RShift = 0x99 => "OP_RSHIFT", + /// Boolean AND of the top two items. + BoolAnd = 0x9a => "OP_BOOLAND", + /// Boolean OR of the top two items. + BoolOr = 0x9b => "OP_BOOLOR", + /// Push true if the top two items are numerically equal. + NumEqual = 0x9c => "OP_NUMEQUAL", + /// Same as OP_NUMEQUAL followed by OP_VERIFY. + NumEqualVerify = 0x9d => "OP_NUMEQUALVERIFY", + /// Push true if the top two items are not equal. + NumNotEqual = 0x9e => "OP_NUMNOTEQUAL", + /// Push true if the second item is less than the top. + LessThan = 0x9f => "OP_LESSTHAN", + /// Push true if the second item is greater than the top. + GreaterThan = 0xa0 => "OP_GREATERTHAN", + /// Push true if the second item is <= the top. + LessThanOrEqual = 0xa1 => "OP_LESSTHANOREQUAL", + /// Push true if the second item is >= the top. + GreaterThanOrEqual = 0xa2 => "OP_GREATERTHANOREQUAL", + /// Push the smaller of the top two items. + Min = 0xa3 => "OP_MIN", + /// Push the larger of the top two items. + Max = 0xa4 => "OP_MAX", + /// Push true if x is within the range [min, max). + Within = 0xa5 => "OP_WITHIN", - /// RIPEMD-160 hash of the top item. - Ripemd160 = 0xa6 => "OP_RIPEMD160", - /// SHA-1 hash of the top item. - Sha1 = 0xa7 => "OP_SHA1", - /// SHA-256 hash of the top item. - Sha256 = 0xa8 => "OP_SHA256", - /// RIPEMD-160(SHA-256(x)) of the top item. - Hash160 = 0xa9 => "OP_HASH160", - /// SHA-256(SHA-256(x)) of the top item. - Hash256 = 0xaa => "OP_HASH256", - /// Mark the start of signature-checked data. - CodeSeparator = 0xab => "OP_CODESEPARATOR", - /// Verify a signature against a public key. - CheckSig = 0xac => "OP_CHECKSIG", - /// Same as OP_CHECKSIG followed by OP_VERIFY. - CheckSigVerify = 0xad => "OP_CHECKSIGVERIFY", - /// Verify an m-of-n multisig. - CheckMultiSig = 0xae => "OP_CHECKMULTISIG", - /// Same as OP_CHECKMULTISIG followed by OP_VERIFY. - CheckMultiSigVerify = 0xaf => "OP_CHECKMULTISIGVERIFY", + /// RIPEMD-160 hash of the top item. + Ripemd160 = 0xa6 => "OP_RIPEMD160", + /// SHA-1 hash of the top item. + Sha1 = 0xa7 => "OP_SHA1", + /// SHA-256 hash of the top item. + Sha256 = 0xa8 => "OP_SHA256", + /// RIPEMD-160(SHA-256(x)) of the top item. + Hash160 = 0xa9 => "OP_HASH160", + /// SHA-256(SHA-256(x)) of the top item. + Hash256 = 0xaa => "OP_HASH256", + /// Mark the start of signature-checked data. + CodeSeparator = 0xab => "OP_CODESEPARATOR", + /// Verify a signature against a public key. + CheckSig = 0xac => "OP_CHECKSIG", + /// Same as OP_CHECKSIG followed by OP_VERIFY. + CheckSigVerify = 0xad => "OP_CHECKSIGVERIFY", + /// Verify an m-of-n multisig. + CheckMultiSig = 0xae => "OP_CHECKMULTISIG", + /// Same as OP_CHECKMULTISIG followed by OP_VERIFY. + CheckMultiSigVerify = 0xaf => "OP_CHECKMULTISIGVERIFY", - /// Do nothing (reserved for future soft-fork). - Nop1 = 0xb0 => "OP_NOP1", - /// Fail unless the lock-time condition is met (BIP65). - CheckLockTimeVerify = 0xb1 => "OP_CHECKLOCKTIMEVERIFY", - /// Fail unless the sequence condition is met (BIP112). - CheckSequenceVerify = 0xb2 => "OP_CHECKSEQUENCEVERIFY", - /// Do nothing (reserved for future soft-fork). - Nop4 = 0xb3 => "OP_NOP4", - /// Do nothing (reserved for future soft-fork). - Nop5 = 0xb4 => "OP_NOP5", - /// Do nothing (reserved for future soft-fork). - Nop6 = 0xb5 => "OP_NOP6", - /// Do nothing (reserved for future soft-fork). - Nop7 = 0xb6 => "OP_NOP7", - /// Do nothing (reserved for future soft-fork). - Nop8 = 0xb7 => "OP_NOP8", - /// Do nothing (reserved for future soft-fork). - Nop9 = 0xb8 => "OP_NOP9", - /// Do nothing (reserved for future soft-fork). - Nop10 = 0xb9 => "OP_NOP10", + /// Do nothing (reserved for future soft-fork). + Nop1 = 0xb0 => "OP_NOP1", + /// Fail unless the lock-time condition is met (BIP65). + CheckLockTimeVerify = 0xb1 => "OP_CHECKLOCKTIMEVERIFY", + /// Fail unless the sequence condition is met (BIP112). + CheckSequenceVerify = 0xb2 => "OP_CHECKSEQUENCEVERIFY", + /// Do nothing (reserved for future soft-fork). + Nop4 = 0xb3 => "OP_NOP4", + /// Do nothing (reserved for future soft-fork). + Nop5 = 0xb4 => "OP_NOP5", + /// Do nothing (reserved for future soft-fork). + Nop6 = 0xb5 => "OP_NOP6", + /// Do nothing (reserved for future soft-fork). + Nop7 = 0xb6 => "OP_NOP7", + /// Do nothing (reserved for future soft-fork). + Nop8 = 0xb7 => "OP_NOP8", + /// Do nothing (reserved for future soft-fork). + Nop9 = 0xb8 => "OP_NOP9", + /// Do nothing (reserved for future soft-fork). + Nop10 = 0xb9 => "OP_NOP10", - /// Verify a data signature (not part of transaction digest). - CheckDataSig = 0xba => "OP_CHECKDATASIG", - /// Same as OP_CHECKDATASIG followed by OP_VERIFY. - CheckDataSigVerify = 0xbb => "OP_CHECKDATASIGVERIFY", - /// Invalid opcode (causes immediate script failure). - InvalidOpcode = 0xff => "OP_INVALIDOPCODE", + /// Verify a data signature (not part of transaction digest). + CheckDataSig = 0xba => "OP_CHECKDATASIG", + /// Same as OP_CHECKDATASIG followed by OP_VERIFY. + CheckDataSigVerify = 0xbb => "OP_CHECKDATASIGVERIFY", + /// Invalid opcode (causes immediate script failure). + InvalidOpcode = 0xff => "OP_INVALIDOPCODE", + } } -// Aliases matching the canonical naming - impl Opcode { /// Alias: `OP_FALSE` = [`Op0`](Opcode::Op0). pub const FALSE: Self = Self::Op0; @@ -333,63 +293,65 @@ impl Opcode { impl fmt::Debug for Opcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Opcode({:#04x})", self.to_u8()) + write!(f, "Opcode({:#04x})", self.to_base()) } } #[cfg(test)] mod tests { - use super::Opcode; + use super::{NumCodec, Opcode}; + use crate::prelude::*; - #[test] - fn round_trip_named_opcodes() { - let cases: &[(u8, Opcode)] = &[ - (0x00, Opcode::Op0), - (0x6a, Opcode::Return), - (0x76, Opcode::Dup), - (0xa9, Opcode::Hash160), - (0xac, Opcode::CheckSig), - (0x88, Opcode::EqualVerify), - (0x87, Opcode::Equal), - (0xb1, Opcode::CheckLockTimeVerify), - (0xff, Opcode::InvalidOpcode), - ]; - for &(byte, expected) in cases { - assert_eq!(Opcode::from_u8(byte), expected); - assert_eq!(expected.to_u8(), byte); - } - } + use rstest::*; - #[test] - fn direct_push_range() { - assert!(!Opcode::is_direct_push(0x00)); - assert!(Opcode::is_direct_push(0x01)); - assert!(Opcode::is_direct_push(0x4b)); - assert!(!Opcode::is_direct_push(0x4c)); + #[rstest] + #[case::op0(0x00, Opcode::Op0)] + #[case::op_return(0x6a, Opcode::Return)] + #[case::dup(0x76, Opcode::Dup)] + #[case::hash160(0xa9, Opcode::Hash160)] + #[case::checksig(0xac, Opcode::CheckSig)] + #[case::equalverify(0x88, Opcode::EqualVerify)] + #[case::equal(0x87, Opcode::Equal)] + #[case::cltv(0xb1, Opcode::CheckLockTimeVerify)] + #[case::invalid(0xff, Opcode::InvalidOpcode)] + fn round_trip(#[case] byte: u8, #[case] expected: Opcode) { + assert_eq!(Opcode::from_base(byte), expected); + assert_eq!(expected.to_base(), byte); } - #[test] - fn aliases() { - assert_eq!(Opcode::FALSE, Opcode::Op0); - assert_eq!(Opcode::TRUE, Opcode::Op1); - assert_eq!(Opcode::NOP2, Opcode::CheckLockTimeVerify); - assert_eq!(Opcode::NOP3, Opcode::CheckSequenceVerify); + #[rstest] + #[case::below_range(0x00, false)] + #[case::range_start(0x01, true)] + #[case::range_end(0x4b, true)] + #[case::above_range(0x4c, false)] + fn direct_push_range(#[case] byte: u8, #[case] expected: bool) { + assert_eq!(Opcode::is_direct_push(byte), expected); } - #[test] - fn unmapped_bytes_return_invalid() { - // 0x01..=0x4b are direct pushes, not named opcodes - assert_eq!(Opcode::from_u8(0x01), Opcode::InvalidOpcode); - assert_eq!(Opcode::from_u8(0x4b), Opcode::InvalidOpcode); - assert_eq!(Opcode::from_u8(0xcc), Opcode::InvalidOpcode); + #[rstest] + #[case::op_false(Opcode::FALSE, Opcode::Op0)] + #[case::op_true(Opcode::TRUE, Opcode::Op1)] + #[case::nop2(Opcode::NOP2, Opcode::CheckLockTimeVerify)] + #[case::nop3(Opcode::NOP3, Opcode::CheckSequenceVerify)] + fn aliases(#[case] alias: Opcode, #[case] canonical: Opcode) { + assert_eq!(alias, canonical); } - #[test] - fn display_formatting() { - use crate::prelude::*; + #[rstest] + #[case::direct_push_start(0x01)] + #[case::direct_push_end(0x4b)] + #[case::unmapped(0xcc)] + fn unmapped_bytes_passthrough(#[case] byte: u8) { + assert_eq!(Opcode::from_base(byte), Opcode::Unknown(byte)); + assert_eq!(Opcode::Unknown(byte).to_base(), byte); + } - assert_eq!(Opcode::Dup.to_string(), "OP_DUP"); - assert_eq!(Opcode::Return.to_string(), "OP_RETURN"); - assert_eq!(Opcode::CheckLockTimeVerify.to_string(), "OP_CHECKLOCKTIMEVERIFY"); + #[rstest] + #[case::dup(Opcode::Dup, "OP_DUP")] + #[case::op_return(Opcode::Return, "OP_RETURN")] + #[case::cltv(Opcode::CheckLockTimeVerify, "OP_CHECKLOCKTIMEVERIFY")] + #[case::unknown(Opcode::Unknown(0x42), "unknown(66)")] + fn display_formatting(#[case] op: Opcode, #[case] expected: &str) { + assert_eq!(op.to_string(), expected); } } diff --git a/pkgs/types/Cargo.toml b/pkgs/types/Cargo.toml index 8cfc51de..73c4acad 100644 --- a/pkgs/types/Cargo.toml +++ b/pkgs/types/Cargo.toml @@ -21,8 +21,10 @@ serde = { version = "1", default-features = false, features = [ "derive", "alloc", ], optional = true } +zeroize = { version = "1", default-features = false } [dev-dependencies] +rstest = "0.25" serde = { version = "1", features = ["derive"] } [lints] diff --git a/pkgs/types/src/codec.rs b/pkgs/types/src/codec.rs index 6c047daa..20f54c4e 100644 --- a/pkgs/types/src/codec.rs +++ b/pkgs/types/src/codec.rs @@ -8,6 +8,9 @@ use crate::prelude::*; +use zeroize::Zeroize; + +use core::convert::Infallible; use core::fmt; /// Maximum bytes to pre-allocate per batch when deserializing vectors. @@ -15,19 +18,16 @@ const MAX_VECTOR_ALLOCATE: usize = 5_000_000; /// An error encountered during consensus decoding. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum DecodeError { - /// Not enough bytes remaining in the cursor. - Eof { - /// Bytes needed for the read. - needed: usize, - /// Bytes actually remaining. - remaining: usize, - }, - /// CompactSize encoding is not minimal. - NonMinimalCompactSize { - /// The decoded value that was not minimally encoded. - value: u64, +pub enum DecodeError { + /// A decoded field has an invalid byte length. + BadLen { + /// Acceptable lengths. + expected: Vec, + /// The length that was decoded. + actual: usize, }, + /// Decode validation error. + DecError(E), /// CompactSize value exceeds the allowed limit. CompactSizeExceedsLimit { /// The configured limit. @@ -35,47 +35,86 @@ pub enum DecodeError { /// The decoded value. value: u64, }, - /// A decoded value does not match the expected value. + /// Not enough bytes remaining in the cursor. + Eof { + /// Bytes needed for the read. + needed: usize, + /// Bytes actually remaining. + remaining: usize, + }, + /// Decoded bytes are not valid UTF-8. + InvalidUtf8, + /// A decoded value does not match any expected value. InvalidValue { - /// The value that was expected. - expected: u64, + /// Acceptable values. + expected: Vec, /// The value that was decoded. actual: u64, }, + /// CompactSize encoding is not minimal. + NonMinimalCompactSize { + /// The decoded value that was not minimally encoded. + value: u64, + }, /// Unconsumed bytes remain after decoding. TrailingBytes { /// Number of bytes left over. remaining: usize, }, - /// Decoded bytes are not valid UTF-8. - InvalidUtf8, } -impl fmt::Display for DecodeError { +impl DecodeError { + /// Convert a `DecodeError` into `DecodeError`. + pub fn lift(self) -> DecodeError { + match self { + Self::BadLen { expected, actual } => DecodeError::BadLen { expected, actual }, + Self::CompactSizeExceedsLimit { limit, value } => DecodeError::CompactSizeExceedsLimit { limit, value }, + Self::DecError(inf) => match inf {}, + Self::Eof { needed, remaining } => DecodeError::Eof { needed, remaining }, + Self::InvalidUtf8 => DecodeError::InvalidUtf8, + Self::InvalidValue { expected, actual } => DecodeError::InvalidValue { expected, actual }, + Self::NonMinimalCompactSize { value } => DecodeError::NonMinimalCompactSize { value }, + Self::TrailingBytes { remaining } => DecodeError::TrailingBytes { remaining }, + } + } +} + +impl fmt::Display for DecodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Eof { needed, remaining } => { - write!(f, "unexpected eof: needed {needed} bytes, {remaining} remaining",) - } - Self::NonMinimalCompactSize { value } => { - write!(f, "non-minimal compact size encoding for value {value}",) + Self::BadLen { expected, actual } => { + write!(f, "invalid length: expected one of {expected:?}, got {actual}") } + Self::DecError(e) => write!(f, "decode validation: {e}"), Self::CompactSizeExceedsLimit { limit, value } => { write!(f, "compact size value {value} exceeds limit {limit}",) } + Self::Eof { needed, remaining } => { + write!(f, "unexpected eof: needed {needed} bytes, {remaining} remaining",) + } + Self::InvalidUtf8 => write!(f, "invalid utf-8 in string"), Self::InvalidValue { expected, actual } => { - write!(f, "invalid value: expected {expected}, got {actual}") + write!(f, "invalid value: expected one of {expected:?}, got {actual}") + } + Self::NonMinimalCompactSize { value } => { + write!(f, "non-minimal compact size encoding for value {value}",) } Self::TrailingBytes { remaining } => { write!(f, "{remaining} trailing bytes after decode") } - Self::InvalidUtf8 => write!(f, "invalid utf-8 in string"), } } } #[cfg(feature = "std")] -impl std::error::Error for DecodeError {} +impl std::error::Error for DecodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::DecError(e) => Some(e), + _ => None, + } + } +} /// Checks that `data` has at least `n` bytes remaining. /// @@ -195,6 +234,26 @@ impl ArrayBuf { Self { buf: [0u8; N], len: 0 } } + /// Borrows the written bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.buf[..self.len] + } + + /// Returns `true` when nothing has been written. + pub const fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Number of bytes written so far. + pub const fn len(&self) -> usize { + self.len + } + + /// Remaining writable capacity. + pub const fn spare(&self) -> usize { + N - self.len + } + /// Returns the written bytes as a fixed array. /// /// # Panics @@ -206,6 +265,13 @@ impl ArrayBuf { } } +impl Zeroize for ArrayBuf { + fn zeroize(&mut self) { + self.buf.zeroize(); + self.len = 0; + } +} + impl Default for ArrayBuf { fn default() -> Self { Self::new() @@ -263,13 +329,13 @@ pub trait TypeId { } /// Cursor-based encode/decode for consensus wire types. -pub trait BaseCodec: Sized { +pub trait BaseCodec: Sized { /// Decodes from the cursor, advancing it past consumed bytes. /// /// # Errors /// /// Returns `DecodeError` on malformed input. - fn decode(data: &mut &[u8]) -> Result; + fn decode(data: &mut &[u8]) -> Result>; /// Encodes into the buffer. fn encode(&self, buf: &mut impl EncodeBuf); @@ -362,7 +428,7 @@ impl BaseCodec for bool { 0 => Ok(false), 1 => Ok(true), _ => Err(DecodeError::InvalidValue { - expected: 1, + expected: vec![0, 1], actual: u64::from(byte), }), } @@ -456,29 +522,65 @@ impl __UnencodableMarker for T {} cfg_if::cfg_if! { if #[cfg(feature = "serde")] { - pub trait Codec: - BaseCodec - + Hashable - + TypeId - + ::serde::Serialize - + ::serde::de::DeserializeOwned - { - } + use serde::{Serialize, de::DeserializeOwned}; - impl< - T: BaseCodec - + Hashable - + TypeId - + ::serde::Serialize - + ::serde::de::DeserializeOwned, - > Codec for T - { - } + pub trait Codec: BaseCodec + Hashable + TypeId + Serialize + DeserializeOwned {} + + impl + Hashable + TypeId + Serialize + DeserializeOwned, E> Codec for T {} } else { - pub trait Codec: BaseCodec + Hashable + TypeId {} + pub trait Codec: BaseCodec + Hashable + TypeId {} - impl Codec for T {} + impl + Hashable + TypeId, E> Codec for T {} } } impl __CodecMarker for T {} + +#[cfg(test)] +mod tests { + use super::DecodeError; + use crate::prelude::*; + + use rstest::*; + + use core::fmt; + + #[derive(Clone, Debug, Eq, PartialEq)] + struct SampleError; + + impl fmt::Display for SampleError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "sample text") + } + } + + #[rstest] + fn dec_error_displays_the_inner_error() { + let err: DecodeError = DecodeError::DecError(SampleError); + assert_eq!(err.to_string(), "decode validation: sample text"); + } + + #[rstest] + fn expected_sets_are_rendered_in_full() { + let err = DecodeError::::BadLen { + expected: vec![33, 65], + actual: 12, + }; + assert_eq!(err.to_string(), "invalid length: expected one of [33, 65], got 12"); + } + + #[rstest] + #[case::bad_len(DecodeError::BadLen { expected: vec![33, 65], actual: 12 })] + #[case::exceeds_limit(DecodeError::CompactSizeExceedsLimit { limit: 8, value: 9 })] + #[case::eof(DecodeError::Eof { needed: 4, remaining: 1 })] + #[case::invalid_utf8(DecodeError::InvalidUtf8)] + #[case::invalid_value(DecodeError::InvalidValue { expected: vec![0, 1], actual: 7 })] + #[case::non_minimal(DecodeError::NonMinimalCompactSize { value: 1 })] + #[case::trailing(DecodeError::TrailingBytes { remaining: 3 })] + fn lift_preserves_variant_and_message(#[case] err: DecodeError) { + let before = err.to_string(); + let lifted: DecodeError = err.lift(); + assert_eq!(lifted.to_string(), before); + assert!(!matches!(lifted, DecodeError::DecError(_))); + } +} diff --git a/pkgs/types/src/entity.rs b/pkgs/types/src/entity.rs index d80f7675..eaf96817 100644 --- a/pkgs/types/src/entity.rs +++ b/pkgs/types/src/entity.rs @@ -7,31 +7,36 @@ //! Bridge utilities for `BaseCodec` types to `bitcoin_consensus_encoding` //! traits. -use crate::codec::DecodeError; +use crate::codec::{ArrayBuf, DecodeError, EncodeBuf}; use crate::prelude::*; -use bitcoin_consensus_encoding as encoding; +use bitcoin_consensus_encoding::{Decoder, Encoder}; +use zeroize::Zeroize; +use core::convert::Infallible; use core::fmt; /// Maximum serialized object size (32 MiB). pub const MAX_SER_SIZE: usize = 0x0200_0000; +/// Widest buffer [`ArrEncoder`] and [`ArrDecoder`] will wipe. +pub const MAX_ARR_SIZE: usize = 512; + /// A decoder that buffers all input and decodes in `end()`. /// /// Wraps types with complex sequential decode logic (conditional fields, /// version branching) that cannot be expressed as a composable push-decoder /// without excessive boilerplate. -pub struct BufferDecoder { +pub struct BufferDecoder { buf: Vec, limit: usize, - decode_fn: fn(&mut &[u8]) -> Result, + decode_fn: fn(&mut &[u8]) -> Result>, } -impl BufferDecoder { +impl BufferDecoder { /// Creates a new decoder with the given decode function and /// maximum buffer size. - pub const fn new(decode_fn: fn(&mut &[u8]) -> Result, limit: usize) -> Self { + pub const fn new(decode_fn: fn(&mut &[u8]) -> Result>, limit: usize) -> Self { Self { buf: Vec::new(), limit, @@ -40,7 +45,7 @@ impl BufferDecoder { } } -impl fmt::Debug for BufferDecoder { +impl fmt::Debug for BufferDecoder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("BufferDecoder") .field("buf_len", &self.buf.len()) @@ -49,7 +54,7 @@ impl fmt::Debug for BufferDecoder { } } -impl Clone for BufferDecoder { +impl Clone for BufferDecoder { fn clone(&self) -> Self { Self { buf: self.buf.clone(), @@ -59,9 +64,9 @@ impl Clone for BufferDecoder { } } -impl encoding::Decoder for BufferDecoder { +impl Decoder for BufferDecoder { type Output = T; - type Error = DecodeError; + type Error = DecodeError; fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result { let remaining = self.limit.saturating_sub(self.buf.len()); @@ -90,8 +95,129 @@ impl encoding::Decoder for BufferDecoder { } } +/// An encoder for values whose encoded width is bounded at compile time. +/// +/// Costs a byte-wise volatile write per byte of `N`, so it suits key material +/// and other small fixed records, not block-sized payloads. [`MAX_ARR_SIZE`] +/// caps `N` due to performance cost. +pub struct ArrEncoder { + data: ArrayBuf, + done: bool, +} + +impl ArrEncoder { + /// Wraps a filled buffer. + /// + /// Refuses to compile when `N` exceeds [`MAX_ARR_SIZE`]. + pub const fn new(data: ArrayBuf) -> Self { + const { assert!(N <= MAX_ARR_SIZE, "unusually large zeroized buffer") }; + Self { data, done: false } + } +} + +impl fmt::Debug for ArrEncoder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ArrEncoder") + .field("len", &self.data.len()) + .field("done", &self.done) + .finish() + } +} + +impl Drop for ArrEncoder { + fn drop(&mut self) { + self.data.zeroize(); + } +} + +impl Encoder for ArrEncoder { + fn current_chunk(&self) -> &[u8] { + if self.done { + &[] + } else { + self.data.as_bytes() + } + } + + fn advance(&mut self) -> bool { + if self.done { + false + } else { + self.done = true; + false + } + } +} + +/// A decoder for values whose encoded width is bounded by `N`. +pub struct ArrDecoder { + buf: ArrayBuf, + decode_fn: fn(&mut &[u8]) -> Result>, +} + +impl ArrDecoder { + /// Creates a decoder that accepts at most `N` bytes. + /// + /// Refuses to compile when `N` exceeds [`MAX_ARR_SIZE`]. + pub const fn new(decode_fn: fn(&mut &[u8]) -> Result>) -> Self { + const { assert!(N <= MAX_ARR_SIZE, "unusually large zeroized buffer") }; + Self { + buf: ArrayBuf::new(), + decode_fn, + } + } +} + +impl fmt::Debug for ArrDecoder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ArrDecoder") + .field("buf_len", &self.buf.len()) + .field("limit", &N) + .finish() + } +} + +impl Drop for ArrDecoder { + fn drop(&mut self) { + self.buf.zeroize(); + } +} + +impl Decoder for ArrDecoder { + type Output = T; + type Error = DecodeError; + + fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result { + let remaining = self.buf.spare(); + if remaining == 0 { + return Ok(false); + } + let take = bytes.len().min(remaining); + self.buf.extend_from_slice(&bytes[..take]); + *bytes = &bytes[take..]; + Ok(true) + } + + fn end(self) -> Result { + // Borrow rather than destructure: `Drop` wipes the buffer on the way out, + // including on the early return below. + let mut cursor = self.buf.as_bytes(); + let result = (self.decode_fn)(&mut cursor)?; + if !cursor.is_empty() { + return Err(DecodeError::TrailingBytes { + remaining: cursor.len(), + }); + } + Ok(result) + } + + fn read_limit(&self) -> usize { + self.buf.spare() + } +} + /// An encoder that wraps a pre-built byte vector. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct VecEncoder { data: Vec, done: bool, @@ -104,7 +230,16 @@ impl VecEncoder { } } -impl encoding::Encoder for VecEncoder { +impl fmt::Debug for VecEncoder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("VecEncoder") + .field("len", &self.data.len()) + .field("done", &self.done) + .finish() + } +} + +impl Encoder for VecEncoder { fn current_chunk(&self) -> &[u8] { if self.done { &[] @@ -124,13 +259,14 @@ impl encoding::Encoder for VecEncoder { } /// Generates `Encodable` + `Decodable` for a `BaseCodec` implementor. +/// +/// Stages through the growable [`VecEncoder`]/[`BufferDecoder`] pair. For +/// secret material use [`impl_stype!`](crate::impl_stype) instead, which is +/// the same generator over the wiping fixed-width pair. #[macro_export] macro_rules! impl_type { - ($ty:ty) => { - $crate::impl_type!($ty, $crate::MAX_SER_SIZE); - }; - ($ty:ty, $max:expr) => { - impl $crate::__private::bitcoin_consensus_encoding::Encodable for $ty { + (@parse [$($impl_generics:tt)*] $ty:ty, $max:expr, $err:ty) => { + impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Encodable for $ty { type Encoder<'e> = $crate::VecEncoder; fn encoder(&self) -> Self::Encoder<'_> { let mut buf = ::alloc::vec::Vec::new(); @@ -139,11 +275,170 @@ macro_rules! impl_type { } } - impl $crate::__private::bitcoin_consensus_encoding::Decodable for $ty { - type Decoder = $crate::BufferDecoder<$ty>; + impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Decodable for $ty { + type Decoder = $crate::BufferDecoder<$ty, $err>; + fn decoder() -> Self::Decoder { + $crate::BufferDecoder::new(<$ty as $crate::codec::BaseCodec<$err>>::decode, $max) + } + } + }; + (@parse [$($impl_generics:tt)*] $ty:ty, $max:expr) => { + $crate::impl_type!( + @parse [$($impl_generics)*] $ty, + $max, + ::core::convert::Infallible + ); + }; + (@parse [$($impl_generics:tt)*] $ty:ty) => { + $crate::impl_type!(@parse [$($impl_generics)*] $ty, $crate::MAX_SER_SIZE); + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::impl_type!(@parse [<$($generic)*>] $($args)*); + }; + ($($args:tt)*) => { + $crate::impl_type!(@parse [] $($args)*); + }; +} + +/// Generates `Encodable` + `Decodable` for a `BaseCodec` implementor whose +/// wire image is secret. +#[macro_export] +macro_rules! impl_stype { + (@parse [$($impl_generics:tt)*] $ty:ty, $n:expr, $err:ty) => { + impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Encodable for $ty { + type Encoder<'e> = $crate::ArrEncoder<{ $n }>; + fn encoder(&self) -> Self::Encoder<'_> { + let mut buf = $crate::codec::ArrayBuf::<{ $n }>::new(); + <$ty as $crate::codec::BaseCodec<$err>>::encode(self, &mut buf); + $crate::ArrEncoder::new(buf) + } + } + + impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Decodable for $ty { + type Decoder = $crate::ArrDecoder<$ty, { $n }, $err>; fn decoder() -> Self::Decoder { - $crate::BufferDecoder::new(<$ty as $crate::codec::BaseCodec>::decode, $max) + $crate::ArrDecoder::new(<$ty as $crate::codec::BaseCodec<$err>>::decode) } } }; + (@parse [$($impl_generics:tt)*] $ty:ty, $n:expr) => { + $crate::impl_stype!( + @parse [$($impl_generics)*] $ty, + $n, + ::core::convert::Infallible + ); + }; + (@parse [$($impl_generics:tt)*] $ty:ty) => { + ::core::compile_error!(concat!( + "impl_stype! needs the fixed width of ", + stringify!($ty), + ": write impl_stype!(", stringify!($ty), ", N)" + )); + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::impl_stype!(@parse [<$($generic)*>] $($args)*); + }; + ($($args:tt)*) => { + $crate::impl_stype!(@parse [] $($args)*); + }; +} + +#[cfg(test)] +mod tests { + use super::{ArrDecoder, ArrEncoder, BufferDecoder, VecEncoder, MAX_ARR_SIZE}; + use crate::codec::{ArrayBuf, DecodeError, EncodeBuf}; + use crate::prelude::*; + + use bitcoin_consensus_encoding::{Decoder, Encoder}; + use rstest::*; + use zeroize::Zeroize; + + fn filled(fill: u8, len: usize) -> ArrayBuf { + let mut b = ArrayBuf::::new(); + b.extend_from_slice(&vec![fill; len]); + b + } + + /// Consumes the whole cursor, so `end()` sees no trailing bytes. + fn take_all(data: &mut &[u8]) -> Result, DecodeError> { + let out = data.to_vec(); + *data = &[]; + Ok(out) + } + + #[rstest] + fn arr_encoder_emits_written_prefix_only() { + // A short write into a wide buffer must not leak the zero padding. + let mut enc = ArrEncoder::new(filled::<64>(0xAB, 10)); + assert_eq!(enc.current_chunk(), [0xAB; 10]); + assert!(!enc.advance()); + assert_eq!(enc.current_chunk(), &[] as &[u8]); + } + + /// The wipe itself. `Drop` on both types delegates straight to this, and + /// observing the freed storage directly would need `unsafe`, which the + /// workspace denies. + #[rstest] + fn arrbuf_zeroize_clears_contents_and_len() { + let mut buf = filled::<32>(0xCD, 32); + assert_eq!(buf.as_bytes(), [0xCD; 32]); + buf.zeroize(); + assert_eq!(buf.len(), 0); + assert_eq!(buf.spare(), 32); + assert_eq!(buf.as_bytes(), &[] as &[u8]); + // Re-fill and confirm the backing array really was zeroed, not just the + // length reset. + buf.extend_from_slice(&[0u8; 32]); + assert_eq!(buf.as_bytes(), [0u8; 32]); + } + + /// The cap is a compile-time assert, so only the accepted side is testable + /// here; `N` above the bound fails to build with "unusually large zeroized + /// buffer" wherever the encoder or decoder is instantiated. + #[rstest] + fn max_width_is_accepted() { + let enc = ArrEncoder::new(ArrayBuf::<{ MAX_ARR_SIZE }>::new()); + assert_eq!(enc.current_chunk(), &[] as &[u8]); + let dec = ArrDecoder::, { MAX_ARR_SIZE }>::new(take_all); + assert_eq!(dec.read_limit(), MAX_ARR_SIZE); + } + + #[rstest] + fn arr_decoder_roundtrips_and_bounds_reads() { + let mut dec = ArrDecoder::, 8>::new(take_all); + assert_eq!(dec.read_limit(), 8); + let mut input: &[u8] = &[1, 2, 3]; + assert!(dec.push_bytes(&mut input).unwrap_or(false)); + assert!(input.is_empty()); + assert_eq!(dec.read_limit(), 5); + assert_eq!(dec.end().unwrap_or_default(), vec![1, 2, 3]); + } + + #[rstest] + fn arr_decoder_stops_at_capacity() { + let mut dec = ArrDecoder::, 4>::new(take_all); + let mut input: &[u8] = &[9; 10]; + assert!(dec.push_bytes(&mut input).unwrap_or(false)); + assert_eq!(input.len(), 6, "excess must be left for the caller"); + assert_eq!(dec.read_limit(), 0); + assert!(!dec.push_bytes(&mut input).unwrap_or(true)); + } + + /// Both encoders redact: a `{:?}` in a panic must not print key material. + #[rstest] + fn debug_impls_redact_contents() { + let enc = ArrEncoder::new(filled::<8>(0xFF, 8)); + let dbg = format!("{enc:?}"); + assert!(!dbg.contains("255") && !dbg.contains("ff"), "{dbg}"); + assert!(dbg.contains("len: 8")); + + let venc = VecEncoder::new(vec![0xFFu8; 8]); + assert!(!format!("{venc:?}").contains("255")); + + let vdec = BufferDecoder::>::new(take_all, 16); + assert!(format!("{vdec:?}").contains("limit: 16")); + + let adec = ArrDecoder::, 16>::new(take_all); + assert!(format!("{adec:?}").contains("limit: 16")); + } } diff --git a/pkgs/types/src/hex.rs b/pkgs/types/src/hex.rs index 0f69c847..2268f3cb 100644 --- a/pkgs/types/src/hex.rs +++ b/pkgs/types/src/hex.rs @@ -32,6 +32,113 @@ macro_rules! impl_bytes { )* }; } +/// Generates the consensus encoding traits for a fixed-size byte newtype with +/// secret contents. +#[macro_export] +macro_rules! impl_sbyte { + ($n:literal, $($name:ident),* $(,)?) => { $( + impl $crate::codec::BaseCodec for $name { + fn decode( + data: &mut &[u8], + ) -> Result { + $crate::codec::take::<$n>(data).map(|b| Self(b)) + } + + fn encode(&self, buf: &mut impl $crate::codec::EncodeBuf) { + buf.extend_from_slice(self.as_bytes()); + } + } + + $crate::impl_stype!($name, $n); + + impl From<[u8; $n]> for $name { + fn from(bytes: [u8; $n]) -> Self { Self(bytes) } + } + )* }; +} + +/// The standard trait set for a fixed-size byte newtype, expressed only +/// through `from_bytes` / `as_bytes`. +/// +/// Emits `Clone`, `Copy`, `Default`, `Eq`, `PartialEq`, `Ord`, `PartialOrd`, +/// `Hash`, `AsRef<[u8]>`, `AsRef<[u8; N]>`, `From for [u8; N]`, and the +/// hex `serde` pair. +#[macro_export] +macro_rules! derive_bytes { + (@parse [$($g:tt)*] $ty:ty, $n:expr) => { + impl<$($g)*> ::core::clone::Clone for $ty { + fn clone(&self) -> Self { *self } + } + + impl<$($g)*> ::core::marker::Copy for $ty {} + + impl<$($g)*> ::core::default::Default for $ty { + fn default() -> Self { Self::from_bytes([0u8; $n]) } + } + + impl<$($g)*> ::core::cmp::Eq for $ty {} + + impl<$($g)*> ::core::cmp::PartialEq for $ty { + fn eq(&self, other: &Self) -> bool { self.as_bytes() == other.as_bytes() } + } + + impl<$($g)*> ::core::cmp::Ord for $ty { + fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { + self.as_bytes().cmp(other.as_bytes()) + } + } + + impl<$($g)*> ::core::cmp::PartialOrd for $ty { + fn partial_cmp(&self, other: &Self) -> ::core::option::Option<::core::cmp::Ordering> { + ::core::option::Option::Some(::core::cmp::Ord::cmp(self, other)) + } + } + + impl<$($g)*> ::core::hash::Hash for $ty { + fn hash(&self, state: &mut H) { + ::core::hash::Hash::hash(self.as_bytes(), state); + } + } + + impl<$($g)*> ::core::convert::AsRef<[u8]> for $ty { + fn as_ref(&self) -> &[u8] { self.as_bytes() } + } + + impl<$($g)*> ::core::convert::AsRef<[u8; $n]> for $ty { + fn as_ref(&self) -> &[u8; $n] { self.as_bytes() } + } + + impl<$($g)*> ::core::convert::From<$ty> for [u8; $n] { + fn from(val: $ty) -> Self { *val.as_bytes() } + } + + #[cfg(feature = "serde")] + impl<$($g)*> ::serde::Serialize for $ty { + fn serialize(&self, serializer: Z) -> Result { + use $crate::__private::hex_conservative::DisplayHex as _; + serializer.serialize_str(&self.as_bytes().to_lower_hex_string()) + } + } + + #[cfg(feature = "serde")] + impl<'de, $($g)*> ::serde::Deserialize<'de> for $ty { + fn deserialize>(deserializer: D) -> Result { + use ::serde::de::Error as _; + let s = <::alloc::string::String as ::serde::Deserialize>::deserialize(deserializer)?; + <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) + .map(Self::from_bytes) + .map_err(D::Error::custom) + } + } + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::derive_bytes!(@parse [$($generic)*] $($args)*); + }; + ($($args:tt)*) => { + $crate::derive_bytes!(@parse [] $($args)*); + }; +} + /// Generates a fixed-size byte newtype with consensus encoding traits and /// standard trait implementations. #[macro_export] @@ -41,12 +148,19 @@ macro_rules! make_bytes { $name:ident, $n:literal ) => { $(#[$attr])* - #[derive(Clone, Copy, Eq, Hash, PartialEq, $crate::TypeId)] + #[derive($crate::TypeId)] pub struct $name(pub [u8; $n]); $crate::impl_bytes!($n, $name); + $crate::derive_bytes!($name, $n); + impl $name { + /// Wraps raw bytes without validation. + pub const fn from_bytes(bytes: [u8; $n]) -> Self { + Self(bytes) + } + /// Returns the inner byte array. pub const fn to_bytes(self) -> [u8; $n] { self.0 @@ -63,22 +177,6 @@ macro_rules! make_bytes { } } - impl Default for $name { - fn default() -> Self { Self([0u8; $n]) } - } - - impl From<$name> for [u8; $n] { - fn from(val: $name) -> Self { val.0 } - } - - impl AsRef<[u8]> for $name { - fn as_ref(&self) -> &[u8] { &self.0 } - } - - impl AsRef<[u8; $n]> for $name { - fn as_ref(&self) -> &[u8; $n] { &self.0 } - } - impl core::fmt::Debug for $name { fn fmt( &self, @@ -103,24 +201,6 @@ macro_rules! make_bytes { Ok(()) } } - - #[cfg(feature = "serde")] - impl ::serde::Serialize for $name { - fn serialize(&self, serializer: S) -> Result { - use $crate::__private::hex_conservative::DisplayHex; - serializer.serialize_str(&self.0.to_lower_hex_string()) - } - } - - #[cfg(feature = "serde")] - impl<'de> ::serde::Deserialize<'de> for $name { - fn deserialize>(deserializer: D) -> Result { - let s = <::alloc::string::String as ::serde::Deserialize>::deserialize(deserializer)?; - <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) - .map(Self) - .map_err(::serde::de::Error::custom) - } - } }; } diff --git a/pkgs/types/src/lib.rs b/pkgs/types/src/lib.rs index 07ce3ef5..d2d4cf81 100644 --- a/pkgs/types/src/lib.rs +++ b/pkgs/types/src/lib.rs @@ -15,6 +15,7 @@ extern crate std; mod entity; mod hex; +mod macros; #[allow(unused_imports, reason = "ergonomic shim, exports may be unused")] mod prelude; mod uint; @@ -24,7 +25,7 @@ pub mod codec; pub mod serialize; pub use dash_types_marker::{TypeId, Unencodable}; -pub use entity::{BufferDecoder, VecEncoder, MAX_SER_SIZE}; +pub use entity::{ArrDecoder, ArrEncoder, BufferDecoder, VecEncoder, MAX_ARR_SIZE, MAX_SER_SIZE}; #[doc(hidden)] pub mod __private { @@ -32,35 +33,3 @@ pub mod __private { #[cfg(feature = "serde")] pub use hex_conservative; } - -/// Generates \`From\` + \`From<&T>\` (or \`TryFrom\` equivalents). -/// The closure body receives \`&$src\`; the owned impl delegates. -#[macro_export] -macro_rules! type_cvrt { - (From<$src:ty> for $dst:ty, |$v:ident| $body:expr) => { - impl From<&$src> for $dst { - fn from($v: &$src) -> Self { - $body - } - } - impl From<$src> for $dst { - fn from(v: $src) -> Self { - Self::from(&v) - } - } - }; - (TryFrom<$src:ty> for $dst:ty, $err:ty, |$v:ident| $body:expr) => { - impl TryFrom<&$src> for $dst { - type Error = $err; - fn try_from($v: &$src) -> Result { - $body - } - } - impl TryFrom<$src> for $dst { - type Error = $err; - fn try_from(v: $src) -> Result { - Self::try_from(&v) - } - } - }; -} diff --git a/pkgs/types/src/macros.rs b/pkgs/types/src/macros.rs new file mode 100644 index 00000000..de38328c --- /dev/null +++ b/pkgs/types/src/macros.rs @@ -0,0 +1,384 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Shared macro definitions. + +/// Maps enum variants to integer constants and display strings. +/// +/// Generates the enum definition, integer mapping (via `NumCodec` or inherent +/// `const fn`), and `impl Display` from a single table. +/// +/// # Syntax +/// +/// Each variant uses one of two forms: +/// +/// - `Variant = VALUE` -- display string is `stringify!(Variant)` +/// - `Variant = VALUE => "label"` -- display string is `"label"` +/// +/// All variants within one invocation must use the same form. +/// +/// ## Infallible +/// +/// Generates the enum with a catch-all variant, `impl NumCodec`, `new`, +/// `is_canonical`, `variants`, and `impl Display`. The catch-all displays as +/// `unknown({v})`; build values with `new` so it never shadows a named +/// variant. +/// +/// ```ignore +/// enum_map! { +/// #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +/// pub enum NIPurpose, u8, Unknown { +/// /// Core P2P port. +/// CoreP2p = 0 => "core_p2p", +/// /// Platform P2P port. +/// PlatformP2p = 1 => "platform_p2p", +/// } +/// } +/// ``` +/// +/// ## Fallible +/// +/// Generates the enum (closed), inherent `const fn from_base` / `to_base` +/// methods, and `impl Display`. +/// +/// ```ignore +/// enum_map! { +/// #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +/// pub enum Sec1Byte, u8 { +/// /// Compressed, even Y coordinate. +/// CompEven = 0x02, +/// /// Compressed, odd Y coordinate. +/// CompOdd = 0x03, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! enum_map { + // Infallible + manual display strings. + ( + $(#[$enum_attr:meta])* + $vis:vis enum $enum:ident, $base:ty, $catch_all:ident { + $( + $(#[$var_attr:meta])* + $variant:ident = $value:literal => $display:expr + ),+ $(,)? + } + ) => { + $crate::enum_map!(@enum $(#[$enum_attr])* $vis $enum, $base, $catch_all { + $($(#[$var_attr])* $variant,)+ + }); + $crate::enum_map!(@infallible $enum, $base, $catch_all { $($variant = $value),+ }); + $crate::enum_map!(@display_catch_all $enum, $catch_all { $($variant = $display),+ }); + }; + + // Infallible + auto-stringize. + ( + $(#[$enum_attr:meta])* + $vis:vis enum $enum:ident, $base:ty, $catch_all:ident { + $( + $(#[$var_attr:meta])* + $variant:ident = $value:literal + ),+ $(,)? + } + ) => { + $crate::enum_map!(@enum $(#[$enum_attr])* $vis $enum, $base, $catch_all { + $($(#[$var_attr])* $variant,)+ + }); + $crate::enum_map!(@infallible $enum, $base, $catch_all { $($variant = $value),+ }); + $crate::enum_map!(@display_catch_all $enum, $catch_all { $($variant = stringify!($variant)),+ }); + }; + + // Fallible + manual display strings. + ( + $(#[$enum_attr:meta])* + $vis:vis enum $enum:ident, $base:ty { + $( + $(#[$var_attr:meta])* + $variant:ident = $value:literal => $display:expr + ),+ $(,)? + } + ) => { + $crate::enum_map!(@enum_closed $(#[$enum_attr])* $vis $enum { + $($(#[$var_attr])* $variant,)+ + }); + $crate::enum_map!(@fallible $enum, $base { $($variant = $value),+ }); + $crate::enum_map!(@display $enum { $($variant = $display),+ }); + }; + + // Fallible + auto-stringize. + ( + $(#[$enum_attr:meta])* + $vis:vis enum $enum:ident, $base:ty { + $( + $(#[$var_attr:meta])* + $variant:ident = $value:literal + ),+ $(,)? + } + ) => { + $crate::enum_map!(@enum_closed $(#[$enum_attr])* $vis $enum { + $($(#[$var_attr])* $variant,)+ + }); + $crate::enum_map!(@fallible $enum, $base { $($variant = $value),+ }); + $crate::enum_map!(@display $enum { $($variant = stringify!($variant)),+ }); + }; + + (@enum $(#[$enum_attr:meta])* $vis:vis $enum:ident, $base:ty, $catch_all:ident { + $($(#[$var_attr:meta])* $variant:ident,)+ + }) => { + $(#[$enum_attr])* + $vis enum $enum { + $( + $(#[$var_attr])* + $variant, + )+ + /// Unrecognized value, construct through [`new`](Self::new) rather than directly + $catch_all($base), + } + }; + + (@enum_closed $(#[$enum_attr:meta])* $vis:vis $enum:ident { + $($(#[$var_attr:meta])* $variant:ident,)+ + }) => { + $(#[$enum_attr])* + $vis enum $enum { + $( + $(#[$var_attr])* + $variant, + )+ + } + }; + + (@infallible $enum:ident, $base:ty, $catch_all:ident { + $($variant:ident = $value:literal),+ + }) => { + impl $crate::codec::NumCodec<$base> for $enum { + fn from_base(val: $base) -> Self { + match val { + $($value => Self::$variant,)+ + other => Self::$catch_all(other), + } + } + + fn to_base(&self) -> $base { + match self { + $(Self::$variant => $value,)+ + Self::$catch_all(v) => *v, + } + } + } + + impl $enum { + /// Canonical constructor. + /// + /// Routes through `from_base`, so a value a named variant covers yields + /// that variant instead of a catch-all holding the same number. Decoded + /// values already take this path. + pub fn new(val: $base) -> Self { + >::from_base(val) + } + + /// Whether this value is in canonical form. + /// + /// False only for a catch-all holding a value that a named variant + /// already covers. + pub fn is_canonical(&self) -> bool { + !matches!(self, Self::$catch_all(v) if matches!( + >::from_base(*v), + $(Self::$variant)|+ + )) + } + + /// Named variants. + pub const fn variants() -> &'static [Self] { + &[$(Self::$variant),+] + } + } + }; + + (@fallible $enum:ident, $base:ty { + $($variant:ident = $value:literal),+ + }) => { + impl $enum { + /// Constructs from the base integer value. + pub const fn from_base(v: $base) -> Option { + match v { + $($value => Some(Self::$variant),)+ + _ => None, + } + } + + /// Returns the base integer value. + pub const fn to_base(self) -> $base { + match self { + $(Self::$variant => $value,)+ + } + } + + /// All variants. + pub const fn variants() -> &'static [Self] { + &[$(Self::$variant),+] + } + } + }; + + (@display_catch_all $enum:ident, $catch_all:ident { + $($variant:ident = $display:expr),+ + }) => { + impl core::fmt::Display for $enum { + fn fmt( + &self, f: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { + match self { + $(Self::$variant => f.write_str($display),)+ + Self::$catch_all(v) => write!(f, "unknown({v})"), + } + } + } + }; + + (@display $enum:ident { + $($variant:ident = $display:expr),+ + }) => { + impl core::fmt::Display for $enum { + fn fmt( + &self, f: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { + match self { + $(Self::$variant => f.write_str($display),)+ + } + } + } + }; +} + +/// Generates `From` + `From<&T>` (or `TryFrom` equivalents). The closure +/// body receives `&$src`; the owned impl delegates. +#[macro_export] +macro_rules! type_cvrt { + (From<$src:ty> for $dst:ty, |$v:ident| $body:expr) => { + impl core::convert::From<&$src> for $dst { + fn from($v: &$src) -> Self { + $body + } + } + impl core::convert::From<$src> for $dst { + fn from(v: $src) -> Self { + Self::from(&v) + } + } + }; + (TryFrom<$src:ty> for $dst:ty, $err:ty, |$v:ident| $body:expr) => { + impl core::convert::TryFrom<&$src> for $dst { + type Error = $err; + fn try_from($v: &$src) -> Result { + $body + } + } + impl core::convert::TryFrom<$src> for $dst { + type Error = $err; + fn try_from(v: $src) -> Result { + Self::try_from(&v) + } + } + }; +} + +#[cfg(test)] +mod tests { + use crate::codec::NumCodec; + use crate::prelude::*; + + use rstest::*; + + enum_map! { + /// Open enum: unrecognized codes survive a round trip. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub enum Open, u8, Unknown { + /// First. + One = 1 => "one", + /// Second. + Two = 2 => "two", + } + } + + enum_map! { + /// Closed enum: only the listed codes are representable. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub enum Closed, u16 { + /// Low. + Lo = 0x0100, + /// High. + Hi = 0x0200, + } + } + + enum_map! { + /// Auto-stringized display labels. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub enum Auto, u8, Other { + /// Alpha. + Alpha = 7, + } + } + + #[rstest] + #[case::named_low(1, Open::One)] + #[case::named_high(2, Open::Two)] + #[case::unknown(9, Open::Unknown(9))] + fn open_maps_both_ways(#[case] raw: u8, #[case] expected: Open) { + assert_eq!(Open::from_base(raw), expected); + assert_eq!(expected.to_base(), raw); + } + + #[rstest] + fn new_canonicalizes_a_shadowing_catch_all() { + let shadow = Open::Unknown(1); + assert_eq!(shadow.to_base(), Open::One.to_base()); + assert_ne!(shadow, Open::One); + assert!(!shadow.is_canonical()); + + assert_eq!(Open::new(1), Open::One); + assert!(Open::new(1).is_canonical()); + assert!(Open::new(9).is_canonical()); + } + + #[rstest] + fn open_display_and_variants() { + assert_eq!(Open::One.to_string(), "one"); + assert_eq!(Open::Unknown(9).to_string(), "unknown(9)"); + assert_eq!(Open::variants(), &[Open::One, Open::Two]); + } + + #[rstest] + fn auto_stringize_uses_the_variant_name() { + assert_eq!(Auto::Alpha.to_string(), "Alpha"); + assert_eq!(Auto::Other(3).to_string(), "unknown(3)"); + assert_eq!(Auto::variants(), &[Auto::Alpha]); + assert_eq!(Auto::new(7), Auto::Alpha); + assert!(!Auto::Other(7).is_canonical()); + } + + #[rstest] + #[case::lo(0x0100, Some(Closed::Lo))] + #[case::hi(0x0200, Some(Closed::Hi))] + #[case::unmapped(0x0300, None)] + #[case::zero(0, None)] + fn closed_rejects_unmapped(#[case] raw: u16, #[case] expected: Option) { + assert_eq!(Closed::from_base(raw), expected); + } + + #[rstest] + fn closed_roundtrips_every_variant() { + for v in Closed::variants() { + assert_eq!(Closed::from_base(v.to_base()), Some(*v)); + } + } + + #[rstest] + fn closed_display() { + assert_eq!(Closed::Lo.to_string(), "Lo"); + } +} diff --git a/pkgs/types/src/prelude.rs b/pkgs/types/src/prelude.rs index 16d40d0b..f00ae53d 100644 --- a/pkgs/types/src/prelude.rs +++ b/pkgs/types/src/prelude.rs @@ -6,5 +6,7 @@ //! Re-exports for no_std compatibility. -pub(crate) use alloc::string::String; +pub(crate) use alloc::format; +pub(crate) use alloc::string::{String, ToString}; +pub(crate) use alloc::vec; pub(crate) use alloc::vec::Vec;