Programming lesson
Building a PayCoin Transaction Validator in C++: A Blockchain Ledger Assignment Guide
Learn how to implement a PayCoin transaction validator for a blockchain ledger in C++. This tutorial covers creating transaction classes, building a blockchain, and writing validation logic with real-world crypto and gaming analogies.
Introduction: Why Transaction Validation Matters in Blockchain
Blockchain technology powers cryptocurrencies like Bitcoin and Ethereum, but its core principle—immutable, decentralized ledgers—also inspires modern gaming economies, NFT marketplaces, and even school project grade tracking. In this tutorial, you'll build a PayCoin transaction validator for a simplified blockchain, similar to the SimpleCoin2 approach from your CSCI 181 lecture. By the end, you'll have a working C++ program that checks transaction validity and appends valid transactions to a ledger. This assignment is worth 52 points and tests your understanding of data structures, blockchain logic, and digital signatures.
Step 1: Create a PayCoin Transaction Class (10 points)
First, we need a data structure to represent a PayCoin transaction. Each transaction must include fields like sender, recipient (with public keys), amount, a unique ID, and a digital signature. Here's a C++ class that encapsulates these fields:
class PayCoinTransaction {
public:
std::string transactionID;
std::string senderPublicKey;
std::string recipientPublicKey;
double amount;
std::string signature; // digital signature over the transaction data
PayCoinTransaction(std::string tID, std::string sender, std::string recipient, double amt, std::string sig)
: transactionID(tID), senderPublicKey(sender), recipientPublicKey(recipient), amount(amt), signature(sig) {}
// Helper to get the message that was signed (e.g., concatenation of fields)
std::string getMessage() const {
return transactionID + senderPublicKey + recipientPublicKey + std::to_string(amount);
}
};Explanation: The transaction ID ensures uniqueness. The sender's public key identifies who sends the coins, and the recipient's public key is where they go. The signature is created by the sender using their private key over the message (the concatenation of transaction data). You can think of this like a digital receipt in a gaming marketplace—when you trade a rare skin in Fortnite, the transaction must be signed by your account to prove you authorized it.
Step 2: Build the Blockchain (Ledger) (10 points)
Now we need a blockchain to store these transactions. For simplicity, each block contains exactly one PayCoin transaction. We'll use a std::vector to represent the chain, but you could also use a linked list. No hashing is required for this assignment.
struct Block {
int index;
PayCoinTransaction transaction;
// In a real blockchain, you'd have previousHash and timestamp, but we omit them per spec.
Block(int idx, PayCoinTransaction tx) : index(idx), transaction(tx) {}
};
class Blockchain {
private:
std::vector<Block> chain;
public:
Blockchain() {
// Genesis block with a dummy transaction (optional)
PayCoinTransaction genesisTx("genesis", "0", "0", 0, "0");
chain.push_back(Block(0, genesisTx));
}
void addBlock(const PayCoinTransaction& tx) {
int newIndex = chain.size();
chain.push_back(Block(newIndex, tx));
}
const std::vector<Block>& getChain() const { return chain; }
};Explanation: The blockchain starts with a genesis block. When a transaction is validated, it gets appended. This mimics how a real blockchain adds blocks—like adding a new level to a leaderboard in a competitive game such as Apex Legends. Each new block builds on the previous one, creating an immutable history.
Step 3: Write the Transaction Validation Function (32 points)
This is the core of the assignment. The function must check multiple conditions to determine if a transaction is valid. According to the lecture notes, a valid PayCoin transaction must satisfy:
- Signature verification: The transaction's signature must be valid when checked against the sender's public key and the transaction message.
- Sufficient balance: The sender must have enough coins to send the amount. (We'll simulate this with a simple balance map.)
- No double-spending: The transaction ID must not already exist in the blockchain.
- Positive amount: The amount must be greater than zero.
We assume verifySignature(pubKey, message, signature) is provided and returns a boolean. Also, we assume we have access to everyone's correct public key (e.g., stored in a map). For balance tracking, we can maintain a std::map<std::string, double> that records balances after each valid transaction.
class TransactionValidator {
private:
Blockchain& blockchain;
std::map<std::string, double> balances; // publicKey -> balance
// Helper to check if a transaction ID already exists in the chain
bool isTransactionIDUnique(const std::string& txID) const {
for (const auto& block : blockchain.getChain()) {
if (block.transaction.transactionID == txID)
return false;
}
return true;
}
// Helper to get the balance of a public key (initial balance assumed from genesis or external)
double getBalance(const std::string& pubKey) const {
auto it = balances.find(pubKey);
if (it != balances.end())
return it->second;
// Assume initial balance of 100 for simplicity; in real life, this comes from coinbase transactions
return 100.0;
}
public:
TransactionValidator(Blockchain& bc) : blockchain(bc) {
// Initialize balances for known public keys (for demo)
balances["AlicePubKey"] = 100.0;
balances["BobPubKey"] = 50.0;
}
bool isTransactionValid(const PayCoinTransaction& tx) {
// 1. Check signature
if (!verifySignature(tx.senderPublicKey, tx.getMessage(), tx.signature)) {
std::cout << "Invalid: Signature verification failed.\n";
return false;
}
// 2. Check amount positive
if (tx.amount <= 0) {
std::cout << "Invalid: Amount must be positive.\n";
return false;
}
// 3. Check sufficient balance
double senderBalance = getBalance(tx.senderPublicKey);
if (tx.amount > senderBalance) {
std::cout << "Invalid: Insufficient balance. Sender has " << senderBalance << ", needs " << tx.amount << ".\n";
return false;
}
// 4. Check no double-spending (unique transaction ID)
if (!isTransactionIDUnique(tx.transactionID)) {
std::cout << "Invalid: Transaction ID already exists (double-spend attempt).\n";
return false;
}
// If all checks pass, update balances and add to blockchain
balances[tx.senderPublicKey] -= tx.amount;
balances[tx.recipientPublicKey] += tx.amount;
return true;
}
void processTransaction(const PayCoinTransaction& tx) {
if (isTransactionValid(tx)) {
blockchain.addBlock(tx);
std::cout << "Transaction added to blockchain.\n";
} else {
std::cout << "Transaction rejected.\n";
}
}
};Explanation: The isTransactionValid function performs four checks. Signature verification ensures the sender authorized the transaction. The balance check prevents overspending—like checking if you have enough in-game currency before buying a skin in Valorant. The unique ID check prevents double-spending, a classic blockchain problem. Finally, the amount must be positive. If all pass, we update balances and add the block. This is similar to how a bank processes a transfer, but decentralized.
Putting It All Together: Example Usage
Here's a simple main() function that demonstrates the workflow:
int main() {
// Assume we have a signature function that works (provided by assignment)
// For demo, we create a mock signature that always returns true
// In reality, you'd use cryptographic libraries like OpenSSL.
Blockchain ledger;
TransactionValidator validator(ledger);
// Create a valid transaction from Alice to Bob
PayCoinTransaction tx1("tx001", "AlicePubKey", "BobPubKey", 25.0, "mocked_signature_here");
validator.processTransaction(tx1);
// Try a double-spend (same ID)
PayCoinTransaction tx2("tx001", "AlicePubKey", "BobPubKey", 10.0, "mocked_signature_here");
validator.processTransaction(tx2); // Should be rejected
// Try insufficient balance (Alice now has 75, sending 100)
PayCoinTransaction tx3("tx002", "AlicePubKey", "BobPubKey", 100.0, "mocked_signature_here");
validator.processTransaction(tx3); // Should be rejected
// Display the ledger
for (const auto& block : ledger.getChain()) {
std::cout << "Block " << block.index << ": " << block.transaction.transactionID << "\n";
}
return 0;
}This example shows how the validator catches invalid transactions. In a real blockchain like Bitcoin, miners perform similar checks before adding a block. The concept also applies to modern apps like Venmo or even school grading systems where each grade change is a transaction on a private ledger.
Why This Matters: Real-World Connections
Blockchain isn't just for crypto. In 2026, many games like Fortnite and Roblox use blockchain-like systems to track in-game item ownership. When you trade a skin, the game checks your inventory (balance), verifies your identity (signature), and prevents duplicate trades (double-spending). Similarly, AI-driven apps use distributed ledgers to ensure data integrity. Understanding transaction validation is a foundational skill for any developer working with decentralized systems.
Common Pitfalls and Tips
- Signature verification: Make sure you pass the exact message that was signed. In our example,
getMessage()concatenates fields in a specific order. If the order changes, verification fails. - Balance initialization: In a real system, balances come from previous coinbase transactions. For this assignment, you can assume initial balances or store them externally.
- Thread safety: This example is single-threaded. In production, you'd need locks or atomic operations to prevent race conditions.
- Comments: Your code must include clear comments explaining each validation step, as required by the assignment rubric.
Conclusion
You've now built a PayCoin transaction validator and a simple blockchain ledger in C++. This covers 52 points of your Homework 6. The key takeaway is that blockchain validation relies on a few critical checks: signature, balance, uniqueness, and positivity. By mastering these, you're ready to tackle more advanced topics like mining, consensus algorithms, or even building your own cryptocurrency. Good luck, and remember to test your code with edge cases like zero amounts or invalid signatures!