Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions packages/rs-drive-proof-verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ derive_more = { version = "1.0", features = ["from"] }
dpp = { path = "../rs-dpp", features = [
"fixtures-and-mocks",
], default-features = false }
dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [
"platform",
"client",
] }
drive = { path = "../rs-drive", default-features = false, features = [
"verify",
] }
hex = { version = "0.4.3" }
indexmap = { version = "2.6.0" }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0" }
282 changes: 282 additions & 0 deletions packages/rs-drive-proof-verifier/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
//! Loader for the proof-vector regression corpus in `tests/vectors/`.
//!
//! Each case directory carries a `manifest.json` (request parameters, block
//! metadata, expected outcome) plus the raw blobs (`proof.hex`,
//! `signature.hex`, `quorum_pubkey.hex`). The corpus was generated from the
//! Dash Core fixture set (`drive_query_vectors.json` /
//! `quorum_sig_vectors.json`, platform v4.0.0 state, protocol version 12);
//! every grovedb proof commits to the same root hash, which the fixture
//! quorum signed, so positive cases run the full grovedb + tenderdash
//! verification pipeline with real key material.

// Each integration-test binary compiles its own copy of this module and uses
// a different subset of it.
#![allow(dead_code)]

use std::path::PathBuf;
use std::sync::Arc;

use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
use dpp::dashcore::Network;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::TokenConfiguration;
use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
use dpp::version::PlatformVersion;
use drive_proof_verifier::{ContextProvider, ContextProviderError};
use serde::Deserialize;

/// The network the fixture chain id (`dash-testnet-51`) belongs to.
pub const NETWORK: Network = Network::Testnet;

#[derive(Deserialize)]
pub struct Manifest {
pub description: String,
pub request: RequestSpec,
pub block: BlockMeta,
pub proof_meta: ProofMeta,
pub expected: Expected,
#[serde(default)]
pub expected_root_hash_hex: Option<String>,
}

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RequestSpec {
IdentityBalance {
identity_id: String,
},
IdentityNonce {
identity_id: String,
},
IdentityContractNonce {
identity_id: String,
contract_id: String,
},
IdentityKeys {
identity_id: String,
},
DocumentsDpnsExact {
normalized_label: String,
limit: u16,
},
DocumentsDpnsPrefix {
normalized_prefix: String,
limit: u16,
},
DocumentsDashpayProfile {
owner_id: String,
},
DocumentsDashpayContacts {
identity_id: String,
to_identity: bool,
limit: u16,
},
ContestedVoteState {
contract_id: String,
document_type_name: String,
index_name: String,
index_values: Vec<String>,
count: u16,
},
}

#[derive(Deserialize)]
pub struct BlockMeta {
pub height: u64,
pub core_chain_locked_height: u32,
pub epoch: u32,
pub time_ms: u64,
pub protocol_version: u32,
pub chain_id: String,
}

#[derive(Deserialize)]
pub struct ProofMeta {
pub round: u32,
pub quorum_type: u32,
pub quorum_hash_hex: String,
pub block_id_hash_hex: String,
}

#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Expected {
IdentityBalance {
balance: u64,
},
IdentityNonce {
nonce: u64,
},
IdentityContractNonce {
nonce: u64,
},
IdentityKeys {
serialized_keys: Vec<String>,
},
/// The fixture grovedb state stores placeholder payloads at document
/// positions; the grovedb layer must verify and yield exactly these
/// bytes, and decoding them as DPP documents must fail cleanly.
DocumentsPlaceholder {
serialized_documents: Vec<String>,
},
Contested {
contenders: Vec<ExpectedContender>,
abstain_votes: Option<u32>,
lock_votes: Option<u32>,
finished: bool,
winner_identity_id: Option<String>,
},
ContestedAbsent,
Error {
class: ErrorClass,
},
}

#[derive(Deserialize)]
pub struct ExpectedContender {
pub identity_id: String,
pub votes: Option<u32>,
}

#[derive(Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ErrorClass {
InvalidSignature,
ProofInvalid,
}

pub struct Case {
pub name: String,
pub manifest: Manifest,
pub grovedb_proof: Vec<u8>,
pub signature: Vec<u8>,
pub quorum_pubkey: [u8; 48],
}

pub fn hex_vec(s: &str) -> Vec<u8> {
hex::decode(s.trim()).expect("corpus hex blob must decode")
}

