Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [Unreleased]

### Tests

* **platform:** freeze historical BLS, GroveDB/checkpoint, transaction signing, DashPay encryption, and FFI node-ID compatibility vectors, with a script to reproduce them through ignored tests on a pinned historical checkout.

## [5.0.0-beta.2](https://github.com/dashpay/platform/compare/v5.0.0-beta.1...v5.0.0-beta.2) (2026-10-06)


Expand Down
317 changes: 317 additions & 0 deletions packages/rs-dpp/tests/bls_compatibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
use dpp::bls_signatures::{
AggregateSignature, Bls12381G2Impl, Pairing, PublicKey, SecretKey, SerializationFormat,
Signature, SignatureSchemes,
};
use dpp::core_types::validator::v0::ValidatorV0;
use dpp::core_types::validator_set::v0::ValidatorSetV0;
use dpp::native_bls::NativeBlsModule;
use dpp::{BlsModule, ProtocolError};
use serde::Deserialize;

#[derive(Deserialize)]
struct Corpus {
key_generation: Vec<KeyGeneration>,
basic: Vec<Basic>,
secure_aggregation: Vec<Aggregate>,
scalars: Vec<Scalar>,
storage: Vec<Storage>,
}

#[derive(Deserialize)]
struct KeyGeneration {
ikm: String,
secret_key: String,
public_key: String,
}

#[derive(Deserialize)]
struct Basic {
secret_key: String,
message: String,
public_key: String,
legacy_public_key: String,
signature: String,
}

#[derive(Deserialize)]
struct Aggregate {
secret_keys: Vec<String>,
message: String,
public_keys: Vec<String>,
signature: String,
}

#[derive(Deserialize)]
struct Scalar {
name: String,
input: String,
normalized: Option<String>,
public_key: Option<String>,
signature: Option<String>,
}

#[derive(Deserialize)]
struct Storage {
name: String,
validator: serde_json::Value,
validator_bytes: String,
validator_set: serde_json::Value,
validator_set_bytes: String,
}

fn corpus() -> Corpus {
serde_json::from_str(include_str!("fixtures/bls_compatibility/vectors.json")).unwrap()
}

fn bytes<const N: usize>(hex: &str) -> [u8; N] {
hex::decode(hex).unwrap().try_into().unwrap()
}

#[test]
fn should_preserve_key_generation_from_fixed_material() {
let vectors = corpus().key_generation;
assert_eq!(vectors.len(), 3);
for v in vectors {
let ikm = bytes::<32>(&v.ikm);
let current = SecretKey::<Bls12381G2Impl>::from_hash(ikm);
assert_eq!(current.to_be_bytes(), bytes::<32>(&v.secret_key));
assert_eq!(
current.public_key().0.to_compressed(),
bytes::<48>(&v.public_key)
);
}
}

#[test]
fn should_match_frozen_basic_signatures() {
let vectors = corpus().basic;
assert_eq!(vectors.len(), 16);
for v in vectors {
let secret = bytes::<32>(&v.secret_key);
let message = hex::decode(v.message).unwrap();
let public: [u8; 48] = bytes(&v.public_key);
let signature: [u8; 96] = bytes(&v.signature);
assert_eq!(
NativeBlsModule.private_key_to_public_key(&secret).unwrap(),
public
);
assert_eq!(NativeBlsModule.sign(&message, &secret).unwrap(), signature);
assert!(NativeBlsModule
.verify_signature(&signature, &message, &public)
.unwrap());
let key = PublicKey::<Bls12381G2Impl>::try_from(public.as_slice()).unwrap();
assert_eq!(
key.to_bytes_with_mode(SerializationFormat::Legacy),
bytes::<48>(&v.legacy_public_key)
);
let mut wrong_message = message.clone();
wrong_message.push(1);
assert!(!NativeBlsModule
.verify_signature(&signature, &wrong_message, &public)
.unwrap());
let mut other_secret = [0; 32];
other_secret[31] = 3;
let other = SecretKey::<Bls12381G2Impl>::from_be_bytes(&other_secret)
.unwrap()
.public_key();
assert!(!NativeBlsModule
.verify_signature(&signature, &message, &other.to_bytes())
.unwrap());
}
}

