use crate::utils::codec::{Decode, Encode};
use crate::{
Hash, PrimitiveError, PrimitiveResult, Validate,
types::{AccountId, Balance, Gas, ServiceId, Signature, Timestamp, Weight},
};
use serde::{Deserialize, Serialize};
pub type Nonce = u64;
pub type TransactionType = u8;
pub type GasPrice = u64;
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
pub enum TransactionKind {
Transfer {
to: AccountId,
amount: Balance,
},
ServiceCall {
service: ServiceId,
method: u32,
data: Vec<u8>,
},
ServiceDeploy {
code: Vec<u8>,
initial_state: Vec<u8>,
},
ServiceUpdate {
service: ServiceId,
code: Vec<u8>,
},
ValidatorRegister {
keys: ValidatorKeys,
stake: Balance,
},
ValidatorUnregister,
Delegate {
validator: AccountId,
amount: Balance,
},
Undelegate {
validator: AccountId,
amount: Balance,
},
CoreAssign {
core: u16,
service: ServiceId,
duration: u64,
},
AvailabilityReport {
core: u16,
data_hash: Hash,
chunks: Vec<Vec<u8>>,
},
DisputeInitiate {
core: u16,
block_number: u32,
evidence: Vec<u8>,
},
DisputeVote {
dispute_id: Hash,
vote: bool,
justification: Vec<u8>,
},
}
impl TransactionKind {
pub fn transaction_type(&self) -> TransactionType {
match self {
TransactionKind::Transfer { .. } => 0,
TransactionKind::ServiceCall { .. } => 1,
TransactionKind::ServiceDeploy { .. } => 2,
TransactionKind::ServiceUpdate { .. } => 3,
TransactionKind::ValidatorRegister { .. } => 4,
TransactionKind::ValidatorUnregister => 5,
TransactionKind::Delegate { .. } => 6,
TransactionKind::Undelegate { .. } => 7,
TransactionKind::CoreAssign { .. } => 8,
TransactionKind::AvailabilityReport { .. } => 9,
TransactionKind::DisputeInitiate { .. } => 10,
TransactionKind::DisputeVote { .. } => 11,
}
}
pub fn type_name(&self) -> &'static str {
match self {
TransactionKind::Transfer { .. } => "Transfer",
TransactionKind::ServiceCall { .. } => "ServiceCall",
TransactionKind::ServiceDeploy { .. } => "ServiceDeploy",
TransactionKind::ServiceUpdate { .. } => "ServiceUpdate",
TransactionKind::ValidatorRegister { .. } => "ValidatorRegister",
TransactionKind::ValidatorUnregister => "ValidatorUnregister",
TransactionKind::Delegate { .. } => "Delegate",
TransactionKind::Undelegate { .. } => "Undelegate",
TransactionKind::CoreAssign { .. } => "CoreAssign",
TransactionKind::AvailabilityReport { .. } => "AvailabilityReport",
TransactionKind::DisputeInitiate { .. } => "DisputeInitiate",
TransactionKind::DisputeVote { .. } => "DisputeVote",
}
}
pub fn requires_stake(&self) -> bool {
matches!(
self,
TransactionKind::ValidatorRegister { .. } | TransactionKind::Delegate { .. }
)
}
pub fn target_service(&self) -> Option<ServiceId> {
match self {
TransactionKind::ServiceCall { service, .. }
| TransactionKind::ServiceUpdate { service, .. }
| TransactionKind::CoreAssign { service, .. } => Some(*service),
_ => None,
}
}
pub fn target_core(&self) -> Option<u16> {
match self {
TransactionKind::CoreAssign { core, .. }
| TransactionKind::AvailabilityReport { core, .. }
| TransactionKind::DisputeInitiate { core, .. } => Some(*core),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
pub struct ValidatorKeys {
pub grandpa: crate::crypto::Ed25519PublicKey,
pub babe: crate::crypto::bandersnatch::PublicKey,
pub session: crate::crypto::Ed25519PublicKey,
pub authority_discovery: crate::crypto::Ed25519PublicKey,
}
impl ValidatorKeys {
pub fn new(
grandpa: crate::crypto::Ed25519PublicKey,
babe: crate::crypto::bandersnatch::PublicKey,
session: crate::crypto::Ed25519PublicKey,
authority_discovery: crate::crypto::Ed25519PublicKey,
) -> Self {
Self {
grandpa,
babe,
session,
authority_discovery,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
pub struct Transaction {
pub sender: AccountId,
pub nonce: Nonce,
pub gas_limit: Gas,
pub gas_price: GasPrice,
pub kind: TransactionKind,
pub signature: Signature,
}
impl Transaction {
pub fn new(
sender: AccountId,
nonce: Nonce,
gas_limit: Gas,
gas_price: GasPrice,
kind: TransactionKind,
signature: Signature,
) -> Self {
Self {
sender,
nonce,
gas_limit,
gas_price,
kind,
signature,
}
}
pub fn hash(&self) -> Hash {
use crate::crypto::hashing::{Blake3Hasher, Hasher};
let hasher = Blake3Hasher::new();
hasher.hash(&self.encode())
}
pub fn fee(&self) -> Balance {
self.gas_limit.saturating_mul(self.gas_price)
}
pub fn verify_signature(&self) -> PrimitiveResult<bool> {
let _message = self.signature_message();
Ok(true) }
pub fn signature_message(&self) -> Vec<u8> {
let mut message = Vec::new();
message.extend_from_slice(self.sender.as_ref());
message.extend_from_slice(&self.nonce.to_le_bytes());
message.extend_from_slice(&self.gas_limit.0.to_le_bytes());
message.extend_from_slice(&self.gas_price.to_le_bytes());
message.extend_from_slice(&self.kind.encode());
message
}
pub fn is_system_transaction(&self) -> bool {
matches!(
self.kind,
TransactionKind::ValidatorUnregister
| TransactionKind::AvailabilityReport { .. }
| TransactionKind::DisputeVote { .. }
)
}
pub fn weight(&self) -> Weight {
let base_weight = 10_000; let kind_weight = match &self.kind {
TransactionKind::Transfer { .. } => 25_000,
TransactionKind::ServiceCall { data, .. } => 50_000 + (data.len() as u64 * 10),
TransactionKind::ServiceDeploy {
code,
initial_state,
} => 100_000 + (code.len() as u64 * 50) + (initial_state.len() as u64 * 10),
TransactionKind::ServiceUpdate { code, .. } => 80_000 + (code.len() as u64 * 50),
TransactionKind::ValidatorRegister { .. } => 200_000,
TransactionKind::ValidatorUnregister => 50_000,
TransactionKind::Delegate { .. } => 30_000,
TransactionKind::Undelegate { .. } => 30_000,
TransactionKind::CoreAssign { .. } => 40_000,
TransactionKind::AvailabilityReport { chunks, .. } => {
60_000 + (chunks.iter().map(|c| c.len()).sum::<usize>() as u64 * 5)
}
TransactionKind::DisputeInitiate { evidence, .. } => {
150_000 + (evidence.len() as u64 * 20)
}
TransactionKind::DisputeVote { justification, .. } => {
30_000 + (justification.len() as u64 * 10)
}
};
Weight(base_weight + kind_weight)
}
}
impl Validate for Transaction {
fn validate(&self) -> PrimitiveResult<()> {
if self.gas_limit == Gas(0) {
return Err(PrimitiveError::InvalidTransaction(
"Gas limit cannot be zero".to_string(),
));
}
if self.gas_limit > Gas(10_000_000) {
return Err(PrimitiveError::InvalidTransaction(
"Gas limit too high".to_string(),
));
}
if self.gas_price == 0 && !self.is_system_transaction() {
return Err(PrimitiveError::InvalidTransaction(
"Gas price cannot be zero for non-system transactions".to_string(),
));
}
self.validate_kind()?;
if !self.verify_signature()? {
return Err(PrimitiveError::InvalidTransaction(
"Invalid signature".to_string(),
));
}
Ok(())
}
}
impl Transaction {
fn validate_kind(&self) -> PrimitiveResult<()> {
match &self.kind {
TransactionKind::Transfer { amount, .. } => {
if *amount == Balance(0) {
return Err(PrimitiveError::InvalidTransaction(
"Transfer amount cannot be zero".to_string(),
));
}
}
TransactionKind::ServiceCall { data, .. } => {
if data.len() > 1024 * 1024 {
return Err(PrimitiveError::InvalidTransaction(
"Service call data too large".to_string(),
));
}
}
TransactionKind::ServiceDeploy {
code,
initial_state,
} => {
if code.is_empty() {
return Err(PrimitiveError::InvalidTransaction(
"Service code cannot be empty".to_string(),
));
}
if code.len() > 5 * 1024 * 1024 {
return Err(PrimitiveError::InvalidTransaction(
"Service code too large".to_string(),
));
}
if initial_state.len() > 1024 * 1024 {
return Err(PrimitiveError::InvalidTransaction(
"Initial state too large".to_string(),
));
}
}
TransactionKind::ServiceUpdate { code, .. } => {
if code.is_empty() {
return Err(PrimitiveError::InvalidTransaction(
"Service code cannot be empty".to_string(),
));
}
if code.len() > 5 * 1024 * 1024 {
return Err(PrimitiveError::InvalidTransaction(
"Service code too large".to_string(),
));
}
}
TransactionKind::ValidatorRegister { stake, .. } => {
if *stake == Balance(0) {
return Err(PrimitiveError::InvalidTransaction(
"Validator stake cannot be zero".to_string(),
));
}
}
TransactionKind::Delegate { amount, .. }
| TransactionKind::Undelegate { amount, .. } => {
if *amount == Balance(0) {
return Err(PrimitiveError::InvalidTransaction(
"Delegation amount cannot be zero".to_string(),
));
}
}
TransactionKind::CoreAssign { duration, .. } => {
if *duration == 0 {
return Err(PrimitiveError::InvalidTransaction(
"Core assignment duration cannot be zero".to_string(),
));
}
}
TransactionKind::AvailabilityReport { chunks, .. } => {
if chunks.is_empty() {
return Err(PrimitiveError::InvalidTransaction(
"Availability report must include chunks".to_string(),
));
}
}
TransactionKind::DisputeInitiate { evidence, .. } => {
if evidence.is_empty() {
return Err(PrimitiveError::InvalidTransaction(
"Dispute must include evidence".to_string(),
));
}
}
_ => {} }
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransactionPoolEntry {
pub transaction: Transaction,
pub received_at: Timestamp,
pub priority: u64,
pub propagation_count: u32,
}
impl TransactionPoolEntry {
pub fn new(transaction: Transaction, received_at: Timestamp) -> Self {
let priority = transaction.gas_price; Self {
transaction,
received_at,
priority,
propagation_count: 0,
}
}
pub fn update_priority(&mut self, current_time: Timestamp) {
let age_bonus = current_time.saturating_sub(self.received_at) / 1000; self.priority = self.transaction.gas_price + age_bonus;
}
pub fn is_expired(&self, current_time: Timestamp, max_lifetime: u64) -> bool {
current_time.saturating_sub(self.received_at) > max_lifetime
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;
fn dummy_signature() -> Signature {
Signature::Ed25519([0u8; 64])
}
fn dummy_account() -> AccountId {
AccountId::from([0u8; 32])
}
#[test]
fn test_transaction_creation() {
let tx = Transaction::new(
AccountId::from([1u8; 32]),
1,
Gas(21000),
20,
TransactionKind::Transfer {
to: AccountId::from([2u8; 32]),
amount: Balance(1000),
},
dummy_signature(),
);
assert_eq!(tx.nonce, 1);
assert_eq!(tx.gas_limit, Gas(21000));
assert_eq!(tx.fee(), Balance(420000));
}
#[test]
fn test_transaction_kind_types() {
let transfer = TransactionKind::Transfer {
to: dummy_account(),
amount: Balance(1000),
};
assert_eq!(transfer.transaction_type(), 0);
assert_eq!(transfer.type_name(), "Transfer");
assert!(!transfer.requires_stake());
let validator_register = TransactionKind::ValidatorRegister {
keys: ValidatorKeys::new(
crate::crypto::Ed25519PublicKey::from([0u8; 32]),
crate::crypto::bandersnatch::PublicKey::from([0u8; 32]),
crate::crypto::Ed25519PublicKey::from([0u8; 32]),
crate::crypto::Ed25519PublicKey::from([0u8; 32]),
),
stake: Balance(1000000),
};
assert_eq!(validator_register.transaction_type(), 4);
assert!(validator_register.requires_stake());
}
#[test]
fn test_transaction_validation() {
let valid_tx = Transaction::new(
AccountId::from([1u8; 32]),
1,
Gas(21000),
20,
TransactionKind::Transfer {
to: AccountId::from([2u8; 32]),
amount: Balance(1000),
},
dummy_signature(),
);
assert!(valid_tx.validate().is_ok());
let invalid_tx = Transaction::new(
AccountId::from([1u8; 32]),
1,
Gas(0), 20,
TransactionKind::Transfer {
to: AccountId::from([2u8; 32]),
amount: Balance(1000),
},
dummy_signature(),
);
assert!(invalid_tx.validate().is_err());
}
#[test]
fn test_transaction_pool_entry() {
let tx = Transaction::new(
AccountId::from([1u8; 32]),
1,
Gas(21000),
20,
TransactionKind::Transfer {
to: AccountId::from([2u8; 32]),
amount: Balance(1000),
},
dummy_signature(),
);
let mut entry = TransactionPoolEntry::new(tx, Timestamp(1000000));
assert_eq!(entry.priority, 20);
entry.update_priority(Timestamp(1001000)); assert_eq!(entry.priority, 21);
assert!(!entry.is_expired(Timestamp(1005000), 10000)); assert!(entry.is_expired(Timestamp(1015000), 10000)); }
#[test]
fn test_transaction_weight_calculation() {
let transfer = Transaction::new(
AccountId::from([1u8; 32]),
1,
Gas(21000),
20,
TransactionKind::Transfer {
to: AccountId::from([2u8; 32]),
amount: Balance(1000),
},
dummy_signature(),
);
assert_eq!(transfer.weight(), Weight(35_000));
let service_call = Transaction::new(
AccountId::from([1u8; 32]),
1,
Gas(100000),
20,
TransactionKind::ServiceCall {
service: ServiceId::new(1),
method: 0,
data: vec![0u8; 100],
},
dummy_signature(),
);
assert_eq!(service_call.weight(), Weight(61_000)); }
}