diff --git a/.gitignore b/.gitignore index 3038ad8..831296e 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,7 @@ devcontainer.json # Terminalizer /demo.yml -/config.yml \ No newline at end of file +/config.yml + +# Runtime data +/data/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index a1dacfd..cdc6be5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,9 @@ endif() if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/core/miner.cpp) list(APPEND ASTRO_CORE_SOURCES src/core/miner.cpp) endif() +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/core/block_store.cpp) + list(APPEND ASTRO_CORE_SOURCES src/core/block_store.cpp) +endif() if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/net/p2p.cpp) list(APPEND ASTRO_CORE_SOURCES src/net/p2p.cpp) endif() @@ -209,6 +212,26 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/cli/mine.cpp) install(TARGETS astro-mine RUNTIME DESTINATION bin) endif() +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/cli/store.cpp) + add_executable(astro-store src/cli/store.cpp) + target_include_directories(astro-store PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/include + ) + if(TARGET astro_core) + target_link_libraries(astro-store PRIVATE astro_core) + endif() + if(OpenSSL_FOUND) + if(TARGET OpenSSL::Crypto) + target_link_libraries(astro-store PRIVATE OpenSSL::Crypto) + else() + target_link_libraries(astro-store PRIVATE OpenSSL::SSL OpenSSL::Crypto) + endif() + endif() + set_target_properties(astro-store PROPERTIES OUTPUT_NAME "astro-store") + install(TARGETS astro-store RUNTIME DESTINATION bin) +endif() + if(ASTRO_BUILD_TESTS) if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests) enable_testing() diff --git a/include/astro/core/block.hpp b/include/astro/core/block.hpp index a96419b..e3f388e 100644 --- a/include/astro/core/block.hpp +++ b/include/astro/core/block.hpp @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include #include "astro/core/hash.hpp" #include "astro/core/transaction.hpp" diff --git a/include/astro/core/chain.hpp b/include/astro/core/chain.hpp index 67af96e..2563a06 100644 --- a/include/astro/core/chain.hpp +++ b/include/astro/core/chain.hpp @@ -4,7 +4,8 @@ #include #include "astro/core/block.hpp" #include "astro/core/transaction.hpp" -#include "astro/core/pow.hpp" + +namespace astro { namespace storage { class BlockStore; } } namespace astro::core { @@ -50,6 +51,15 @@ namespace astro::core { Block build_block_from_transactions(std::vector transactions, uint64_t timestamp) const; + // Load blocks from the block store (verifies each via validate_block). + // If the chain is empty, the first valid block becomes genesis. + void restore_from_store(astro::storage::BlockStore& store); + + // Validate then append AND persist atomically. + ValidationResult append_and_store(const Block& block, astro::storage::BlockStore& store); + + const std::vector& blocks() const { return blocks_; } + private: ChainConfig config_{}; std::vector blocks_; diff --git a/include/astro/storage/block_store.hpp b/include/astro/storage/block_store.hpp new file mode 100644 index 0000000..de1431b --- /dev/null +++ b/include/astro/storage/block_store.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include +#include + +namespace astro::storage { + struct RecordHeader { + uint32_t magic; + uint64_t version; + uint16_t kind; + uint64_t length; + }; + + class BlockStore { + public: + explicit BlockStore(std::filesystem::path root_path); + ~BlockStore(); + + void append_block(const astro::core::Block& block); + + std::vector load_all_blocks(); + + const std::filesystem::path& directory() const { return root_path_; } + const std::filesystem::path& log_path() const { return log_path_; } + + private: + void open_write_log(); + void close_write_log(); + void fsync_fd(); + std::filesystem::path root_path_; + std::filesystem::path log_path_; + int log_fd = -1; + }; +} \ No newline at end of file diff --git a/src/cli/store.cpp b/src/cli/store.cpp new file mode 100644 index 0000000..3ef5376 --- /dev/null +++ b/src/cli/store.cpp @@ -0,0 +1,41 @@ +#include +#include +#include + +#include "astro/storage/block_store.hpp" +#include "astro/core/chain.hpp" +#include "astro/core/keys.hpp" +#include "astro/core/hash.hpp" + +using namespace astro::core; +namespace fs = std::filesystem; + +int main() { + if (!crypto_init()) { std::cerr << "OpenSSL init failed\n"; return 1; } + + fs::path data = "./data"; + astro::storage::BlockStore store(data); + + Chain chain(ChainConfig{.difficulty_bits=0}); + chain.restore_from_store(store); + std::cout << "[💾] restored height: " << chain.height() << "\n"; + + if (chain.height() == 0) { + auto genesis_block = make_genesis_block("Astro: Persisted.", 1700000000ULL); + auto validation_result = chain.append_and_store(genesis_block, store); + std::cout << (validation_result.is_valid ? "[+] wrote genesis\n" : "[x] failed genesis\n"); + } else { + auto key_pair = generate_ec_keypair(); + Transaction transaction; transaction.version=1; transaction.nonce=chain.height(); transaction.amount=1; + transaction.from_pub_pem=key_pair.pubkey_pem; transaction.to_label="demo"; transaction.sign(key_pair.privkey_pem); + auto new_block = chain.build_block_from_transactions({transaction}, 1700000000ULL + chain.height()); + auto validation_result = chain.append_and_store(new_block, store); + std::cout << (validation_result.is_valid ? "[+] appended block\n" : "[x] append failed\n"); + } + + auto tip_hash = chain.tip_hash(); + if (tip_hash) { + std::cout << "tip: " << to_hex(std::span(tip_hash->data(), tip_hash->size())).substr(0,16) << "...\n"; + } + return 0; +} \ No newline at end of file diff --git a/src/cli/tui.cpp b/src/cli/tui.cpp index 9a421e0..6b7b2d4 100644 --- a/src/cli/tui.cpp +++ b/src/cli/tui.cpp @@ -12,11 +12,13 @@ #include #include #include +#include #include "astro/core/chain.hpp" #include "astro/core/keys.hpp" #include "astro/core/hash.hpp" #include "astro/core/block.hpp" +#include "astro/storage/block_store.hpp" using namespace astro::core; @@ -121,7 +123,7 @@ struct KeyDebounce { } }; -} // namespace tui +} static std::string short_hash(const Hash256& h, size_t keep = 10) { auto hex = to_hex(std::span(h.data(), h.size())); @@ -133,14 +135,17 @@ struct LogLine { std::string text; int color = 37; }; struct App { Chain chain; + astro::storage::BlockStore store{std::filesystem::path("./data")}; std::vector log; size_t max_log = 200; size_t chain_scroll = 0; + bool dirty = true; void push_log(std::string s, int color=37) { log.push_back({std::move(s), color}); if (log.size() > max_log) log.erase(log.begin(), log.begin()+ (log.size()-max_log)); + dirty = true; } }; @@ -154,9 +159,10 @@ static bool do_genesis(App& app) { std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count()); Block genesis_block = make_genesis_block("Astro: Born from bytes.", unix_time); - auto validation_result = app.chain.append_block(genesis_block); + auto validation_result = app.chain.append_and_store(genesis_block, app.store); if (validation_result.is_valid) { app.push_log("genesis appended ✓", 32); + app.dirty = true; return true; } else { app.push_log("genesis append failed", 31); @@ -181,7 +187,7 @@ static bool do_append_signed_block(App& app) { std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count()); Block new_block = app.chain.build_block_from_transactions({transaction}, unix_time); - auto validation_result = app.chain.append_block(new_block); + auto validation_result = app.chain.append_and_store(new_block, app.store); if (validation_result.is_valid) { app.push_log("block appended ✓", 32); return true; } app.push_log("append failed (validation error)", 31); return false; @@ -300,6 +306,10 @@ int main() { App app; tui::FPS fps; + app.chain.restore_from_store(app.store); + if (app.chain.height() > 0) { + app.push_log("restored chain from ./data", 36); + } app.push_log("TUI started", 36); app.push_log("Press G to create genesis", 33); int rows = 36, cols = 120; @@ -313,6 +323,8 @@ int main() { using clock = std::chrono::steady_clock; auto next = clock::now(); tui::KeyDebounce debounce; + auto last_draw = clock::now(); + const auto min_draw_interval = std::chrono::milliseconds(120); while (tui::g_running) { for (int k; (k = tui::read_key()) != -1; ) { @@ -326,8 +338,13 @@ int main() { } } - draw(app, rows, cols, fps); - fps.tick(); + auto now = clock::now(); + if (app.dirty || (now - last_draw) >= min_draw_interval) { + draw(app, rows, cols, fps); + fps.tick(); + app.dirty = false; + last_draw = now; + } next += std::chrono::milliseconds(33); std::this_thread::sleep_until(next); diff --git a/src/core/block_store.cpp b/src/core/block_store.cpp new file mode 100644 index 0000000..ac338c0 --- /dev/null +++ b/src/core/block_store.cpp @@ -0,0 +1,156 @@ +#include "astro/storage/block_store.hpp" +#include "astro/core/serializer.hpp" +#include "astro/core/hash.hpp" +#include +#include +#include +#include +#include + +#ifndef _WIN32 + #include + #include +#endif + +namespace fs = std::filesystem; +using namespace astro::core; + +namespace astro::storage { + static constexpr uint32_t MAGIC = 0x41535452; // "ASTR" + static constexpr uint64_t VER = 1; + static constexpr uint16_t KIND_BLOCK = 1; + + static Transaction parse_transaction(std::span bytes) { + ByteReader reader(bytes); + (void)reader.read_u8(); // 0xA1 + (void)reader.read_u8(); // 0x01 + (void)reader.read_u32(); // reserved/schema + Transaction tx; + tx.version = reader.read_u32(); + tx.nonce = reader.read_u64(); + tx.amount = reader.read_u64(); + tx.from_pub_pem = reader.read_bytes(); + tx.to_label = reader.read_string(); + tx.signature = reader.read_bytes(); + return tx; + } + BlockStore::BlockStore(fs::path root_path) : root_path_(std::move(root_path)) { + if (!fs::exists(root_path_)) fs::create_directories(root_path_); + log_path_ = root_path_ / "chain.log"; + open_write_log(); + } + + BlockStore::~BlockStore() { close_write_log(); } + + void BlockStore::open_write_log() { + #ifndef _WIN32 + log_fd = ::open(log_path_.c_str(), O_CREAT | O_APPEND | O_WRONLY, 0644); + if (log_fd < 0) throw std::system_error(errno, std::generic_category(), "open write log"); + #else + log_fd = 1; + #endif + } + + void BlockStore::close_write_log() { + #ifndef _WIN32 + if (log_fd >= 0) { ::close(log_fd); log_fd = -1; } + #endif + } + + void BlockStore::fsync_fd() { + #ifndef _WIN32 + if (log_fd >= 0) ::fsync(log_fd); + #endif + } + + void BlockStore::append_block(const Block& block) { + auto payload = block.serialize(); + auto check = sha256(std::span(payload.data(), payload.size())); + + RecordHeader header{MAGIC, VER, KIND_BLOCK, static_cast(payload.size())}; + + std::ofstream out(log_path_, std::ios::binary | std::ios::app); + if (!out) throw std::runtime_error("BlockStore: open append failed"); + + out.write(reinterpret_cast(&header.magic), sizeof(header.magic)); + out.write(reinterpret_cast(&header.version), sizeof(header.version)); + out.write(reinterpret_cast(&header.kind), sizeof(header.kind)); + out.write(reinterpret_cast(&header.length), sizeof(header.length)); + out.write(reinterpret_cast(payload.data()), payload.size()); + out.write(reinterpret_cast(check.data()), check.size()); + + if (!out.good()) throw std::runtime_error("BlockStore: write failed"); + out.flush(); + fsync_fd(); + } + + static uint64_t read_u64(std::istream& in) { + uint64_t v; in.read(reinterpret_cast(&v), sizeof(v)); return v; + } + + static uint32_t read_u32(std::istream& in) { + uint32_t v; in.read(reinterpret_cast(&v), sizeof(v)); return v; + } + + static uint16_t read_u16(std::istream& in) { + uint16_t v; in.read(reinterpret_cast(&v), sizeof(v)); return v; + } + + std::vector BlockStore::load_all_blocks() { + std::vector out; + if (!fs::exists(log_path_)) return out; + + std::ifstream in(log_path_, std::ios::binary); + if(!in) throw std::runtime_error("BlockStore: open read failed"); + + while (in.peek() != std::char_traits::eof()) { + uint32_t magic = read_u32(in); + if (!in) break; + uint64_t version = read_u64(in); + uint16_t kind = read_u16(in); + uint64_t length = read_u64(in); + if (!in) break; + + if (magic != MAGIC || version != VER || kind != KIND_BLOCK) { + break; + } + + std::vector payload; + payload.resize(length); + in.read(reinterpret_cast(payload.data()), payload.size()); + if (!in) break; + + Hash256 check{}; + in.read(reinterpret_cast(check.data()), check.size()); + if (!in) break; + + auto calculated_check = sha256(std::span(payload.data(), payload.size())); + if (calculated_check != check) { + break; + } + + // Reconstructing via serializer primitives to match Block::serialize + ByteReader reader(payload); + BlockHeader header; + header.version = reader.read_u32(); + for (size_t i = 0; i < header.prev_hash.size(); ++i) header.prev_hash[i] = reader.read_u8(); + for (size_t i = 0; i < header.merkle_root.size(); ++i) header.merkle_root[i] = reader.read_u8(); + header.timestamp = reader.read_u64(); + header.nonce = reader.read_u64(); + + Block block; + block.header = header; + + auto num_txs = reader.read_u32(); + block.transactions.reserve(num_txs); + for (uint32_t i=0; i(tx_bytes.data(), tx_bytes.size())); + block.transactions.push_back(std::move(tx)); + } + + out.push_back(std::move(block)); + } + return out; + } +} \ No newline at end of file diff --git a/src/core/chain.cpp b/src/core/chain.cpp index 45c7dde..9e06f2d 100644 --- a/src/core/chain.cpp +++ b/src/core/chain.cpp @@ -1,9 +1,11 @@ #include "astro/core/chain.hpp" #include "astro/core/block.hpp" #include "astro/core/pow.hpp" +#include "astro/storage/block_store.hpp" #include namespace astro::core { + Chain::Chain(ChainConfig config) : config_(config) {}; std::optional Chain::tip_hash() const { @@ -86,6 +88,26 @@ namespace astro::core { return validation_result; } + void Chain::restore_from_store(astro::storage::BlockStore& store) { + auto stored_blocks = store.load_all_blocks(); + for (const auto& block : stored_blocks) { + auto validation_result = append_block(block); + if (!validation_result.is_valid) break; + } + } + + ValidationResult Chain::append_and_store(const Block& block, astro::storage::BlockStore& store) { + auto validation_result = validate_block(block); + if (!validation_result.is_valid) return validation_result; + try { + store.append_block(block); + } catch (...) { + return {false, ValidationError::None, ~0ull}; + } + blocks_.push_back(block); + return validation_result; + } + Block Chain::build_block_from_transactions(std::vector transactions, uint64_t timestamp) const { Block output; output.transactions = std::move(transactions); @@ -99,4 +121,5 @@ namespace astro::core { return output; } + } \ No newline at end of file diff --git a/tests/test_store.cpp b/tests/test_store.cpp new file mode 100644 index 0000000..93f1847 --- /dev/null +++ b/tests/test_store.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include "astro/storage/block_store.hpp" +#include "astro/core/chain.hpp" +#include "astro/core/keys.hpp" +#include "astro/core/hash.hpp" + +using namespace astro::core; +namespace fs = std::filesystem; + +static fs::path tmpdir(const char* name) { + auto p = fs::temp_directory_path() / (std::string("astro_") + name); + fs::remove_all(p); + fs::create_directories(p); + return p; +} + +TEST(Store, AppendRestoreRoundTrip) { + ASSERT_TRUE(crypto_init()); + Chain c(ChainConfig{.difficulty_bits=0}); + auto dir = tmpdir("store"); + astro::storage::BlockStore store(dir); + + // genesis + auto g = make_genesis_block("g", 1700000000ULL); + ASSERT_TRUE(c.append_and_store(g, store).is_valid); + + // one signed block + auto kp = generate_ec_keypair(); + Transaction tx; tx.version=1; tx.nonce=1; tx.amount=7; tx.from_pub_pem=kp.pubkey_pem; tx.to_label="x"; tx.sign(kp.privkey_pem); + auto b1 = c.build_block_from_transactions({tx}, 1700000001ULL); + ASSERT_TRUE(c.append_and_store(b1, store).is_valid); + + // new chain instance, restore + Chain c2(ChainConfig{.difficulty_bits=0}); + c2.restore_from_store(store); + EXPECT_EQ(c2.height(), 2u); + auto tip1 = c.tip_hash(); + auto tip2 = c2.tip_hash(); + ASSERT_TRUE(tip1.has_value() && tip2.has_value()); + EXPECT_EQ(to_hex(std::span(tip1->data(), tip1->size())).substr(0,16), + to_hex(std::span(tip2->data(), tip2->size())).substr(0,16)); +} \ No newline at end of file