#[test]
fn should_match_frozen_secure_aggregates() {
let vectors = corpus().secure_aggregation;
assert_eq!(vectors.len(), 5);
for v in vectors {
let message = hex::decode(v.message).unwrap();
let public_keys: Vec<_> = v
.public_keys
.iter()
.map(|key| PublicKey::<Bls12381G2Impl>::try_from(bytes::<48>(key).as_slice()).unwrap())
.collect();
let signatures: Vec<_> = v
.secret_keys
.iter()
.map(|key| {
SecretKey::<Bls12381G2Impl>::from_be_bytes(&bytes(key))
.unwrap()
.sign(SignatureSchemes::Basic, &message)
.unwrap()
})
.collect();
let AggregateSignature::Basic(point) =
AggregateSignature::from_signatures_secure(&signatures, &public_keys).unwrap()
else {
panic!("expected Basic");
};
assert_eq!(point.to_compressed(), bytes::<96>(&v.signature));
assert!(Signature::Basic(point)
.verify_secure(&public_keys, &message)
.is_ok());
let mut wrong_message = message;
wrong_message.push(1);
assert!(Signature::Basic(point)
.verify_secure(&public_keys, &wrong_message)
.is_err());
}
}

#[test]
fn should_preserve_historical_scalar_reduction() {
let vectors = corpus().scalars;
assert_eq!(vectors.len(), 4);
for v in vectors {
let input = bytes(&v.input);
let key = SecretKey::<Bls12381G2Impl>::from_be_bytes(&input).into_option();
assert_eq!(
key.as_ref().map(|key| hex::encode(key.to_be_bytes())),
v.normalized,
"{}",
v.name
);
if v.normalized.is_some() {
assert_eq!(
hex::encode(NativeBlsModule.private_key_to_public_key(&input).unwrap()),
v.public_key.unwrap()
);
assert_eq!(
hex::encode(NativeBlsModule.sign(b"scalar boundary", &input).unwrap()),
v.signature.unwrap()
);
} else {
assert!(matches!(
NativeBlsModule.sign(b"scalar boundary", &input),
Err(ProtocolError::InvalidBLSPrivateKeyError(_))
));
assert!(matches!(
NativeBlsModule.private_key_to_public_key(&input),
Err(ProtocolError::InvalidBLSPrivateKeyError(_))
));
}
}
}

#[test]
fn should_preserve_identity_key_parsing_without_accepting_identity_signatures() {
let mut public_key = [0; 48];
public_key[0] = 0xc0;
let mut signature = [0; 96];
signature[0] = 0xc0;
// This is historical parsing behavior, not permission to verify an identity signature.
assert!(NativeBlsModule.validate_public_key(&public_key).is_ok());
assert!(!NativeBlsModule
.verify_signature(&signature, b"identity", &public_key)
.unwrap());
let valid = bytes::<48>(&corpus().basic[0].public_key);
assert!(!NativeBlsModule
.verify_signature(&signature, b"identity", &valid)
.unwrap());
}

#[test]
fn should_preserve_invalid_input_results_and_validation_order() {
let v = &corpus().basic[0];
let public_key = bytes::<48>(&v.public_key);
let secret = bytes::<32>(&v.secret_key);
for len in [0, 31, 33] {
assert!(
matches!(NativeBlsModule.sign(b"", &vec![1;len]), Err(ProtocolError::PrivateKeySizeError { got }) if got == len as u32)
);
assert!(
matches!(NativeBlsModule.private_key_to_public_key(&vec![1;len]), Err(ProtocolError::PrivateKeySizeError { got }) if got == len as u32)
);
}
for len in [0, 95, 97] {
assert!(
matches!(NativeBlsModule.verify_signature(&vec![0;len], b"", &public_key), Err(ProtocolError::BlsSignatureSizeError { got }) if got == len as u32)
);
assert!(matches!(
NativeBlsModule.verify_signature(&vec![0; len], b"", &[0; 48]),
Err(ProtocolError::BlsError(_))
));
}
for public in [vec![], vec![0; 47], vec![0; 48], vec![255; 48], vec![0; 49]] {
assert!(NativeBlsModule.validate_public_key(&public).is_err());
assert!(matches!(
NativeBlsModule.verify_signature(&[0; 96], b"", &public),
Err(ProtocolError::BlsError(_))
));
}
for signature in [[0; 96], [255; 96]] {
assert!(!NativeBlsModule
.verify_signature(&signature, b"", &public_key)
.unwrap());
}
assert!(NativeBlsModule.sign(b"", &secret).is_ok());
}