pub fn hex32(s: &str) -> [u8; 32] {
hex_vec(s).try_into().expect("expected 32 bytes of hex")
}

pub fn identifier(s: &str) -> Identifier {
Identifier::from_bytes(&hex_vec(s)).expect("corpus identifier must be 32 bytes")
}

pub fn load_case(name: &str) -> Case {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/vectors")
.join(name);
let read = |file: &str| {
std::fs::read_to_string(dir.join(file))
.unwrap_or_else(|e| panic!("read corpus file {name}/{file}: {e}"))
};
let manifest: Manifest =
serde_json::from_str(&read("manifest.json")).expect("parse corpus manifest");
Case {
name: name.to_string(),
grovedb_proof: hex_vec(&read("proof.hex")),
signature: hex_vec(&read("signature.hex")),
quorum_pubkey: hex_vec(&read("quorum_pubkey.hex"))
.try_into()
.expect("quorum public key must be 48 bytes"),
manifest,
}
}

impl Case {
/// The tenderdash proof envelope for the DAPI response.
pub fn grpc_proof(&self) -> Proof {
Proof {
grovedb_proof: self.grovedb_proof.clone(),
quorum_hash: hex_vec(&self.manifest.proof_meta.quorum_hash_hex),
signature: self.signature.clone(),
round: self.manifest.proof_meta.round,
block_id_hash: hex_vec(&self.manifest.proof_meta.block_id_hash_hex),
quorum_type: self.manifest.proof_meta.quorum_type,
}
}

/// The response metadata (block context the quorum signed over).
pub fn metadata(&self) -> ResponseMetadata {
ResponseMetadata {
height: self.manifest.block.height,
core_chain_locked_height: self.manifest.block.core_chain_locked_height,
epoch: self.manifest.block.epoch,
time_ms: self.manifest.block.time_ms,
protocol_version: self.manifest.block.protocol_version,
chain_id: self.manifest.block.chain_id.clone(),
}
}

/// The platform version the vectors were generated with. The proofs are
/// self-contained, so they must keep verifying under this version even
/// as the crate's latest version moves on.
pub fn platform_version(&self) -> &'static PlatformVersion {
PlatformVersion::get(self.manifest.block.protocol_version)
.expect("corpus protocol version must be known")
}

/// A [ContextProvider] serving this case's quorum public key and the
/// system data contracts referenced by the fixture proofs.
pub fn provider(&self) -> VectorContextProvider {
VectorContextProvider {
quorum_type: self.manifest.proof_meta.quorum_type,
quorum_hash: hex32(&self.manifest.proof_meta.quorum_hash_hex),
quorum_pubkey: self.quorum_pubkey,
}
}
}

/// [ContextProvider] backed by the per-case corpus quorum key material.
pub struct VectorContextProvider {
quorum_type: u32,
quorum_hash: [u8; 32],
quorum_pubkey: [u8; 48],
}

impl ContextProvider for VectorContextProvider {
fn get_data_contract(
&self,
id: &Identifier,
platform_version: &PlatformVersion,
) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
for system_contract in [SystemDataContract::DPNS, SystemDataContract::Dashpay] {
let contract = load_system_data_contract(system_contract, platform_version)
.map_err(|e| ContextProviderError::DataContractFailure(e.to_string()))?;
if contract.id() == *id {
return Ok(Some(Arc::new(contract)));
}
}
Ok(None)
}

fn get_token_configuration(
&self,
_token_id: &Identifier,
) -> Result<Option<TokenConfiguration>, ContextProviderError> {
Ok(None)
}

fn get_quorum_public_key(
&self,
quorum_type: u32,
quorum_hash: [u8; 32],
_core_chain_locked_height: u32,
) -> Result<[u8; 48], ContextProviderError> {
if quorum_type != self.quorum_type || quorum_hash != self.quorum_hash {
return Err(ContextProviderError::InvalidQuorum(format!(
"unexpected quorum requested: type {quorum_type}, hash {}",
hex::encode(quorum_hash)
)));
}
Ok(self.quorum_pubkey)
}

fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
Ok(1)
}
}
35 changes: 35 additions & 0 deletions packages/rs-drive-proof-verifier/tests/vectors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Proof-vector regression corpus

Fixture cases generated from a real Drive state (platform v4.0.0 fixtures,
protocol version 12, grovedb 5.0.0), replayed through the crate's public
`FromProof` entry points. The same fixtures are replayed byte-exact by Dash
Core's platform GUI implementation, so drift between what Drive proves and
what any client verifies fails loudly here.

