The transaction module provides the Transaction type and related functionality for creating, signing, and verifying blockchain transactions.
Overview
Transactions are the primary way users interact with the blockchain. They can transfer value, deploy contracts, or call contract functions.
Types
Transaction
pub struct Transaction {
pub nonce: u64,
pub from: Address,
pub to: Option<Address>,
pub value: u64,
pub data: Vec<u8>,
pub gas_limit: u64,
pub gas_price: u64,
pub signature: Signature,
}
Represents a transaction on the blockchain.
Sender’s nonce (sequence number). Must match the account’s current nonce.
Recipient’s address. None for contract deployment transactions.
Amount to transfer to the recipient.
Transaction data. Can be:
- Empty for simple value transfers
- Contract bytecode for deployments
- Calldata for contract calls
Maximum gas to use for this transaction.
Price per unit of gas (in smallest token unit).
Ed25519 signature over the transaction data.
Constructors
new
fn(nonce: u64, from: Address, to: Option<Address>, value: u64, data: Vec<u8>, gas_limit: u64, gas_price: u64) -> Self
Creates a new unsigned transaction.use minichain_core::Transaction;
use minichain_core::crypto::Address;
let tx = Transaction::new(
0, // nonce
sender_address, // from
Some(recipient_address), // to
1000, // value
vec![], // data
21_000, // gas_limit
1, // gas_price
);
transfer
fn(from: Address, to: Address, value: u64, nonce: u64, gas_price: u64) -> Self
Creates a value transfer transaction.Automatically sets gas_limit to 21,000 (the base cost for transfers).use minichain_core::Transaction;
let tx = Transaction::transfer(
sender_address,
recipient_address,
1000, // value
0, // nonce
1, // gas_price
);
assert!(tx.is_transfer());
assert_eq!(tx.gas_limit, 21_000);
deploy
fn(from: Address, bytecode: Vec<u8>, nonce: u64, gas_limit: u64, gas_price: u64) -> Self
Creates a contract deployment transaction.use minichain_core::Transaction;
let bytecode = vec![0x60, 0x80, 0x60, 0x40];
let tx = Transaction::deploy(
deployer_address,
bytecode,
0, // nonce
100_000, // gas_limit
1, // gas_price
);
assert!(tx.is_deploy());
assert!(tx.to.is_none());
call
fn(from: Address, to: Address, data: Vec<u8>, value: u64, nonce: u64, gas_limit: u64, gas_price: u64) -> Self
Creates a contract call transaction.use minichain_core::Transaction;
let calldata = vec![0x12, 0x34, 0x56, 0x78];
let tx = Transaction::call(
caller_address,
contract_address,
calldata,
0, // value
0, // nonce
50_000, // gas_limit
1, // gas_price
);
assert!(tx.is_call());
Methods
Computes the hash of the unsigned transaction data (for signing).This hash includes all transaction fields except the signature.let tx = Transaction::transfer(from, to, 100, 0, 1);
let hash = tx.signing_hash();
Computes the hash of the full transaction (including signature).This is the transaction ID used for lookups.let tx = Transaction::transfer(from, to, 100, 0, 1).signed(&keypair);
let tx_id = tx.hash();
println!("Transaction ID: {}", tx_id.to_hex());
sign
fn(&mut self, keypair: &Keypair)
Signs the transaction with the given keypair (mutates in place).let mut tx = Transaction::transfer(from, to, 100, 0, 1);
tx.sign(&keypair);
signed
fn(self, keypair: &Keypair) -> Self
Signs the transaction and returns it (builder pattern).let tx = Transaction::transfer(from, to, 100, 0, 1)
.signed(&keypair);
verify
fn(&self, public_key: &PublicKey) -> Result<(), TransactionError>
Verifies the transaction signature against a public key.let tx = Transaction::transfer(from, to, 100, 0, 1).signed(&keypair);
tx.verify(&keypair.public_key)?;
Checks if this is a contract deployment transaction.let tx = Transaction::deploy(from, bytecode, 0, 100_000, 1);
assert!(tx.is_deploy());
Checks if this is a simple value transfer (no data).let tx = Transaction::transfer(from, to, 100, 0, 1);
assert!(tx.is_transfer());
Checks if this is a contract call (has recipient and data).let tx = Transaction::call(from, to, calldata, 0, 0, 50_000, 1);
assert!(tx.is_call());
Calculates the maximum cost of this transaction (value + gas).let tx = Transaction::new(0, from, Some(to), 1000, vec![], 21_000, 2);
assert_eq!(tx.max_cost(), 1000 + 21_000 * 2);
contract_address
fn(&self) -> Option<Address>
Calculates the contract address for a deployment transaction.Returns None if this is not a deployment transaction.The address is derived from hash(sender || nonce).let tx = Transaction::deploy(from, bytecode, 0, 100_000, 1);
let contract_addr = tx.contract_address().unwrap();
println!("Contract will be deployed at: {}", contract_addr.to_hex());
Trait Implementations
- Debug, Clone, PartialEq, Eq: Standard derivations
- Serialize, Deserialize: Serde support
Errors
TransactionError
pub enum TransactionError {
InvalidSignature,
VerificationFailed,
MissingSignature,
}
The signature format is invalid
Signature verification failed (signature doesn’t match transaction/key)
Transaction has no signature
Usage Examples
Simple Value Transfer
use minichain_core::{Transaction, crypto::Keypair};
// Create keypairs
let alice = Keypair::generate();
let bob_address = Keypair::generate().address();
// Create and sign a transfer transaction
let tx = Transaction::transfer(
alice.address(),
bob_address,
1000, // transfer 1000 tokens
0, // nonce
1, // gas price
).signed(&alice);
// Verify signature
assert!(tx.verify(&alice.public_key).is_ok());
// Check transaction details
assert!(tx.is_transfer());
assert_eq!(tx.value, 1000);
assert_eq!(tx.gas_limit, 21_000);
Contract Deployment
use minichain_core::{Transaction, crypto::Keypair};
// Compile contract bytecode (simplified)
let bytecode = vec![
0x60, 0x80, 0x60, 0x40, // Push instructions
// ... more bytecode
];
// Create deployer
let deployer = Keypair::generate();
// Create deployment transaction
let tx = Transaction::deploy(
deployer.address(),
bytecode,
0, // nonce
100_000, // gas limit
1, // gas price
).signed(&deployer);
// Get the contract address (deterministic)
let contract_address = tx.contract_address().unwrap();
println!("Contract address: {}", contract_address.to_hex());
Contract Call
use minichain_core::{Transaction, crypto::Keypair};
use minichain_core::crypto::Address;
// Create caller
let caller = Keypair::generate();
// Contract address (from previous deployment)
let contract_address = Address::from_hex("0x1234...").unwrap();
// Prepare calldata (function selector + arguments)
let calldata = vec![0x12, 0x34, 0x56, 0x78];
// Create call transaction
let tx = Transaction::call(
caller.address(),
contract_address,
calldata,
0, // value (can send tokens with call)
0, // nonce
50_000, // gas limit
1, // gas price
).signed(&caller);
assert!(tx.is_call());
Transaction Verification
use minichain_core::{Transaction, crypto::Keypair};
let keypair = Keypair::generate();
let to_address = Keypair::generate().address();
// Create and sign transaction
let tx = Transaction::transfer(
keypair.address(),
to_address,
100,
0,
1,
).signed(&keypair);
// Verify with correct key
match tx.verify(&keypair.public_key) {
Ok(()) => println!("Signature valid"),
Err(e) => println!("Signature invalid: {}", e),
}
// Verify with wrong key fails
let other_keypair = Keypair::generate();
assert!(tx.verify(&other_keypair.public_key).is_err());
Checking Transaction Cost
use minichain_core::{Transaction, Account};
let mut account = Account::new_user(50_000);
let tx = Transaction::transfer(
account_address,
recipient,
1000,
0,
2, // gas price
);
// Check if account has enough balance
let max_cost = tx.max_cost();
if account.has_balance(max_cost) {
// Debit account
account.debit(max_cost);
println!("Transaction can be processed");
} else {
println!("Insufficient balance");
}
Transaction Hashing
use minichain_core::Transaction;
let tx = Transaction::transfer(from, to, 100, 0, 1).signed(&keypair);
// Get signing hash (without signature)
let signing_hash = tx.signing_hash();
// Get full transaction hash (with signature)
let tx_hash = tx.hash();
println!("Transaction ID: {}", tx_hash.to_hex());
Contract Address Calculation
use minichain_core::Transaction;
let deployer = Keypair::generate();
// First contract deployment (nonce = 0)
let tx1 = Transaction::deploy(
deployer.address(),
vec![0x01, 0x02],
0,
100_000,
1,
);
let addr1 = tx1.contract_address().unwrap();
// Second contract deployment (nonce = 1)
let tx2 = Transaction::deploy(
deployer.address(),
vec![0x03, 0x04],
1,
100_000,
1,
);
let addr2 = tx2.contract_address().unwrap();
// Different nonces produce different addresses
assert_ne!(addr1, addr2);
Transaction Type Detection
use minichain_core::Transaction;
fn process_transaction(tx: &Transaction) {
if tx.is_transfer() {
println!("Processing value transfer: {} tokens", tx.value);
} else if tx.is_deploy() {
println!("Processing contract deployment: {} bytes", tx.data.len());
if let Some(addr) = tx.contract_address() {
println!(" Contract address: {}", addr.to_hex());
}
} else if tx.is_call() {
println!("Processing contract call: {} bytes of data", tx.data.len());
}
}
Implementation Details
Transaction Types
Three types of transactions are distinguished by the to field and data:
| Type | to | data | Purpose |
|---|
| Transfer | Some(address) | Empty | Send tokens |
| Deploy | None | Bytecode | Deploy contract |
| Call | Some(address) | Calldata | Call contract |
Signing Process
- Serialize unsigned transaction fields (excluding signature)
- Hash the serialized data with Blake3
- Sign the hash with Ed25519
- Store signature in transaction
pub fn sign(&mut self, keypair: &Keypair) {
let hash = self.signing_hash();
self.signature = keypair.sign_hash(&hash);
}
Contract Address Derivation
Contract addresses are deterministically derived from:
address = first_20_bytes(hash(sender_address || nonce))
This ensures:
- Contracts have unique addresses
- Address can be calculated before deployment
- Same deployer + nonce always produces same address
Gas Cost Calculation
max_cost = value + (gas_limit * gas_price)
The sender must have at least max_cost balance to submit the transaction.
Nonce Ordering
Transactions must be processed in nonce order:
- First transaction: nonce = 0
- Second transaction: nonce = 1
- Etc.
This prevents replay attacks and ensures deterministic execution order.
Source Location
Defined in crates/core/src/transaction.rs