keetanetwork-block 0.2.0

Block structure and operations for Keetanetwork blockchain
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
//! Error types for block construction, serialization and validation.

use alloc::string::ToString;

use keetanetwork_account::AccountError;
use keetanetwork_asn1::Asn1Error;
use keetanetwork_crypto::error::CryptoError;
use keetanetwork_crypto::hash::BlockHash;
use keetanetwork_error::KeetaNetError;
use keetanetwork_utils::impl_source_error_from;
use snafu::Snafu;

/// Block field names used in structured errors.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockField {
	/// Block version
	Version,
	/// Network identifier
	Network,
	/// Block account
	Account,
	/// Previous block hash
	Previous,
	/// Block signer
	Signer,
	/// Block date
	Date,
}

/// Account info fields validated by `SET_INFO`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InfoField {
	/// Account name
	Name,
	/// Account description
	Description,
	/// Account metadata
	Metadata,
}

impl core::fmt::Display for BlockField {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		let name = match self {
			BlockField::Version => "version",
			BlockField::Network => "network",
			BlockField::Account => "account",
			BlockField::Previous => "previous",
			BlockField::Signer => "signer",
			BlockField::Date => "date",
		};
		write!(f, "{name}")
	}
}

impl core::fmt::Display for InfoField {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		let name = match self {
			InfoField::Name => "name",
			InfoField::Description => "description",
			InfoField::Metadata => "metadata",
		};
		write!(f, "{name}")
	}
}