#[test]
fn should_reject_points_outside_the_prime_order_subgroup() {
// On-curve points x=4 in G1 and x=(2,0) in G2, in compressed IETF encoding.
let mut public_key = [0; 48];
public_key[0] = 0x80;
public_key[47] = 4;
let mut signature = [0; 96];
signature[0] = 0xa0;
signature[95] = 2;
assert!(NativeBlsModule.validate_public_key(&public_key).is_err());
assert!(bool::from(
<Bls12381G2Impl as Pairing>::Signature::from_compressed(&signature).is_none()
));
let valid = bytes::<48>(&corpus().basic[0].public_key);
assert!(!NativeBlsModule
.verify_signature(&signature, b"", &valid)
.unwrap());
}

#[test]
fn should_read_and_reemit_frozen_validator_storage() {
let config = bincode::config::standard().with_big_endian();
let vectors = corpus().storage;
assert_eq!(vectors.len(), 3);
for v in vectors {
let encoded = hex::decode(v.validator_bytes).unwrap();
let (validator, consumed): (ValidatorV0, _) =
bincode::decode_from_slice(&encoded, config).unwrap();
assert_eq!(consumed, encoded.len(), "{}", v.name);
assert_eq!(bincode::encode_to_vec(&validator, config).unwrap(), encoded);
assert_eq!(serde_json::to_value(&validator).unwrap(), v.validator);
if let Some(key) = &validator.public_key {
// Quorum storage uses Serde's fixed 48-byte tuple, without a length prefix.
let bytes = bincode::serde::encode_to_vec(key, config).unwrap();
assert_eq!(bytes, key.0.to_compressed());
let decoded =
bincode::serde::decode_from_slice::<PublicKey<Bls12381G2Impl>, _>(&bytes, config)
.unwrap();
assert_eq!(decoded, (*key, 48));
}
assert_eq!(
serde_json::from_value::<ValidatorV0>(v.validator.clone()).unwrap(),
validator
);
assert_eq!(
bincode::decode_from_slice_untrusted::<ValidatorV0, _>(&encoded, config)
.unwrap()
.0,
validator
);
assert_eq!(
bincode::borrow_decode_from_slice_untrusted::<ValidatorV0, _>(&encoded, config)
.unwrap()
.0,
validator
);
let encoded = hex::decode(v.validator_set_bytes).unwrap();
let (set, consumed): (ValidatorSetV0, _) =
bincode::decode_from_slice(&encoded, config).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(bincode::encode_to_vec(&set, config).unwrap(), encoded);
assert_eq!(serde_json::to_value(&set).unwrap(), v.validator_set);
assert_eq!(
serde_json::from_value::<ValidatorSetV0>(v.validator_set).unwrap(),
set
);
}
}
32 changes: 32 additions & 0 deletions packages/rs-dpp/tests/fixtures/bls_compatibility/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# BLS compatibility vectors

These public test keys and fixtures were generated before the backend migration,
from Platform `c0b4b97f8a1a8626c2f51bc8d7baeab9c65e10da` (#5307):

- `dashpay/agora-blsful` at `0c34a7a488a0bd1c9a9a2196e793b303ad35c900`;
- `blstrs_plus` 0.8.18;
- `blst` at `71a00877c75a482e7d44c4e6370c1a7fb82444be` (0.3.12).

`vectors.json` contains 16 Basic signatures (four canonical scalars × empty,
text, zero digest and 256-byte messages), five secure aggregates (one/two/three
signers, reordered keys and a duplicate key), three key-generation cases, four
scalar boundaries, and three validator/validator-set storage cases. Storage uses
`grovedb-bincode` 2.1.0 with `standard().with_big_endian()` and DPP's JSON schema.

The scalar and infinity cases characterize **historical compatibility**, not
recommended validation for a new protocol. DPP accepts nonzero scalars reduced
modulo the group order and permits an identity public key to deserialize; an
identity signature still fails verification. Replacing those rules requires a
separate protocol decision.

The frozen expected values are copied unchanged from #5307. This standalone
suite exercises the backend on `v5.1-dev` without adding the candidate backend
as a dependency. The generator uses the original blsful API.

Reproduce the corpus with the [historical generation script](../../../../../scripts/historical-test-vectors/README.md).
It runs ignored tests on a pinned checkout and writes a separate output directory.

Keep the frozen fixture unchanged when migrating the implementation. These tests
cover the listed cases, not an exhaustive proof of equivalence or every persisted
Platform structure. They do not test legacy signature hashing; the legacy field
is the public-key encoding of the same point.
Loading
Loading