Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion include/astro/core/block.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ namespace astro::core {
std::vector<uint8_t> serialize() const;
};

// ------- Utilities -------
Hash256 compute_merkle_root(const std::vector<Transaction>& transactions);
Hash256 empty_merkle_root();

Expand Down
46 changes: 46 additions & 0 deletions include/astro/core/chain.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once
#include <cstdint>
#include <vector>
#include <optional>
#include "astro/core/block.hpp"
#include "astro/core/transaction.hpp"

namespace astro::core {

enum class ValidationError {
None = 0,
EmptyChainButNotGenesis,
NonZeroPrevHashForGenesis,
BadPrevLink,
NonMonotonicTimestamp,
BadMerkleRoot,
BadTransactionSignature,
CoinBaseMisplaced,
CoinbaseInNonGenesisBlock,
};

struct ValidationResult {
bool is_valid;
ValidationError error;
size_t transaction_index = ~0LL;
};

class Chain {
public:
Chain();

size_t height() const { return blocks_.size();}

std::optional<Hash256> tip_hash() const;
const Block* tip() const { return blocks_.empty() ? nullptr : &blocks_.back();}

ValidationResult validate_block(const Block& block) const;

ValidationResult append_block(const Block& block);

Block build_block_from_transactions(std::vector<Transaction> transactions, uint64_t timestamp) const;

private:
std::vector<Block> blocks_;
};
}
85 changes: 85 additions & 0 deletions src/core/chain.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "astro/core/chain.hpp"
#include "astro/core/block.hpp"
#include <cstdint>

namespace astro::core {
Chain::Chain() = default;

std::optional<Hash256> Chain::tip_hash() const {
if (blocks_.empty()) return std::nullopt;
return blocks_.back().header.hash();
}

static bool is_zero_hash(const Hash256& hash) {
for (auto byte : hash) if (byte != 0) return false;
return true;
}

ValidationResult Chain::validate_block(const Block& block) const {
const bool is_genesis_candidate = blocks_.empty();

if (is_genesis_candidate) {
if (!is_zero_hash(block.header.prev_hash)) {
return {false, ValidationError::NonZeroPrevHashForGenesis, ~0ull};
}

if (!block.transactions.empty()) {
if (!block.transactions.front().from_pub_pem.empty()) {
return {false, ValidationError::CoinBaseMisplaced, 0};
}
for (size_t i = 1; i < block.transactions.size(); ++i) {
if (block.transactions[i].from_pub_pem.empty()) return {false, ValidationError::CoinBaseMisplaced, i};
}
}
} else {
auto parent_hash = blocks_.back().header.hash();
if (block.header.prev_hash != parent_hash) {
return {false, ValidationError::BadPrevLink, ~0ull};
}

if (block.header.timestamp < blocks_.back().header.timestamp) {
return {false, ValidationError::NonMonotonicTimestamp, ~0ull};
}

for (size_t i = 0; i < block.transactions.size(); ++i) {
if (block.transactions[i].from_pub_pem.empty()) return {false, ValidationError::CoinbaseInNonGenesisBlock, i};
}
}

auto computed_merkle_root = compute_merkle_root(block.transactions);
if (computed_merkle_root!= block.header.merkle_root) {
return {false, ValidationError::BadMerkleRoot, ~0ull};
}

for (size_t i = 0; i < block.transactions.size(); ++i) {
const auto& tx = block.transactions[i];
// Skip signature verification for an allowed coinbase at genesis (empty from_pub_pem)
if (is_genesis_candidate && i == 0 && tx.from_pub_pem.empty()) continue;
if (!tx.verify()) {
return {false, ValidationError::BadTransactionSignature, i};
}
}
return {true, ValidationError::None, ~0ull};
}

ValidationResult Chain::append_block(const Block& block) {
auto validation_result = validate_block(block);
if (!validation_result.is_valid) return validation_result;
blocks_.push_back(block);
return validation_result;
}

Block Chain::build_block_from_transactions(std::vector<Transaction> transactions, uint64_t timestamp) const {
Block output;
output.transactions = std::move(transactions);
BlockHeader header;
header.version = 1;
if (!blocks_.empty()) header.prev_hash = blocks_.back().header.hash();
header.merkle_root = compute_merkle_root(output.transactions);
header.timestamp = timestamp;
header.nonce = 0;
output.header = header;
return output;
}

}
32 changes: 31 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <astro/core/serializer.hpp>
#include <astro/core/block.hpp>
#include <astro/core/merkle.hpp>
#include <astro/core/chain.hpp>
#include <chrono>