/// Errors produced by the block crate.
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum BlockError {
	/// ASN.1 serialization or deserialization failed
	#[snafu(display("ASN.1 codec error: {source}"))]
	Codec {
		/// Underlying codec error
		source: Asn1Error,
	},
	/// Account operation failed
	#[snafu(display("account error: {source}"))]
	Account {
		/// Underlying account error
		source: AccountError,
	},
	/// Cryptographic operation failed
	#[snafu(display("crypto error: {source}"))]
	Crypto {
		/// Underlying crypto error
		source: CryptoError,
	},
	/// X.509 certificate handling failed
	#[snafu(display("certificate error: {source}"))]
	Certificate {
		/// Underlying certificate error
		source: keetanetwork_x509::error::CertificateError,
	},

	/// Unsupported block version
	#[snafu(display("unsupported block version"))]
	InvalidVersion,
	/// Block references itself as previous
	#[snafu(display("block references itself as previous"))]
	PreviousSelf,
	/// Network identifier must not be negative
	#[snafu(display("network ID must be a positive number"))]
	NegativeNetwork,
	/// Subnet identifier must not be negative
	#[snafu(display("subnet ID must be a positive number"))]
	NegativeSubnet,
	/// Network identifier is not a known network
	#[snafu(display("unknown network ID"))]
	UnknownNetwork,
	/// Blocks cannot be created for multisig accounts
	#[snafu(display("cannot create a block for a multisig account"))]
	MultisigAccountForbidden,
	/// A required builder field is missing
	#[snafu(display("missing required block field: {field}"))]
	MissingField {
		/// The missing field
		field: BlockField,
	},
	/// Account field redundantly repeats the signer (or vice versa)
	#[snafu(display("account and signer must not both be present when equal"))]
	RedundantAccountField,

	/// Block requires signatures but has none
	#[snafu(display("block has not been signed"))]
	SignatureRequired,
	/// Signature count does not match required signer count
	#[snafu(display("signer count {expected} does not match signature count {actual}"))]
	SignatureCountMismatch {
		/// Required signer count
		expected: usize,
		/// Provided signature count
		actual: usize,
	},
	/// A signature failed verification
	#[snafu(display("unable to validate signature at index {index} for block {hash}"))]
	InvalidSignature {
		/// Index of the failing signature
		index: usize,
		/// Hash of the block being verified
		hash: BlockHash,
	},
	/// Signature has the wrong length
	#[snafu(display("signature must be exactly 64 bytes, got {length}"))]
	InvalidSignatureLength {
		/// Provided length
		length: usize,
	},
	/// A signature sequence must contain more than one signature
	#[snafu(display("signature sequence must contain more than one signature"))]
	InvalidSignatureSequence,
	/// Decoded bytes do not match the re-encoded canonical form
	#[snafu(display("block bytes do not match recalculated bytes"))]
	RecalculatedBytesMismatch,
	/// V1 blocks support exactly one signer and signature
	#[snafu(display("V1 blocks support only a single signer and signature"))]
	V1SingleSignerOnly,
	/// V1 blocks only support the generic purpose
	#[snafu(display("V1 block purpose must be generic"))]
	V1PurposeInvalid,

	/// Idempotent key exceeds the maximum length
	#[snafu(display("idempotent key length {length} exceeds maximum {max}"))]
	IdempotentTooLong {
		/// Provided length
		length: usize,
		/// Maximum permitted length
		max: usize,
	},
	/// FEE purpose blocks may only contain SEND operations
	#[snafu(display("FEE purpose block contains non-SEND operation at index {operation_index}"))]
	FeePurposeRequiresSend {
		/// Index of the offending operation
		operation_index: usize,
	},
	/// Unknown block purpose value
	#[snafu(display("invalid block purpose"))]
	InvalidPurpose,
	/// Unknown operation type tag
	#[snafu(display("invalid operation type"))]
	InvalidOperationType,
	/// Unknown adjust method value
	#[snafu(display("invalid adjust method"))]
	InvalidAdjustMethod,
	/// SET adjust method is forbidden for this operation
	#[snafu(display("cannot use the SET adjust method for this operation"))]
	AdjustMethodSetForbidden,

	/// Multisig signer tree exceeds the maximum depth
	#[snafu(display("multisig signer depth {depth} exceeds maximum {max}"))]
	MultisigSignerDepthExceeded {
		/// Observed depth
		depth: u64,
		/// Maximum permitted depth
		max: u64,
	},
	/// Multisig signer count out of range
	#[snafu(display("multisig signer count {count} out of range [1, {max}]"))]
	MultisigSignerCountInvalid {
		/// Observed count
		count: u64,
		/// Maximum permitted count
		max: u64,
	},
	/// Duplicate signer within a multisig level
	#[snafu(display("duplicate multisig signer"))]
	MultisigSignerDuplicate,
	/// Multisig quorum out of range
	#[snafu(display("multisig quorum out of range"))]
	MultisigQuorumInvalid,
	/// Malformed multisig signer structure
	#[snafu(display("malformed multisig signer structure"))]
	MalformedSigner,

	/// Amounts must not be negative
	#[snafu(display("amount cannot be negative"))]
	AmountBelowZero,
	/// Token supply exceeds maximum
	#[snafu(display("supply exceeds maximum value"))]
	SupplyInvalid,
	/// SEND/RECEIVE token field must reference a token account
	#[snafu(display("token field must reference a token account"))]
	TokenFieldNotToken,
	/// Token accounts cannot use this operation
	#[snafu(display("token accounts cannot use this operation"))]
	TokenOperationForbidden,
	/// Only token accounts can use this operation
	#[snafu(display("only token accounts can use this operation"))]
	TokenAccountRequired,
	/// Sending a token to a different token account
	#[snafu(display("cannot send a token to a token account different from itself"))]
	TokenReceiveDiffers,
	/// External data exceeds the maximum length
	#[snafu(display("external length {length} exceeds maximum {max}"))]
	ExternalTooLong {
		/// Provided length
		length: usize,
		/// Maximum permitted length
		max: usize,
	},
	/// External data contains invalid characters
	#[snafu(display("external has invalid characters"))]
	ExternalInvalid,
	/// External data is required
	#[snafu(display("external is required when using SEND"))]
	ExternalMissing,
	/// RECEIVE cannot forward to self
	#[snafu(display("cannot use forward field to send to self"))]
	ForwardToSelf,
	/// RECEIVE forward requires exact
	#[snafu(display("cannot use forward field without exact being set to true"))]
	ForwardRequiresExact,
	/// Account type cannot delegate or target is an identifier
	#[snafu(display("identifier accounts cannot be used for delegation"))]
	IdentifierDelegationForbidden,
	/// SET_REP may only appear once per block
	#[snafu(display("SET_REP may only be used once per block"))]
	MultipleSetRep,
	/// Account info field fails format validation
	#[snafu(display("{field} does not fit the required format"))]
	InfoFieldInvalid {
		/// The failing field
		field: InfoField,
	},
	/// Identifier accounts require default permissions in SET_INFO
	#[snafu(display("identifier accounts need default permissions in SET_INFO"))]
	DefaultPermissionRequired,
	/// Only identifier accounts may use this construct
	#[snafu(display("only identifier accounts may use this construct"))]
	IdentifierAccountRequired,
	/// Requested identifier does not match the derived identifier
	#[snafu(display("requested identifier is not valid"))]
	IdentifierInvalid,
	/// Invalid create-identifier arguments
	#[snafu(display("invalid create identifier arguments"))]
	InvalidCreateIdentifierArguments,
	/// Invalid principal for MODIFY_PERMISSIONS
	#[snafu(display("invalid principal for MODIFY_PERMISSIONS"))]
	InvalidPrincipal,
	/// Method must be SET when permissions are absent
	#[snafu(display("method must be SET when permissions are absent"))]
	PermissionsRequireSet,
	/// Cannot set admin or higher with a target specified
	#[snafu(display("cannot set admin or higher with a target specified"))]
	AdminWithTarget,
	/// Delegation not allowed for these permission flags
	#[snafu(display("cannot use delegation for these permission flags"))]
	DelegationForbidden,
	/// Duplicate permission modification for the same principal/target
	#[snafu(display("cannot have a SET after another change with the same target in MODIFY_PERMISSIONS"))]
	DuplicatePermissionModification,

	/// Base permission flags from different groups cannot be mixed
	#[snafu(display("cannot mix base permission flags with different groups"))]
	PermissionsCannotMix,
	/// External permission offsets exceed the maximum
	#[snafu(display("external permission size {size} exceeds maximum offset {max}"))]
	PermissionsExternalOffsetTooLarge {
		/// Observed bitfield size
		size: u64,
		/// Maximum permitted offset
		max: u64,
	},
	/// Permissions are not valid as defaults
	#[snafu(display("permissions are not valid as default permissions"))]
	PermissionsInvalidDefault,
	/// Entity account does not match the permission flag group
	#[snafu(display("incorrect entity for permission flags"))]
	PermissionsInvalidEntity,
	/// Principal account does not match the permission flag group
	#[snafu(display("incorrect principal for permission flags"))]
	PermissionsInvalidPrincipal,
	/// Target account does not match the permission flag group
	#[snafu(display("incorrect target for permission flags"))]
	PermissionsInvalidTarget,
	/// External permissions cannot be used for default permissions
	#[snafu(display("cannot set default permissions with external permissions"))]
	PermissionsExternalDefaultForbidden,

	/// Certificate value invalid for the requested method
	#[snafu(display("invalid certificate value for MANAGE_CERTIFICATE"))]
	InvalidCertificateValue,
	/// Certificate subject does not match the block account
	#[snafu(display("certificate subject does not match the block account"))]
	CertificateSubjectMismatch,
	/// Intermediate certificates only valid on ADD
	#[snafu(display("intermediate certificates can only be provided when adding"))]
	IntermediateCertificatesOnlyAdd,
	/// The same certificate cannot be operated on twice in one block
	#[snafu(display("cannot operate on the same certificate twice"))]
	DuplicateCertificateOperation,
}

