From 193e56d2fd87c487e997e0862cd3e6cb16f66740 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Thu, 12 Feb 2026 21:14:36 -0600 Subject: [PATCH 1/7] security: [T1-07] reject oracle_id > 255 to prevent truncation (DGB-SEC-004) The on-chain script format stores oracle_id as a single byte (uint8). The in-memory type is uint32_t. Without validation, IDs > 255 are silently truncated via `& 0xFF`, causing ID collisions (e.g. oracle 1 and 257 become indistinguishable) and signature verification failures. Fix adds validation at two layers: - IsValidOracleMessage(): rejects at P2P ingestion boundary - CreateOracleScript(): defense-in-depth rejection at serialization for both Phase One and Phase Two script formats --- src/oracle/bundle_manager.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/oracle/bundle_manager.cpp b/src/oracle/bundle_manager.cpp index e45e394602..d984a38718 100644 --- a/src/oracle/bundle_manager.cpp +++ b/src/oracle/bundle_manager.cpp @@ -454,6 +454,11 @@ CScript OracleBundleManager::CreateOracleScript(const COracleBundle& bundle) con // Per-oracle entries: oracle_id + schnorr_sig for (const auto& msg : bundle.messages) { + // SECURITY (DGB-SEC-004): Defense-in-depth — reject at serialization + if (msg.oracle_id > 255) { + LogPrintf("Oracle: Phase Two rejecting oracle_id %d > 255\n", msg.oracle_id); + return CScript(); + } p2_data.push_back(static_cast(msg.oracle_id & 0xFF)); if (msg.schnorr_sig.size() == 64) { p2_data.insert(p2_data.end(), msg.schnorr_sig.begin(), msg.schnorr_sig.end()); @@ -487,6 +492,11 @@ CScript OracleBundleManager::CreateOracleScript(const COracleBundle& bundle) con compact_data.reserve(17); // Oracle ID (uint8 for Phase One, expandable to uint32 for Phase Two) + // SECURITY (DGB-SEC-004): Defense-in-depth — reject at serialization + if (msg.oracle_id > 255) { + LogPrintf("Oracle: Phase One rejecting oracle_id %d > 255\n", msg.oracle_id); + return CScript(); + } compact_data.push_back(static_cast(msg.oracle_id & 0xFF)); // Price in micro-USD (uint64, little-endian) @@ -1033,6 +1043,15 @@ void OracleBundleManager::UpdateEpochBundle(int32_t epoch) bool OracleBundleManager::IsValidOracleMessage(const COraclePriceMessage& message) const { + // SECURITY (DGB-SEC-004): Reject oracle_id > 255. The on-chain script + // format stores oracle_id as a single byte. Accepting larger IDs would + // silently truncate, causing ID collisions and signature mismatches. + if (message.oracle_id > 255) { + LogPrintf("Oracle: Rejecting message with oracle_id %d > 255 (exceeds 1-byte on-chain format)\n", + message.oracle_id); + return false; + } + // Phase One: Skip chainparams check when min_oracle_count == 1 (testing mode) if (min_oracle_count == 1) { if (!message.IsValid()) return false; From edf9b996065dbebc7e5c1d54d6db05b67c218808 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Thu, 12 Feb 2026 21:42:21 -0600 Subject: [PATCH 2/7] build: fix CI depends failures for macOS ARM64 and Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenSSL 1.1.1w Configure misparses multi-word flags like "-arch arm64" when passed as positional args — the shell splits them and Configure treats the second word as a target name, causing "target already defined" on ARM64 macOS. Pass CFLAGS/CPPFLAGS as VAR=value assignments per OpenSSL's official INSTALL docs. libcurl 8.5.0 fopen.c uses fileno/fdopen (POSIX) which are hidden by the depends system's strict -std=c11. Add -D_GNU_SOURCE for Linux. --- depends/packages/libcurl.mk | 3 +++ depends/packages/openssl.mk | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/depends/packages/libcurl.mk b/depends/packages/libcurl.mk index 6376e37653..2a3e5ddce6 100644 --- a/depends/packages/libcurl.mk +++ b/depends/packages/libcurl.mk @@ -15,6 +15,9 @@ define $(package)_set_vars $(package)_config_opts += --disable-tftp --without-brotli --without-zstd --without-libidn2 $(package)_config_opts += --without-libpsl --without-nghttp2 --disable-dependency-tracking $(package)_config_opts_linux=--with-pic + # -D_GNU_SOURCE exposes POSIX functions (fileno, fdopen) that libcurl's + # fopen.c needs but are hidden by the depends system's strict -std=c11. + $(package)_cppflags_linux=-D_GNU_SOURCE $(package)_config_env_linux=LIBS="-ldl -lpthread" $(package)_config_opts_mingw32=--with-pic $(package)_config_env_mingw32=LIBS="-lws2_32 -lcrypt32" diff --git a/depends/packages/openssl.mk b/depends/packages/openssl.mk index 1bbcb671cf..83f676062d 100644 --- a/depends/packages/openssl.mk +++ b/depends/packages/openssl.mk @@ -11,7 +11,13 @@ define $(package)_set_vars $(package)_config_opts+=no-md2 no-rc5 no-rdrand no-rfc3779 no-sctp no-shared $(package)_config_opts+=no-ssl-trace no-ssl2 no-ssl3 no-tests no-unit-test no-weak-ssl-ciphers $(package)_config_opts+=no-zlib no-zlib-dynamic no-static-engine no-comp no-afalgeng - $(package)_config_opts+=no-engine no-hw no-asm $($(package)_cflags) $($(package)_cppflags) + $(package)_config_opts+=no-engine no-hw no-asm + # Pass compiler flags as VAR=value assignments (per OpenSSL INSTALL docs) + # rather than positional args, because multi-word flags like "-arch arm64" + # get split by the shell and OpenSSL's Configure misparses the second word + # as a target name, causing "target already defined" errors on ARM64 macOS. + $(package)_config_opts+=CFLAGS="$($(package)_cflags)" + $(package)_config_opts+=CPPFLAGS="$($(package)_cppflags)" $(package)_config_opts_linux=-fPIC -D_GNU_SOURCE $(package)_config_opts_freebsd=-fPIC $(package)_config_opts_x86_64_linux=linux-x86_64 From 9d129ba54ebad65b142b8b60762e3dd226980f8b Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 11:23:23 -0600 Subject: [PATCH 3/7] build: fix deferred evaluation for OpenSSL CFLAGS in depends Use $$ deferred evaluation for CFLAGS/CPPFLAGS assignments so OS-specific flags (cflags_linux, cppflags_linux) appended by funcs.mk after set_vars are included. Route -fPIC and -D_GNU_SOURCE through proper cflags/cppflags variables instead of bare config_opts to avoid OpenSSL's "Mixing make variables" rejection. --- depends/packages/openssl.mk | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/depends/packages/openssl.mk b/depends/packages/openssl.mk index 83f676062d..45b730600c 100644 --- a/depends/packages/openssl.mk +++ b/depends/packages/openssl.mk @@ -16,10 +16,13 @@ define $(package)_set_vars # rather than positional args, because multi-word flags like "-arch arm64" # get split by the shell and OpenSSL's Configure misparses the second word # as a target name, causing "target already defined" errors on ARM64 macOS. - $(package)_config_opts+=CFLAGS="$($(package)_cflags)" - $(package)_config_opts+=CPPFLAGS="$($(package)_cppflags)" - $(package)_config_opts_linux=-fPIC -D_GNU_SOURCE - $(package)_config_opts_freebsd=-fPIC + # Use $$ for deferred evaluation so OS-specific flags (cflags_linux, etc.) + # appended by funcs.mk after set_vars runs are included in the expansion. + $(package)_config_opts+=CFLAGS="$$($(package)_cflags)" + $(package)_config_opts+=CPPFLAGS="$$($(package)_cppflags)" + $(package)_cflags_linux=-fPIC + $(package)_cppflags_linux=-D_GNU_SOURCE + $(package)_cflags_freebsd=-fPIC $(package)_config_opts_x86_64_linux=linux-x86_64 $(package)_config_opts_i686_linux=linux-generic32 $(package)_config_opts_arm_linux=linux-generic32 From 20d733161f73b43232d52eee461b75d60ea0580b Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 11:53:32 -0600 Subject: [PATCH 4/7] build: remove empty ARFLAGS from OpenSSL config_env The depends system never defines $(package)_arflags, so ARFLAGS=$($(package)_arflags) in config_env exported an empty ARFLAGS="" to the environment. When CFLAGS/CPPFLAGS are passed as VAR=value assignments (not positional flags), OpenSSL 1.1.1w's Configure sets $anyuseradd=false and falls back to reading env vars. The empty ARFLAGS overrides the target default "r" from Configurations/00-base-templates.conf, producing a Makefile with ARFLAGS= (empty). This causes "ar: two different operation options specified" on Linux and "ar: illegal option -- /" on macOS during build_libs, as ar interprets the archive path as flags. Fix: remove ARFLAGS from config_env entirely, letting Configure use its target default ARFLAGS="r". Verified locally: - Linux x86_64: ar r apps/libapps.a ... (ARFLAGS=r in Makefile) - macOS ARM64: ar r apps/libapps.a ... (ARFLAGS=r in Makefile) - Full build_libs completes successfully on darwin64-arm64-cc --- depends/packages/openssl.mk | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/depends/packages/openssl.mk b/depends/packages/openssl.mk index 45b730600c..5780089a92 100644 --- a/depends/packages/openssl.mk +++ b/depends/packages/openssl.mk @@ -5,7 +5,14 @@ $(package)_file_name=$(package)-$($(package)_version).tar.gz $(package)_sha256_hash=cf3098950cb4d853ad95c0841f1f9c6d3dc102dccfcacd521d93925208b76ac8 define $(package)_set_vars - $(package)_config_env=AR="$($(package)_ar)" ARFLAGS=$($(package)_arflags) RANLIB="$($(package)_ranlib)" CC="$($(package)_cc)" + # Do NOT export ARFLAGS in config_env. The depends system never defines + # $(package)_arflags, so it expands to empty string. When CFLAGS/CPPFLAGS + # are passed as VAR=value (no positional flags), OpenSSL's Configure sets + # $anyuseradd=false and falls back to reading env vars. An empty ARFLAGS + # in the environment overrides the target default ("r"), producing a + # Makefile with ARFLAGS= (empty), which causes "ar: two different + # operation options specified" at build time. + $(package)_config_env=AR="$($(package)_ar)" RANLIB="$($(package)_ranlib)" CC="$($(package)_cc)" $(package)_config_env_android=ANDROID_NDK_ROOT=$(host_prefix)/native $(package)_config_opts=no-capieng no-dso no-dtls1 no-ec_nistp_64_gcc_128 no-gost $(package)_config_opts+=no-md2 no-rc5 no-rdrand no-rfc3779 no-sctp no-shared From cbbfd017a5a1a66ad3838478eb0b1e9a6ba1d58c Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 13:38:17 -0600 Subject: [PATCH 5/7] test: add diagnostic messages to CI-failing mint validation tests Replace BOOST_CHECK with BOOST_CHECK_MESSAGE in 5 mint validation tests that fail on CI but pass locally. Diagnostic output captures the exact reject reason, script sizes, collateral amounts, and script type identification to help debug the CI-specific failure. --- src/test/digidollar_validation_tests.cpp | 47 +++++++++++++++++++----- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/test/digidollar_validation_tests.cpp b/src/test/digidollar_validation_tests.cpp index 0ded290409..06f4171a41 100644 --- a/src/test/digidollar_validation_tests.cpp +++ b/src/test/digidollar_validation_tests.cpp @@ -342,8 +342,14 @@ BOOST_FIXTURE_TEST_CASE(transaction_validation_mint_tx, DigiDollarValidationTest CTransaction tx(mtx); TxValidationState state; - BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); - BOOST_CHECK(state.IsValid()); + bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); + BOOST_CHECK_MESSAGE(result, "[transaction_validation_mint_tx] Expected valid, got reject: " + state.GetRejectReason() + + " | collateralScript.size()=" + std::to_string(collateralScript.size()) + + " ddScript.size()=" + std::to_string(ddScript.size()) + + " requiredCollateral=" + std::to_string(requiredCollateral) + + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) + + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); + BOOST_CHECK_MESSAGE(state.IsValid(), "[transaction_validation_mint_tx] state invalid: " + state.GetRejectReason()); } BOOST_FIXTURE_TEST_CASE(transaction_validation_invalid_mint_amount, DigiDollarValidationTestSetup) @@ -466,8 +472,14 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_valid_basic_mint, DigiDollarValidationTe TxValidationState state; // This should pass when implementation is complete - BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); - BOOST_CHECK(state.IsValid()); + bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); + BOOST_CHECK_MESSAGE(result, "[mint_validation_valid_basic_mint] Expected valid, got reject: " + state.GetRejectReason() + + " | collateralScript.size()=" + std::to_string(collateralScript.size()) + + " ddScript.size()=" + std::to_string(ddScript.size()) + + " requiredCollateral=" + std::to_string(requiredCollateral) + + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) + + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); + BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_valid_basic_mint] state invalid: " + state.GetRejectReason()); } BOOST_FIXTURE_TEST_CASE(mint_validation_insufficient_collateral, DigiDollarValidationTestSetup) @@ -724,8 +736,13 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_dca_multiplier_adjustment, DigiDollarVal TxValidationState state; // Should pass with adjusted collateral - BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); - BOOST_CHECK(state.IsValid()); + bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); + BOOST_CHECK_MESSAGE(result, "[mint_validation_dca_multiplier] Expected valid, got reject: " + state.GetRejectReason() + + " | systemCollateral=" + std::to_string(validationContext.systemCollateral) + + " adjustedCollateral=" + std::to_string(adjustedCollateral) + + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) + + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); + BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_dca_multiplier] state invalid: " + state.GetRejectReason()); // Test with original 500% collateral - should fail CAmount originalCollateral = (static_cast(ddAmount) * COIN * 500 * 100) / mockOraclePrice; @@ -881,8 +898,13 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_edge_case_exact_minimum, DigiDollarValid CTransaction tx(mtx); TxValidationState state; - BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); - BOOST_CHECK(state.IsValid()); + bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); + BOOST_CHECK_MESSAGE(result, "[mint_validation_edge_case_exact_minimum] Expected valid, got reject: " + state.GetRejectReason() + + " | ddAmount=" + std::to_string(ddAmount) + + " exactCollateral=" + std::to_string(exactCollateral) + + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) + + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); + BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_edge_case_exact_minimum] state invalid: " + state.GetRejectReason()); } BOOST_FIXTURE_TEST_CASE(mint_validation_edge_case_exact_maximum, DigiDollarValidationTestSetup) @@ -917,8 +939,13 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_edge_case_exact_maximum, DigiDollarValid CTransaction tx(mtx); TxValidationState state; - BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); - BOOST_CHECK(state.IsValid()); + bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); + BOOST_CHECK_MESSAGE(result, "[mint_validation_edge_case_exact_maximum] Expected valid, got reject: " + state.GetRejectReason() + + " | ddAmount=" + std::to_string(ddAmount) + + " requiredCollateral=" + std::to_string(requiredCollateral) + + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) + + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); + BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_edge_case_exact_maximum] state invalid: " + state.GetRejectReason()); } // ============================================================================= From bb6a079b858b7cb6ecd5fabdc01f4b6aa1296a7e Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 13:46:07 -0600 Subject: [PATCH 6/7] test: fix cross-suite state pollution causing CI-only mint test failures Replace ClearFreeze() with ClearHistory() in the validation test fixture. ClearFreeze() only clears freeze flags, but UpdateState() (called during ValidateDigiDollarTransaction) recalculates volatility from stale price history left by earlier test suites (e.g., digidollar_health_tests) and re-sets the freeze. This caused all "valid mint" tests to fail with "minting-frozen-volatility" on CI where the test suite ordering (linker-dependent) places health_tests before validation_tests. ClearHistory() resets price history, volatility state, and all freeze flags, ensuring each test starts from a clean state. --- src/test/digidollar_validation_tests.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/digidollar_validation_tests.cpp b/src/test/digidollar_validation_tests.cpp index 06f4171a41..f83a28f6a2 100644 --- a/src/test/digidollar_validation_tests.cpp +++ b/src/test/digidollar_validation_tests.cpp @@ -37,8 +37,10 @@ struct DigiDollarValidationTestSetup : public TestingSetup { testPubKey = testKey.GetPubKey(); testXOnlyKey = XOnlyPubKey(testPubKey); - // Clear volatility freeze state from previous tests - DigiDollar::Volatility::VolatilityMonitor::ClearFreeze(); + // Clear ALL volatility state from previous test suites. + // ClearFreeze() alone is insufficient — UpdateState() recalculates + // from stale price history and can re-set freeze flags. + DigiDollar::Volatility::VolatilityMonitor::ClearHistory(); // Validation context is initialized in member initializer list } From ee87600400ba053a5a7c1d1e55bae5c191ad7af2 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 14:51:18 -0600 Subject: [PATCH 7/7] fix: resolve bad auto-merge that dropped DD amount consistency check The merge with feature/digidollar-v1 silently dropped critical code: 1. DD amount double-counting prevention in ValidateMintTransaction() The OP_RETURN and DD token output both encode ddAmount. Without the consistency check, totalDD was counted twice (e.g. 10000 + 10000 = 20000), requiring 2x the collateral and failing all valid mint tests. 2. Missing #include 3. Improved log format for insufficient-collateral diagnostic Also adopts the base branch's test file which includes: - DD OP_RETURN outputs required for T1-04b NUMS verification - Proper NUMS key usage via GetCollateralNUMSKey() - ClearHistory() fix for cross-suite volatility state pollution --- src/digidollar/validation.cpp | 19 ++++- src/test/digidollar_validation_tests.cpp | 93 +++++++++++++----------- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/src/digidollar/validation.cpp b/src/digidollar/validation.cpp index 8ea1ad5962..9ef339c0b8 100644 --- a/src/digidollar/validation.cpp +++ b/src/digidollar/validation.cpp @@ -5,6 +5,7 @@ #include #include #include +#include // Phase 1 metadata tracking support using DigiDollar::ScriptMetadata; @@ -870,7 +871,18 @@ bool ValidateMintTransaction(const CTransaction& tx, LogPrintf("DigiDollar: Invalid DD amount: %d\n", ddAmount); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-dd-amount"); } - totalDD += ddAmount; + // If totalDD was already set from OP_RETURN, verify consistency + // rather than double-counting the DD amount. + if (totalDD > 0) { + if (ddAmount != totalDD) { + LogPrintf("DigiDollar: DD amount mismatch: token output=%lld, OP_RETURN=%lld\n", + (long long)ddAmount, (long long)totalDD); + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-dd-amount-mismatch", + "DD token output amount does not match OP_RETURN amount"); + } + } else { + totalDD += ddAmount; + } } // If we can't extract (cross-node validation), we'll calculate after loop } @@ -1001,8 +1013,9 @@ bool ValidateMintTransaction(const CTransaction& tx, // Verify sufficient collateral if (totalCollateral < requiredCollateral) { - LogPrintf("DigiDollar: Insufficient collateral: provided %d, required %d\n", - totalCollateral, requiredCollateral); + LogPrintf("DigiDollar: Insufficient collateral: provided %lld, required %lld (totalDD=%lld, lockPeriod=%lld)\n", + (long long)totalCollateral, (long long)requiredCollateral, + (long long)totalDD, (long long)lockPeriod); return state.Invalid(TxValidationResult::TX_CONSENSUS, "insufficient-collateral"); } diff --git a/src/test/digidollar_validation_tests.cpp b/src/test/digidollar_validation_tests.cpp index fea9021140..b788410e73 100644 --- a/src/test/digidollar_validation_tests.cpp +++ b/src/test/digidollar_validation_tests.cpp @@ -52,6 +52,22 @@ struct DigiDollarValidationTestSetup : public TestingSetup { int mockSystemCollateral; int mockHeight; DigiDollar::ValidationContext validationContext; + + // Helper: Build a DD mint OP_RETURN output script + // Format: OP_RETURN <"DD"> + CScript MakeDDMintOpReturn(CAmount ddAmount, int64_t lockHeight, int lockTier) { + CScript script; + script << OP_RETURN; + std::vector dd_marker = {'D', 'D'}; + script << dd_marker; + script << CScriptNum(1); // Type = MINT + script << CScriptNum::serialize(ddAmount); + script << CScriptNum::serialize(lockHeight); + script << CScriptNum(lockTier); + std::vector keyData(testXOnlyKey.begin(), testXOnlyKey.end()); + script << keyData; + return script; + } }; // ============================================================================ @@ -315,43 +331,51 @@ BOOST_FIXTURE_TEST_CASE(script_validation_non_dd_script, DigiDollarValidationTes BOOST_FIXTURE_TEST_CASE(transaction_validation_mint_tx, DigiDollarValidationTestSetup) { - // Create a mock mint transaction + // Create a valid mint transaction with all required components: + // - Collateral input + // - DD OP_RETURN with owner pubkey (required for NUMS verification per T1-04b) + // - Collateral output (P2TR with NUMS internal key) + // - DD token output CMutableTransaction mtx; mtx.nVersion = 0x01000770; // DD_TX_MINT (type=1 in bits 24-31, marker=0x0770 in bits 0-15) - // Add collateral input (simplified for test) + // Add collateral input mtx.vin.resize(1); mtx.vin[0].prevout = COutPoint(uint256S("0x1234"), 0); - // Add collateral output + // Set up mint parameters DigiDollar::MintParams params; params.ddAmount = 10000; // $100.00 - params.lockHeight = mockHeight + 30 * 24 * 60 * 4; + params.lockHeight = mockHeight + 30 * 24 * 60 * 4; // 30-day lock params.ownerKey = testXOnlyKey; - params.internalKey = testXOnlyKey; + params.internalKey = DigiDollar::GetCollateralNUMSKey(); params.oracleKeys = DigiDollar::GetOracleKeys(15); CScript collateralScript = DigiDollar::CreateCollateralP2TR(params); CAmount requiredCollateral = (static_cast(params.ddAmount) * COIN * 500 * 100) / mockOraclePrice; - mtx.vout.resize(2); - mtx.vout[0] = CTxOut(requiredCollateral, collateralScript); + // DD OP_RETURN with owner pubkey (required for NUMS verification) + CScript opReturn = CScript() << OP_RETURN + << std::vector{'D', 'D'} + << CScriptNum(1) + << CScriptNum(params.ddAmount) + << CScriptNum(params.lockHeight) + << CScriptNum(1) // lockTier 1 = 30 days + << std::vector(testXOnlyKey.begin(), testXOnlyKey.end()); + + mtx.vout.resize(3); + mtx.vout[0] = CTxOut(0, opReturn); + mtx.vout[1] = CTxOut(requiredCollateral, collateralScript); // Add DD token output CScript ddScript = DigiDollar::CreateDigiDollarP2TR(testXOnlyKey, params.ddAmount); - mtx.vout[1] = CTxOut(0, ddScript); // DD tokens have no DGB value + mtx.vout[2] = CTxOut(0, ddScript); // DD tokens have no DGB value CTransaction tx(mtx); TxValidationState state; - bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); - BOOST_CHECK_MESSAGE(result, "[transaction_validation_mint_tx] Expected valid, got reject: " + state.GetRejectReason() - + " | collateralScript.size()=" + std::to_string(collateralScript.size()) - + " ddScript.size()=" + std::to_string(ddScript.size()) - + " requiredCollateral=" + std::to_string(requiredCollateral) - + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) - + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); - BOOST_CHECK_MESSAGE(state.IsValid(), "[transaction_validation_mint_tx] state invalid: " + state.GetRejectReason()); + BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); + BOOST_CHECK(state.IsValid()); } BOOST_FIXTURE_TEST_CASE(transaction_validation_invalid_mint_amount, DigiDollarValidationTestSetup) @@ -494,13 +518,9 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_valid_basic_mint, DigiDollarValidationTe // This should pass when implementation is complete bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); - BOOST_CHECK_MESSAGE(result, "[mint_validation_valid_basic_mint] Expected valid, got reject: " + state.GetRejectReason() - + " | collateralScript.size()=" + std::to_string(collateralScript.size()) - + " ddScript.size()=" + std::to_string(ddScript.size()) - + " requiredCollateral=" + std::to_string(requiredCollateral) - + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) - + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); - BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_valid_basic_mint] state invalid: " + state.GetRejectReason()); + BOOST_TEST_MESSAGE("mint_validation_valid_basic_mint result: " + std::to_string(result) + " reason: " + state.GetRejectReason()); + BOOST_CHECK(result); + BOOST_CHECK(state.IsValid()); } BOOST_FIXTURE_TEST_CASE(mint_validation_insufficient_collateral, DigiDollarValidationTestSetup) @@ -779,13 +799,8 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_dca_multiplier_adjustment, DigiDollarVal TxValidationState state; // Should pass with adjusted collateral - bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); - BOOST_CHECK_MESSAGE(result, "[mint_validation_dca_multiplier] Expected valid, got reject: " + state.GetRejectReason() - + " | systemCollateral=" + std::to_string(validationContext.systemCollateral) - + " adjustedCollateral=" + std::to_string(adjustedCollateral) - + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) - + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); - BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_dca_multiplier] state invalid: " + state.GetRejectReason()); + BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); + BOOST_CHECK(state.IsValid()); // Test with original 500% collateral - should fail CAmount originalCollateral = (static_cast(ddAmount) * COIN * 500 * 100) / mockOraclePrice; @@ -962,13 +977,8 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_edge_case_exact_minimum, DigiDollarValid CTransaction tx(mtx); TxValidationState state; - bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); - BOOST_CHECK_MESSAGE(result, "[mint_validation_edge_case_exact_minimum] Expected valid, got reject: " + state.GetRejectReason() - + " | ddAmount=" + std::to_string(ddAmount) - + " exactCollateral=" + std::to_string(exactCollateral) - + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) - + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); - BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_edge_case_exact_minimum] state invalid: " + state.GetRejectReason()); + BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); + BOOST_CHECK(state.IsValid()); } BOOST_FIXTURE_TEST_CASE(mint_validation_edge_case_exact_maximum, DigiDollarValidationTestSetup) @@ -1013,13 +1023,8 @@ BOOST_FIXTURE_TEST_CASE(mint_validation_edge_case_exact_maximum, DigiDollarValid CTransaction tx(mtx); TxValidationState state; - bool result = DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state); - BOOST_CHECK_MESSAGE(result, "[mint_validation_edge_case_exact_maximum] Expected valid, got reject: " + state.GetRejectReason() - + " | ddAmount=" + std::to_string(ddAmount) - + " requiredCollateral=" + std::to_string(requiredCollateral) - + " collateralType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(collateralScript))) - + " ddType=" + std::to_string(static_cast(DigiDollar::IdentifyScriptType(ddScript)))); - BOOST_CHECK_MESSAGE(state.IsValid(), "[mint_validation_edge_case_exact_maximum] state invalid: " + state.GetRejectReason()); + BOOST_CHECK(DigiDollar::ValidateDigiDollarTransaction(tx, validationContext, state)); + BOOST_CHECK(state.IsValid()); } // =============================================================================