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: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# Build folder
build/
build_write_enabled/
bin/
out/
_codeql_build_dir/
Expand Down
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,19 @@ elseif(ZeroMQ_LIBRARY)
target_compile_definitions(ailee_adapters PRIVATE AILEE_HAS_ZMQ=1)
endif()

# ============================================================================
# Bitcoin Write Safety Gate
# ============================================================================

option(AILEE_BITCOIN_WRITE_ENABLED "Enable Bitcoin write operations (sendrawtransaction, sign, etc.)" OFF)
if(NOT AILEE_BITCOIN_WRITE_ENABLED)
target_compile_definitions(ailee_node PRIVATE AILEE_BITCOIN_WRITE_DISABLED=1)
target_compile_definitions(ailee_adapters PRIVATE AILEE_BITCOIN_WRITE_DISABLED=1)
message(STATUS "Bitcoin write operations: DISABLED (shadow/read-only mode)")
else()
message(STATUS "Bitcoin write operations: ENABLED")
endif()
Comment on lines +403 to +410

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compile definitions for AILEE_BITCOIN_WRITE_DISABLED are only applied to ailee_node and ailee_adapters targets, but not to the test target ailee_tests (defined at line 437). This means tests will not have the same compile-time configuration as the production code they're testing.

The test at lines 108-119 in tests/AdapterRegistryTests.cpp checks which macro is defined, but it will always execute the "write enabled" path since the test target doesn't receive the AILEE_BITCOIN_WRITE_DISABLED definition.

Consider adding the compile definition to the test target as well to ensure tests run with the same configuration as production code.

Copilot uses AI. Check for mistakes.

# ============================================================================
# Unit Tests (Optional)
# ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion include/Global_Seven.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ struct AdapterConfig {
std::string network; // "mainnet", "testnet", "devnet"
std::unordered_map<std::string, std::string> extra; // per‑chain params
bool enableTelemetry{true};
bool readOnly{false}; // listen‑only
bool readOnly{true}; // listen‑only (default: shadow/read-only mode)
FeePolicy feePolicy{};
SlippagePolicy slippagePolicy{};
double minOracleConfidence{0.7};
Expand Down
23 changes: 23 additions & 0 deletions src/l1/BitcoinAdapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ class BitcoinTxBuilder {
const std::vector<TxOut>& outputs,
const std::unordered_map<std::string, std::string>& opts
) {
#if defined(AILEE_BITCOIN_WRITE_DISABLED)
(void)rpc; (void)outputs; (void)opts;
throw std::runtime_error(
"Bitcoin write operations disabled at compile time "
"(AILEE_BITCOIN_WRITE_DISABLED). "
"Rebuild with -DAILEE_BITCOIN_WRITE_ENABLED=ON to enable.");
#else
// Get UTXOs
Json::Value utxosJson = rpc.call("listunspent", Json::Value(Json::arrayValue));

Expand Down Expand Up @@ -365,6 +372,7 @@ class BitcoinTxBuilder {
}

return signedTx["hex"].asString();
#endif
}
};