Each case directory contains `manifest.json` (request parameters, block
metadata, expected outcome, pinned root hash) plus `proof.hex`,
`signature.hex`, and `quorum_pubkey.hex`. Loaders live in
`../common/mod.rs`; the suite is gated behind the `mocks` feature.

## Coverage matrix — what each family actually exercises

| family | grovedb proof replay | tenderdash BLS check | notes |
|---|---|---|---|
| identity (4 cases) | ✅ | ✅ | full pipeline through `FromProof` |
| contested vote state (3) | ✅ | ✅ | incl. `Ok(None)` proof-of-absence |
| quorum-sig (4 fixtures / 7 cases) | ✅ | ✅ | 1 positive + 3 negatives on-disk (tampered sig, wrong key, wrong block-id hash) + 3 in-test negatives that tamper the `quorum-sig-valid` response's `ResponseMetadata` (height, time_ms, core_chain_locked_height) to prove those fields are inside the signed `StateId` too |
| documents / DPNS / DashPay (4) | ✅ | ❌ (not reached) | fixture state stores placeholder payloads at document positions, so `FromProof` fails at document decode *before* the signature check; these cases pin the `DriveDocumentQuery` shape (root hash + serialized payloads byte-for-byte via `verify_proof_keep_serialized`) and the clean-`Err` decode failure |
| identity-balance corrupted proof (1) | ✅ (rejects) | ❌ (not reached) | negative: bit-flipped proof fails as a GroveDB error |

The `quorum-sig-valid` case shares its proof bytes with `identity-balance`
(the corpus has 15 distinct fixtures across 16 on-disk cases): all drive
fixtures commit to the same root hash, which is exactly the app hash the
quorum signature signs — that is what lets the positive cases run a genuine
BLS verification with real fixture key material. The 3 metadata-tamper
negatives in `vectors_quorum_sig.rs` reuse `quorum-sig-valid`'s on-disk bytes
and mutate the `ResponseMetadata` in-test rather than adding new fixture
directories, bringing the total to 19 `#[test]` functions over 16 fixture
directories.

Regenerate only deliberately (fixture-generation lives with the Dash Core
platform GUI's vector tooling); a regeneration should be its own commit.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"description": "Absent DPNS name contest ('carol'): proof of non-existence, FromProof returns Ok(None).",
"source": {
"platform_repo_tag": "v4.0.0",
"grovedb_repo_tag": "v5.0.0",
"generated_protocol_version": 12
},
"request": {
"type": "contested_vote_state",
"contract_id": "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155",
"document_type_name": "domain",
"index_name": "parentNameAndLabel",
"index_values": [
"dash",
"carol"
],
"count": 100
},
"block": {
"height": 123456,
"core_chain_locked_height": 2000000,
"epoch": 0,
"time_ms": 1700000000000,
"protocol_version": 12,
"chain_id": "dash-testnet-51"
},
"proof_meta": {
"round": 0,
"quorum_type": 106,
"quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da",
"block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366"
},
"expected": {
"kind": "contested_absent"
},
"expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0100b201039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a02dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1004017000050201016300ade908a28152707d22ed66d575d6f3ce38c27447c295a245272dc100dd0c4a621111010170002a040163000502010170001e5c60ed1b4311b6baf76a42f41550bcbba0f9f87d71b48e76d800c95f22cccc01016300490401700024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155005401be78056200f8a616eddb15fbd41fbc4204330111709350c20535528492b5010170004e0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000a020106646f6d61696e004412968f90b3c569b0e5b9760130f021137946563c0d05c6cb64233e4a35a57f0120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f0406646f6d61696e00050201010100bba90275bf4b39ce726f641378ede6c4fcd17162611f992a34fea2ad303f4eeb0106646f6d61696e002d0401010008020104646173680060b4e46b6e0fca7c9c1634f80d1d3a18a9188165d64e81782800392ba3f08c5901010100310404646173680009020105616c6963650078f7ad26be337eb5f110a07a8d21736265aef4cb370c25b3aaa3f8d354ee191101046461736800470246f1e3eb733288ba8ee508b3b972872149b7ee87c6b0c05bbd4bb5982d93e7140503626f62818e82b602d0fff0c045f136090f3816039b90af1ac6f33f3ec84deba88908d61100
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319
Loading
Loading