impl_source_error_from!(BlockError, {
	Asn1Error => Codec,
	AccountError => Account,
	CryptoError => Crypto,
});

impl_source_error_from!(BlockError, {
	keetanetwork_x509::error::CertificateError => Certificate,
});

impl BlockError {
	/// The TypeScript-compatible error code for this error, when one exists.
	pub fn code(&self) -> Option<&'static str> {
		let code = match self {
			BlockError::InvalidVersion => "BLOCK_INVALID_VERSION",
			BlockError::PreviousSelf => "BLOCK_PREVIOUS_SELF",
			BlockError::MultisigAccountForbidden => "BLOCK_NO_MULTISIG_OP",
			BlockError::SignatureRequired => "BLOCK_SIGNATURE_REQUIRED",
			BlockError::SignatureCountMismatch { .. } | BlockError::V1SingleSignerOnly => "BLOCK_INVALID_SIGNER",
			BlockError::InvalidSignature { .. } | BlockError::RecalculatedBytesMismatch => "BLOCK_INVALID_SIGNATURE",
			BlockError::IdempotentTooLong { .. } => "BLOCK_INVALID_IDEMPOTENT_LENGTH",
			BlockError::FeePurposeRequiresSend { .. } | BlockError::V1PurposeInvalid => {
				"BLOCK_INVALID_PURPOSE_VALIDATION"
			}
			BlockError::InvalidOperationType => "BLOCK_INVALID_TYPE",
			BlockError::MultisigSignerDepthExceeded { .. } => "BLOCK_INVALID_MULTISIG_SIGNER_DEPTH",
			BlockError::MultisigSignerCountInvalid { .. } => "BLOCK_INVALID_MULTISIG_SIGNER_COUNT",
			BlockError::MultisigSignerDuplicate => "BLOCK_INVALID_MULTISIG_SIGNER_DUPLICATE",
			BlockError::MultisigQuorumInvalid => "BLOCK_INVALID_MULTISIG_QUORUM",
			BlockError::AmountBelowZero => "BLOCK_AMOUNT_BELOW_ZERO",
			BlockError::SupplyInvalid => "BLOCK_SUPPLY_INVALID",
			BlockError::TokenFieldNotToken => "BLOCK_CANNOT_SEND_NON_TOKEN",
			BlockError::TokenOperationForbidden => "BLOCK_NO_TOKEN_OP",
			BlockError::TokenAccountRequired => "BLOCK_ONLY_TOKEN_OP",
			BlockError::TokenReceiveDiffers => "BLOCK_TOKEN_RECEIVE_DIFFERS",
			BlockError::ExternalTooLong { .. } => "BLOCK_EXTERNAL_TOO_LONG",
			BlockError::ExternalInvalid => "BLOCK_EXTERNAL_INVALID",
			BlockError::ExternalMissing => "BLOCK_EXTERNAL_MISSING",
			BlockError::ForwardToSelf => "BLOCK_CANNOT_FORWARD_TO_SELF",
			BlockError::ForwardRequiresExact => "BLOCK_EXACT_TRUE_WHEN_FORWARDING",
			BlockError::IdentifierDelegationForbidden => "BLOCK_NO_IDENTIFIER_OP",
			BlockError::MultipleSetRep => "BLOCK_NO_MULTIPLE_SET_REP",
			BlockError::InfoFieldInvalid { .. } | BlockError::PermissionsExternalDefaultForbidden => {
				"BLOCK_GENERAL_FIELD_INVALID"
			}
			BlockError::DefaultPermissionRequired => "BLOCK_IDENTIFIER_NEED_DEFAULT_PERMISSIONS",
			BlockError::IdentifierAccountRequired => "BLOCK_ONLY_IDENTIFIER_OP",
			BlockError::IdentifierInvalid => "BLOCK_IDENTIFIER_INVALID",
			BlockError::InvalidCreateIdentifierArguments => "BLOCK_INVALID_CREATE_IDENTIFIER_ARGS",
			BlockError::InvalidPrincipal => "BLOCK_INVALID_PRINCIPAL",
			BlockError::AdminWithTarget => "BLOCK_NO_ADMIN_ON_TARGET",
			BlockError::DelegationForbidden => "BLOCK_NO_DELEGATE_ADMIN",
			BlockError::DuplicatePermissionModification => "BLOCK_NO_MODIFY_PERMISSION_DUPE",
			BlockError::PermissionsCannotMix => "PERMISSIONS_CANNOT_MIX_FLAGS_AND_TYPES",
			BlockError::PermissionsExternalOffsetTooLarge { .. } => "PERMISSIONS_EXTERNAL_OFFSET_TOO_LARGE",
			BlockError::PermissionsInvalidDefault => "BLOCK_PERMISSIONS_INVALID_DEFAULT",
			BlockError::PermissionsInvalidEntity => "BLOCK_PERMISSIONS_INVALID_ENTITY",
			BlockError::PermissionsInvalidPrincipal => "BLOCK_PERMISSIONS_INVALID_PRINCIPAL",
			BlockError::PermissionsInvalidTarget => "BLOCK_PERMISSIONS_INVALID_TARGET",
			BlockError::InvalidCertificateValue => "BLOCK_INVALID_CERTIFICATE_VALUE",
			BlockError::CertificateSubjectMismatch => "BLOCK_CERTIFICATE_SUBJECT_MISMATCH",
			BlockError::IntermediateCertificatesOnlyAdd => "BLOCK_INTERMEDIATE_CERTIFICATES_ONLY_ADD",
			BlockError::DuplicateCertificateOperation => "BLOCK_NO_DUPLICATE_CERTIFICATE_OPERATION",
			_ => return None,
		};