Expand Down Expand Up @@ -515,6 +523,20 @@ class BTCInternal {
bool broadcastRaw(const std::string& rawHex,
std::string& outTxId,
ErrorCallback onError) {
#if defined(AILEE_BITCOIN_WRITE_DISABLED)
(void)rawHex; (void)outTxId;
if (onError) {
onError(AdapterError{
Severity::Error,
"Bitcoin write operations disabled at compile time "
"(AILEE_BITCOIN_WRITE_DISABLED). "
"Rebuild with -DAILEE_BITCOIN_WRITE_ENABLED=ON to enable.",
"Broadcast",
-12
});
}
return false;
#else
try {
// Check if already broadcast (idempotency)
std::lock_guard<std::mutex> lock(broadcastMutex_);
Expand Down Expand Up @@ -550,6 +572,7 @@ class BTCInternal {
}
return false;
}
#endif
}

std::optional<NormalizedTx> fetchTx(const std::string& txid, ErrorCallback onError) {
Expand Down
18 changes: 18 additions & 0 deletions tests/AdapterRegistryTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,21 @@ TEST(AdapterRegistryTest, GetBlockHeightReturnsValue) {
ASSERT_TRUE(h.has_value());
EXPECT_EQ(h.value(), 42ULL);
}

TEST(AdapterConfigTest, ReadOnlyDefaultIsTrue) {
AdapterConfig cfg;
EXPECT_TRUE(cfg.readOnly);
}

#if defined(AILEE_BITCOIN_WRITE_DISABLED)
TEST(BitcoinShadowModeTest, WriteDisabledAtCompileTime) {
// When AILEE_BITCOIN_WRITE_DISABLED is set, the macro is active.
// This test documents that the compile-time gate is in effect.
EXPECT_TRUE(true);
}
#else
TEST(BitcoinShadowModeTest, WriteEnabledAtCompileTime) {
// AILEE_BITCOIN_WRITE_ENABLED=ON was set; write ops are compiled in.
EXPECT_TRUE(true);
Comment on lines +109 to +117

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests only verify that the macro is defined, but don't actually test the behavior of the compile-time guards. The tests always pass with EXPECT_TRUE(true) regardless of whether the guards work correctly.

Consider adding tests that actually verify the runtime behavior:

  • For the DISABLED case: test that buildRawTx and broadcastRaw throw the expected runtime_error with the correct message
  • For the ENABLED case: test that these functions can execute (or use mocks to verify they reach the RPC layer)

This would provide actual coverage for the safety mechanism rather than just documenting the macro state.

Suggested change
TEST(BitcoinShadowModeTest, WriteDisabledAtCompileTime) {
// When AILEE_BITCOIN_WRITE_DISABLED is set, the macro is active.
// This test documents that the compile-time gate is in effect.
EXPECT_TRUE(true);
}
#else
TEST(BitcoinShadowModeTest, WriteEnabledAtCompileTime) {
// AILEE_BITCOIN_WRITE_ENABLED=ON was set; write ops are compiled in.
EXPECT_TRUE(true);
TEST(BitcoinShadowModeTest, BitcoinWritesThrowWhenDisabled) {
// When AILEE_BITCOIN_WRITE_DISABLED is set, Bitcoin write operations
// should be blocked at runtime by throwing std::runtime_error.
// buildRawTx should reject any attempt to construct a spendable tx.
EXPECT_THROW(
{
try {
// Parameters are illustrative; exact semantics are handled by the implementation.
auto rawTx = buildRawTx("from_address", "to_address", 1000);
(void)rawTx;
} catch (const std::runtime_error &e) {
// Optionally verify that the error message indicates writes are disabled.
std::string msg = e.what();
// Do not require a specific string to avoid brittleness, but ensure it's non-empty.
EXPECT_FALSE(msg.empty());
throw;
}
},
std::runtime_error);
// broadcastRaw should also be blocked and throw std::runtime_error.
EXPECT_THROW(
{
try {
auto txid = broadcastRaw("deadbeef");
(void)txid;
} catch (const std::runtime_error &e) {
std::string msg = e.what();
EXPECT_FALSE(msg.empty());
throw;
}
},
std::runtime_error);
}
#else
TEST(BitcoinShadowModeTest, BitcoinWritesSucceedWhenEnabled) {
// When AILEE_BITCOIN_WRITE_DISABLED is not set, Bitcoin write operations
// are compiled in and should be callable without throwing.
std::string rawTx;
EXPECT_NO_THROW(
{
// Construct a raw transaction; implementation defines address/amount handling.
rawTx = buildRawTx("from_address", "to_address", 1000);
});
// If buildRawTx returns a string, it should be non-empty for a valid call.
EXPECT_FALSE(rawTx.empty());
std::string txid;
EXPECT_NO_THROW(
{
txid = broadcastRaw(rawTx);
});
// A successfully broadcast transaction should typically have a non-empty txid.
EXPECT_FALSE(txid.empty());

Copilot uses AI. Check for mistakes.
}
#endif