Phase 2.1 PROVENANCE pallet — 9 integration tests E2E via FRAME mock runtime
composition (`construct_runtime!` macro + derive_impl + sp_io::TestExternalities).
Cierra honest disclosure sesión 2 ("real chain runtime aún NO arranca"). Pallet
end-to-end validado canonical sin esperar Slice 2e WASM resolution structural.
## 9 integration tests canonical PROV-3 flow
5 scenarios:
- scenario_1_passed_info_anchored_canonical (PROV-2/3/7) ✅
- scenario_2_warning_anchored_canonical (PROV-2/3/6/7) ✅
- scenario_3_failed_critical_rejected_canonical (PROV-6) ❌ NotAnchorable
- scenario_4_passed_block_security_rejected_canonical (PROV-6) ❌ NotAnchorable
- scenario_5_anonymous_level_0_rejected_canonical (PROV-5) ❌ ValidatorTierInsufficient
4 additional:
- additional_idempotency_already_attested_rejected_canonical (PROV-2) ❌ AlreadyAttested
- additional_tampered_signature_rejected_canonical (PROV-4) ❌ InvalidSignature
- additional_tampered_payload_rejected_canonical (PROV-4) ❌ InvalidSignature
- additional_6_step_prov_3_walkthrough_canonical (PROV-3 full) ✅ Pre/post assertions
## Own fixtures canonical
Per audit canonical sesión 3+ (docs/L7_roadmaps/AUDIT_CHARON_SESION_3_STATE_V1.md),
Ergon E2E fixture diary claim aspirational sin shipped commit verified — filesystem
+ git history confirma no source file canonical. Charon construye own fixtures
basados en vendor copy ValidationAttestation real existente. Pattern análogo
Slice 3 mock-WASM + v0.0.16 Ed25519 interim encoding.
## Files canonical
- NEW: crates/origin-chain-provenance/src/mock.rs (75 LOC mock runtime)
- NEW: crates/origin-chain-provenance/src/tests_integration.rs (270 LOC, 9 tests)
- EDIT: crates/origin-chain-provenance/Cargo.toml (sp-core+sp-io+sp-runtime dev-deps)
- EDIT: crates/origin-chain-provenance/src/lib.rs (mod mock + mod tests_integration + docstring v0.0.18)
- EDIT: CHANGELOG.md (v0.0.18 entry canonical pattern Phase 2.1)
## Verification
- cargo test -p origin-chain-provenance — 17 tests verde, 0 failures
(9 integration + 6 standalone Ed25519 + 2 auto-generated)
- Workspace tests no regressions
- Cero warnings críticos
🎉 Phase 2.1 PROVENANCE pallet validated E2E via mock runtime canonical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
311 lines
12 KiB
Rust
311 lines
12 KiB
Rust
//! # Integration tests canonical v0.0.18 — Mock runtime E2E
|
|
//!
|
|
//! 5 scenarios canonical PROV-3 attest_asset flow end-to-end via mock runtime:
|
|
//!
|
|
//! | Scenario | Verdict | Severity | OIT Level | Expected outcome |
|
|
//! |----------|---------|----------|-----------|------------------|
|
|
//! | passed_info_anchored | Passed | Info | 3 | ✅ AssetAttested event + storage insert |
|
|
//! | warning_anchored | Warning | Warning | 3 | ✅ AssetAttested event + storage insert |
|
|
//! | failed_critical_rejected | Failed | Critical | 3 | ❌ NotAnchorable error |
|
|
//! | passed_block_security_rejected | Passed | Block | 3 | ❌ NotAnchorable error |
|
|
//! | anonymous_level_0_rejected | Passed | Info | 0 | ❌ ValidatorTierInsufficient error |
|
|
//!
|
|
//! Plus 4 additional canonical:
|
|
//! - idempotency: already attested asset rejected con `AlreadyAttested` error
|
|
//! - tampered_signature: Ed25519 verify fails con `InvalidSignature` error
|
|
//! - tampered_payload: signing_payload mismatch con `InvalidSignature` error
|
|
//! - 6_step_walkthrough: explicit PROV-3 flow assertions in sequence
|
|
//!
|
|
//! Total: 9 integration tests canonical.
|
|
//!
|
|
//! ## Own fixtures pattern
|
|
//!
|
|
//! Per audit canonical sesión 3+ (`AUDIT_CHARON_SESION_3_STATE_V1.md`), Ergon
|
|
//! E2E fixture diary claim aspirational sin shipped commit verified.
|
|
//! Charon construye own fixtures basados en vendor copy `ValidationAttestation`
|
|
//! real existente en `origin-chain-types/src/attestation.rs`. Cuando Ergon
|
|
//! ship `arkhe-noesis-attestation-types` sub-crate Cap 42+, fixtures can swap.
|
|
|
|
#![cfg(test)]
|
|
|
|
use crate::mock::{alice_origin, bob_origin, new_test_ext, Provenance, RuntimeEvent, System, Test, TEST_SIGNING_SEED};
|
|
use crate::{Attestations, Error, Event};
|
|
use ed25519_dalek::{Signer, SigningKey};
|
|
use frame_support::{assert_noop, assert_ok};
|
|
use origin_chain_types::{
|
|
AssetHash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict,
|
|
};
|
|
|
|
// ─── Fixture builders canonical ──────────────────────────────────────
|
|
|
|
/// Build canonical signed ValidationAttestation con (verdict, severity, oit_level)
|
|
/// parameters. Deterministic Ed25519 signature.
|
|
///
|
|
/// asset_id es derivado de seed para uniqueness per scenario.
|
|
fn build_signed_attestation(
|
|
asset_seed: u8,
|
|
verdict: ValidationVerdict,
|
|
severity: SeverityTier,
|
|
oit_level: u8,
|
|
) -> ValidationAttestation {
|
|
let signing_key = SigningKey::from_bytes(&TEST_SIGNING_SEED);
|
|
let verifying_key = signing_key.verifying_key();
|
|
|
|
let mut att = ValidationAttestation {
|
|
schema_version: *b"1.0.0 ",
|
|
asset_id: AssetHash::new([asset_seed; 32]),
|
|
validator_oit: OitIdentity {
|
|
public_key: verifying_key.to_bytes(),
|
|
level: oit_level,
|
|
},
|
|
validated_at_us: 1_234_567_890,
|
|
verdict,
|
|
severity,
|
|
constraints_checked: 42,
|
|
signature: [0u8; 64], // placeholder, replaced post sign
|
|
};
|
|
|
|
let payload = att.signing_payload();
|
|
let signature = signing_key.sign(&payload);
|
|
att.signature = signature.to_bytes();
|
|
att
|
|
}
|
|
|
|
/// Build canonical fixture passed_info (most common anchorable scenario).
|
|
fn fixture_passed_info(asset_seed: u8) -> ValidationAttestation {
|
|
build_signed_attestation(asset_seed, ValidationVerdict::Passed, SeverityTier::Info, 3)
|
|
}
|
|
|
|
// ─── Scenario 1: passed_info anchored canonical ──────────────────────
|
|
|
|
#[test]
|
|
fn scenario_1_passed_info_anchored_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
let attestation = fixture_passed_info(1);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
// PROV-3 attest_asset extrinsic canonical
|
|
assert_ok!(Provenance::attest_asset(alice_origin(), attestation.clone()));
|
|
|
|
// PROV-2 storage insert verified
|
|
let stored = Attestations::<Test>::get(asset_hash);
|
|
assert!(stored.is_some(), "PROV-2 canonical: AssetAttestation inserted in StorageMap");
|
|
let stored_unwrap = stored.unwrap();
|
|
assert_eq!(stored_unwrap.hash, asset_hash);
|
|
assert_eq!(stored_unwrap.signature.as_bytes(), &attestation.signature);
|
|
|
|
// PROV-7 event emitted canonical
|
|
System::assert_last_event(RuntimeEvent::Provenance(Event::AssetAttested {
|
|
asset_hash,
|
|
block_number: 1,
|
|
}));
|
|
});
|
|
}
|
|
|
|
// ─── Scenario 2: warning anchored canonical (non-blocking) ────────────
|
|
|
|
#[test]
|
|
fn scenario_2_warning_anchored_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
let attestation = build_signed_attestation(
|
|
2,
|
|
ValidationVerdict::Warning,
|
|
SeverityTier::Warning,
|
|
3,
|
|
);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
assert_ok!(Provenance::attest_asset(alice_origin(), attestation));
|
|
|
|
assert!(Attestations::<Test>::get(asset_hash).is_some());
|
|
System::assert_last_event(RuntimeEvent::Provenance(Event::AssetAttested {
|
|
asset_hash,
|
|
block_number: 1,
|
|
}));
|
|
});
|
|
}
|
|
|
|
// ─── Scenario 3: failed_critical rejected canonical (PROV-6) ──────────
|
|
|
|
#[test]
|
|
fn scenario_3_failed_critical_rejected_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
let attestation = build_signed_attestation(
|
|
3,
|
|
ValidationVerdict::Failed,
|
|
SeverityTier::Critical,
|
|
3,
|
|
);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
// PROV-6 is_anchorable canonical check fails (Failed verdict + Critical severity)
|
|
assert_noop!(
|
|
Provenance::attest_asset(alice_origin(), attestation),
|
|
Error::<Test>::NotAnchorable
|
|
);
|
|
|
|
// Storage NOT inserted
|
|
assert!(Attestations::<Test>::get(asset_hash).is_none());
|
|
});
|
|
}
|
|
|
|
// ─── Scenario 4: passed_block security rejected canonical (PROV-6) ────
|
|
|
|
#[test]
|
|
fn scenario_4_passed_block_security_rejected_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
// Passed verdict pero Block severity (security violation override)
|
|
let attestation = build_signed_attestation(
|
|
4,
|
|
ValidationVerdict::Passed,
|
|
SeverityTier::Block,
|
|
3,
|
|
);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
// PROV-6 is_anchorable rejects Block severity even with Passed verdict
|
|
assert_noop!(
|
|
Provenance::attest_asset(alice_origin(), attestation),
|
|
Error::<Test>::NotAnchorable
|
|
);
|
|
|
|
assert!(Attestations::<Test>::get(asset_hash).is_none());
|
|
});
|
|
}
|
|
|
|
// ─── Scenario 5: anonymous_level_0 rejected canonical (PROV-5) ────────
|
|
|
|
#[test]
|
|
fn scenario_5_anonymous_level_0_rejected_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
// Valid verdict + severity, pero OIT level 0 (anonymous) — PROV-5 reject
|
|
let attestation = build_signed_attestation(
|
|
5,
|
|
ValidationVerdict::Passed,
|
|
SeverityTier::Info,
|
|
0, // ANONYMOUS — institutional level >= 3 required
|
|
);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
// PROV-5 validator OIT tier check canonical fails
|
|
assert_noop!(
|
|
Provenance::attest_asset(alice_origin(), attestation),
|
|
Error::<Test>::ValidatorTierInsufficient
|
|
);
|
|
|
|
assert!(Attestations::<Test>::get(asset_hash).is_none());
|
|
});
|
|
}
|
|
|
|
// ─── Additional canonical: idempotency ────────────────────────────────
|
|
|
|
#[test]
|
|
fn additional_idempotency_already_attested_rejected_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
let attestation = fixture_passed_info(6);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
// First attest_asset succeeds canonical
|
|
assert_ok!(Provenance::attest_asset(alice_origin(), attestation.clone()));
|
|
assert!(Attestations::<Test>::get(asset_hash).is_some());
|
|
|
|
// Second attest_asset (same hash) — idempotency canonical rejects
|
|
assert_noop!(
|
|
Provenance::attest_asset(bob_origin(), attestation),
|
|
Error::<Test>::AlreadyAttested
|
|
);
|
|
});
|
|
}
|
|
|
|
// ─── Additional canonical: tampered signature (PROV-4) ────────────────
|
|
|
|
#[test]
|
|
fn additional_tampered_signature_rejected_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
let mut attestation = fixture_passed_info(7);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
// Flip bit en signature canonical post-sign
|
|
attestation.signature[0] ^= 0xFF;
|
|
|
|
// PROV-4 Ed25519 verify canonical fails
|
|
assert_noop!(
|
|
Provenance::attest_asset(alice_origin(), attestation),
|
|
Error::<Test>::InvalidSignature
|
|
);
|
|
|
|
assert!(Attestations::<Test>::get(asset_hash).is_none());
|
|
});
|
|
}
|
|
|
|
// ─── Additional canonical: tampered payload (PROV-4) ──────────────────
|
|
|
|
#[test]
|
|
fn additional_tampered_payload_rejected_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
let mut attestation = fixture_passed_info(8);
|
|
let original_asset_hash = attestation.asset_id;
|
|
|
|
// Tamper asset_id post-sign — cambia signing_payload pero mantiene signature original
|
|
attestation.asset_id = AssetHash::new([99u8; 32]);
|
|
let new_asset_hash = attestation.asset_id;
|
|
|
|
// PROV-4 signing_payload mismatch canonical, Ed25519 verify fails
|
|
assert_noop!(
|
|
Provenance::attest_asset(alice_origin(), attestation),
|
|
Error::<Test>::InvalidSignature
|
|
);
|
|
|
|
// Ninguno de los dos hashes está en storage
|
|
assert!(Attestations::<Test>::get(original_asset_hash).is_none());
|
|
assert!(Attestations::<Test>::get(new_asset_hash).is_none());
|
|
});
|
|
}
|
|
|
|
// ─── Additional canonical: 6-step PROV-3 walkthrough explicit ─────────
|
|
|
|
#[test]
|
|
fn additional_6_step_prov_3_walkthrough_canonical() {
|
|
new_test_ext().execute_with(|| {
|
|
let attestation = fixture_passed_info(9);
|
|
let asset_hash = attestation.asset_id;
|
|
|
|
// Pre-flight assertions canonical
|
|
assert!(
|
|
attestation.is_anchorable(),
|
|
"PRE-flight PROV-6: fixture anchorable canonical"
|
|
);
|
|
assert!(
|
|
attestation.validator_oit.is_institutional(),
|
|
"PRE-flight PROV-5: validator institutional canonical"
|
|
);
|
|
assert!(
|
|
Attestations::<Test>::get(asset_hash).is_none(),
|
|
"PRE-flight PROV-2: storage empty para asset_hash canonical"
|
|
);
|
|
|
|
// Step 1: PROV-4 canonical Ed25519 signature verification (implicit via dispatchable)
|
|
// Step 2: PROV-5 canonical validate validator_oit.level >= 3 institutional
|
|
// Step 3: PROV-6 canonical check is_anchorable (verdict + severity)
|
|
// Step 4: PROV-2 canonical convert ValidationAttestation → AssetAttestation
|
|
// Step 5: PROV-2 canonical StorageMap insert
|
|
// Step 6: PROV-7 canonical emit AssetAttested event
|
|
assert_ok!(Provenance::attest_asset(alice_origin(), attestation.clone()));
|
|
|
|
// Post-flight assertions canonical
|
|
let stored = Attestations::<Test>::get(asset_hash)
|
|
.expect("POST-flight PROV-2: AssetAttestation persisted canonical");
|
|
assert_eq!(stored.hash, asset_hash, "POST-flight: hash preserved");
|
|
assert_eq!(
|
|
stored.signature.as_bytes(),
|
|
&attestation.signature,
|
|
"POST-flight: signature preserved canonical"
|
|
);
|
|
|
|
System::assert_last_event(RuntimeEvent::Provenance(Event::AssetAttested {
|
|
asset_hash,
|
|
block_number: 1,
|
|
}));
|
|
});
|
|
}
|