feat(provenance): FIX-4 — verificador attestation on-chain (cierra V3-C1 + squat)
El verificador attestation queda VIVO: una firma producida por el código REAL de Ergon (arkhe-noesis-core, postcard + mensaje domain-separado) verifica byte-fiel en el pallet Substrate. Cierra el FIX-DEFERRED del roadmap (helper de Ergon ya en árbol) + el replay cross-chain V3-C1. origin-chain-types/attestation_verify.rs (nuevo): - ATTESTATION_DOMAIN_TAG + attestation_signed_message: espejo BYTE-IDÉNTICO del oráculo (DC 076d15cc ASP-3). Mensaje = TAG ‖ genesis_hash(32) ‖ payload. - decode_signing_payload: lector postcard SECUENCIAL total y estricto del SigningPayload de Ergon (sin dep nueva en el runtime wasm frágil). Lectores varint u64/u32 con los bounds exactos de postcard (cierra el LOW de fidelidad flagged: verdict/severity/constraints son u32, máx 5 bytes / 5º ≤ 15). Cotas DoS en strings. NUNCA re-serializa (contrato ASP-3). pallet-origin-provenance: - attest_asset(signing_payload: BoundedVec<u8,512>, signature: [u8;64]): recibe los bytes postcard OPACOS + la firma; decodifica, verifica Ed25519 vía sp_io::crypto::ed25519_verify (host function, no bloatea wasm, precedente FIX-3) sobre el mensaje domain-separado con el genesis_hash de ESTA cadena. La pubkey se decodifica del payload (no param aparte). creator = el validador, anchored_at_block real (cerró los placeholders del scaffold). - Dedup-by-BLAKE3 con rechazo EXPLÍCITO (asset_id = BLAKE3 content hash, dentro del payload firmado → replay del mismo blob reusa el asset_id → AlreadyAttested). Hallazgos de la verificación adversarial (workflow ultracode, 3 lentes) cerrados de RAÍZ: - HIGH asset-id squat / provenance poisoning + MEDIUM tier-forgery: el gate era self-attested (level del payload) → cualquiera con una clave Ed25519 anclaba basura institucional bajo el hash PÚBLICO de un asset legítimo y lo envenenaba de forma inmutable. FIX: registro on-chain AuthorizedValidators (pubkey → nivel OIT) gobernado por Root (register_validator/revoke_validator); la membresía es la VERDAD, no el payload. Mismo patrón que TierProfiles de FIX-6 Stage 2a (legitimidad on-chain, no caller-supplied). Sunset a attestation-driven cuando aterrice el registro OIT de Mathi. - MEDIUM método muerto: removido el ValidationAttestation::signing_payload() SCALE divergente (producía bytes que jamás verificarían — footgun). Decisiones (autoridad Pilar 4): sp_io ed25519_verify (consistente FIX-3, estándar Substrate) · dedup-by-asset_id subsume replay exacto · golden vectors generados desde el oráculo REAL de Ergon (seed [7u8;32], genesis 0xAB) = prueba cross-runtime. PENDIENTE (gap legítimo, no bug): OriginChainAttestationSink (impl del trait ValidationAttestationSink de Ergon) es código cliente off-chain — vive en un crate bridge del monorepo (subxt), NO en el chain repo (dependency direction submódulo→monorepo prohibida). Se cablea con la integración Noesis→chain. Tests: 19 provenance (golden vectors cross-runtime + squat cerrado + revoke + anti-replay cross/intra-chain + tamper + malformed) + 72 types (decoder fuzz- equivalente a postcard + fidelidad u32) + 21 suites workspace verde. cargo +1.88. WASM: build VPS-gated (UnknownOpcode 252 ambiental, ver v0.0.58). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3364994cce
commit
ce18e191d0
7 files changed
+769
-475
No files matched your search
@@ -20,21 +20,26 @@ frame-system = { workspace = true }
|
|||||||
# Runtime API canonical (v0.0.17 PROV-8 — verify_provenance read-only RPC query)
|
# Runtime API canonical (v0.0.17 PROV-8 — verify_provenance read-only RPC query)
|
||||||
sp-api = { workspace = true }
|
sp-api = { workspace = true }
|
||||||
|
|
||||||
# Crypto canonical
|
# Crypto canonical. FIX-4: sp-io host function ed25519_verify (no bloatea wasm,
|
||||||
|
# precedente FIX-3 agent-identity) + blake3 para dedup. ed25519-dalek se mantiene
|
||||||
|
# solo para los fixtures de test (dev-deps) — el runtime usa sp_io.
|
||||||
blake3 = { workspace = true }
|
blake3 = { workspace = true }
|
||||||
ed25519-dalek = { workspace = true }
|
|
||||||
|
# FIX-4 (V3-C1): verificacion Ed25519 on-chain via host function + genesis read.
|
||||||
|
sp-core = { workspace = true }
|
||||||
|
sp-io = { workspace = true }
|
||||||
|
|
||||||
# SCALE codec canonical
|
# SCALE codec canonical
|
||||||
parity-scale-codec = { workspace = true }
|
parity-scale-codec = { workspace = true }
|
||||||
scale-info = { workspace = true }
|
scale-info = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Mock runtime canonical para integration tests v0.0.18 — PROV-3 E2E flow.
|
# Mock runtime canonical para integration tests — PROV-3 E2E flow + golden
|
||||||
# Per pallet Substrate convention: sp-io TestExternalities + sp-runtime
|
# vectors ASP-3. ed25519-dalek solo aqui (fixtures); el runtime usa sp_io.
|
||||||
# AccountId32 + frame_support construct_runtime macro.
|
|
||||||
sp-core = { workspace = true, default-features = true }
|
sp-core = { workspace = true, default-features = true }
|
||||||
sp-io = { workspace = true, default-features = true }
|
sp-io = { workspace = true, default-features = true }
|
||||||
sp-runtime = { workspace = true, default-features = true }
|
sp-runtime = { workspace = true, default-features = true }
|
||||||
|
ed25519-dalek = { workspace = true, default-features = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
@@ -43,6 +48,8 @@ std = [
|
|||||||
"frame-support/std",
|
"frame-support/std",
|
||||||
"frame-system/std",
|
"frame-system/std",
|
||||||
"sp-api/std",
|
"sp-api/std",
|
||||||
|
"sp-core/std",
|
||||||
|
"sp-io/std",
|
||||||
"parity-scale-codec/std",
|
"parity-scale-codec/std",
|
||||||
"scale-info/std",
|
"scale-info/std",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -103,7 +103,14 @@ sp_api::decl_runtime_apis! {
|
|||||||
pub mod pallet {
|
pub mod pallet {
|
||||||
use frame_support::pallet_prelude::*;
|
use frame_support::pallet_prelude::*;
|
||||||
use frame_system::pallet_prelude::*;
|
use frame_system::pallet_prelude::*;
|
||||||
use origin_chain_types::{AssetAttestation, AssetHash, ValidationAttestation};
|
use frame_support::sp_runtime::traits::{SaturatedConversion, Zero};
|
||||||
|
use origin_chain_types::{
|
||||||
|
attestation_signed_message, decode_signing_payload, AssetAttestation, AssetHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Cota DoS del `signing_payload` postcard recibido (el real ~115 bytes;
|
||||||
|
/// 512 es holgado para schema/identifier futuros sin permitir flooding).
|
||||||
|
pub type MaxPayloadLen = ConstU32<512>;
|
||||||
|
|
||||||
/// Pallet configuration trait canonical.
|
/// Pallet configuration trait canonical.
|
||||||
///
|
///
|
||||||
@@ -140,6 +147,25 @@ pub mod pallet {
|
|||||||
pub type Attestations<T: Config> =
|
pub type Attestations<T: Config> =
|
||||||
StorageMap<_, Blake2_128Concat, AssetHash, AssetAttestation, OptionQuery>;
|
StorageMap<_, Blake2_128Concat, AssetHash, AssetAttestation, OptionQuery>;
|
||||||
|
|
||||||
|
/// FIX-4 (cierra el HIGH "asset-id squat"): registro on-chain de validadores
|
||||||
|
/// Noesis autorizados → nivel OIT asignado por governance. **La membresía es
|
||||||
|
/// la VERDAD**, no el `level` que el firmante pone en su propio payload
|
||||||
|
/// (que era self-attested → cualquiera con una clave Ed25519 podía anclar
|
||||||
|
/// basura institucional bajo el hash público de un asset legítimo y
|
||||||
|
/// envenenarlo de forma inmutable). Ahora solo una pubkey REGISTRADA puede
|
||||||
|
/// anclar; un validador registrado que ancle basura es revocable y auditable
|
||||||
|
/// (problema de governance, no ataque anónimo). Mismo patrón que
|
||||||
|
/// `TierProfiles` de FIX-6 Stage 2a: legitimidad on-chain, no caller-supplied.
|
||||||
|
///
|
||||||
|
/// **Root-only interim** (sudo Index 12 / governance). Sunset: cuando el
|
||||||
|
/// registro OIT real de Mathi (PerceptionAdapter + DID binding) aterrice,
|
||||||
|
/// migra a auto-registro attestation-driven. Cross-link futuro: AgentIdentity
|
||||||
|
/// (Index 9).
|
||||||
|
#[pallet::storage]
|
||||||
|
#[pallet::getter(fn authorized_validator_level)]
|
||||||
|
pub type AuthorizedValidators<T: Config> =
|
||||||
|
StorageMap<_, Blake2_128Concat, [u8; 32], u8, OptionQuery>;
|
||||||
|
|
||||||
/// PROV-7 canonical: Events emisión canonical.
|
/// PROV-7 canonical: Events emisión canonical.
|
||||||
#[pallet::event]
|
#[pallet::event]
|
||||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||||
@@ -158,6 +184,19 @@ pub mod pallet {
|
|||||||
/// Reason canonical de rejection per RejectionReason enum.
|
/// Reason canonical de rejection per RejectionReason enum.
|
||||||
reason: RejectionReason,
|
reason: RejectionReason,
|
||||||
},
|
},
|
||||||
|
/// FIX-4: validador Noesis autorizado (Root) con su nivel OIT. Auditable
|
||||||
|
/// on-chain: quién quedó habilitado para anclar y a qué nivel.
|
||||||
|
ValidatorRegistered {
|
||||||
|
/// Ed25519 public key del validador autorizado.
|
||||||
|
public_key: [u8; 32],
|
||||||
|
/// Nivel OIT asignado por governance.
|
||||||
|
level: u8,
|
||||||
|
},
|
||||||
|
/// FIX-4: validador revocado (Root) — ya no puede anclar.
|
||||||
|
ValidatorRevoked {
|
||||||
|
/// Ed25519 public key del validador revocado.
|
||||||
|
public_key: [u8; 32],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rejection reasons canonical PROV-7.
|
/// Rejection reasons canonical PROV-7.
|
||||||
@@ -204,102 +243,138 @@ pub mod pallet {
|
|||||||
AlreadyAttested,
|
AlreadyAttested,
|
||||||
/// Marihan refinement Phase 2.2 noted canonical.
|
/// Marihan refinement Phase 2.2 noted canonical.
|
||||||
MalformedValidationAttestation,
|
MalformedValidationAttestation,
|
||||||
|
/// FIX-4: la pubkey del firmante NO está en el registro on-chain de
|
||||||
|
/// validadores autorizados (cierra el squat — solo validadores
|
||||||
|
/// registrados por governance pueden anclar).
|
||||||
|
ValidatorNotAuthorized,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatchables canonical (PROV-3 canonical: attest_asset).
|
/// Dispatchables (PROV-3: attest_asset).
|
||||||
#[pallet::call]
|
#[pallet::call]
|
||||||
impl<T: Config> Pallet<T> {
|
impl<T: Config> Pallet<T> {
|
||||||
/// PROV-3 canonical: `attest_asset` extrinsic canonical.
|
/// PROV-3 + FIX-4: ancla una attestation Noesis verificada on-chain.
|
||||||
///
|
///
|
||||||
/// Submits a `ValidationAttestation` (Ergon Slice 7.0r struct) para
|
/// El validador firma OFF-CHAIN (con el helper de Ergon
|
||||||
/// anchoring canonical on-chain. Flow canonical 6-step per DC PROV-3:
|
/// `attestation_signed_message`) sobre `TAG ‖ genesis_hash ‖ payload`,
|
||||||
|
/// donde `payload` es el `SigningPayload` postcard (Slice 7.0r). El
|
||||||
|
/// extrinsic recibe esos **bytes postcard opacos** + la firma; el pallet
|
||||||
|
/// re-deriva el mensaje y verifica — NUNCA re-serializa (contrato ASP-3
|
||||||
|
/// DC `076d15cc`). El `public_key` se DECODIFICA del payload (no es un
|
||||||
|
/// parámetro aparte: la firma prueba que esa clave firmó esta attestation
|
||||||
|
/// para esta cadena).
|
||||||
///
|
///
|
||||||
/// 1. PROV-4 canonical: Ed25519 signature verification canonical
|
/// Flow (gates baratos antes del crypto caro, patrón FIX-3):
|
||||||
/// 2. PROV-5 canonical: validate `validator_oit.level >= 3` institutional
|
/// 1. `ensure_signed` — el submitter (relayer/pagador del fee).
|
||||||
/// 3. PROV-6 canonical: check `is_anchorable()` (verdict + severity)
|
/// 2. Decode postcard → (asset_id, public_key, level, verdict, severity).
|
||||||
/// 4. PROV-2 canonical: convert ValidationAttestation → AssetAttestation
|
/// 3. **Dedup-by-BLAKE3 con RECHAZO EXPLÍCITO**: la storage key ES el
|
||||||
/// 5. PROV-2 canonical: insert StorageMap canonical
|
/// `asset_id` (= BLAKE3 content hash del asset); un replay del mismo
|
||||||
/// 6. PROV-7 canonical: emit AssetAttested event canonical
|
/// attestation firmado reusa el mismo asset_id → `AlreadyAttested`.
|
||||||
///
|
/// (Decisión Pilar 4 sesión 6+: replay = rechazo explícito, no dedup
|
||||||
/// **SCAFFOLD v0.0.14 canonical**: real impl deferred post Ergon Cap 42+
|
/// silencioso.) Va ANTES del verify para no gastar crypto en replays.
|
||||||
/// sub-crate ship + Ed25519 verification wire-up. Current canonical solo
|
/// 4. FIX-4: gate de legitimidad ON-CHAIN — la pubkey del firmante debe
|
||||||
/// validates basic state (origin signed + not already attested) + emite
|
/// estar en `AuthorizedValidators` (registro governance) con nivel
|
||||||
/// placeholder event canonical.
|
/// >= 3. NO se confía en el `level` self-attested del payload (cierra
|
||||||
|
/// el squat + tier-forgery).
|
||||||
|
/// 5. PROV-6: gate anchorable (verdict Passed|Warning, severity Info|Warning).
|
||||||
|
/// 6. FIX-4/V3-C1: Ed25519 verify via `sp_io::crypto::ed25519_verify`
|
||||||
|
/// (host function, no bloatea wasm) sobre el mensaje domain-separado
|
||||||
|
/// con el `genesis_hash` de ESTA cadena (cierra replay cross-chain).
|
||||||
|
/// 7. Insert `AssetAttestation` (creator = validador, block real) + evento.
|
||||||
#[pallet::call_index(0)]
|
#[pallet::call_index(0)]
|
||||||
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
#[pallet::weight(Weight::from_parts(20_000, 0))]
|
||||||
pub fn attest_asset(
|
pub fn attest_asset(
|
||||||
origin: OriginFor<T>,
|
origin: OriginFor<T>,
|
||||||
validation_attestation: ValidationAttestation,
|
signing_payload: BoundedVec<u8, MaxPayloadLen>,
|
||||||
|
signature: [u8; 64],
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
// Verify origin canonical signed.
|
// 1. Origin signed (submitter — paga el fee; el creator semántico es
|
||||||
let _signer = ensure_signed(origin)?;
|
// el validador cuya clave está dentro del payload firmado).
|
||||||
|
let _submitter = ensure_signed(origin)?;
|
||||||
|
|
||||||
let asset_hash = validation_attestation.asset_id;
|
// 2. Decode postcard del SigningPayload real de Ergon (total, DoS-safe).
|
||||||
|
let decoded = decode_signing_payload(&signing_payload)
|
||||||
|
.map_err(|_| Error::<T>::MalformedValidationAttestation)?;
|
||||||
|
let asset_hash = AssetHash::new(decoded.asset_id);
|
||||||
|
|
||||||
// PROV-2 idempotency check canonical.
|
// 3. Dedup-by-BLAKE3 (asset_id) — rechazo explícito de replay, antes
|
||||||
|
// del crypto (FIX-3 discipline).
|
||||||
ensure!(
|
ensure!(
|
||||||
!Attestations::<T>::contains_key(asset_hash),
|
!Attestations::<T>::contains_key(asset_hash),
|
||||||
Error::<T>::AlreadyAttested
|
Error::<T>::AlreadyAttested
|
||||||
);
|
);
|
||||||
|
|
||||||
// PROV-5 OIT level >= 3 enforcement canonical.
|
// 4. FIX-4: legitimidad ON-CHAIN (cierra el squat + tier-forgery).
|
||||||
|
// La pubkey del firmante DEBE estar en el registro de validadores
|
||||||
|
// autorizados; el nivel es el del REGISTRO (governance), NO el
|
||||||
|
// `level` self-attested del payload. Sin esto, cualquiera con una
|
||||||
|
// clave Ed25519 anclaba basura institucional bajo el hash público
|
||||||
|
// de un asset legítimo.
|
||||||
|
let authorized_level = AuthorizedValidators::<T>::get(decoded.public_key)
|
||||||
|
.ok_or(Error::<T>::ValidatorNotAuthorized)?;
|
||||||
|
ensure!(authorized_level >= 3, Error::<T>::ValidatorTierInsufficient);
|
||||||
|
|
||||||
|
// 5. PROV-6: anchorable (Inmutable #26 — una vez anclado es inmutable).
|
||||||
|
ensure!(decoded.is_anchorable(), Error::<T>::NotAnchorable);
|
||||||
|
|
||||||
|
// 6. FIX-4 (V3-C1): re-derivar el mensaje domain-separado con el
|
||||||
|
// genesis_hash de ESTA cadena y verificar Ed25519 sobre los BYTES
|
||||||
|
// recibidos (nunca re-serializar). Host function = no wasm bloat.
|
||||||
|
let genesis_full = <frame_system::Pallet<T>>::block_hash(BlockNumberFor::<T>::zero());
|
||||||
|
let genesis_ref: &[u8] = genesis_full.as_ref();
|
||||||
|
let mut genesis_hash = [0u8; 32];
|
||||||
|
let n = genesis_ref.len().min(32);
|
||||||
|
genesis_hash[..n].copy_from_slice(&genesis_ref[..n]);
|
||||||
|
|
||||||
|
let message = attestation_signed_message(&signing_payload, &genesis_hash);
|
||||||
|
let sig = sp_core::ed25519::Signature::from_raw(signature);
|
||||||
|
let pubkey = sp_core::ed25519::Public::from_raw(decoded.public_key);
|
||||||
ensure!(
|
ensure!(
|
||||||
validation_attestation.validator_oit.is_institutional(),
|
sp_io::crypto::ed25519_verify(&sig, &message, &pubkey),
|
||||||
Error::<T>::ValidatorTierInsufficient
|
Error::<T>::InvalidSignature
|
||||||
);
|
);
|
||||||
|
|
||||||
// PROV-6 is_anchorable check canonical.
|
// 7. Anclar: creator = el validador (su Ed25519 pubkey), block real.
|
||||||
ensure!(
|
let block_number = <frame_system::Pallet<T>>::block_number();
|
||||||
validation_attestation.is_anchorable(),
|
|
||||||
Error::<T>::NotAnchorable
|
|
||||||
);
|
|
||||||
|
|
||||||
// PROV-4 canonical: Real Ed25519 verification via ed25519-dalek (v0.0.16).
|
|
||||||
//
|
|
||||||
// Pattern canonical:
|
|
||||||
// 1. Reconstruct VerifyingKey desde validator_oit.public_key (32 bytes)
|
|
||||||
// 2. Reconstruct Signature desde attestation.signature (64 bytes)
|
|
||||||
// 3. Compute signing_payload canonical (SCALE encoding interim Option B)
|
|
||||||
// 4. verify_strict canonical (rejects malleable signatures + zero-keys)
|
|
||||||
//
|
|
||||||
// Cuando Ergon arkhe-noesis-attestation-types ship Cap 42+, swap canonical
|
|
||||||
// a postcard encoding canonical sin breaking pallet logic — verify call site
|
|
||||||
// remains identical canonical.
|
|
||||||
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(
|
|
||||||
&validation_attestation.validator_oit.public_key,
|
|
||||||
)
|
|
||||||
.map_err(|_| Error::<T>::InvalidSignature)?;
|
|
||||||
|
|
||||||
let signature = ed25519_dalek::Signature::from_bytes(
|
|
||||||
&validation_attestation.signature,
|
|
||||||
);
|
|
||||||
|
|
||||||
let payload = validation_attestation.signing_payload();
|
|
||||||
|
|
||||||
pubkey
|
|
||||||
.verify_strict(&payload, &signature)
|
|
||||||
.map_err(|_| Error::<T>::InvalidSignature)?;
|
|
||||||
|
|
||||||
// PROV-2 canonical: convert + insert canonical.
|
|
||||||
// SCAFFOLD: minimal AssetAttestation construction (without creator/sig
|
|
||||||
// mapping canonical — that's Ergon sub-crate concern post Cap 42+).
|
|
||||||
let attestation = AssetAttestation {
|
let attestation = AssetAttestation {
|
||||||
hash: asset_hash,
|
hash: asset_hash,
|
||||||
creator: origin_chain_types::AccountId::new([0u8; 32]), // SCAFFOLD placeholder
|
creator: origin_chain_types::AccountId::new(decoded.public_key),
|
||||||
signature: origin_chain_types::Signature::new(validation_attestation.signature),
|
signature: origin_chain_types::Signature::new(signature),
|
||||||
anchored_at_block: 0, // SCAFFOLD — block_number wire post v0.0.16
|
anchored_at_block: block_number.saturated_into::<u64>(),
|
||||||
agent_chain: origin_chain_types::AgentChain::default(), // BoundedVec empty
|
agent_chain: origin_chain_types::AgentChain::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Attestations::<T>::insert(asset_hash, attestation);
|
Attestations::<T>::insert(asset_hash, attestation);
|
||||||
|
|
||||||
// PROV-7 emit event canonical.
|
Self::deposit_event(Event::AssetAttested { asset_hash, block_number });
|
||||||
let block_number = <frame_system::Pallet<T>>::block_number();
|
Ok(())
|
||||||
Self::deposit_event(Event::AssetAttested {
|
}
|
||||||
asset_hash,
|
|
||||||
block_number,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/// FIX-4: registra (o re-nivela) un validador Noesis autorizado a anclar.
|
||||||
|
/// **Root-only** (sudo Index 12 / governance) — cierra el squat: solo
|
||||||
|
/// validadores que governance habilita pueden anclar. Sunset a
|
||||||
|
/// attestation-driven cuando el registro OIT de Mathi aterrice.
|
||||||
|
#[pallet::call_index(1)]
|
||||||
|
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||||
|
pub fn register_validator(
|
||||||
|
origin: OriginFor<T>,
|
||||||
|
public_key: [u8; 32],
|
||||||
|
level: u8,
|
||||||
|
) -> DispatchResult {
|
||||||
|
ensure_root(origin)?;
|
||||||
|
AuthorizedValidators::<T>::insert(public_key, level);
|
||||||
|
Self::deposit_event(Event::ValidatorRegistered { public_key, level });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIX-4: revoca un validador — ya no puede anclar. **Root-only.**
|
||||||
|
#[pallet::call_index(2)]
|
||||||
|
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||||
|
pub fn revoke_validator(
|
||||||
|
origin: OriginFor<T>,
|
||||||
|
public_key: [u8; 32],
|
||||||
|
) -> DispatchResult {
|
||||||
|
ensure_root(origin)?;
|
||||||
|
AuthorizedValidators::<T>::remove(public_key);
|
||||||
|
Self::deposit_event(Event::ValidatorRevoked { public_key });
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,96 +395,15 @@ mod tests_integration;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use ed25519_dalek::{Signer, SigningKey};
|
|
||||||
use origin_chain_types::{OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict};
|
|
||||||
|
|
||||||
// Compile-time + Ed25519 verify standalone tests canonical.
|
// FIX-4: la verificación Ed25519 real (sp_io host function sobre el mensaje
|
||||||
// Mock runtime integration tests canonical = Phase 2.1 v0.0.17 next slice.
|
// domain-separado, con golden vectors del oráculo de Ergon) vive en
|
||||||
|
// `tests_integration`. El decoder postcard + el mensaje firmado se prueban
|
||||||
fn signed_attestation(verdict: ValidationVerdict, severity: SeverityTier) -> ValidationAttestation {
|
// en `origin-chain-types::attestation_verify`. Aquí quedan compile-checks.
|
||||||
let signing_key = SigningKey::from_bytes(&[7u8; 32]);
|
|
||||||
let verifying_key = signing_key.verifying_key();
|
|
||||||
|
|
||||||
let mut att = ValidationAttestation {
|
|
||||||
schema_version: *b"1.0.0 ",
|
|
||||||
asset_id: origin_chain_types::AssetHash::new([1u8; 32]),
|
|
||||||
validator_oit: OitIdentity {
|
|
||||||
public_key: verifying_key.to_bytes(),
|
|
||||||
level: 3,
|
|
||||||
},
|
|
||||||
validated_at_us: 1234567890,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ed25519_verify_real_valid_signature_canonical() {
|
fn pallet_compiles_canonical() {
|
||||||
// PROV-4 canonical: signed attestation canonical verifies correctamente.
|
// Verified by cargo check. E2E del fee-path verify = tests_integration.
|
||||||
let att = signed_attestation(ValidationVerdict::Passed, SeverityTier::Info);
|
|
||||||
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(
|
|
||||||
&att.validator_oit.public_key,
|
|
||||||
)
|
|
||||||
.expect("valid pubkey canonical");
|
|
||||||
let sig = ed25519_dalek::Signature::from_bytes(&att.signature);
|
|
||||||
let payload = att.signing_payload();
|
|
||||||
assert!(pubkey.verify_strict(&payload, &sig).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ed25519_verify_tampered_signature_rejected_canonical() {
|
|
||||||
// PROV-4 canonical: tampered signature canonical rejected.
|
|
||||||
let mut att = signed_attestation(ValidationVerdict::Passed, SeverityTier::Info);
|
|
||||||
// Flip a bit en signature canonical.
|
|
||||||
att.signature[0] ^= 0xFF;
|
|
||||||
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(
|
|
||||||
&att.validator_oit.public_key,
|
|
||||||
)
|
|
||||||
.expect("valid pubkey canonical");
|
|
||||||
let sig = ed25519_dalek::Signature::from_bytes(&att.signature);
|
|
||||||
let payload = att.signing_payload();
|
|
||||||
assert!(pubkey.verify_strict(&payload, &sig).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ed25519_verify_tampered_payload_rejected_canonical() {
|
|
||||||
// PROV-4 canonical: tampered payload canonical (signing_payload mismatch) rejected.
|
|
||||||
let mut att = signed_attestation(ValidationVerdict::Passed, SeverityTier::Info);
|
|
||||||
let original_sig = att.signature;
|
|
||||||
// Flip a bit en asset_id (cambia signing_payload pero mantiene signature original).
|
|
||||||
att.asset_id = origin_chain_types::AssetHash::new([99u8; 32]);
|
|
||||||
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(
|
|
||||||
&att.validator_oit.public_key,
|
|
||||||
)
|
|
||||||
.expect("valid pubkey canonical");
|
|
||||||
let sig = ed25519_dalek::Signature::from_bytes(&original_sig);
|
|
||||||
let payload = att.signing_payload(); // payload canonical NEW (con asset_id tampered)
|
|
||||||
assert!(pubkey.verify_strict(&payload, &sig).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ed25519_zero_signature_fails_verify_canonical() {
|
|
||||||
// PROV-4 canonical: all-zero signature canonical rejected por verify_strict.
|
|
||||||
// (Note: ed25519-dalek `from_bytes` acepta all-zero pubkey/sig — son
|
|
||||||
// valid Edwards points; el rejection canonical pasa en verify_strict.)
|
|
||||||
let signing_key = SigningKey::from_bytes(&[7u8; 32]);
|
|
||||||
let verifying_key = signing_key.verifying_key();
|
|
||||||
let payload = b"any payload canonical";
|
|
||||||
let zero_sig = ed25519_dalek::Signature::from_bytes(&[0u8; 64]);
|
|
||||||
assert!(verifying_key.verify_strict(payload, &zero_sig).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pallet_scaffold_compiles_canonical() {
|
|
||||||
// Scaffold compiles — verified by cargo check.
|
|
||||||
// Real mock runtime integration tests = Phase 2.1 v0.0.17.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
//! # Mock runtime canonical para integration tests v0.0.18
|
//! # Mock runtime para integration tests
|
||||||
//!
|
//!
|
||||||
//! Provee mock minimal `Test` runtime composition (`System` + `Provenance` solo)
|
//! Mock minimal (`System` + `Provenance`) para E2E del flow PROV-3 + FIX-4:
|
||||||
//! para E2E tests dispatchable + storage + event assertions per PROV-3 flow
|
//! decode postcard → gates → Ed25519 verify domain-separado → anclar.
|
||||||
//! 6-step canonical:
|
|
||||||
//!
|
//!
|
||||||
//! 1. PROV-4 Ed25519 signature verification
|
//! FIX-4: `new_test_ext_with_genesis` siembra `block_hash(0)` = el genesis con
|
||||||
//! 2. PROV-5 validator OIT level >= 3 institutional
|
//! el que se firmaron los golden vectors (generados desde el oráculo REAL de
|
||||||
//! 3. PROV-6 is_anchorable check (verdict + severity)
|
//! Ergon `arkhe-noesis-core`), para probar la verificación cross-runtime.
|
||||||
//! 4. PROV-2 ValidationAttestation → AssetAttestation conversion
|
|
||||||
//! 5. PROV-2 StorageMap insert
|
|
||||||
//! 6. PROV-7 AssetAttested event emission
|
|
||||||
//!
|
|
||||||
//! ## Cross-reference canonical
|
|
||||||
//!
|
|
||||||
//! - DC `ORIGIN_PROVENANCE_PALLET_V1` PROV-1..8 sub-decisions
|
|
||||||
//! - `tests_integration.rs` 5 scenarios canonical E2E
|
|
||||||
//! - `lib.rs::pallet::attest_asset` dispatchable impl real
|
|
||||||
|
|
||||||
#![cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use crate as origin_chain_provenance;
|
use crate as origin_chain_provenance;
|
||||||
use frame_support::derive_impl;
|
use frame_support::derive_impl;
|
||||||
|
use sp_core::H256;
|
||||||
use sp_runtime::BuildStorage;
|
use sp_runtime::BuildStorage;
|
||||||
|
|
||||||
/// Mock block type canonical para tests.
|
/// Mock block type para tests.
|
||||||
type Block = frame_system::mocking::MockBlock<Test>;
|
type Block = frame_system::mocking::MockBlock<Test>;
|
||||||
|
|
||||||
frame_support::construct_runtime!(
|
frame_support::construct_runtime!(
|
||||||
@@ -44,34 +35,52 @@ impl origin_chain_provenance::Config for Test {
|
|||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test externalities builder canonical.
|
/// Genesis hash de los golden vectors ASP-3 (`/tmp/golden-asp3`, GENESIS=0xAB×32).
|
||||||
///
|
/// El pallet lee `block_hash(0)`; el mock lo siembra con ESTE valor para que la
|
||||||
/// Returns new `sp_io::TestExternalities` con frame_system genesis configurado
|
/// verificación Ed25519 coincida con las firmas del oráculo.
|
||||||
/// y block_number set to 1 (PROV-7 events requieren block_number > 0 para
|
pub const GOLDEN_GENESIS: [u8; 32] = [0xABu8; 32];
|
||||||
/// proper testing canonical).
|
|
||||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
/// Pubkey del validador golden (seed [7u8;32] → todos los vectores la usan).
|
||||||
|
pub const GOLDEN_VALIDATOR_PUBKEY: [u8; 32] = [
|
||||||
|
0xea, 0x4a, 0x6c, 0x63, 0xe2, 0x9c, 0x52, 0x0a, 0xbe, 0xf5, 0x50, 0x7b, 0x13, 0x2e, 0xc5, 0xf9,
|
||||||
|
0x95, 0x47, 0x76, 0xae, 0xbe, 0xbe, 0x7b, 0x92, 0x42, 0x1e, 0xea, 0x69, 0x14, 0x46, 0xd2, 0x2c,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Registra el validador golden con el nivel dado (vía Root) — helper de tests.
|
||||||
|
pub fn register_golden_validator(level: u8) {
|
||||||
|
crate::pallet::Pallet::<Test>::register_validator(
|
||||||
|
RuntimeOrigin::root(),
|
||||||
|
GOLDEN_VALIDATOR_PUBKEY,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
.expect("Root registra el validador");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test externalities con `block_hash(0)` sembrado al genesis dado.
|
||||||
|
pub fn new_test_ext_with_genesis(genesis: [u8; 32]) -> sp_io::TestExternalities {
|
||||||
let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::<Test>::default()
|
let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::<Test>::default()
|
||||||
.build_storage()
|
.build_storage()
|
||||||
.expect("genesis canonical builds")
|
.expect("genesis builds")
|
||||||
.into();
|
.into();
|
||||||
ext.execute_with(|| {
|
ext.execute_with(|| {
|
||||||
System::set_block_number(1);
|
System::set_block_number(1);
|
||||||
|
// Siembra el hash del bloque génesis (que el pallet lee para el dominio).
|
||||||
|
frame_system::BlockHash::<Test>::insert(0u64, H256(genesis));
|
||||||
});
|
});
|
||||||
ext
|
ext
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test signing key seed canonical (deterministic Ed25519 keypair).
|
/// Test externalities con el genesis de los golden vectors (caso por defecto).
|
||||||
///
|
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||||
/// Per ed25519-dalek pattern: seed [7u8; 32] produces deterministic verifying_key.
|
new_test_ext_with_genesis(GOLDEN_GENESIS)
|
||||||
/// Used across integration tests para reproducible signing.
|
}
|
||||||
pub const TEST_SIGNING_SEED: [u8; 32] = [7u8; 32];
|
|
||||||
|
|
||||||
/// Test origin account canonical (32-byte zero pattern).
|
/// Submitter de prueba (un relayer cualquiera — paga el fee, no es el validador).
|
||||||
pub fn alice_origin() -> RuntimeOrigin {
|
pub fn alice_origin() -> RuntimeOrigin {
|
||||||
RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32]))
|
RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test origin account canonical (BOB pattern).
|
/// Segundo submitter (BOB).
|
||||||
pub fn bob_origin() -> RuntimeOrigin {
|
pub fn bob_origin() -> RuntimeOrigin {
|
||||||
RuntimeOrigin::signed(sp_runtime::AccountId32::new([2u8; 32]))
|
RuntimeOrigin::signed(sp_runtime::AccountId32::new([2u8; 32]))
|
||||||
}
|
}
|
||||||
@@ -1,310 +1,241 @@
|
|||||||
//! # Integration tests canonical v0.0.18 — Mock runtime E2E
|
//! # Integration tests E2E — FIX-4 verificador attestation cross-runtime
|
||||||
//!
|
//!
|
||||||
//! 5 scenarios canonical PROV-3 attest_asset flow end-to-end via mock runtime:
|
//! Prueban el dispatchable `attest_asset` contra **golden vectors generados
|
||||||
|
//! desde el oráculo REAL de Ergon** (`arkhe-noesis-core::attestation`, en
|
||||||
|
//! `/tmp/golden-asp3` con keypair seed [7u8;32], genesis 0xAB×32). Esto cierra
|
||||||
|
//! el caso V3-C1: una firma producida por el código real de Ergon (postcard +
|
||||||
|
//! mensaje domain-separado) verifica byte-fiel en el pallet Substrate.
|
||||||
//!
|
//!
|
||||||
//! | Scenario | Verdict | Severity | OIT Level | Expected outcome |
|
//! Vectores (hex):
|
||||||
//! |----------|---------|----------|-----------|------------------|
|
//! - PAYLOAD1/SIG1: asset [1×32], atelier level 3, Passed/Info → ANCLA.
|
||||||
//! | passed_info_anchored | Passed | Info | 3 | ✅ AssetAttested event + storage insert |
|
//! - PAYLOAD2/SIG2: asset [2×32], atelier level 3, Warning/Warning → ANCLA.
|
||||||
//! | warning_anchored | Warning | Warning | 3 | ✅ AssetAttested event + storage insert |
|
//! - PAYLOAD3/SIG3: asset [3×32], atelier level 3, Failed/Critical → NotAnchorable.
|
||||||
//! | failed_critical_rejected | Failed | Critical | 3 | ❌ NotAnchorable error |
|
//! - PAYLOAD4/SIG4: asset [4×32], anonymous level 0, Passed/Info → TierInsufficient.
|
||||||
//! | 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)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use crate::mock::{alice_origin, bob_origin, new_test_ext, Provenance, RuntimeEvent, System, Test, TEST_SIGNING_SEED};
|
extern crate alloc;
|
||||||
use crate::{Attestations, Error, Event};
|
|
||||||
use ed25519_dalek::{Signer, SigningKey};
|
use crate::mock::{
|
||||||
use frame_support::{assert_noop, assert_ok};
|
alice_origin, bob_origin, new_test_ext, new_test_ext_with_genesis, register_golden_validator,
|
||||||
use origin_chain_types::{
|
Provenance, RuntimeEvent, RuntimeOrigin, System, Test, GOLDEN_GENESIS, GOLDEN_VALIDATOR_PUBKEY,
|
||||||
AssetHash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict,
|
|
||||||
};
|
};
|
||||||
|
use crate::{Attestations, Error, Event};
|
||||||
|
use frame_support::{assert_noop, assert_ok, BoundedVec};
|
||||||
|
|
||||||
// ─── Fixture builders canonical ──────────────────────────────────────
|
// ── Golden vectors (oráculo Ergon real) ──────────────────────────────
|
||||||
|
const PAYLOAD1: &str = "05312e302e300101010101010101010101010101010101010101010101010101010101010101146174656c6965725f63616e6f6e6963616c5f763103ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22cd285d8cc0400002a";
|
||||||
|
const SIG1: &str = "e155043ad90df787d73e8ba4c3ee961427ecb228637ade77e19bd9b57bb727dcdf3c247462aa8e6facf5697887f3e08f96af1c5f2fdcab10fcfbd2daabe73204";
|
||||||
|
const PAYLOAD2: &str = "05312e302e300202020202020202020202020202020202020202020202020202020202020202146174656c6965725f63616e6f6e6963616c5f763103ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c8080f9c0c1c48203010107";
|
||||||
|
const SIG2: &str = "34aa4a272ea6b264a1a38af912a9dfee9e77a3d37f0ae2427634d9aa0e5582eca741787904b8d0199614f93c14f2d5f3b7a53e1414d29d7ee272e985591fee05";
|
||||||
|
const PAYLOAD3: &str = "05312e302e300303030303030303030303030303030303030303030303030303030303030303146174656c6965725f63616e6f6e6963616c5f763103ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c8080f9c0c1c48203020203";
|
||||||
|
const SIG3: &str = "2c91a93223a105e528ac17879bc6561444ab8af3aac5b0857d781cb6d2c2e52eca6e6ec6673c174dc3cdf28733add7a0dce23e2f543288721e3ad84ddb77fa05";
|
||||||
|
const PAYLOAD4: &str = "05312e302e30040404040404040404040404040404040404040404040404040404040404040413616e6f6e796d6f75735f63616e6f6e6963616c00ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c8080f9c0c1c48203000001";
|
||||||
|
const SIG4: &str = "53898be4c2dfea096fb991a9d37b2ffe078f8ff2a20003612bb6930eefd0ddeeb0056a48b4a55453c519eb75bbfb5d6b05295a5f91050f53087125145b392702";
|
||||||
|
|
||||||
/// Build canonical signed ValidationAttestation con (verdict, severity, oit_level)
|
fn unhex(s: &str) -> alloc::vec::Vec<u8> {
|
||||||
/// parameters. Deterministic Ed25519 signature.
|
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||||||
///
|
|
||||||
/// 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 payload(s: &str) -> BoundedVec<u8, crate::pallet::MaxPayloadLen> {
|
||||||
fn fixture_passed_info(asset_seed: u8) -> ValidationAttestation {
|
BoundedVec::try_from(unhex(s)).expect("payload within bound")
|
||||||
build_signed_attestation(asset_seed, ValidationVerdict::Passed, SeverityTier::Info, 3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 1: passed_info anchored canonical ──────────────────────
|
fn sig(s: &str) -> [u8; 64] {
|
||||||
|
unhex(s).try_into().expect("64-byte sig")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Camino feliz: firma real de Ergon ancla on-chain ─────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_1_passed_info_anchored_canonical() {
|
fn golden_passed_info_anchors_cross_runtime() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let attestation = fixture_passed_info(1);
|
register_golden_validator(3); // governance habilita al validador
|
||||||
let asset_hash = attestation.asset_id;
|
assert_ok!(Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), sig(SIG1)));
|
||||||
|
let hash = origin_chain_types::AssetHash::new([1u8; 32]);
|
||||||
// PROV-3 attest_asset extrinsic canonical
|
let stored = Attestations::<Test>::get(hash).expect("anclado");
|
||||||
assert_ok!(Provenance::attest_asset(alice_origin(), attestation.clone()));
|
// creator = el VALIDADOR (su pubkey Ed25519), no el submitter.
|
||||||
|
assert_eq!(stored.creator.as_bytes(), &GOLDEN_VALIDATOR_PUBKEY[..]);
|
||||||
// PROV-2 storage insert verified
|
// block real (no el placeholder 0 del scaffold).
|
||||||
let stored = Attestations::<Test>::get(asset_hash);
|
assert_eq!(stored.anchored_at_block, 1);
|
||||||
assert!(stored.is_some(), "PROV-2 canonical: AssetAttestation inserted in StorageMap");
|
System::assert_has_event(RuntimeEvent::Provenance(Event::AssetAttested {
|
||||||
let stored_unwrap = stored.unwrap();
|
asset_hash: hash,
|
||||||
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,
|
block_number: 1,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 2: warning anchored canonical (non-blocking) ────────────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_2_warning_anchored_canonical() {
|
fn golden_warning_warning_anchors() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let attestation = build_signed_attestation(
|
register_golden_validator(3);
|
||||||
2,
|
assert_ok!(Provenance::attest_asset(alice_origin(), payload(PAYLOAD2), sig(SIG2)));
|
||||||
ValidationVerdict::Warning,
|
assert!(Attestations::<Test>::contains_key(origin_chain_types::AssetHash::new([2u8; 32])));
|
||||||
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) ──────────
|
// ── FIX-4: legitimidad ON-CHAIN (cierra squat + tier-forgery) ────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_3_failed_critical_rejected_canonical() {
|
fn unregistered_validator_rejected_closes_squat() {
|
||||||
|
// Firma VÁLIDA + level 3 en el payload, pero la pubkey NO está registrada
|
||||||
|
// → rechazo. Cierra el squat: un atacante anónimo no ancla nada.
|
||||||
new_test_ext().execute_with(|| {
|
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!(
|
assert_noop!(
|
||||||
Provenance::attest_asset(alice_origin(), attestation),
|
Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), sig(SIG1)),
|
||||||
Error::<Test>::NotAnchorable
|
Error::<Test>::ValidatorNotAuthorized
|
||||||
);
|
);
|
||||||
|
assert!(!Attestations::<Test>::contains_key(origin_chain_types::AssetHash::new([1u8; 32])));
|
||||||
// Storage NOT inserted
|
|
||||||
assert!(Attestations::<Test>::get(asset_hash).is_none());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 4: passed_block security rejected canonical (PROV-6) ────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_4_passed_block_security_rejected_canonical() {
|
fn registered_but_low_level_rejected() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
// Passed verdict pero Block severity (security violation override)
|
register_golden_validator(1); // registrado, pero nivel < 3
|
||||||
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!(
|
assert_noop!(
|
||||||
Provenance::attest_asset(alice_origin(), attestation),
|
Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), sig(SIG1)),
|
||||||
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
|
Error::<Test>::ValidatorTierInsufficient
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(Attestations::<Test>::get(asset_hash).is_none());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Additional canonical: idempotency ────────────────────────────────
|
#[test]
|
||||||
|
fn registry_level_overrides_payload_self_claimed_level() {
|
||||||
|
// PAYLOAD4 declara identifier "anonymous" + level 0 EN EL PAYLOAD, pero la
|
||||||
|
// pubkey está registrada a nivel 3 → ancla. La VERDAD es el registro, no el
|
||||||
|
// self-attested del payload (esto es exactamente lo que cierra el forgery).
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
register_golden_validator(3);
|
||||||
|
assert_ok!(Provenance::attest_asset(alice_origin(), payload(PAYLOAD4), sig(SIG4)));
|
||||||
|
assert!(Attestations::<Test>::contains_key(origin_chain_types::AssetHash::new([4u8; 32])));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn additional_idempotency_already_attested_rejected_canonical() {
|
fn register_validator_is_root_only() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let attestation = fixture_passed_info(6);
|
// Signed origin → BadOrigin (solo governance habilita validadores).
|
||||||
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!(
|
assert_noop!(
|
||||||
Provenance::attest_asset(bob_origin(), attestation),
|
Provenance::register_validator(alice_origin(), GOLDEN_VALIDATOR_PUBKEY, 3),
|
||||||
|
sp_runtime::DispatchError::BadOrigin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn revoked_validator_cannot_anchor() {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
register_golden_validator(3);
|
||||||
|
assert_ok!(Provenance::revoke_validator(RuntimeOrigin::root(), GOLDEN_VALIDATOR_PUBKEY));
|
||||||
|
assert_noop!(
|
||||||
|
Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), sig(SIG1)),
|
||||||
|
Error::<Test>::ValidatorNotAuthorized
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gate anchorable ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_failed_critical_rejected_not_anchorable() {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
register_golden_validator(3);
|
||||||
|
assert_noop!(
|
||||||
|
Provenance::attest_asset(alice_origin(), payload(PAYLOAD3), sig(SIG3)),
|
||||||
|
Error::<Test>::NotAnchorable
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Crypto: tamper + anti-replay cross-chain (V3-C1) ─────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tampered_signature_rejected() {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
register_golden_validator(3);
|
||||||
|
let mut bad = sig(SIG1);
|
||||||
|
bad[0] ^= 0xFF;
|
||||||
|
assert_noop!(
|
||||||
|
Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), bad),
|
||||||
|
Error::<Test>::InvalidSignature
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tampered_payload_rejected() {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
register_golden_validator(3);
|
||||||
|
// PAYLOAD2 con SIG1: la firma no corresponde a estos bytes.
|
||||||
|
assert_noop!(
|
||||||
|
Provenance::attest_asset(alice_origin(), payload(PAYLOAD2), sig(SIG1)),
|
||||||
|
Error::<Test>::InvalidSignature
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_genesis_rejected_anti_replay_cross_chain() {
|
||||||
|
// V3-C1: la MISMA attestation firmada para la cadena 0xAB no verifica en
|
||||||
|
// una cadena con genesis distinto (0xCC) — el genesis_hash liga la firma.
|
||||||
|
new_test_ext_with_genesis([0xCCu8; 32]).execute_with(|| {
|
||||||
|
register_golden_validator(3);
|
||||||
|
assert_noop!(
|
||||||
|
Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), sig(SIG1)),
|
||||||
|
Error::<Test>::InvalidSignature
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Replay intra-chain: rechazo EXPLÍCITO (decisión Pilar 4) ─────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replay_same_attestation_rejected_explicitly() {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
register_golden_validator(3);
|
||||||
|
assert_ok!(Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), sig(SIG1)));
|
||||||
|
// Re-submit del MISMO attestation firmado (incluso por otro submitter):
|
||||||
|
// mismo asset_id (BLAKE3 content hash) → rechazo explícito.
|
||||||
|
assert_noop!(
|
||||||
|
Provenance::attest_asset(bob_origin(), payload(PAYLOAD1), sig(SIG1)),
|
||||||
Error::<Test>::AlreadyAttested
|
Error::<Test>::AlreadyAttested
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Additional canonical: tampered signature (PROV-4) ────────────────
|
// ── Malformed payload ────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn additional_tampered_signature_rejected_canonical() {
|
fn malformed_payload_rejected() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let mut attestation = fixture_passed_info(7);
|
let mut bytes = unhex(PAYLOAD1);
|
||||||
let asset_hash = attestation.asset_id;
|
bytes.push(0x99); // trailing byte → decoder estricto rechaza
|
||||||
|
let bad = BoundedVec::try_from(bytes).unwrap();
|
||||||
// Flip bit en signature canonical post-sign
|
|
||||||
attestation.signature[0] ^= 0xFF;
|
|
||||||
|
|
||||||
// PROV-4 Ed25519 verify canonical fails
|
|
||||||
assert_noop!(
|
assert_noop!(
|
||||||
Provenance::attest_asset(alice_origin(), attestation),
|
Provenance::attest_asset(alice_origin(), bad, sig(SIG1)),
|
||||||
Error::<Test>::InvalidSignature
|
Error::<Test>::MalformedValidationAttestation
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(Attestations::<Test>::get(asset_hash).is_none());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Additional canonical: tampered payload (PROV-4) ──────────────────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn additional_tampered_payload_rejected_canonical() {
|
fn empty_payload_rejected() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let mut attestation = fixture_passed_info(8);
|
let empty = BoundedVec::try_from(alloc::vec::Vec::new()).unwrap();
|
||||||
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!(
|
assert_noop!(
|
||||||
Provenance::attest_asset(alice_origin(), attestation),
|
Provenance::attest_asset(alice_origin(), empty, [0u8; 64]),
|
||||||
Error::<Test>::InvalidSignature
|
Error::<Test>::MalformedValidationAttestation
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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 ─────────
|
// ── Determinismo: el genesis correcto SÍ ancla ───────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn additional_6_step_prov_3_walkthrough_canonical() {
|
fn correct_genesis_verifies() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext_with_genesis(GOLDEN_GENESIS).execute_with(|| {
|
||||||
let attestation = fixture_passed_info(9);
|
register_golden_validator(3);
|
||||||
let asset_hash = attestation.asset_id;
|
assert_ok!(Provenance::attest_asset(alice_origin(), payload(PAYLOAD1), sig(SIG1)));
|
||||||
|
|
||||||
// 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,
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -19,9 +19,6 @@
|
|||||||
//! - Update re-exports lib.rs canonical
|
//! - Update re-exports lib.rs canonical
|
||||||
//! - Diary canonical Ergon coordination switch
|
//! - Diary canonical Ergon coordination switch
|
||||||
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
use alloc::vec::Vec;
|
|
||||||
use serde_big_array::BigArray;
|
use serde_big_array::BigArray;
|
||||||
|
|
||||||
use crate::AssetHash;
|
use crate::AssetHash;
|
||||||
@@ -139,26 +136,14 @@ impl ValidationAttestation {
|
|||||||
verdict_ok && severity_ok
|
verdict_ok && severity_ok
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns signing payload canonical (postcard de fields EXCEPT signature).
|
// FIX-4 (sesión 7+): el método `signing_payload()` SCALE fue REMOVIDO.
|
||||||
///
|
// Producía bytes byte-DIVERGENTES del postcard real de Ergon (su propio doc
|
||||||
/// Per Ergon canonical pattern Slice 7.0r:
|
// lo admitía) → un firmante que lo usara generaba firmas que NUNCA
|
||||||
/// ```text
|
// verificarían contra el verificador on-chain. El verificador real
|
||||||
/// payload = postcard(ValidationAttestation { ..., signature: [0; 64] })
|
// (`crate::attestation_verify`) opera sobre los bytes postcard opacos del
|
||||||
/// ```
|
// oráculo de Ergon; este método muerto era un footgun (flagged en la
|
||||||
///
|
// verificación adversarial). El struct se conserva como type-mirror hasta
|
||||||
/// **VENDOR canonical NOTE**: stub implementation canonical. Real impl en
|
// que aterrice `arkhe-noesis-attestation-types`.
|
||||||
/// `arkhe-noesis-attestation-types` usa `postcard` crate canonical. Aquí
|
|
||||||
/// usamos parity-scale-codec `Encode` para compat FRAME canonical pallet
|
|
||||||
/// runtime canonical. Esto puede diverger del Ergon real canonical — DC
|
|
||||||
/// PROVENANCE_PALLET_V1 PROV-4 confirma Ed25519 verification handled
|
|
||||||
/// directly via ed25519-dalek canonical en pallet (trait-seam ADR-0057).
|
|
||||||
pub fn signing_payload(&self) -> Vec<u8> {
|
|
||||||
use parity_scale_codec::Encode;
|
|
||||||
// Clone with signature zeroed canonical para deterministic payload.
|
|
||||||
let mut payload_struct = self.clone();
|
|
||||||
payload_struct.signature = [0u8; 64];
|
|
||||||
payload_struct.encode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -228,23 +213,6 @@ mod tests {
|
|||||||
assert!(att.is_anchorable());
|
assert!(att.is_anchorable());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn signing_payload_zeroes_signature_canonical() {
|
|
||||||
let att = mock_attestation();
|
|
||||||
let payload = att.signing_payload();
|
|
||||||
// Signature bytes (last 64 in encode order) should be zeros canonical.
|
|
||||||
// Verify payload non-empty + deterministic.
|
|
||||||
assert!(!payload.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn signing_payload_deterministic_canonical() {
|
|
||||||
let att = mock_attestation();
|
|
||||||
let p1 = att.signing_payload();
|
|
||||||
let p2 = att.signing_payload();
|
|
||||||
assert_eq!(p1, p2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validation_attestation_serde_roundtrip_canonical() {
|
fn validation_attestation_serde_roundtrip_canonical() {
|
||||||
let att = mock_attestation();
|
let att = mock_attestation();
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
//! # Verificador de attestation on-chain — FIX-4 (V3-C1 + firma legítima)
|
||||||
|
//!
|
||||||
|
//! Espejo byte-fiel del oráculo de Ergon (`arkhe-noesis-core::attestation`,
|
||||||
|
//! DC `076d15cc` ASP-3 ratificado 2/2 con Charon 2026-06-09). Dos piezas:
|
||||||
|
//!
|
||||||
|
//! 1. [`ATTESTATION_DOMAIN_TAG`] + [`attestation_signed_message`] — el mensaje
|
||||||
|
//! domain-separado que el validador firma off-chain y el pallet re-deriva:
|
||||||
|
//! `TAG ‖ genesis_hash(32) ‖ signing_payload`. El `genesis_hash` liga la
|
||||||
|
//! firma a UNA cadena (cierra el replay cross-chain V3-C1). Copiados
|
||||||
|
//! byte-idénticos del oráculo; cambiar el TAG es un cambio de protocolo que
|
||||||
|
//! requiere re-ratificar con Pilar 4.
|
||||||
|
//!
|
||||||
|
//! 2. [`decode_signing_payload`] — un lector **postcard secuencial total** del
|
||||||
|
//! `SigningPayload` real de Ergon. El contrato ASP-3 exige verificar sobre
|
||||||
|
//! los BYTES recibidos (nunca re-serializar): este decoder solo EXTRAE los
|
||||||
|
//! campos que el pallet necesita para sus gates (asset_id, public_key, level,
|
||||||
|
//! verdict, severity) avanzando el cursor por los demás. Sin dep nueva en el
|
||||||
|
//! runtime wasm (frágil); con bounds DoS explícitos. La compatibilidad
|
||||||
|
//! byte-fiel con el oráculo se prueba con golden vectors generados desde el
|
||||||
|
//! crate real de Ergon (ver tests del pallet).
|
||||||
|
//!
|
||||||
|
//! ## Layout postcard del `SigningPayload` (orden de declaración de Ergon)
|
||||||
|
//!
|
||||||
|
//! | # | campo | encoding postcard |
|
||||||
|
//! |---|-------|-------------------|
|
||||||
|
//! | 1 | `schema_version: &str` | varint len (LEB128) + UTF-8 |
|
||||||
|
//! | 2 | `asset_id: [u8;32]` | 32 bytes crudos (array fijo, sin prefix) |
|
||||||
|
//! | 3a | `validator_oit.identifier: String` | varint len + UTF-8 |
|
||||||
|
//! | 3b | `validator_oit.level: u8` | 1 byte crudo (u8 NO es varint) |
|
||||||
|
//! | 3c | `validator_oit.public_key: [u8;32]` | 32 bytes crudos |
|
||||||
|
//! | 4 | `validated_at_us: u64` | varint LEB128 |
|
||||||
|
//! | 5 | `verdict: enum` | varint discriminant (índice de declaración) |
|
||||||
|
//! | 6 | `severity: enum` | varint discriminant |
|
||||||
|
//! | 7 | `constraints_checked: u32` | varint LEB128 |
|
||||||
|
//!
|
||||||
|
//! Nota crítica (heredada del mapeo): postcard serializa enums por **índice de
|
||||||
|
//! variante** (orden de declaración), NO por `#[repr(u8)]`. Hoy coinciden
|
||||||
|
//! (Passed=0/Warning=1/Failed=2, Info=0/…/Block=3); el decoder valida el rango.
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
/// Domain tag canónico (DC `076d15cc` ASP-3). 27 bytes ASCII. Byte-idéntico al
|
||||||
|
/// oráculo de Ergon `arkhe-noesis-core::attestation::ATTESTATION_DOMAIN_TAG`.
|
||||||
|
pub const ATTESTATION_DOMAIN_TAG: &[u8] = b"origin-chain/attestation/v1";
|
||||||
|
|
||||||
|
/// Construye el mensaje firmado domain-separado: `TAG ‖ genesis_hash ‖ payload`.
|
||||||
|
/// Espejo byte-idéntico de `attestation_signed_message` del oráculo. El
|
||||||
|
/// verificador re-deriva ESTO (no el payload crudo) antes de `ed25519_verify`.
|
||||||
|
pub fn attestation_signed_message(signing_payload: &[u8], genesis_hash: &[u8; 32]) -> Vec<u8> {
|
||||||
|
let mut message =
|
||||||
|
Vec::with_capacity(ATTESTATION_DOMAIN_TAG.len() + genesis_hash.len() + signing_payload.len());
|
||||||
|
message.extend_from_slice(ATTESTATION_DOMAIN_TAG);
|
||||||
|
message.extend_from_slice(genesis_hash);
|
||||||
|
message.extend_from_slice(signing_payload);
|
||||||
|
message
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cota DoS del identifier de `OitIdentity` (el tipo Rust es `String` sin
|
||||||
|
/// límite; on-chain imponemos uno). Los identifiers canónicos son cortos
|
||||||
|
/// ("atelier_canonical_v1" = 20, "anonymous_canonical" = 19).
|
||||||
|
pub const MAX_OIT_IDENTIFIER_LEN: u32 = 64;
|
||||||
|
|
||||||
|
/// Cota DoS del schema_version (semver string).
|
||||||
|
pub const MAX_SCHEMA_VERSION_LEN: u32 = 16;
|
||||||
|
|
||||||
|
/// Campos extraídos del `SigningPayload` postcard — solo los que el pallet
|
||||||
|
/// necesita para sus gates. El `validated_at_us` y `constraints_checked` se
|
||||||
|
/// decodifican para avanzar el cursor pero no se exponen (no gobiernan gates).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct DecodedSigningPayload {
|
||||||
|
/// BLAKE3 content hash del asset (storage key + dedup).
|
||||||
|
pub asset_id: [u8; 32],
|
||||||
|
/// Ed25519 public key del validador (con la que se verifica la firma).
|
||||||
|
pub public_key: [u8; 32],
|
||||||
|
/// Nivel OIT del validador (gate institucional >= 3).
|
||||||
|
pub level: u8,
|
||||||
|
/// Verdict (0=Passed, 1=Warning, 2=Failed).
|
||||||
|
pub verdict: u8,
|
||||||
|
/// Severity (0=Info, 1=Warning, 2=Critical, 3=Block).
|
||||||
|
pub severity: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodedSigningPayload {
|
||||||
|
/// `true` si el validador es institucional (level >= 3) — PROV-5.
|
||||||
|
pub fn is_institutional(&self) -> bool {
|
||||||
|
self.level >= 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si la attestation es anchorable: verdict Passed|Warning AND
|
||||||
|
/// severity Info|Warning (PROV-6, Inmutable #26 immutabilidad post-anchor).
|
||||||
|
pub fn is_anchorable(&self) -> bool {
|
||||||
|
let verdict_ok = self.verdict <= 1; // Passed | Warning
|
||||||
|
let severity_ok = self.severity <= 1; // Info | Warning
|
||||||
|
verdict_ok && severity_ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error de decodificación del payload postcard (DoS-safe, total).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SigningPayloadError {
|
||||||
|
/// Se acabaron los bytes antes de completar un campo.
|
||||||
|
UnexpectedEnd,
|
||||||
|
/// Un varint LEB128 excede 10 bytes (malformado) o desborda u64.
|
||||||
|
VarintOverflow,
|
||||||
|
/// Un length-prefix excede su cota DoS.
|
||||||
|
LengthTooLarge,
|
||||||
|
/// Discriminant de enum fuera de rango (verdict > 2 o severity > 3).
|
||||||
|
InvalidDiscriminant,
|
||||||
|
/// Quedaron bytes sin consumir tras el último campo (payload no estricto).
|
||||||
|
TrailingBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cursor minimal sobre `&[u8]` con lectura postcard.
|
||||||
|
struct Reader<'a> {
|
||||||
|
buf: &'a [u8],
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Reader<'a> {
|
||||||
|
fn new(buf: &'a [u8]) -> Self {
|
||||||
|
Self { buf, pos: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_byte(&mut self) -> Result<u8, SigningPayloadError> {
|
||||||
|
let b = *self.buf.get(self.pos).ok_or(SigningPayloadError::UnexpectedEnd)?;
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lee un varint LEB128 unsigned u64 (postcard `try_take_varint_u64`):
|
||||||
|
/// máx 10 bytes, el 10º ≤ 1. Usado para `validated_at_us: u64`.
|
||||||
|
fn read_varint_u64(&mut self) -> Result<u64, SigningPayloadError> {
|
||||||
|
let mut result: u64 = 0;
|
||||||
|
let mut shift: u32 = 0;
|
||||||
|
loop {
|
||||||
|
if shift >= 64 {
|
||||||
|
return Err(SigningPayloadError::VarintOverflow);
|
||||||
|
}
|
||||||
|
let byte = self.read_byte()?;
|
||||||
|
let low = (byte & 0x7f) as u64;
|
||||||
|
// El último grupo de un u64 son 64-63=1 bit ⇒ el 10º byte ≤ 1.
|
||||||
|
if shift == 63 && low > 1 {
|
||||||
|
return Err(SigningPayloadError::VarintOverflow);
|
||||||
|
}
|
||||||
|
result |= low << shift;
|
||||||
|
if byte & 0x80 == 0 {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
shift += 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lee un varint LEB128 unsigned u32 ESTRICTO (postcard `try_take_varint_u32`):
|
||||||
|
/// máx 5 bytes, el 5º ≤ 15. Postcard codifica los discriminants de enum y
|
||||||
|
/// `u32` así; usar el lector u64 aceptaría formas sobre-largas que
|
||||||
|
/// `postcard::from_bytes` rechaza (fidelidad de contrato — el verificador
|
||||||
|
/// caza esto). Usado para `verdict`/`severity`/`constraints_checked`.
|
||||||
|
fn read_varint_u32(&mut self) -> Result<u32, SigningPayloadError> {
|
||||||
|
let mut result: u32 = 0;
|
||||||
|
let mut shift: u32 = 0;
|
||||||
|
loop {
|
||||||
|
if shift >= 32 {
|
||||||
|
return Err(SigningPayloadError::VarintOverflow);
|
||||||
|
}
|
||||||
|
let byte = self.read_byte()?;
|
||||||
|
let low = (byte & 0x7f) as u32;
|
||||||
|
// El último grupo de un u32 son 32-28=4 bits ⇒ el 5º byte ≤ 15.
|
||||||
|
if shift == 28 && low > 0x0f {
|
||||||
|
return Err(SigningPayloadError::VarintOverflow);
|
||||||
|
}
|
||||||
|
result |= low << shift;
|
||||||
|
if byte & 0x80 == 0 {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
shift += 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_array32(&mut self) -> Result<[u8; 32], SigningPayloadError> {
|
||||||
|
let end = self.pos.checked_add(32).ok_or(SigningPayloadError::UnexpectedEnd)?;
|
||||||
|
let slice = self.buf.get(self.pos..end).ok_or(SigningPayloadError::UnexpectedEnd)?;
|
||||||
|
let mut out = [0u8; 32];
|
||||||
|
out.copy_from_slice(slice);
|
||||||
|
self.pos = end;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Salta una cadena length-prefixed (varint u32 len + bytes), validando la
|
||||||
|
/// cota. Postcard codifica el length de strings como varint u32.
|
||||||
|
fn skip_string(&mut self, max_len: u32) -> Result<(), SigningPayloadError> {
|
||||||
|
let len = self.read_varint_u32()?;
|
||||||
|
if len > max_len {
|
||||||
|
return Err(SigningPayloadError::LengthTooLarge);
|
||||||
|
}
|
||||||
|
let len = len as usize;
|
||||||
|
let end = self.pos.checked_add(len).ok_or(SigningPayloadError::UnexpectedEnd)?;
|
||||||
|
// Verifica que existan los bytes (sin copiarlos: no necesitamos el valor).
|
||||||
|
let _ = self.buf.get(self.pos..end).ok_or(SigningPayloadError::UnexpectedEnd)?;
|
||||||
|
self.pos = end;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodifica un `SigningPayload` postcard de Ergon extrayendo los campos que
|
||||||
|
/// gobiernan los gates del pallet. **Total y estricto**: rechaza payloads
|
||||||
|
/// malformados, truncados, con length-prefix excesivo, discriminant inválido o
|
||||||
|
/// bytes sobrantes. NO re-serializa nada — el `signing_payload` original es lo
|
||||||
|
/// que se firma/verifica (contrato ASP-3).
|
||||||
|
pub fn decode_signing_payload(payload: &[u8]) -> Result<DecodedSigningPayload, SigningPayloadError> {
|
||||||
|
let mut r = Reader::new(payload);
|
||||||
|
|
||||||
|
// 1. schema_version (string)
|
||||||
|
r.skip_string(MAX_SCHEMA_VERSION_LEN)?;
|
||||||
|
// 2. asset_id [u8;32]
|
||||||
|
let asset_id = r.read_array32()?;
|
||||||
|
// 3a. validator_oit.identifier (string)
|
||||||
|
r.skip_string(MAX_OIT_IDENTIFIER_LEN)?;
|
||||||
|
// 3b. validator_oit.level (u8 crudo)
|
||||||
|
let level = r.read_byte()?;
|
||||||
|
// 3c. validator_oit.public_key [u8;32]
|
||||||
|
let public_key = r.read_array32()?;
|
||||||
|
// 4. validated_at_us: u64 (varint u64, descartado)
|
||||||
|
let _validated_at_us = r.read_varint_u64()?;
|
||||||
|
// 5. verdict: enum discriminant (varint u32 estricto)
|
||||||
|
let verdict = r.read_varint_u32()?;
|
||||||
|
if verdict > 2 {
|
||||||
|
return Err(SigningPayloadError::InvalidDiscriminant);
|
||||||
|
}
|
||||||
|
// 6. severity: enum discriminant (varint u32 estricto)
|
||||||
|
let severity = r.read_varint_u32()?;
|
||||||
|
if severity > 3 {
|
||||||
|
return Err(SigningPayloadError::InvalidDiscriminant);
|
||||||
|
}
|
||||||
|
// 7. constraints_checked: u32 (varint u32 estricto, descartado)
|
||||||
|
let _constraints_checked = r.read_varint_u32()?;
|
||||||
|
|
||||||
|
// Estricto: no debe sobrar nada.
|
||||||
|
if r.pos != payload.len() {
|
||||||
|
return Err(SigningPayloadError::TrailingBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DecodedSigningPayload {
|
||||||
|
asset_id,
|
||||||
|
public_key,
|
||||||
|
level,
|
||||||
|
verdict: verdict as u8,
|
||||||
|
severity: severity as u8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Golden vector 1 generado desde el oráculo REAL de Ergon
|
||||||
|
/// (`/tmp/golden-asp3`, keypair seed [7u8;32], asset_id [1u8;32],
|
||||||
|
/// atelier_canonical level 3, validated_at 1234567890, Passed/Info, 42).
|
||||||
|
const PAYLOAD1_HEX: &str = "05312e302e300101010101010101010101010101010101010101010101010101010101010101146174656c6965725f63616e6f6e6963616c5f763103ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22cd285d8cc0400002a";
|
||||||
|
const PUBKEY1_HEX: &str = "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c";
|
||||||
|
|
||||||
|
/// Golden vector 4: validador anónimo (identifier "anonymous_canonical",
|
||||||
|
/// level 0) — gate institucional debe rechazarlo.
|
||||||
|
const PAYLOAD4_HEX: &str = "05312e302e30040404040404040404040404040404040404040404040404040404040404040413616e6f6e796d6f75735f63616e6f6e6963616c00ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c8080f9c0c1c48203000001";
|
||||||
|
|
||||||
|
/// Golden vector 3: Failed/Critical — NO anchorable.
|
||||||
|
const PAYLOAD3_HEX: &str = "05312e302e300303030303030303030303030303030303030303030303030303030303030303146174656c6965725f63616e6f6e6963616c5f763103ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c8080f9c0c1c48203020203";
|
||||||
|
|
||||||
|
fn unhex(s: &str) -> Vec<u8> {
|
||||||
|
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_golden_vector_1_passed_info_institutional() {
|
||||||
|
let payload = unhex(PAYLOAD1_HEX);
|
||||||
|
let d = decode_signing_payload(&payload).expect("decodes");
|
||||||
|
assert_eq!(d.asset_id, [1u8; 32], "asset_id byte-fiel del oráculo");
|
||||||
|
assert_eq!(d.public_key, unhex(PUBKEY1_HEX).as_slice());
|
||||||
|
assert_eq!(d.level, 3);
|
||||||
|
assert_eq!(d.verdict, 0); // Passed
|
||||||
|
assert_eq!(d.severity, 0); // Info
|
||||||
|
assert!(d.is_institutional());
|
||||||
|
assert!(d.is_anchorable());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_golden_vector_4_anonymous_level_0_not_institutional() {
|
||||||
|
let payload = unhex(PAYLOAD4_HEX);
|
||||||
|
let d = decode_signing_payload(&payload).expect("decodes");
|
||||||
|
assert_eq!(d.asset_id, [4u8; 32]);
|
||||||
|
assert_eq!(d.level, 0);
|
||||||
|
assert!(!d.is_institutional(), "anónimo level 0 NO es institucional");
|
||||||
|
assert!(d.is_anchorable(), "Passed/Info sí es anchorable (el gate de tier es ortogonal)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_golden_vector_3_failed_critical_not_anchorable() {
|
||||||
|
let payload = unhex(PAYLOAD3_HEX);
|
||||||
|
let d = decode_signing_payload(&payload).expect("decodes");
|
||||||
|
assert_eq!(d.asset_id, [3u8; 32]);
|
||||||
|
assert_eq!(d.verdict, 2); // Failed
|
||||||
|
assert_eq!(d.severity, 2); // Critical
|
||||||
|
assert!(!d.is_anchorable(), "Failed/Critical NO anchorable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signed_message_structure_tag_genesis_payload() {
|
||||||
|
let payload = unhex(PAYLOAD1_HEX);
|
||||||
|
let genesis = [0xABu8; 32];
|
||||||
|
let msg = attestation_signed_message(&payload, &genesis);
|
||||||
|
assert_eq!(msg.len(), ATTESTATION_DOMAIN_TAG.len() + 32 + payload.len());
|
||||||
|
assert!(msg.starts_with(ATTESTATION_DOMAIN_TAG));
|
||||||
|
let after_tag = &msg[ATTESTATION_DOMAIN_TAG.len()..];
|
||||||
|
assert_eq!(&after_tag[..32], &genesis);
|
||||||
|
assert_eq!(&after_tag[32..], &payload[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signed_message_genesis_binds_anti_replay() {
|
||||||
|
let payload = unhex(PAYLOAD1_HEX);
|
||||||
|
let a = attestation_signed_message(&payload, &[0xAAu8; 32]);
|
||||||
|
let b = attestation_signed_message(&payload, &[0xBBu8; 32]);
|
||||||
|
assert_ne!(a, b, "distinto genesis ⇒ distinto mensaje (cierra replay cross-chain)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_truncated() {
|
||||||
|
let payload = unhex(PAYLOAD1_HEX);
|
||||||
|
// Truncar el último byte (constraints_checked varint) → se acaba antes.
|
||||||
|
let err = decode_signing_payload(&payload[..payload.len() - 1]).unwrap_err();
|
||||||
|
assert_eq!(err, SigningPayloadError::UnexpectedEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_trailing_bytes() {
|
||||||
|
let mut payload = unhex(PAYLOAD1_HEX);
|
||||||
|
payload.push(0x00);
|
||||||
|
assert_eq!(decode_signing_payload(&payload), Err(SigningPayloadError::TrailingBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_oversized_identifier() {
|
||||||
|
// schema "1.0.0" (ok) + asset_id 32B + identifier con len 200 (> 64 cota).
|
||||||
|
let mut payload = unhex("05312e302e30");
|
||||||
|
payload.extend_from_slice(&[1u8; 32]);
|
||||||
|
payload.push(0xC8); // varint 200 = 0xC8 0x01
|
||||||
|
payload.push(0x01);
|
||||||
|
assert_eq!(decode_signing_payload(&payload), Err(SigningPayloadError::LengthTooLarge));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_empty() {
|
||||||
|
assert_eq!(decode_signing_payload(&[]), Err(SigningPayloadError::UnexpectedEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_overlong_u32_varint_for_verdict() {
|
||||||
|
// Fidelidad postcard: el verdict (enum discriminant u32) NO admite
|
||||||
|
// varint sobre-largo. Construyo P1 reemplazando el verdict (1 byte 0x00,
|
||||||
|
// penúltimo del payload antes de severity+constraints) por un varint de
|
||||||
|
// 6 bytes que postcard::from_bytes rechazaría. Reconstruyo el prefijo
|
||||||
|
// hasta verdict y le añado el verdict sobre-largo + severity + constraints.
|
||||||
|
// schema(6) + asset(32) + identifier(21) + level(1) + pubkey(32) + ts(5)
|
||||||
|
let mut p = unhex("05312e302e30"); // schema "1.0.0"
|
||||||
|
p.extend_from_slice(&[1u8; 32]); // asset_id
|
||||||
|
p.push(0x14); // identifier len 20
|
||||||
|
p.extend_from_slice(b"atelier_canonical_v1");
|
||||||
|
p.push(0x03); // level
|
||||||
|
p.extend_from_slice(&unhex("ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c"));
|
||||||
|
p.extend_from_slice(&unhex("d285d8cc04")); // validated_at_us varint
|
||||||
|
// verdict = 0 pero codificado sobre-largo en 6 bytes (5º byte presente).
|
||||||
|
p.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x00]);
|
||||||
|
p.push(0x00); // severity Info
|
||||||
|
p.push(0x2a); // constraints 42
|
||||||
|
assert_eq!(decode_signing_payload(&p), Err(SigningPayloadError::VarintOverflow));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,9 @@ pub mod accounts;
|
|||||||
pub mod actions;
|
pub mod actions;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod attestation;
|
pub mod attestation;
|
||||||
|
/// FIX-4: verificador de attestation on-chain — mensaje domain-separado +
|
||||||
|
/// decoder postcard secuencial del SigningPayload de Ergon (espejo ASP-3).
|
||||||
|
pub mod attestation_verify;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod fees;
|
pub mod fees;
|
||||||
pub mod governance;
|
pub mod governance;
|
||||||
@@ -58,6 +61,10 @@ pub use assets::AssetHash;
|
|||||||
pub use attestation::{
|
pub use attestation::{
|
||||||
Blake3Hash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict,
|
Blake3Hash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict,
|
||||||
};
|
};
|
||||||
|
pub use attestation_verify::{
|
||||||
|
attestation_signed_message, decode_signing_payload, DecodedSigningPayload,
|
||||||
|
SigningPayloadError, ATTESTATION_DOMAIN_TAG,
|
||||||
|
};
|
||||||
pub use errors::ChainError;
|
pub use errors::ChainError;
|
||||||
pub use fees::{FeeCalculator, OperationKind};
|
pub use fees::{FeeCalculator, OperationKind};
|
||||||
pub use governance::{GovernanceProposal, ProposalId, Vote, VoteStance};
|
pub use governance::{GovernanceProposal, ProposalId, Vote, VoteStance};
|
||||||
|
|||||||
Reference in new issue
Block a user