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