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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- **`DecodeUnchecked` trait for trusted-source SCALE decoding.** Exposes
a `decode_unchecked` entry point on the four ring types (`MembersSet`,
`MembersCommitment`, `StaticChunk`, `ProverState`) that reads the same
wire format as the default SCALE `Decode` impl but skips the arkworks
curve-point validation.

## [0.3.0]

### Breaking Changes
Expand Down
30 changes: 30 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,36 @@ use smallvec::SmallVec;
pub mod mock;
pub mod ring;

/// SCALE decoding that may skip backend-specific validation (e.g. arkworks
/// curve-point subgroup checks) for values arriving from a trusted source.
///
/// The default `Decode` impls for the ring types ([`ring::MembersSet`],
/// [`ring::MembersCommitment`], [`ring::StaticChunk`], [`ring::ProverState`])
/// validate every curve point — the right behaviour at trust boundaries
/// (extrinsic arguments, XCM payloads, anything crossing an untrusted edge).
/// Reading a value that was already validated on the way in repeats that work
/// for nothing.
///
/// Implementors expose a parallel `decode_unchecked` entry point that reads
/// the exact same bytes without revalidating. The supertrait [`Decode`]
/// expresses that the two paths share the same wire format; only the
/// validation policy differs.
///
/// The default method body just delegates to [`Decode::decode`], which is
/// the correct behaviour for types with no curve content. Backends
/// override with a validation-bypassing path; see
/// [`ring::impl_common_traits`].
///
/// Use only for values that were validated at their ingress point. Do not
/// call on data that arrives from an untrusted source.
pub trait DecodeUnchecked: Sized + Decode {
fn decode_unchecked<I: parity_scale_codec::Input>(
input: &mut I,
) -> Result<Self, parity_scale_codec::Error> {
Self::decode(input)
}
}

// Fixed types:

/// Cryptographic identifier for a person within a specific application which deals with people.
Expand Down
76 changes: 72 additions & 4 deletions src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,79 @@
//! - Set membership is checked by linear scan, not zero-knowledge.

use super::*;
use bounded_collections::{BoundedVec, ConstU32};
use bounded_collections::{BoundedVec, ConstU32, Get};
use core::ops::{Deref, DerefMut};
use sha2::{Digest, Sha256};

pub const MAX_MEMBERS: u32 = 1024;

/// Generic mock-side ring members container.
///
/// Newtype around `BoundedVec<T, N>` carrying a default-body
/// [`DecodeUnchecked`] impl (no curve content to skip-validate). Used by
/// [`Mock`] itself and reusable by downstream test crypto impls without
/// local duplication.
///
/// The inner field is `pub` for ergonomic construction; `Deref` /
/// `DerefMut` expose `BoundedVec`'s API directly so call sites (`iter`,
/// `contains`, `try_push`, ...) work unchanged. Impls are hand-written
/// rather than derived so they bound only on `T`; the `N` parameter is a
/// `Get<u32>` marker, not a value type.
#[derive(Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo)]
#[scale_info(skip_type_params(N))]
pub struct MockMembers<T, N: Get<u32> = ConstU32<MAX_MEMBERS>>(pub BoundedVec<T, N>);

impl<T: Decode, N: Get<u32>> DecodeUnchecked for MockMembers<T, N> {}

// `Mock`'s `StaticChunk = ()` — no curve content to skip-validate.
impl DecodeUnchecked for () {}

impl<T: Clone, N: Get<u32>> Clone for MockMembers<T, N> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}

impl<T: core::fmt::Debug, N: Get<u32>> core::fmt::Debug for MockMembers<T, N> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("MockMembers").field(&self.0).finish()
}
}

impl<T, N: Get<u32>> Default for MockMembers<T, N> {
fn default() -> Self {
Self(BoundedVec::new())
}
}

impl<T: PartialEq, N: Get<u32>> PartialEq for MockMembers<T, N> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}

impl<T: Eq, N: Get<u32>> Eq for MockMembers<T, N> {}

