From 0882a537f5020043491493fe5bf74e6dd6cd6fcd Mon Sep 17 00:00:00 2001 From: Vara Rahul Rajana Date: Mon, 20 Apr 2026 23:29:42 +0530 Subject: [PATCH 1/4] fix: enhance numeric parsing to support scientific notation and ambiguous hex --- backend/calc_functions/calc_func.py | 19 ++++++++++++------- backend/tests/test_calc_func.py | 8 ++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/backend/calc_functions/calc_func.py b/backend/calc_functions/calc_func.py index 7b52bee..776b394 100644 --- a/backend/calc_functions/calc_func.py +++ b/backend/calc_functions/calc_func.py @@ -2760,22 +2760,27 @@ def _parse_numeric_exact(raw: str): if not s: raise ValueError("empty number") - # hex? - if s.lower().startswith("0x") or ( - all(c in "0123456789abcdefABCDEF" for c in s) - and any(c in "abcdefABCDEF" for c in s) - ): + # explicit hex prefix – always hex + if s.lower().startswith("0x"): return int(s, 16) # plain integer? if _INT_DEC_RE.fullmatch(s): return int(s, 10) - # decimal/exp → Decimal + # decimal / fraction / scientific-notation → Decimal try: return Decimal(s) except InvalidOperation: - raise ValueError(f"'{raw}' is not a valid number") + pass + + # ambiguous hex (digits + a-f only, no 0x prefix) + if all(c in "0123456789abcdefABCDEF" for c in s) and any( + c in "abcdefABCDEF" for c in s + ): + return int(s, 16) + + raise ValueError(f"'{raw}' is not a valid number") def _coerce_for_op(a, b): """Promote to Decimal if either is Decimal; keep ints otherwise.""" diff --git a/backend/tests/test_calc_func.py b/backend/tests/test_calc_func.py index 72da372..8dabb67 100644 --- a/backend/tests/test_calc_func.py +++ b/backend/tests/test_calc_func.py @@ -846,6 +846,10 @@ def test_compare_equal_and_numeric_parsers(): assert calc._parse_numeric_exact("0x10") == 16 assert calc._parse_numeric_exact("10") == 10 assert calc._parse_numeric_exact("1.5") == Decimal("1.5") + assert calc._parse_numeric_exact("1e6") == Decimal("1e6") + assert calc._parse_numeric_exact("1e8") == Decimal("1e8") + assert calc._parse_numeric_exact("2.5e3") == Decimal("2.5e3") + assert calc._parse_numeric_exact("deadbeef") == 0xDEADBEEF with pytest.raises(ValueError): calc._parse_numeric_exact("") @@ -858,6 +862,8 @@ def test_compare_equal_and_numeric_parsers(): def test_compare_numbers_and_math_operations(): assert calc.compare_numbers(["10", "<", "20"]) == "true" assert calc.compare_numbers(["10", ">", "20"]) == "false" + assert calc.compare_numbers(["1e8", ">", "1000000"]) == "true" + assert calc.compare_numbers(["1e6", "<=", "1000000"]) == "true" with pytest.raises(ValueError): calc.compare_numbers(["10", "!=", "20"]) @@ -865,6 +871,8 @@ def test_compare_numbers_and_math_operations(): assert calc.math_operation(["10", "-", "5"]) == "5" assert calc.math_operation(["10", "*", "5"]) == "50" assert calc.math_operation(["3", "/", "2"]) == "1.5" + assert calc.math_operation(["1e8", "+", "1"]) == "100000001" + assert calc.math_operation(["1e6", "*", "2"]) == "2000000" with pytest.raises(ValueError): calc.math_operation(["1", "/", "0"]) From 0e361ae7240c9a0bec153c306f47fa9a4d25f127 Mon Sep 17 00:00:00 2001 From: Vara Rahul Rajana Date: Wed, 22 Apr 2026 19:36:44 +0530 Subject: [PATCH 2/4] fix: enforce strict numeric parsing, reject malformed scientific/hex, and fix secp256k1 lib path --- backend/calc_functions/calc_func.py | 54 ++++++++++++++++++++++++----- backend/tests/test_calc_func.py | 38 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/backend/calc_functions/calc_func.py b/backend/calc_functions/calc_func.py index 776b394..e593f73 100644 --- a/backend/calc_functions/calc_func.py +++ b/backend/calc_functions/calc_func.py @@ -19,6 +19,13 @@ getcontext().prec = 50 # plenty for money math _INT_DEC_RE = re.compile(r'^[+-]?\d+$', re.ASCII) +_STRICT_DECIMAL_RE = re.compile( + r'^[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$', re.ASCII +) + +_LOOKS_LIKE_SCI_RE = re.compile( + r'(^[+-]?\d+\.?\d*[eE])|(^[eE]\d+)', re.ASCII +) _CURVE_ORDER = SECP256k1.order _CURVE_GEN = SECP256k1.generator @@ -30,6 +37,28 @@ VerifyScriptWithTrace ) +# Wire bitcointx to the secp256k1 library bundled with the secp256k1 Python +# package. bitcointx.util._secp256k1_library_path defaults to None, which +# causes ctypes.util.find_library('secp256k1') to fail on systems where the +# shared library is not installed system-wide (e.g. macOS + pip-only install). +# The secp256k1 package bundles its own compiled extension that ctypes can +# load directly, so we point bitcointx at it before any verification call. +try: + import bitcointx.util as _btx_util + if _btx_util._secp256k1_library_path is None: + import os as _os + _secp256k1_pkg = _os.path.dirname(secp256k1.__file__) + for _candidate in _os.listdir(_secp256k1_pkg): + if _candidate.startswith('_libsecp256k1') and ( + _candidate.endswith('.so') or _candidate.endswith('.dylib') + ): + _btx_util._secp256k1_library_path = _os.path.join( + _secp256k1_pkg, _candidate + ) + break +except Exception: + pass # non-fatal: verification will fail with a clear error if still missing + from bitcointx.core.scripteval import ( # flag constants SCRIPT_VERIFY_P2SH, @@ -2032,8 +2061,10 @@ def _hasher(msg: bytes) -> bytes: amount_supplied = bool(amount_raw) if amount_supplied: try: + # Bitcoin amounts are always whole satoshis; reject any non-integer + # string (fractions, scientific notation) to prevent silent truncation + # that would produce a wrong sighash. amount_param = int(amount_raw) - # Validate amount is non-negative if amount_param < 0: raise ValueError("Amount must be non-negative") except ValueError as e: @@ -2755,7 +2786,11 @@ def _parse_numeric_exact(raw: str): - decimal ints: '144', '+10', '-7' - hex: '0x90', '90' with A–F present (e.g. 'deadbeef') - decimal with fraction/exp: '12.5', '1e6', '0.1' + Raises ValueError for any input that does not fit a supported format + exactly, including NaN, Infinity, underscores, binary literals, trailing + dots, and malformed scientific-notation strings. """ + print("DEBUG parsing:", raw) s = str(raw).strip() if not s: raise ValueError("empty number") @@ -2764,19 +2799,22 @@ def _parse_numeric_exact(raw: str): if s.lower().startswith("0x"): return int(s, 16) - # plain integer? + if s.lower().startswith("0b"): + raise ValueError(f"'{raw}' is not a valid number") + + # plain integer if _INT_DEC_RE.fullmatch(s): return int(s, 10) # decimal / fraction / scientific-notation → Decimal - try: + if _STRICT_DECIMAL_RE.fullmatch(s): return Decimal(s) - except InvalidOperation: - pass - # ambiguous hex (digits + a-f only, no 0x prefix) - if all(c in "0123456789abcdefABCDEF" for c in s) and any( - c in "abcdefABCDEF" for c in s + # Ambiguous hex: all hex digits, at least one a–f letter, no recognised + if ( + all(c in "0123456789abcdefABCDEF" for c in s) + and any(c in "abcdefABCDEF" for c in s) + and not _LOOKS_LIKE_SCI_RE.search(s) ): return int(s, 16) diff --git a/backend/tests/test_calc_func.py b/backend/tests/test_calc_func.py index 8dabb67..cef0e46 100644 --- a/backend/tests/test_calc_func.py +++ b/backend/tests/test_calc_func.py @@ -876,6 +876,44 @@ def test_compare_numbers_and_math_operations(): with pytest.raises(ValueError): calc.math_operation(["1", "/", "0"]) +@pytest.mark.parametrize("raw", [ + "1e", "e10", "1E", "E10", "1e1e", + "0b10", "0B10", + # special Decimal values that must be rejected + "NaN", "nan", "Infinity", "-Infinity", "sNaN", + # underscore-separated numeric literals + "_10", "1__0", "1_", + # trailing dots – looks like a decimal but the fractional part is absent + "123.", "0.", + # malformed scientific notation: trailing dot before the exponent + "1.e6", "0.e3", +]) +def test_parse_numeric_exact_rejects_malformed(raw): + with pytest.raises(ValueError): + calc._parse_numeric_exact(raw) + + +@pytest.mark.parametrize("raw,expected", [ + ("fe1", 0xfe1), + ("a1e4", 0xa1e4), + ("b3e2f", 0xb3e2f), + (".5e3", Decimal(".5e3")), + ("+1e6", Decimal("+1e6")), + ("-0.5", Decimal("-0.5")), + ("0e0", Decimal("0e0")), + ("0X1F", 0x1F), + ("0xDEAD", 0xDEAD), +]) +def test_parse_numeric_exact_accepts_valid(raw, expected): + assert calc._parse_numeric_exact(raw) == expected + + +def test_parse_numeric_exact_malformed_propagates_to_api(): + with pytest.raises(ValueError): + calc.compare_numbers(["1e", "<", "31"]) + with pytest.raises(ValueError): + calc.math_operation(["e10", "+", "1"]) + def test_hash160_and_sha256_address_helpers(): p2pkh = calc.hash160_to_p2pkh_address(GENESIS_HASH160) From 8b5e347bbb6ae5b0402c41ad892cd2de4b02b334 Mon Sep 17 00:00:00 2001 From: Vara Rahul Rajana Date: Wed, 22 Apr 2026 19:38:11 +0530 Subject: [PATCH 3/4] changes --- backend/calc_functions/calc_func.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/calc_functions/calc_func.py b/backend/calc_functions/calc_func.py index e593f73..1de8cef 100644 --- a/backend/calc_functions/calc_func.py +++ b/backend/calc_functions/calc_func.py @@ -37,12 +37,6 @@ VerifyScriptWithTrace ) -# Wire bitcointx to the secp256k1 library bundled with the secp256k1 Python -# package. bitcointx.util._secp256k1_library_path defaults to None, which -# causes ctypes.util.find_library('secp256k1') to fail on systems where the -# shared library is not installed system-wide (e.g. macOS + pip-only install). -# The secp256k1 package bundles its own compiled extension that ctypes can -# load directly, so we point bitcointx at it before any verification call. try: import bitcointx.util as _btx_util if _btx_util._secp256k1_library_path is None: @@ -57,7 +51,7 @@ ) break except Exception: - pass # non-fatal: verification will fail with a clear error if still missing + pass from bitcointx.core.scripteval import ( # flag constants @@ -2062,8 +2056,6 @@ def _hasher(msg: bytes) -> bytes: if amount_supplied: try: # Bitcoin amounts are always whole satoshis; reject any non-integer - # string (fractions, scientific notation) to prevent silent truncation - # that would produce a wrong sighash. amount_param = int(amount_raw) if amount_param < 0: raise ValueError("Amount must be non-negative") From a6f0a801ac186824032691c88836365f623d8b16 Mon Sep 17 00:00:00 2001 From: Vara Rahul Rajana Date: Wed, 22 Apr 2026 19:40:25 +0530 Subject: [PATCH 4/4] fix: remove debug print statement from _parse_numeric_exact --- backend/calc_functions/calc_func.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/calc_functions/calc_func.py b/backend/calc_functions/calc_func.py index 1de8cef..8c5dfae 100644 --- a/backend/calc_functions/calc_func.py +++ b/backend/calc_functions/calc_func.py @@ -2782,7 +2782,6 @@ def _parse_numeric_exact(raw: str): exactly, including NaN, Infinity, underscores, binary literals, trailing dots, and malformed scientific-notation strings. """ - print("DEBUG parsing:", raw) s = str(raw).strip() if not s: raise ValueError("empty number")