From f91cd9303937fb8268f06000501dbdfbbab0c797 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Thu, 12 Feb 2026 21:12:53 -0600 Subject: [PATCH 1/8] security: [T1-06] fix division by zero in CalculateSystemHealth() (DGB-SEC-003) In the overflow-protection branch of DCA::CalculateSystemHealth(), totalDD is scaled down by dividing by 1000. When totalDD is between 1 and 999, this produces 0, causing a division-by-zero crash. Fix: check for scaledDD==0 and return max health (30000) since a tiny DD supply with large collateral is maximally healthy. Also harden SystemHealthMonitor::CalculateSystemHealth() with overflow protection for the collateral*price multiplication and the health numerator calculation, matching the pattern in dca.cpp. --- src/consensus/dca.cpp | 13 +++++++++++-- src/digidollar/health.cpp | 18 ++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/consensus/dca.cpp b/src/consensus/dca.cpp index f78b8942d4..edbf3ee592 100644 --- a/src/consensus/dca.cpp +++ b/src/consensus/dca.cpp @@ -84,8 +84,17 @@ int DynamicCollateralAdjustment::CalculateSystemHealth(CAmount totalCollateral, CAmount healthCalculation; const CAmount maxSafeDividend = std::numeric_limits::max() / 100; if (collateralValueCents > maxSafeDividend) { - // Scale down both numerator and denominator to avoid overflow - healthCalculation = (collateralValueCents / 1000) * 100 / (totalDD / 1000); + // Scale down both numerator and denominator to avoid overflow. + // SECURITY FIX (DGB-SEC-003): Guard against totalDD/1000==0 which + // causes division by zero when totalDD is between 1 and 999. + CAmount scaledDD = totalDD / 1000; + if (scaledDD == 0) { + // totalDD is tiny (< 1000 cents = $10) — collateral dwarfs it, + // so health is at maximum. + healthCalculation = 30000; + } else { + healthCalculation = (collateralValueCents / 1000) * 100 / scaledDD; + } } else { healthCalculation = (collateralValueCents * 100) / totalDD; } diff --git a/src/digidollar/health.cpp b/src/digidollar/health.cpp index 7d4a153d06..9d893c1d3c 100644 --- a/src/digidollar/health.cpp +++ b/src/digidollar/health.cpp @@ -689,10 +689,24 @@ int SystemHealthMonitor::CalculateSystemHealth(CAmount ddSupply, CAmount collate // price is in cents (100 = $1.00 DGB price) // collateral is in satoshis // Formula: (satoshis * price_cents) / COIN = cents - CAmount collateralValue = (collateral * price) / COIN; + // Guard against overflow: divide first when collateral is large + CAmount collateralValue; + const CAmount maxSafe = std::numeric_limits::max() / (price > 0 ? price : 1); + if (collateral > maxSafe) { + collateralValue = (collateral / COIN) * price; + } else { + collateralValue = (collateral * price) / COIN; + } // Health = (Collateral Value / DD Value) * 100 - int health = static_cast((collateralValue * 100) / ddSupply); + // Guard against overflow in numerator + int health; + const CAmount maxSafeMul = std::numeric_limits::max() / 100; + if (collateralValue > maxSafeMul) { + health = static_cast(collateralValue / (ddSupply / 100 > 0 ? ddSupply / 100 : 1)); + } else { + health = static_cast((collateralValue * 100) / ddSupply); + } // Cap at reasonable maximum return std::min(health, 300); From 699e29b050c4af02001ed0bd7f3cfa0c3e1b97f3 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Thu, 12 Feb 2026 21:19:46 -0600 Subject: [PATCH 2/8] fix: address Copilot review comments on SEC-003 - Fix division by zero in health.cpp overflow branch (ddSupply/100==0) - Add early return for invalid price in SystemHealthMonitor - Fix comment accuracy: "between 1 and 999 cents" not "< 1000 cents" - Remove ternary fallback, use explicit guard pattern matching dca.cpp --- src/consensus/dca.cpp | 2 +- src/digidollar/health.cpp | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/consensus/dca.cpp b/src/consensus/dca.cpp index edbf3ee592..161e335911 100644 --- a/src/consensus/dca.cpp +++ b/src/consensus/dca.cpp @@ -89,7 +89,7 @@ int DynamicCollateralAdjustment::CalculateSystemHealth(CAmount totalCollateral, // causes division by zero when totalDD is between 1 and 999. CAmount scaledDD = totalDD / 1000; if (scaledDD == 0) { - // totalDD is tiny (< 1000 cents = $10) — collateral dwarfs it, + // totalDD is between 1 and 999 cents (< $10) — collateral dwarfs it, // so health is at maximum. healthCalculation = 30000; } else { diff --git a/src/digidollar/health.cpp b/src/digidollar/health.cpp index 9d893c1d3c..43e03726c2 100644 --- a/src/digidollar/health.cpp +++ b/src/digidollar/health.cpp @@ -685,13 +685,18 @@ int SystemHealthMonitor::CalculateSystemHealth(CAmount ddSupply, CAmount collate return 300; // Perfect health if no DD issued } + // Guard against invalid price (same pattern as DCA::CalculateSystemHealth) + if (price <= 0) { + return 0; // Cannot calculate without valid price + } + // Calculate collateral value in cents // price is in cents (100 = $1.00 DGB price) // collateral is in satoshis // Formula: (satoshis * price_cents) / COIN = cents // Guard against overflow: divide first when collateral is large CAmount collateralValue; - const CAmount maxSafe = std::numeric_limits::max() / (price > 0 ? price : 1); + const CAmount maxSafe = std::numeric_limits::max() / price; if (collateral > maxSafe) { collateralValue = (collateral / COIN) * price; } else { @@ -703,7 +708,13 @@ int SystemHealthMonitor::CalculateSystemHealth(CAmount ddSupply, CAmount collate int health; const CAmount maxSafeMul = std::numeric_limits::max() / 100; if (collateralValue > maxSafeMul) { - health = static_cast(collateralValue / (ddSupply / 100 > 0 ? ddSupply / 100 : 1)); + // When ddSupply is 1-99, ddSupply/100 is 0 due to integer division. + // Return max health since collateral dwarfs the tiny supply. + CAmount scaledSupply = ddSupply / 100; + if (scaledSupply == 0) { + return 300; + } + health = static_cast(collateralValue / scaledSupply); } else { health = static_cast((collateralValue * 100) / ddSupply); } From 9f197c82e8d2a8d17b9d26eb83075fac1c929baa Mon Sep 17 00:00:00 2001 From: GTO90 Date: Thu, 12 Feb 2026 21:42:21 -0600 Subject: [PATCH 3/8] 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 cf959b6bdeee1a87057be5e0cddffe3ec9f62be4 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 11:23:23 -0600 Subject: [PATCH 4/8] 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 601a561b8d2084ccbe9b7daa3c069eff6941ba6a Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 11:53:32 -0600 Subject: [PATCH 5/8] 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 068f8b8b01d831009190c8308d48c3ea67e7b0d0 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 13:38:17 -0600 Subject: [PATCH 6/8] 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 317d63d9649fa7b416d02e9ea30a2a2e7799ce5c Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 13:46:07 -0600 Subject: [PATCH 7/8] 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 59faad32beaeab51efbdc283c56d563775bbc3f8 Mon Sep 17 00:00:00 2001 From: GTO90 Date: Fri, 13 Feb 2026 14:51:18 -0600 Subject: [PATCH 8/8] 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()); } // =============================================================================