impl<T, N: Get<u32>> Deref for MockMembers<T, N> {
type Target = BoundedVec<T, N>;
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T, N: Get<u32>> DerefMut for MockMembers<T, N> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl<T, N: Get<u32>> TryFrom<Vec<T>> for MockMembers<T, N> {
type Error = ();
fn try_from(v: Vec<T>) -> Result<Self, Self::Error> {
BoundedVec::try_from(v).map(Self).map_err(|_| ())
}
}

const TAG_ALIAS: &[u8] = b"verifiable-mock:v1:alias";
const TAG_SIG: &[u8] = b"verifiable-mock:v1:sig";
const TAG_PROOF: &[u8] = b"verifiable-mock:v1:proof";
Expand Down Expand Up @@ -76,8 +144,8 @@ pub struct MockProof {
pub struct Mock;

impl GenerateVerifiable for Mock {
type Members = BoundedVec<Self::Member, ConstU32<MAX_MEMBERS>>;
type Intermediate = BoundedVec<Self::Member, ConstU32<MAX_MEMBERS>>;
type Members = MockMembers<Self::Member>;
type Intermediate = MockMembers<Self::Member>;
type Member = [u8; 32];
type Secret = [u8; 32];
type Commitment = (Self::Member, Vec<Self::Member>);
Expand All @@ -87,7 +155,7 @@ impl GenerateVerifiable for Mock {
type Config = ();

fn start_members(_config: Self::Config) -> Self::Intermediate {
BoundedVec::new()
MockMembers::default()
}

fn push_members(
Expand Down
34 changes: 34 additions & 0 deletions src/ring/bandersnatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,40 @@ mod builder_tests {
assert!(MembersCommitment::decode(&mut &zero_bytes[..]).is_err());
}

// The `DecodeUnchecked::decode_unchecked` entry point shares the wire format
// with the validating `Decode::decode` — this is what makes routing a
// storage field through `decode_unchecked` a zero-migration change.
#[test]
fn decode_unchecked_reads_same_bytes() {
use crate::ring::DecodeUnchecked;

let domain_size = RingDomainSize::Domain11;
let _ = bandersnatch_ring_setup(domain_size);

let members_set = BandersnatchVrfVerifiable::start_members(domain_size);
let commitment = BandersnatchVrfVerifiable::finish_members(members_set);

let bytes = commitment.encode();
let decoded = MembersCommitment::decode_unchecked(&mut &bytes[..]).expect("decode ok");
assert_eq!(decoded.encode(), bytes);
}

// `decode_unchecked` must accept inputs that the validating decode path
// rejects. Pair this with `decode_bogus_commitment_fails` — without that
// test the property below would still hold trivially.
#[test]
fn decode_unchecked_skips_validation() {
use crate::ring::DecodeUnchecked;

let zero_bytes = vec![0u8; BandersnatchSha512Ell2::MEMBERS_COMMITMENT_SIZE];

// Validated path still rejects (sanity).
assert!(MembersCommitment::decode(&mut &zero_bytes[..]).is_err());

// Unchecked path accepts the same bytes.
assert!(MembersCommitment::decode_unchecked(&mut &zero_bytes[..]).is_ok());
}

// A proof with trailing garbage bytes must not be accepted, otherwise the same
// underlying signature could be re-encoded as multiple distinct byte arrays.
#[test]
Expand Down
20 changes: 20 additions & 0 deletions src/ring/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ pub mod bandersnatch;
const UNCOMPRESSED: ark_scale::Usage =
ark_scale::make_usage(ark_serialize::Compress::No, ark_serialize::Validate::Yes);

/// Uncompressed encoding mode without curve-point validation. Used by the
/// [`DecodeUnchecked`] entry points for decoding values from trusted sources
/// where the validation cost has already been paid on the way in.
const UNCOMPRESSED_UNCHECKED: ark_scale::Usage =
ark_scale::make_usage(ark_serialize::Compress::No, ark_serialize::Validate::No);

pub use crate::DecodeUnchecked;

/// Domain sizes for the PCS (Polynomial Commitment Scheme).
///
/// This determines the maximum ring size that can be supported for a ring suite.
Expand Down Expand Up @@ -355,6 +363,18 @@ macro_rules! impl_common_traits {
impl<S: $bound> core::cmp::Eq for $type_name<S> {}

impl<S: $bound> DecodeWithMemTracking for $type_name<S> {}

impl<S: $bound> DecodeUnchecked for $type_name<S> {
fn decode_unchecked<I: ark_scale::scale::Input>(
input: &mut I,
) -> Result<Self, ark_scale::scale::Error> {
let a: ark_scale::ArkScale<Self, { UNCOMPRESSED_UNCHECKED }> =
<ark_scale::ArkScale<Self, { UNCOMPRESSED_UNCHECKED }> as ark_scale::scale::Decode>::decode(
input,
)?;
Ok(a.0)
}
}
};
}

Expand Down
Loading