using namespace astro::core;
Expand All @@ -21,7 +22,8 @@ static void print_usage() {
" astro-node demo-keys [--curve CURVE] [--message MESSAGE]\n"
" astro-node demo-tx [--amount N] [--nonce N] [--to LABEL]\n"
" astro-node demo-genesis\n"
" astro-node demo-merkle [--leaves CSV] [--index N]\n\n"
" astro-node demo-merkle [--leaves CSV] [--index N]\n"
" astro-node demo-chain\n\n"
"Options:\n"
" --curve EC curve name (default: secp256k1)\n"
" --message Message to sign (default: 'astro demo')\n"
Expand Down Expand Up @@ -218,6 +220,34 @@ int main(int argc, char** argv) {
return 0;
}

if (command == "demo-chain") {
if (!crypto_init()) {
std::fprintf(stderr, "crypto_init failed\n");
return 1;
}
Chain chain;
uint64_t t0 = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
auto genesis = make_genesis_block("Astro begins", t0);
auto res0 = chain.append_block(genesis);
std::cout << "append genesis: " << (res0.is_valid ? "OK" : "FAIL") << "\n";

auto key_pair = generate_ec_keypair();
Transaction tx;
tx.version = 1; tx.nonce = 1; tx.amount = 42;
tx.from_pub_pem = key_pair.pubkey_pem; tx.to_label = "alice";
tx.sign(key_pair.privkey_pem);

auto block1 = chain.build_block_from_transactions({tx}, t0 + 1);
auto res1 = chain.append_block(block1);
std::cout << "append block #2: " << (res1.is_valid ? "OK" : "FAIL") << "\n";
if (auto th = chain.tip_hash()) {
std::cout << "tip: " << to_hex(std::span<const uint8_t>(th->data(), th->size())) << "\n";
std::cout << "height: " << chain.height() << "\n";
}
return res1.is_valid ? 0 : 2;
}

print_usage();
return 0;
}
4 changes: 2 additions & 2 deletions tests/test_block.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ using namespace astro::core;
TEST(BlockHeader, SerializeAndHashDeterminism) {
BlockHeader header;
header.version = 1;
header.prev_hash = {}; // zeros
header.merkle_root = {}; // zeros
header.prev_hash = {};
header.merkle_root = {};
header.timestamp = 1700000000ULL;
header.nonce = 123;

Expand Down
105 changes: 105 additions & 0 deletions tests/test_chain.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#include <gtest/gtest.h>
#include <chrono>
#include "astro/core/chain.hpp"
#include "astro/core/keys.hpp"

using namespace astro::core;

static uint64_t now_sec() {
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
).count()
);
}

TEST(Chain, AppendGenesisThenValidBlock) {
ASSERT_TRUE(crypto_init());
Chain c;
uint64_t t0 = now_sec();

// Build genesis with a single coinbase-like tx (no signer)
Transaction coinbase;
coinbase.to_label = "genesis-note";
Block g = make_genesis_block("genesis-note", t0);
auto r0 = c.append_block(g);
ASSERT_TRUE(r0.is_valid);
EXPECT_EQ(c.height(), 1u);

// Next block with a signed tx
auto kp = generate_ec_keypair();
ASSERT_FALSE(kp.privkey_pem.empty());
ASSERT_FALSE(kp.pubkey_pem.empty());
Transaction tx;
tx.version = 1; tx.nonce = 1; tx.amount = 10;
tx.from_pub_pem = kp.pubkey_pem; tx.to_label = "alice";
tx.sign(kp.privkey_pem);

Block b1 = c.build_block_from_transactions({tx}, t0 + 1);
auto r1 = c.append_block(b1);
ASSERT_TRUE(r1.is_valid);
EXPECT_EQ(c.height(), 2u);
EXPECT_TRUE(c.tip_hash().has_value());
}

TEST(Chain, RejectsBadPrevLink) {
ASSERT_TRUE(crypto_init());
Chain c;
uint64_t t = now_sec();
auto g = make_genesis_block("g", t);
ASSERT_TRUE(c.append_block(g).is_valid);

// Build a block that points to zero (wrong prev)
auto kp = generate_ec_keypair();
ASSERT_FALSE(kp.privkey_pem.empty());
ASSERT_FALSE(kp.pubkey_pem.empty());
Transaction tx; tx.version=1; tx.nonce=1; tx.amount=1; tx.from_pub_pem=kp.pubkey_pem; tx.to_label="x"; tx.sign(kp.privkey_pem);

Block bad = c.build_block_from_transactions({tx}, t+1);
bad.header.prev_hash = {};
auto r = c.append_block(bad);
EXPECT_FALSE(r.is_valid);
EXPECT_EQ(r.error, ValidationError::BadPrevLink);
}

TEST(Chain, RejectsBadMerkleOrSig) {
ASSERT_TRUE(crypto_init());
Chain c;
uint64_t t = now_sec();
ASSERT_TRUE(c.append_block(make_genesis_block("g", t)).is_valid);

auto kp = generate_ec_keypair();
ASSERT_FALSE(kp.privkey_pem.empty());
ASSERT_FALSE(kp.pubkey_pem.empty());
Transaction tx; tx.version=1; tx.nonce=1; tx.amount=1; tx.from_pub_pem=kp.pubkey_pem; tx.to_label="x"; tx.sign(kp.privkey_pem);
Block b = c.build_block_from_transactions({tx}, t+1);

// Tamper merkle
b.header.merkle_root = {};
auto r1 = c.append_block(b);
EXPECT_FALSE(r1.is_valid);
EXPECT_EQ(r1.error, ValidationError::BadMerkleRoot);

// Fix merkle, break signature
b.header.merkle_root = compute_merkle_root(b.transactions);
ASSERT_FALSE(b.transactions[0].signature.empty());
b.transactions[0].signature[0] ^= 0x01;
auto r2 = c.append_block(b);
EXPECT_FALSE(r2.is_valid);
EXPECT_EQ(r2.error, ValidationError::BadTransactionSignature);
}

TEST(Chain, RejectsCoinbaseInNonGenesis) {
ASSERT_TRUE(crypto_init());
Chain c;
uint64_t t = now_sec();
ASSERT_TRUE(c.append_block(make_genesis_block("g", t)).is_valid);

Transaction cb;
cb.to_label = "illegal";
Block b = c.build_block_from_transactions({cb}, t+1);

auto r = c.append_block(b);
EXPECT_FALSE(r.is_valid);
EXPECT_EQ(r.error, ValidationError::CoinbaseInNonGenesisBlock);
}
Loading