		Some(code)
	}
}

impl From<BlockError> for KeetaNetError {
	fn from(error: BlockError) -> Self {
		if let Some(code) = error.code() {
			KeetaNetError::Code { code: code.to_string(), message: error.to_string() }
		} else {
			KeetaNetError::Unknown { msg: error.to_string() }
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_code_mapping() {
		assert_eq!(BlockError::InvalidVersion.code(), Some("BLOCK_INVALID_VERSION"));
		assert_eq!(BlockError::PreviousSelf.code(), Some("BLOCK_PREVIOUS_SELF"));
		assert_eq!(BlockError::NegativeNetwork.code(), None);
	}

	#[test]
	fn test_keetanet_error_bridge() {
		let bridged = KeetaNetError::from(BlockError::PreviousSelf);
		assert!(matches!(bridged, KeetaNetError::Code { code, .. } if code == "BLOCK_PREVIOUS_SELF"));

		let unknown = KeetaNetError::from(BlockError::NegativeNetwork);
		assert!(matches!(unknown, KeetaNetError::Unknown { .. }));
	}

	#[test]
	fn test_source_conversions() {
		let codec_error = Asn1Error::RasnError { reason: "boom".to_string() };
		assert!(matches!(BlockError::from(codec_error), BlockError::Codec { .. }));
		assert!(matches!(BlockError::from(AccountError::InvalidKeyType), BlockError::Account { .. }));
		assert!(matches!(BlockError::from(CryptoError::InvalidInput), BlockError::Crypto { .. }));
	}
}