Skip to main content

sumchain_primitives/
receipt.rs

1//! Transaction receipts for SUM Chain.
2//!
3//! Receipts record the outcome of transaction execution,
4//! including success/failure status and any fees paid.
5
6use serde::{Deserialize, Serialize};
7
8use crate::{Balance, BlockHeight, Hash};
9
10/// Status of a transaction after execution
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum TxStatus {
13    /// Transaction executed successfully
14    Success,
15    /// Transaction failed - invalid signature
16    InvalidSignature,
17    /// Transaction failed - wrong nonce
18    InvalidNonce,
19    /// Transaction failed - insufficient balance
20    InsufficientBalance,
21    /// Transaction failed - invalid chain ID
22    InvalidChainId,
23    /// Transaction failed - other reason
24    Failed(u32),
25}
26
27impl TxStatus {
28    /// Check if transaction succeeded
29    pub fn is_success(&self) -> bool {
30        matches!(self, TxStatus::Success)
31    }
32
33    /// Get a human-readable description.
34    ///
35    /// `Failed(u32)` codes are mapped here so `chain_getTransactionStatus`
36    /// (and any other receipt-surfacing path) emits the specific reason.
37    /// Allocated codes:
38    ///
39    /// * `22` — V2 `RegisterEncryptionKey`: rejected a low/small-order
40    ///   X25519 public key. Validation lives in
41    ///   `sumchain_crypto::is_low_order_x25519_public_key` (separate crate;
42    ///   `sumchain-primitives` does not depend on `sumchain-crypto`, so this
43    ///   is a plain reference rather than an intra-doc link).
44    ///
45    /// Allocated codes (kept in sync with executor dispatch):
46    ///
47    /// * `20` — V2 NodeRegistry dispatch failed (generic) — falls through to
48    ///   `"failed"` until per-op reasons are added.
49    /// * `21` — V2 StorageMetadata dispatch failed (generic) — falls through.
50    /// * `22` — `RegisterEncryptionKey` rejected a low/small-order X25519
51    ///   public key. See `sumchain_crypto::is_low_order_x25519_public_key`.
52    /// * `30` — `RegisterFilePendingV2` validity failure (size/chunk caps,
53    ///   visibility/bundle/owner rules, recipient X25519 missing, collision).
54    /// * `31` — `AbandonFileV2` validity failure (state/owner/grace).
55    /// * `32` — V2 storage op accepted by the dispatcher but not yet
56    ///   implemented in the current checkpoint (placeholder for 1c stubs).
57    /// * `33` — `AcceptAssignmentV2` validity failure (file state, snapshot
58    ///   membership, per-tx cap, index range, index-not-assigned).
59    /// * `34` — `ActivateFileV2` validity failure (state/owner/incomplete
60    ///   chunk coverage).
61    /// * `35` — `AddAccessV2` / `RemoveAccessV2` / `UpdateAccessV2` validity
62    ///   failure (file state/owner/visibility-bundle/X25519/duplicate/missing/
63    ///   byte-cap).
64    /// * `40` — V2 storage protocol not enabled at this block height. Set
65    ///   `v2_enabled_from_height` in the chain's genesis to opt in.
66    ///   Distinct from validity codes 30–35: this is a chain-level gate
67    ///   rejection, no fee consumed; safe to retry after activation.
68    pub fn description(&self) -> &'static str {
69        match self {
70            TxStatus::Success => "success",
71            TxStatus::InvalidSignature => "invalid signature",
72            TxStatus::InvalidNonce => "invalid nonce",
73            TxStatus::InsufficientBalance => "insufficient balance",
74            TxStatus::InvalidChainId => "invalid chain id",
75            TxStatus::Failed(22) => "low-order x25519 public key rejected",
76            TxStatus::Failed(30) => "RegisterFilePendingV2 validity check failed",
77            TxStatus::Failed(31) => "AbandonFileV2 validity check failed",
78            TxStatus::Failed(32) => "V2 storage op not yet implemented",
79            TxStatus::Failed(33) => "AcceptAssignmentV2 validity check failed",
80            TxStatus::Failed(34) => "ActivateFileV2 validity check failed",
81            TxStatus::Failed(35) => "V2 access op validity check failed",
82            TxStatus::Failed(40) => "V2 storage protocol not enabled at this height",
83            // OmniNode `InferenceAttestation` subprotocol failures.
84            TxStatus::Failed(50) => "OmniNode subprotocol not enabled at this block height",
85            TxStatus::Failed(51) => "duplicate InferenceAttestation for (session_id, verifier)",
86            TxStatus::Failed(52) => "invalid OmniNode Stage 6 verifier signature",
87            TxStatus::Failed(53) => "tx sender does not match verifier address (Ed25519 pubkey hash)",
88            // SRC-817/818 Education-LMS suite failures (Phase 2).
89            TxStatus::Failed(70) => "education subprotocol not enabled at this block height",
90            TxStatus::Failed(71) => "malformed education payload",
91            TxStatus::Failed(72) => "unsupported education operation",
92            TxStatus::Failed(73) => "catalog entry not found",
93            TxStatus::Failed(74) => "catalog entry in wrong state for operation",
94            TxStatus::Failed(75) => "offering not found",
95            TxStatus::Failed(76) => "offering in wrong state for operation",
96            TxStatus::Failed(77) => "assessment not found or wrong kind",
97            TxStatus::Failed(78) => "assessment submission window closed",
98            TxStatus::Failed(79) => "student commitment not enrolled in offering",
99            TxStatus::Failed(80) => "submission attempts exhausted",
100            TxStatus::Failed(81) => "duplicate education record",
101            TxStatus::Failed(82) => "invalid reference (enrollment/employment/catalog)",
102            TxStatus::Failed(83) => "not authorized for education operation",
103            TxStatus::Failed(84) => "insufficient balance for education fee",
104            TxStatus::Failed(_) => "failed",
105        }
106    }
107}
108
109/// Receipt for an executed transaction
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct Receipt {
112    /// Hash of the transaction
113    pub tx_hash: Hash,
114    /// Block height where tx was included
115    pub block_height: BlockHeight,
116    /// Index of tx within the block
117    pub tx_index: u32,
118    /// Execution status
119    pub status: TxStatus,
120    /// Fee actually paid (may differ if tx failed early)
121    pub fee_paid: Balance,
122}
123
124impl Receipt {
125    /// Create a new receipt
126    pub fn new(
127        tx_hash: Hash,
128        block_height: BlockHeight,
129        tx_index: u32,
130        status: TxStatus,
131        fee_paid: Balance,
132    ) -> Self {
133        Self {
134            tx_hash,
135            block_height,
136            tx_index,
137            status,
138            fee_paid,
139        }
140    }
141
142    /// Create a success receipt
143    pub fn success(
144        tx_hash: Hash,
145        block_height: BlockHeight,
146        tx_index: u32,
147        fee_paid: Balance,
148    ) -> Self {
149        Self::new(tx_hash, block_height, tx_index, TxStatus::Success, fee_paid)
150    }
151
152    /// Check if the transaction succeeded
153    pub fn is_success(&self) -> bool {
154        self.status.is_success()
155    }
156
157    /// Serialize to bytes
158    pub fn to_bytes(&self) -> Vec<u8> {
159        bincode::serialize(self).expect("Receipt serialization should not fail")
160    }
161
162    /// Deserialize from bytes
163    pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
164        bincode::deserialize(bytes)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_status_is_success() {
174        assert!(TxStatus::Success.is_success());
175        assert!(!TxStatus::InvalidNonce.is_success());
176        assert!(!TxStatus::InsufficientBalance.is_success());
177    }
178
179    #[test]
180    fn test_receipt_serialization() {
181        let receipt = Receipt::success(Hash::hash(b"tx"), 100, 0, 10);
182        let bytes = receipt.to_bytes();
183        let receipt2 = Receipt::from_bytes(&bytes).unwrap();
184        assert_eq!(receipt, receipt2);
185    }
186}