Skip to content

sync: bring py-native up to date with the C-API surface#11

Open
fpelliccioni wants to merge 1 commit into
masterfrom
sync/kth-0.83-full-regen
Open

sync: bring py-native up to date with the C-API surface#11
fpelliccioni wants to merge 1 commit into
masterfrom
sync/kth-0.83-full-regen

Conversation

@fpelliccioni
Copy link
Copy Markdown
Contributor

@fpelliccioni fpelliccioni commented May 11, 2026

Summary

Brings the Python bindings level with the C-API headers shipped
since the last py-native sync. 17 classes that ship in
`kth/capi/` had Python-side counterparts cabled in the binding
generator but no actual file emitted in this repo — this PR
emits them all and wires them into `module.c`, `setup.py`,
and `kth_native.pyi`.

New classes exposed to Python

  • chain — `input_point` (alias of `point`; PR kth#327)
  • vm — `big_number`, `number`, `script_execution_context`
  • wallet — `bitcoin_uri`, `cashaddr` (encode),
    `coin_selection` + `coin_selection_result`,
    `ek_private` / `ek_public` / `ek_token` (BIP38),
    `encrypted_keys` (BIP38 free fns),
    `language` (BIP39 wordlists), `message` (BIP137),
    `mnemonic` (BIP39), `stealth_address` /
    `stealth_receiver` / `stealth_sender` (BIP63)

Pre-existing classes that gained new methods

  • `vm/program` — `pop_number` / `pop_big_number` /
    `pop_simple` / `top_number` / `top_big_number` (the
    BigInt opcode surface that landed on the C-API recently
    now reaches Python).
  • `chain/output_point`, `wallet/wallet_data` — minor regen
    tweaks.

Plumbing

  • `module.c` auto-emit picked up the new includes + 18
    `KTH_REGISTER_METHODS` lines.
  • `setup.py` extended with the 18 new `.cpp` files under
    the existing chain / wallet / vm groups.
  • `kth_native.pyi` type stubs refreshed.
  • `capsule_names.h` — new capsule names for the new opaque
    handles.

Not in scope

  • `cashaddr_decode` (returns `pair<string, data_chunk>`)
  • `coin_selection::create_tx_template` +
    `create_token_split_tx_template` (return
    `expect<tuple<list<...>, ...>>`)

The C-side has these; the Python side will pick them up when
the Python backend gains the missing return-shape models.

Test plan

  • `pip install -e .` builds successfully.
  • `python -c "import kth_native; print(dir(kth_native))"`
    lists the new surface members.
  • Existing test suite passes.
  • Spot-check one of the new classes from Python (e.g.
    `kth_native.wallet_cashaddr_encode("bitcoincash", b"...")`).

Summary by CodeRabbit

  • New Features

    • Input point operations for transaction handling
    • VM big numbers with arithmetic and comparisons
    • Wallet coin selection and optimization
    • Bitcoin URI construction and parsing
    • Message signing and verification
    • Multilingual mnemonic generation
    • Encrypted key material operations
    • CashAddr address encoding
  • Enhancements

    • Improved wallet data creation with language support
    • Enhanced VM program stack with typed operations

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Warning

Rate limit exceeded

@fpelliccioni has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 9 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbb0634f-8e2e-40a2-abda-9ddb0e948af0

📥 Commits

Reviewing files that changed from the base of the PR and between 8942aeb and 35404a4.

📒 Files selected for processing (60)
  • include/kth/py-native/capsule_names.h
  • include/kth/py-native/chain/abla.h
  • include/kth/py-native/chain/abla_config.h
  • include/kth/py-native/chain/abla_state.h
  • include/kth/py-native/chain/input_point.h
  • include/kth/py-native/chain/output_point.h
  • include/kth/py-native/vm/big_number.h
  • include/kth/py-native/vm/number.h
  • include/kth/py-native/vm/program.h
  • include/kth/py-native/vm/script_execution_context.h
  • include/kth/py-native/wallet/bitcoin_uri.h
  • include/kth/py-native/wallet/cashaddr.h
  • include/kth/py-native/wallet/coin_selection.h
  • include/kth/py-native/wallet/coin_selection_result.h
  • include/kth/py-native/wallet/ek_private.h
  • include/kth/py-native/wallet/ek_public.h
  • include/kth/py-native/wallet/ek_token.h
  • include/kth/py-native/wallet/encrypted_keys.h
  • include/kth/py-native/wallet/language.h
  • include/kth/py-native/wallet/message.h
  • include/kth/py-native/wallet/mnemonic.h
  • include/kth/py-native/wallet/stealth_address.h
  • include/kth/py-native/wallet/stealth_receiver.h
  • include/kth/py-native/wallet/stealth_sender.h
  • include/kth/py-native/wallet/wallet_data.h
  • kth_native.pyi
  • setup.py
  • src/chain/abla.cpp
  • src/chain/abla_config.cpp
  • src/chain/abla_state.cpp
  • src/chain/input_point.cpp
  • src/chain/output_point.cpp
  • src/module.c
  • src/vm/big_number.cpp
  • src/vm/number.cpp
  • src/vm/program.cpp
  • src/vm/script_execution_context.cpp
  • src/wallet/bitcoin_uri.cpp
  • src/wallet/cashaddr.cpp
  • src/wallet/coin_selection.cpp
  • src/wallet/coin_selection_result.cpp
  • src/wallet/ek_private.cpp
  • src/wallet/ek_public.cpp
  • src/wallet/ek_token.cpp
  • src/wallet/encrypted_keys.cpp
  • src/wallet/language.cpp
  • src/wallet/message.cpp
  • src/wallet/mnemonic.cpp
  • src/wallet/stealth_address.cpp
  • src/wallet/stealth_receiver.cpp
  • src/wallet/stealth_sender.cpp
  • src/wallet/wallet_data.cpp
  • tests/test_chain_input_point.py
  • tests/test_vm_big_number.py
  • tests/test_wallet_bitcoin_uri.py
  • tests/test_wallet_cashaddr.py
  • tests/test_wallet_ek_private.py
  • tests/test_wallet_language.py
  • tests/test_wallet_message.py
  • tests/test_wallet_mnemonic.py
📝 Walkthrough

Walkthrough

Adds chain InputPoint bindings; extends VM with Number, BigNumber, ScriptExecutionContext and updates Program pop/top APIs; introduces extensive wallet bindings (bitcoin_uri, cashaddr, coin selection/result, EK keys, encrypted keys, language, message, mnemonic, stealth sender/receiver/address), updates wallet data creation, registers all in module, and updates build and stubs.

Changes

Unified CPython Binding Expansion

Layer / File(s) Summary
Public headers, implementations, stubs, and wiring
include/kth/py-native/*, src/**/*, kth_native.pyi, setup.py, src/module.c
Adds/updates headers and sources for chain InputPoint, VM Number/BigNumber/ScriptExecutionContext, wallet features (URI, cashaddr, coin selection/result, EK keys, encrypted keys, language, message, mnemonic, stealth); updates VM program pop/top; extends wallet data creation; registers new method tables; updates stubs and build sources.

Sequence Diagram(s)

sequenceDiagram
  participant Py as Python
  participant Mod as kth_native module
  participant WAL as Wallet (core)
  Py->>Mod: wallet_bitcoin_uri_construct(uri, strict)
  Mod->>WAL: kth_wallet_bitcoin_uri_construct(uri, strict)
  WAL-->>Mod: bitcoin_uri*
  Mod-->>Py: PyCapsule(wallet.bitcoin_uri)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • k-nuth/py-native#9 — Also extends capsule_names.h with new capsule macros/destructors, overlapping the binding registration surface.
  • k-nuth/py-native#10 — Modifies capsule_names.h for VM-related capsules (e.g., vm.metrics) similar to this PR’s VM additions.
  • k-nuth/py-native#5 — Refactors chain bindings and updates capsule_names, intersecting with chain InputPoint/output changes here.

Poem

I nibble wires of script and hash,
Stack pops leap—numbers flash!
URI crumbs, stealthy trails,
Mnemonic breezes fill my sails.
Capsules warm like burrowed nest—
Hop, compile, import—now the tests! 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/kth-0.83-full-regen

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@kth_native.pyi`:
- Around line 1761-1762: Add module-level stubs for the missing types by
declaring Dictionary and DictionaryList before they are referenced; these names
are used in signatures such as wallet_wallet_data_create and
wallet_wallet_data_create_simple (and the signatures around the later block that
spans the other referenced area), so introduce minimal class stubs or type
aliases for Dictionary and DictionaryList at the top-level of the stub file so
type-checkers can resolve them.
- Line 635: The stub for chain_input_point_null currently returns "Point" | None
which is inconsistent with the other InputPoint API; change its return
annotation to InputPoint | None so it matches the other chain_input_point_*
stubs and enables correct type inference—update the signature of
chain_input_point_null to return InputPoint | None and ensure any necessary
forward reference or import for InputPoint is used consistently with the
surrounding stubs.

In `@src/chain/input_point.cpp`:
- Around line 137-150: The capsule created in
kth_py_native_chain_input_point_null uses the wrong name and destructor; update
kth_py_native_chain_input_point_null to register the PyCapsule with
KTH_PY_CAPSULE_CHAIN_INPUT_POINT and pair it with
kth_py_native_chain_input_point_capsule_dtor (and on capsule creation failure
call kth_chain_input_point_destruct(result) if needed) so that other functions
calling PyCapsule_GetPointer(..., KTH_PY_CAPSULE_CHAIN_INPUT_POINT) (e.g.
is_valid, hash, index, to_data, destruct) succeed.

In `@src/chain/output_point.cpp`:
- Around line 139-152: The constructor kth_py_native_chain_output_point_null
currently raises PyExc_RuntimeError when kth_chain_output_point_null() returns
NULL; change this to raise PyExc_MemoryError with the message "kth: allocation
failed" to match other constructors (construct_default,
construct_from_hash_index, construct_from_point, copy). Locate
kth_py_native_chain_output_point_null, replace the PyErr_SetString call that
uses PyExc_RuntimeError and "kth: NULL handle returned" with
PyErr_SetString(PyExc_MemoryError, "kth: allocation failed") while keeping the
existing NULL return behavior and cleanup logic.

In `@src/vm/script_execution_context.cpp`:
- Around line 69-75: The destruct path
(kth_py_native_vm_script_execution_context_destruct) frees the native context
while vm_script_execution_context_transaction returns a borrowed capsule using
kth_py_native_borrowed_parent_dtor, allowing the returned transaction to point
at freed memory; fix by making the transaction an owned view or by deferring
native free via reference counting: change
vm_script_execution_context_transaction to create an owned transaction capsule
(increment the native context reference or create an independent native
transaction object) instead of a borrowed capsule, and/or add a refcount on the
native context that is incremented when creating a transaction and decremented
in the transaction capsule destructor; then update
kth_py_native_vm_script_execution_context_destruct to decrement and only call
kth_vm_script_execution_context_destruct when the native refcount reaches zero
(or rely on the owned-transaction semantics to keep the context alive). Ensure
changes reference kth_py_native_vm_script_execution_context_destruct,
vm_script_execution_context_transaction, kth_py_native_borrowed_parent_dtor, and
kth_vm_script_execution_context_destruct.

In `@src/wallet/coin_selection.cpp`:
- Around line 130-152: The capsule created in
kth_py_native_wallet_coin_selection_make_change_ratios is missing a destructor
and thus leaks the owned double-list returned by
kth_wallet_coin_selection_make_change_ratios; fix by supplying a proper
PyCapsule destructor that calls kth_core_double_list_destruct (either pass a
wrapper matching PyCapsule_Destructor signature or reuse the existing capsule
destructor used for string lists) when creating the capsule instead of NULL, and
update the capsule-name generator template (used to emit
KTH_PY_CAPSULE_CORE_DOUBLE_LIST handling) so all core list capsules are created
with their corresponding destructors.

In `@src/wallet/encrypted_keys.cpp`:
- Around line 17-60: The wrapper
kth_py_native_wallet_encrypted_keys_create_key_pair incorrectly treats
out_private and out_point as input buffers and discards the native outputs;
instead declare local kth_encrypted_private_t out_private and
kth_ec_compressed_t out_point, remove out_private/out_point from
PyArg_ParseTupleAndKeywords format (do not parse them as "y#"), pass their
addresses to kth_wallet_encrypted_keys_create_key_pair, then build and return a
tuple (success_bool, encrypted_private_bytes, point_bytes) by converting
out_private.data and out_point.data to Python bytes (and only then call
kth_core_secure_zero on the local structs); apply the same change pattern to
other affected wrappers (e.g., wallet_message_sign_message_*), and update the
codegen template that emits these bindings so outputs are declared locally and
returned instead of parsed from Python.

In `@src/wallet/message.cpp`:
- Around line 126-150: The wrappers
kth_py_native_wallet_message_recovery_id_to_magic and
kth_py_native_wallet_message_magic_to_recovery_id currently pass out-parameters
by value; change them to follow the sign_message_* pattern: parse out-parameter
buffers from Python using the "y#" (or appropriate buffer) format instead of
scalar B, allocate local uint8_t and kth_bool_t variables, pass their addresses
(&out_magic, &out_recovery_id, &out_compressed) to
kth_wallet_message_recovery_id_to_magic and
kth_wallet_message_magic_to_recovery_id (which expect uint8_t*/kth_bool_t*),
then build and return a Python tuple containing the boolean result plus the out
values (e.g. (bool, magic) and (bool, recovery_id, compressed)); update
PyArg_ParseTupleAndKeywords kwlists to match the buffer args and mirror the
sign_message_* implementations.
- Around line 29-101: The wrappers
(kth_py_native_wallet_message_sign_message_ec_private,
kth_py_native_wallet_message_sign_message_string,
kth_py_native_wallet_message_sign_message_hash) parse out_signature as a
read-only input and memcpy into a local kth_message_signature_t, then discard
the native-populated signature — instead allocate a local
kth_message_signature_t, pass its pointer to kth_wallet_message_sign_message_*
(do not treat out_signature as an input), then convert the produced signature
into a Python bytes object (PyBytes_FromStringAndSize or Py_BuildValue "y#") and
return it (or return a tuple of (signature, success) if you prefer) rather than
only PyBool_FromLong; apply the same pattern to
src/wallet/encrypted_keys.cpp::create_key_pair. Also fix recovery_id_to_magic
and magic_to_recovery_id to pass pointers for
out_magic/out_recovery_id/out_compressed (not values) so native can write
outputs, then return those outputs to Python.

In `@src/wallet/mnemonic.cpp`:
- Around line 28-38: The capsules wrapping owned lists are created with NULL
destructors causing leaks; add owning destructor functions (e.g.,
kth_py_native_core_string_list_capsule_dtor and
kth_py_native_core_double_list_capsule_dtor) that call
kth_core_string_list_destruct and kth_core_double_list_destruct respectively
(follow the pattern used in ek_token.cpp / ec_private.cpp), and pass those
destructor functions into PyCapsule_New when creating the capsule in
kth_wallet_mnemonic_create_mnemonic (mnemonic.cpp) and in
kth_wallet_coin_selection_make_change_ratios (coin_selection.cpp) so the native
lists are freed when the capsule is garbage-collected.

In `@src/wallet/stealth_receiver.cpp`:
- Around line 151-177: kth_py_native_wallet_stealth_receiver_derive_private
currently parses out_private as an immutable "y#" buffer and never exposes the
derived key to Python; change parsing to accept a writable buffer (use "y*" with
a Py_buffer for out_private), validate out_private_buf.len ==
KTH_BITCOIN_HASH_SIZE, pass its buf to
kth_wallet_stealth_receiver_derive_private so the native call writes the key,
then create a Python bytes object from that buffer to return (or copy it into a
new bytes object) before wiping the buffer with kth_core_secure_zero and
releasing the Py_buffer; also update kth_native.pyi to reflect the new return
type or writable-buffer API as appropriate.
- Around line 87-93: The child capsule returned by
wallet_stealth_receiver_stealth_address currently uses
kth_py_native_borrowed_parent_dtor and thus borrows the native pointer from the
receiver, creating a use-after-free when
kth_py_native_wallet_stealth_receiver_destruct frees the receiver; change the
API to return an owned StealthAddress copy (allocate/clone a new native
StealthAddress and return a capsule whose destructor frees that owned copy)
instead of a borrowed capsule so the child capsule remains valid after destruct.
Also fix wallet_stealth_receiver_derive_private which currently parses
out_private as immutable (y#), copies into a stack buffer and never writes back:
accept a writable buffer (use the writable buffer API / buffer protocol like
writable bytes or a Py_buffer) and write the derived private bytes into the
provided output buffer (mirroring how derive_address handles writable output) so
the caller receives the derived value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49c14f1c-b1e7-4a74-a222-c5f6f071b324

📥 Commits

Reviewing files that changed from the base of the PR and between 189c281 and 8942aeb.

📒 Files selected for processing (46)
  • include/kth/py-native/capsule_names.h
  • include/kth/py-native/chain/input_point.h
  • include/kth/py-native/chain/output_point.h
  • include/kth/py-native/vm/big_number.h
  • include/kth/py-native/vm/number.h
  • include/kth/py-native/vm/program.h
  • include/kth/py-native/vm/script_execution_context.h
  • include/kth/py-native/wallet/bitcoin_uri.h
  • include/kth/py-native/wallet/cashaddr.h
  • include/kth/py-native/wallet/coin_selection.h
  • include/kth/py-native/wallet/coin_selection_result.h
  • include/kth/py-native/wallet/ek_private.h
  • include/kth/py-native/wallet/ek_public.h
  • include/kth/py-native/wallet/ek_token.h
  • include/kth/py-native/wallet/encrypted_keys.h
  • include/kth/py-native/wallet/language.h
  • include/kth/py-native/wallet/message.h
  • include/kth/py-native/wallet/mnemonic.h
  • include/kth/py-native/wallet/stealth_address.h
  • include/kth/py-native/wallet/stealth_receiver.h
  • include/kth/py-native/wallet/stealth_sender.h
  • include/kth/py-native/wallet/wallet_data.h
  • kth_native.pyi
  • setup.py
  • src/chain/input_point.cpp
  • src/chain/output_point.cpp
  • src/module.c
  • src/vm/big_number.cpp
  • src/vm/number.cpp
  • src/vm/program.cpp
  • src/vm/script_execution_context.cpp
  • src/wallet/bitcoin_uri.cpp
  • src/wallet/cashaddr.cpp
  • src/wallet/coin_selection.cpp
  • src/wallet/coin_selection_result.cpp
  • src/wallet/ek_private.cpp
  • src/wallet/ek_public.cpp
  • src/wallet/ek_token.cpp
  • src/wallet/encrypted_keys.cpp
  • src/wallet/language.cpp
  • src/wallet/message.cpp
  • src/wallet/mnemonic.cpp
  • src/wallet/stealth_address.cpp
  • src/wallet/stealth_receiver.cpp
  • src/wallet/stealth_sender.cpp
  • src/wallet/wallet_data.cpp

Comment thread kth_native.pyi
def chain_input_point_copy(self: InputPoint) -> InputPoint: ...
def chain_input_point_destruct(self: InputPoint) -> None: ...
def chain_input_point_equals(self: InputPoint, other: InputPoint) -> bool: ...
def chain_input_point_null() -> "Point" | None: ...
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "kth_native.pyi" -type f

Repository: k-nuth/py-native

Length of output: 77


🏁 Script executed:

wc -l ./kth_native.pyi

Repository: k-nuth/py-native

Length of output: 82


🏁 Script executed:

# Get context around line 635
sed -n '630,650p' ./kth_native.pyi

# Search for all chain_input_point functions
rg "def chain_input_point" ./kth_native.pyi

# Search for InputPoint, Dictionary, DictionaryList definitions
rg "class InputPoint|class Dictionary|class DictionaryList|InputPoint =|Dictionary =|DictionaryList =" ./kth_native.pyi

Repository: k-nuth/py-native

Length of output: 2516


🏁 Script executed:

# Search for Point and InputPoint class/type definitions
rg "class Point|class InputPoint" ./kth_native.pyi -A 5

# Check if Point is a type alias or defined elsewhere
rg "^Point|^InputPoint" ./kth_native.pyi

Repository: k-nuth/py-native

Length of output: 651


🏁 Script executed:

# Search for Dictionary and DictionaryList class definitions
rg "class Dictionary|class DictionaryList" ./kth_native.pyi

# Also search for any references to Dictionary or DictionaryList in function signatures
rg "Dictionary|DictionaryList" ./kth_native.pyi | head -20

Repository: k-nuth/py-native

Length of output: 1027


Make chain_input_point_null() consistent with the rest of the InputPoint API.

Every other chain_input_point_* stub returns InputPoint. This function should return InputPoint | None instead of "Point" | None to maintain consistency across the API surface and ensure proper type inference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kth_native.pyi` at line 635, The stub for chain_input_point_null currently
returns "Point" | None which is inconsistent with the other InputPoint API;
change its return annotation to InputPoint | None so it matches the other
chain_input_point_* stubs and enables correct type inference—update the
signature of chain_input_point_null to return InputPoint | None and ensure any
necessary forward reference or import for InputPoint is used consistently with
the surrounding stubs.

Comment thread kth_native.pyi
Comment on lines +1761 to +1762
def wallet_wallet_data_create(password: str, normalized_passphrase: str, lexicon: "Dictionary") -> "WalletData": ...
def wallet_wallet_data_create_simple(password: str, normalized_passphrase: str) -> "WalletData": ...
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

fd -t f "kth_native.pyi" -x wc -l {}

Repository: k-nuth/py-native

Length of output: 82


🏁 Script executed:

# Get the context around lines 1761-1762
sed -n '1755,1770p' kth_native.pyi | cat -n

Repository: k-nuth/py-native

Length of output: 1099


🏁 Script executed:

# Check if Dictionary and DictionaryList are defined anywhere in the file
rg "^(class|type)\s+(Dictionary|DictionaryList)" kth_native.pyi

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

# Check all references to Dictionary and DictionaryList
rg "Dictionary|DictionaryList" kth_native.pyi | head -30

Repository: k-nuth/py-native

Length of output: 1027


🏁 Script executed:

# Check the lines around 1893-1915
sed -n '1890,1920p' kth_native.pyi | cat -n

Repository: k-nuth/py-native

Length of output: 1633


Add Dictionary and DictionaryList class stubs before referencing them.

The signatures at lines 1761–1762 and 1893–1915 reference Dictionary and DictionaryList, but these types are never defined in this stub file. This causes type-checking failures for strict consumers. Add class stubs (or type aliases) for these types at the module level.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kth_native.pyi` around lines 1761 - 1762, Add module-level stubs for the
missing types by declaring Dictionary and DictionaryList before they are
referenced; these names are used in signatures such as wallet_wallet_data_create
and wallet_wallet_data_create_simple (and the signatures around the later block
that spans the other referenced area), so introduce minimal class stubs or type
aliases for Dictionary and DictionaryList at the top-level of the stub file so
type-checkers can resolve them.

Comment thread src/chain/input_point.cpp
Comment on lines +137 to +150
PyObject*
kth_py_native_chain_input_point_null(PyObject* self, PyObject* Py_UNUSED(args)) {
auto const result = kth_chain_input_point_null();
if (result == NULL) {
PyErr_SetString(PyExc_RuntimeError, "kth: NULL handle returned");
return NULL;
}
PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CHAIN_POINT, kth_py_native_chain_point_capsule_dtor);
if (capsule == NULL) {
kth_chain_point_destruct(result);
return NULL;
}
return capsule;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find any chain_input_point_* function that wraps its result under KTH_PY_CAPSULE_CHAIN_POINT.
rg -nP -C2 'kth_py_native_chain_input_point_\w+' --type=cpp \
  | rg -nP 'KTH_PY_CAPSULE_CHAIN_POINT(?!_)|kth_py_native_chain_point_capsule_dtor|kth_chain_point_destruct'

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

# First, locate the file and verify it exists
ls -la src/chain/input_point.cpp

Repository: k-nuth/py-native

Length of output: 136


🏁 Script executed:

# Read the specific lines in question (137-150)
sed -n '137,150p' src/chain/input_point.cpp

Repository: k-nuth/py-native

Length of output: 573


🏁 Script executed:

# Find all chain_input_point_* functions to see the pattern
rg -n 'kth_py_native_chain_input_point_\w+' src/chain/input_point.cpp | head -20

Repository: k-nuth/py-native

Length of output: 2042


🏁 Script executed:

# Look for capsule name constant definitions
rg -n 'KTH_PY_CAPSULE_CHAIN_INPUT_POINT|KTH_PY_CAPSULE_CHAIN_POINT' --type=cpp | head -30

Repository: k-nuth/py-native

Length of output: 4124


🏁 Script executed:

# Check what destructors are used in other chain_input_point_* functions
rg -B5 -A5 'kth_py_native_chain_input_point_\w+.*PyObject\*' src/chain/input_point.cpp | head -100

Repository: k-nuth/py-native

Length of output: 3762


Capsule name/destructor mismatch breaks chain_input_point_null() interop.

This function is the only one in the file that registers its result as KTH_PY_CAPSULE_CHAIN_POINT and pairs it with kth_py_native_chain_point_capsule_dtor / kth_chain_point_destruct. Every other chain_input_point_* function (e.g. is_valid, hash, index, to_data, destruct) calls PyCapsule_GetPointer(..., KTH_PY_CAPSULE_CHAIN_INPUT_POINT), which will fail the name check on the capsule returned here and raise ValueError: PyCapsule_GetPointer called with invalid PyCapsule object.

Even if input_point is an alias of point in the C-API, the Python-side capsule name still gates accessor calls, making the null() result effectively unusable from the chain_input_point namespace.

Proposed fix: use the input_point capsule name and dtor consistently
-    PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CHAIN_POINT, kth_py_native_chain_point_capsule_dtor);
-    if (capsule == NULL) {
-        kth_chain_point_destruct(result);
-        return NULL;
-    }
+    PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CHAIN_INPUT_POINT, kth_py_native_chain_input_point_capsule_dtor);
+    if (capsule == NULL) {
+        kth_chain_input_point_destruct(result);
+        return NULL;
+    }
     return capsule;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/chain/input_point.cpp` around lines 137 - 150, The capsule created in
kth_py_native_chain_input_point_null uses the wrong name and destructor; update
kth_py_native_chain_input_point_null to register the PyCapsule with
KTH_PY_CAPSULE_CHAIN_INPUT_POINT and pair it with
kth_py_native_chain_input_point_capsule_dtor (and on capsule creation failure
call kth_chain_input_point_destruct(result) if needed) so that other functions
calling PyCapsule_GetPointer(..., KTH_PY_CAPSULE_CHAIN_INPUT_POINT) (e.g.
is_valid, hash, index, to_data, destruct) succeed.

Comment on lines +139 to +152
PyObject*
kth_py_native_chain_output_point_null(PyObject* self, PyObject* Py_UNUSED(args)) {
auto const result = kth_chain_output_point_null();
if (result == NULL) {
PyErr_SetString(PyExc_RuntimeError, "kth: NULL handle returned");
return NULL;
}
PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CHAIN_OUTPUT_POINT, kth_py_native_chain_output_point_capsule_dtor);
if (capsule == NULL) {
kth_chain_output_point_destruct(result);
return NULL;
}
return capsule;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use MemoryError for consistency with other constructors.

Lines 143-144 raise RuntimeError when kth_chain_output_point_null() returns NULL, but all other constructor functions in this file (construct_default, construct_from_hash_index, construct_from_point, copy) consistently raise MemoryError with message "kth: allocation failed" for the same condition. Since kth_chain_output_point_null() constructs an output point object (semantically representing a null point), a NULL return indicates allocation failure.

🔧 Proposed fix for consistency
 PyObject*
 kth_py_native_chain_output_point_null(PyObject* self, PyObject* Py_UNUSED(args)) {
     auto const result = kth_chain_output_point_null();
     if (result == NULL) {
-        PyErr_SetString(PyExc_RuntimeError, "kth: NULL handle returned");
+        PyErr_SetString(PyExc_MemoryError, "kth: allocation failed");
         return NULL;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PyObject*
kth_py_native_chain_output_point_null(PyObject* self, PyObject* Py_UNUSED(args)) {
auto const result = kth_chain_output_point_null();
if (result == NULL) {
PyErr_SetString(PyExc_RuntimeError, "kth: NULL handle returned");
return NULL;
}
PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CHAIN_OUTPUT_POINT, kth_py_native_chain_output_point_capsule_dtor);
if (capsule == NULL) {
kth_chain_output_point_destruct(result);
return NULL;
}
return capsule;
}
PyObject*
kth_py_native_chain_output_point_null(PyObject* self, PyObject* Py_UNUSED(args)) {
auto const result = kth_chain_output_point_null();
if (result == NULL) {
PyErr_SetString(PyExc_MemoryError, "kth: allocation failed");
return NULL;
}
PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CHAIN_OUTPUT_POINT, kth_py_native_chain_output_point_capsule_dtor);
if (capsule == NULL) {
kth_chain_output_point_destruct(result);
return NULL;
}
return capsule;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/chain/output_point.cpp` around lines 139 - 152, The constructor
kth_py_native_chain_output_point_null currently raises PyExc_RuntimeError when
kth_chain_output_point_null() returns NULL; change this to raise
PyExc_MemoryError with the message "kth: allocation failed" to match other
constructors (construct_default, construct_from_hash_index,
construct_from_point, copy). Locate kth_py_native_chain_output_point_null,
replace the PyErr_SetString call that uses PyExc_RuntimeError and "kth: NULL
handle returned" with PyErr_SetString(PyExc_MemoryError, "kth: allocation
failed") while keeping the existing NULL return behavior and cleanup logic.

Comment on lines +69 to +75
kth_py_native_vm_script_execution_context_destruct(PyObject* self, PyObject* py_arg0) {
PyObject* py_self = py_arg0;
kth_script_execution_context_mut_t self_handle = (kth_script_execution_context_mut_t)PyCapsule_GetPointer(py_self, KTH_PY_CAPSULE_VM_SCRIPT_EXECUTION_CONTEXT);
if (self_handle == NULL) return NULL;
kth_vm_script_execution_context_destruct(self_handle);
PyCapsule_SetName(py_self, "kth.destroyed");
Py_RETURN_NONE;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

wc -l src/vm/script_execution_context.cpp

Repository: k-nuth/py-native

Length of output: 100


🏁 Script executed:

cat -n src/vm/script_execution_context.cpp | sed -n '1,30p'

Repository: k-nuth/py-native

Length of output: 1600


🏁 Script executed:

cat -n src/vm/script_execution_context.cpp | sed -n '60,110p'

Repository: k-nuth/py-native

Length of output: 2705


🏁 Script executed:

cat -n src/vm/script_execution_context.cpp | sed -n '1,159p' | tail -50

Repository: k-nuth/py-native

Length of output: 3627


🏁 Script executed:

rg "kth_py_native_borrowed_parent_dtor" --type c --type cpp -A 10

Repository: k-nuth/py-native

Length of output: 34502


🏁 Script executed:

find . -name "*.h" -o -name "*.hpp" | xargs grep -l "borrowed_parent_dtor" 2>/dev/null | head -5

Repository: k-nuth/py-native

Length of output: 92


🏁 Script executed:

cat -n include/kth/py-native/utils.h | head -50

Repository: k-nuth/py-native

Length of output: 1941


🏁 Script executed:

cat -n src/utils.cpp | sed -n '1,100p'

Repository: k-nuth/py-native

Length of output: 1935


🏁 Script executed:

rg "kth_vm_script_execution_context_destruct" -A 5 -B 5

Repository: k-nuth/py-native

Length of output: 3612


🏁 Script executed:

find . -name "*.h" -type f | xargs grep -l "script_execution_context" | head -3

Repository: k-nuth/py-native

Length of output: 154


🏁 Script executed:

cat -n include/kth/py-native/vm/script_execution_context.h

Repository: k-nuth/py-native

Length of output: 1707


🏁 Script executed:

rg "kth_script_execution_context_transaction" --type c --type h -B 3 -A 3 | head -30

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

rg "kth_chain_transaction_" --type c --type h -A 5 | head -50

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

cat -n src/vm/script_execution_context.cpp | sed -n '1,50p'

Repository: k-nuth/py-native

Length of output: 2630


🏁 Script executed:

rg "kth_py_native_.*_destruct\(" --type c -B 10 | grep -A 10 "explicit"

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

cat -n src/vm/script_execution_context.cpp | sed -n '40,65p'

Repository: k-nuth/py-native

Length of output: 1379


🏁 Script executed:

python3 << 'EOF'
# Analyze the borrowed-parent pattern vulnerability
# The issue: Py_INCREF keeps parent *Python object* alive,
# but does NOT prevent explicit destruct() calls

print("Borrowed-Parent Pattern Analysis:")
print("=" * 60)
print()
print("SCENARIO 1: Normal GC flow (parent released first)")
print("-" * 60)
print("  1. User releases parent capsule -> Py_DECREF")
print("  2. Parent Python object stays alive (child has INCREF)")
print("  3. Child GC'd -> borrowed_parent_dtor calls Py_DECREF")
print("  4. Parent Python object freed")
print("  5. Parent capsule dtor runs -> calls native destruct()")
print("  → SAFE: Native payload freed after children are gone")
print()
print("SCENARIO 2: Explicit destruct() call (PROBLEMATIC)")
print("-" * 60)
print("  1. vm_script_execution_context_transaction() returns child")
print("     - Py_INCREF(py_self) to keep parent alive")
print("     - Child stores parent in context")
print("  2. User calls vm_script_execution_context_destruct(parent)")
print("     - Immediately calls kth_vm_script_execution_context_destruct()")
print("     - Sets capsule name to 'kth.destroyed'")
print("  3. Native context freed WHILE child still references it")
print("  4. User uses child capsule")
print("  → UAF: Child points to freed native memory")
print()
print("ROOT CAUSE:")
print("-" * 60)
print("  Py_INCREF(parent) keeps parent Python object alive")
print("  but does NOT prevent explicit destruct() calls")
print()
print("COMPARISON:")
print("-" * 60)
print("  • Normal GC: Parent Python freed -> parent capsule dtor freed native")
print("  • Explicit destruct: Parent native freed -> parent Python still alive!")
EOF

Repository: k-nuth/py-native

Length of output: 1530


🏁 Script executed:

rg "PyCapsule_SetName.*destroyed" --type c -B 5 -A 2

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

fd -name "*.cpp" -path "*/src/chain/*" | xargs grep -l "destruct" | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && rg "borrowed_parent_dtor" {} -B 10 -A 3'

Repository: k-nuth/py-native

Length of output: 291


🏁 Script executed:

find src/chain -name "*.cpp" -type f | head -3

Repository: k-nuth/py-native

Length of output: 125


🏁 Script executed:

rg "PyCapsule_SetName" --type c -B 3 -A 1

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

rg "borrowed_parent_dtor" --type c | wc -l

Repository: k-nuth/py-native

Length of output: 62


🏁 Script executed:

# Check if any transaction methods validate parent capsule
rg "kth_chain_transaction" src/chain/transaction.cpp -A 10 | head -40

Repository: k-nuth/py-native

Length of output: 1720


🏁 Script executed:

rg "PyCapsule_IsValid" --type c | head -10

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

# Check if borrowed child accessors validate parent
fd -name "*.cpp" | xargs rg "PyCapsule_GetContext" -A 5 | head -50

Repository: k-nuth/py-native

Length of output: 519


🏁 Script executed:

# Count all borrowed_parent_dtor usages
rg "borrowed_parent_dtor" --type c | wc -l

Repository: k-nuth/py-native

Length of output: 62


🏁 Script executed:

# Check for any parent validation in child methods
rg "GetPointer.*CHAIN_TRANSACTION" src/chain/transaction.cpp -A 3 | head -30

Repository: k-nuth/py-native

Length of output: 1751


🏁 Script executed:

# Verify there's no validation of parent in borrowed child accessors
rg "inputs\(" src/chain/transaction.cpp | head -5

Repository: k-nuth/py-native

Length of output: 361


🏁 Script executed:

# Check if there's any guard against accessing freed memory in borrowed children
cat src/chain/transaction.cpp | grep -A 10 "kth_py_native_chain_transaction_inputs" | head -15

Repository: k-nuth/py-native

Length of output: 1024


🏁 Script executed:

# Verify the pattern: borrowed children can be accessed indefinitely
python3 << 'EOF'
print("VERIFICATION SUMMARY:")
print("=" * 70)
print()
print("✓ Borrowed-Parent Pattern Confirmed:")
print("  - Child capsule stores parent via Py_INCREF(py_self)")
print("  - Only protects against garbage collection, NOT explicit destruct()")
print()
print("✓ Explicit Destruct Defeats Protection:")
print("  - vm_script_execution_context_destruct() calls native free immediately")
print("  - Sets capsule name to 'kth.destroyed' only for GC protection")
print("  - Does NOT prevent child from dereferencing freed memory")
print()
print("✓ No Validation in Borrowed Child Methods:")
print("  - Child accessors only check if handle != NULL")
print("  - No check if parent is still valid/not destroyed")
print("  - No check if parent native memory still allocated")
print()
print("✓ Scenario: Use-After-Free Vulnerability")
print("  1. ctx = vm_script_execution_context_construct(...)")
print("  2. tx = ctx.vm_script_execution_context_transaction()")
print("  3. ctx.vm_script_execution_context_destruct()  # Frees native memory")
print("  4. tx.input_count()  # ← Dereferences freed memory (UAF)")
print()
print("CONCLUSION: Review comment is CORRECT and CRITICAL")
print("=" * 70)
EOF

Repository: k-nuth/py-native

Length of output: 1156


Borrowed transaction can dereference freed context after explicit destruction.

vm_script_execution_context_transaction() (lines 87–105) creates a borrowed capsule with kth_py_native_borrowed_parent_dtor, which holds a strong reference to the parent only to prevent garbage collection—not explicit calls to vm_script_execution_context_destruct(). When vm_script_execution_context_destruct() (lines 69–75) is invoked, it immediately calls kth_vm_script_execution_context_destruct(self_handle), freeing the native context while the returned transaction capsule still points to it. Subsequent access to that transaction dereferences freed memory.

Return an owned transaction instead, or defer native destruction until all borrowed views are released.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vm/script_execution_context.cpp` around lines 69 - 75, The destruct path
(kth_py_native_vm_script_execution_context_destruct) frees the native context
while vm_script_execution_context_transaction returns a borrowed capsule using
kth_py_native_borrowed_parent_dtor, allowing the returned transaction to point
at freed memory; fix by making the transaction an owned view or by deferring
native free via reference counting: change
vm_script_execution_context_transaction to create an owned transaction capsule
(increment the native context reference or create an independent native
transaction object) instead of a borrowed capsule, and/or add a refcount on the
native context that is incremented when creating a transaction and decremented
in the transaction capsule destructor; then update
kth_py_native_vm_script_execution_context_destruct to decrement and only call
kth_vm_script_execution_context_destruct when the native refcount reaches zero
(or rely on the owned-transaction semantics to keep the context alive). Ensure
changes reference kth_py_native_vm_script_execution_context_destruct,
vm_script_execution_context_transaction, kth_py_native_borrowed_parent_dtor, and
kth_vm_script_execution_context_destruct.

Comment thread src/wallet/message.cpp
Comment on lines +29 to +101
PyObject*
kth_py_native_wallet_message_sign_message_ec_private(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_signature", (char*)"message", (char*)"secret", NULL};
char const* out_signature_buf = NULL;
Py_ssize_t out_signature_size = 0;
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
PyObject* py_secret = NULL;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#O", kwlist, &out_signature_buf, &out_signature_size, &message_buf, &message_size, &py_secret)) {
return NULL;
}
if (out_signature_size != (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte out_signature, got %zd", (int)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE, out_signature_size);
return NULL;
}
kth_message_signature_t out_signature;
memcpy(out_signature.data, out_signature_buf, (size_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
kth_ec_private_const_t secret_handle = (kth_ec_private_const_t)PyCapsule_GetPointer(py_secret, KTH_PY_CAPSULE_WALLET_EC_PRIVATE);
if (secret_handle == NULL) return NULL;
auto const result = kth_wallet_message_sign_message_ec_private(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, secret_handle);
return PyBool_FromLong((long)result);
}

PyObject*
kth_py_native_wallet_message_sign_message_string(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_signature", (char*)"message", (char*)"wif", NULL};
char const* out_signature_buf = NULL;
Py_ssize_t out_signature_size = 0;
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
char const* wif = NULL;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#s", kwlist, &out_signature_buf, &out_signature_size, &message_buf, &message_size, &wif)) {
return NULL;
}
if (out_signature_size != (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte out_signature, got %zd", (int)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE, out_signature_size);
return NULL;
}
kth_message_signature_t out_signature;
memcpy(out_signature.data, out_signature_buf, (size_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
auto const result = kth_wallet_message_sign_message_string(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, wif);
return PyBool_FromLong((long)result);
}

PyObject*
kth_py_native_wallet_message_sign_message_hash(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_signature", (char*)"message", (char*)"secret", (char*)"compressed", NULL};
char const* out_signature_buf = NULL;
Py_ssize_t out_signature_size = 0;
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
char const* secret_buf = NULL;
Py_ssize_t secret_size = 0;
int compressed = 0;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#y#p", kwlist, &out_signature_buf, &out_signature_size, &message_buf, &message_size, &secret_buf, &secret_size, &compressed)) {
return NULL;
}
if (out_signature_size != (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte out_signature, got %zd", (int)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE, out_signature_size);
return NULL;
}
kth_message_signature_t out_signature;
memcpy(out_signature.data, out_signature_buf, (size_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
if (secret_size != (Py_ssize_t)KTH_BITCOIN_HASH_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte secret, got %zd", (int)KTH_BITCOIN_HASH_SIZE, secret_size);
return NULL;
}
kth_hash_t secret;
memcpy(secret.hash, secret_buf, (size_t)KTH_BITCOIN_HASH_SIZE);
auto const result = kth_wallet_message_sign_message_hash(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, &secret, (kth_bool_t)compressed);
kth_core_secure_zero((void*)&secret, sizeof(kth_hash_t));
return PyBool_FromLong((long)result);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'message.h' | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'

Repository: k-nuth/py-native

Length of output: 1364


🏁 Script executed:

# Search for references to the native functions and their definitions
rg "kth_wallet_message_sign_message" --type c --type cpp --type h

# Also check for any build/dependency files that might reference the native library
fd -t f -e "cmake" -e "requirements.txt" -e "setup.py" | head -20

Repository: k-nuth/py-native

Length of output: 589


🏁 Script executed:

# Search in the codebase for any comments or documentation about the native API
rg -l "kth_wallet_message" . --type-list

Repository: k-nuth/py-native

Length of output: 450


🏁 Script executed:

# Check if there are any submodules or external dependencies referenced
cat -n .gitmodules 2>/dev/null || echo "No .gitmodules found"
git config --file .gitmodules --name-only --get-regexp path 2>/dev/null || echo "No git submodules"

Repository: k-nuth/py-native

Length of output: 99


🏁 Script executed:

# Search for encrypted_keys.cpp to see the similar pattern mentioned
fd -t f "encrypted_keys.cpp" -exec cat -n {} \;

# Look for any documentation or comments about the native library
fd -t f -e "md" -e "rst" -e "txt" | head -20 | xargs -I {} sh -c 'echo "=== {} ==="; head -50 "{}"'

# Check for CMakeLists or build files that mention the native library
fd -t f "CMakeLists.txt" -exec cat -n {} \;

Repository: k-nuth/py-native

Length of output: 4218


🏁 Script executed:

# Find and display encrypted_keys.cpp
fd "encrypted_keys.cpp" -x cat {} \; 2>/dev/null | head -200

# Search for any code generation scripts or templates
fd -t f "\.py$" -o -name "Makefile*" -o -name "generate*" | head -20

# Look for any comments about code generation or template patterns
rg "generator\|template\|generated" --type cpp --type h -A 2 -B 2 | head -80

Repository: k-nuth/py-native

Length of output: 3242


🏁 Script executed:

# Search for hash_message and verify_message implementations
rg -A 30 "kth_py_native_wallet_message_hash_message" src/wallet/message.cpp

rg -A 30 "kth_py_native_wallet_message_verify_message" src/wallet/message.cpp

# Also check recovery_id functions to understand the out-parameter pattern
rg -A 30 "recovery_id_to_magic\|magic_to_recovery_id" src/wallet/message.cpp

Repository: k-nuth/py-native

Length of output: 5175


🏁 Script executed:

# Get the full implementation of recovery_id_to_magic and magic_to_recovery_id
rg -A 10 "kth_py_native_wallet_message_recovery_id_to_magic\|kth_py_native_wallet_message_magic_to_recovery_id" src/wallet/message.cpp

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

# Read the full message.cpp file to see all functions
cat src/wallet/message.cpp

Repository: k-nuth/py-native

Length of output: 8764


Critical: sign_message_* wrappers discard the produced signature — out_signature is parsed as an input bytes buffer and never returned.

kth_wallet_message_sign_message_ec_private, …_string, and …_hash all write the signature into their out_signature parameter and return a success bool. The Python wrappers instead:

  1. Parse out_signature as a read-only y# bytes buffer of KTH_BITCOIN_EC_UNCOMPRESSED_SIZE,
  2. memcpy it into a local kth_message_signature_t,
  3. Let native overwrite that local,
  4. Return only PyBool_FromLong(result) — the freshly-produced signature is dropped on the floor.

A Python caller has no way to obtain the resulting signature. The same out-param-as-input pattern is present in src/wallet/encrypted_keys.cpp::create_key_pair (which even calls kth_core_secure_zero afterward, confirming native populates the output). Both are very likely a generator-template gap.

Compare against hash_message (which correctly returns the hash via Py_BuildValue) and verify_message (which correctly treats signature as read-only input). The recovery_id functions have a related issue: recovery_id_to_magic and magic_to_recovery_id pass out_magic, out_recovery_id, and out_compressed by value instead of pointer, making them unable to return output even if the native functions expect pointers.

🛠️ Sketch for `sign_message_ec_private` (apply analogously to `_string` and `_hash`)
-    static char* kwlist[] = {(char*)"out_signature", (char*)"message", (char*)"secret", NULL};
-    char const* out_signature_buf = NULL;
-    Py_ssize_t out_signature_size = 0;
+    static char* kwlist[] = {(char*)"message", (char*)"secret", NULL};
     char const* message_buf = NULL;
     Py_ssize_t message_size = 0;
     PyObject* py_secret = NULL;
-    if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#O", kwlist, &out_signature_buf, &out_signature_size, &message_buf, &message_size, &py_secret)) {
+    if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#O", kwlist, &message_buf, &message_size, &py_secret)) {
         return NULL;
     }
-    if (out_signature_size != (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE) { ... }
     kth_message_signature_t out_signature;
-    memcpy(out_signature.data, out_signature_buf, (size_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
     kth_ec_private_const_t secret_handle = (kth_ec_private_const_t)PyCapsule_GetPointer(py_secret, KTH_PY_CAPSULE_WALLET_EC_PRIVATE);
     if (secret_handle == NULL) return NULL;
     auto const result = kth_wallet_message_sign_message_ec_private(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, secret_handle);
-    return PyBool_FromLong((long)result);
+    return Py_BuildValue("Oy#", result ? Py_True : Py_False, out_signature.data, (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PyObject*
kth_py_native_wallet_message_sign_message_ec_private(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_signature", (char*)"message", (char*)"secret", NULL};
char const* out_signature_buf = NULL;
Py_ssize_t out_signature_size = 0;
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
PyObject* py_secret = NULL;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#O", kwlist, &out_signature_buf, &out_signature_size, &message_buf, &message_size, &py_secret)) {
return NULL;
}
if (out_signature_size != (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte out_signature, got %zd", (int)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE, out_signature_size);
return NULL;
}
kth_message_signature_t out_signature;
memcpy(out_signature.data, out_signature_buf, (size_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
kth_ec_private_const_t secret_handle = (kth_ec_private_const_t)PyCapsule_GetPointer(py_secret, KTH_PY_CAPSULE_WALLET_EC_PRIVATE);
if (secret_handle == NULL) return NULL;
auto const result = kth_wallet_message_sign_message_ec_private(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, secret_handle);
return PyBool_FromLong((long)result);
}
PyObject*
kth_py_native_wallet_message_sign_message_string(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_signature", (char*)"message", (char*)"wif", NULL};
char const* out_signature_buf = NULL;
Py_ssize_t out_signature_size = 0;
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
char const* wif = NULL;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#s", kwlist, &out_signature_buf, &out_signature_size, &message_buf, &message_size, &wif)) {
return NULL;
}
if (out_signature_size != (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte out_signature, got %zd", (int)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE, out_signature_size);
return NULL;
}
kth_message_signature_t out_signature;
memcpy(out_signature.data, out_signature_buf, (size_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
auto const result = kth_wallet_message_sign_message_string(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, wif);
return PyBool_FromLong((long)result);
}
PyObject*
kth_py_native_wallet_message_sign_message_hash(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_signature", (char*)"message", (char*)"secret", (char*)"compressed", NULL};
char const* out_signature_buf = NULL;
Py_ssize_t out_signature_size = 0;
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
char const* secret_buf = NULL;
Py_ssize_t secret_size = 0;
int compressed = 0;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#y#p", kwlist, &out_signature_buf, &out_signature_size, &message_buf, &message_size, &secret_buf, &secret_size, &compressed)) {
return NULL;
}
if (out_signature_size != (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte out_signature, got %zd", (int)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE, out_signature_size);
return NULL;
}
kth_message_signature_t out_signature;
memcpy(out_signature.data, out_signature_buf, (size_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
if (secret_size != (Py_ssize_t)KTH_BITCOIN_HASH_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte secret, got %zd", (int)KTH_BITCOIN_HASH_SIZE, secret_size);
return NULL;
}
kth_hash_t secret;
memcpy(secret.hash, secret_buf, (size_t)KTH_BITCOIN_HASH_SIZE);
auto const result = kth_wallet_message_sign_message_hash(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, &secret, (kth_bool_t)compressed);
kth_core_secure_zero((void*)&secret, sizeof(kth_hash_t));
return PyBool_FromLong((long)result);
}
PyObject*
kth_py_native_wallet_message_sign_message_ec_private(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"message", (char*)"secret", NULL};
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
PyObject* py_secret = NULL;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#O", kwlist, &message_buf, &message_size, &py_secret)) {
return NULL;
}
kth_message_signature_t out_signature;
kth_ec_private_const_t secret_handle = (kth_ec_private_const_t)PyCapsule_GetPointer(py_secret, KTH_PY_CAPSULE_WALLET_EC_PRIVATE);
if (secret_handle == NULL) return NULL;
auto const result = kth_wallet_message_sign_message_ec_private(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, secret_handle);
return Py_BuildValue("Oy#", result ? Py_True : Py_False, out_signature.data, (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
}
PyObject*
kth_py_native_wallet_message_sign_message_string(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"message", (char*)"wif", NULL};
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
char const* wif = NULL;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#s", kwlist, &message_buf, &message_size, &wif)) {
return NULL;
}
kth_message_signature_t out_signature;
auto const result = kth_wallet_message_sign_message_string(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, wif);
return Py_BuildValue("Oy#", result ? Py_True : Py_False, out_signature.data, (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
}
PyObject*
kth_py_native_wallet_message_sign_message_hash(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"message", (char*)"secret", (char*)"compressed", NULL};
char const* message_buf = NULL;
Py_ssize_t message_size = 0;
char const* secret_buf = NULL;
Py_ssize_t secret_size = 0;
int compressed = 0;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "y#y#p", kwlist, &message_buf, &message_size, &secret_buf, &secret_size, &compressed)) {
return NULL;
}
kth_message_signature_t out_signature;
if (secret_size != (Py_ssize_t)KTH_BITCOIN_HASH_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte secret, got %zd", (int)KTH_BITCOIN_HASH_SIZE, secret_size);
return NULL;
}
kth_hash_t secret;
memcpy(secret.hash, secret_buf, (size_t)KTH_BITCOIN_HASH_SIZE);
auto const result = kth_wallet_message_sign_message_hash(&out_signature, (uint8_t const*)message_buf, (kth_size_t)message_size, &secret, (kth_bool_t)compressed);
kth_core_secure_zero((void*)&secret, sizeof(kth_hash_t));
return Py_BuildValue("Oy#", result ? Py_True : Py_False, out_signature.data, (Py_ssize_t)KTH_BITCOIN_EC_UNCOMPRESSED_SIZE);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/message.cpp` around lines 29 - 101, The wrappers
(kth_py_native_wallet_message_sign_message_ec_private,
kth_py_native_wallet_message_sign_message_string,
kth_py_native_wallet_message_sign_message_hash) parse out_signature as a
read-only input and memcpy into a local kth_message_signature_t, then discard
the native-populated signature — instead allocate a local
kth_message_signature_t, pass its pointer to kth_wallet_message_sign_message_*
(do not treat out_signature as an input), then convert the produced signature
into a Python bytes object (PyBytes_FromStringAndSize or Py_BuildValue "y#") and
return it (or return a tuple of (signature, success) if you prefer) rather than
only PyBool_FromLong; apply the same pattern to
src/wallet/encrypted_keys.cpp::create_key_pair. Also fix recovery_id_to_magic
and magic_to_recovery_id to pass pointers for
out_magic/out_recovery_id/out_compressed (not values) so native can write
outputs, then return those outputs to Python.

Comment thread src/wallet/message.cpp
Comment on lines +126 to +150
PyObject*
kth_py_native_wallet_message_recovery_id_to_magic(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_magic", (char*)"recovery_id", (char*)"compressed", NULL};
unsigned char out_magic = 0;
unsigned char recovery_id = 0;
int compressed = 0;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "BBp", kwlist, &out_magic, &recovery_id, &compressed)) {
return NULL;
}
auto const result = kth_wallet_message_recovery_id_to_magic((uint8_t)out_magic, (uint8_t)recovery_id, (kth_bool_t)compressed);
return PyBool_FromLong((long)result);
}

PyObject*
kth_py_native_wallet_message_magic_to_recovery_id(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"out_recovery_id", (char*)"out_compressed", (char*)"magic", NULL};
unsigned char out_recovery_id = 0;
int out_compressed = 0;
unsigned char magic = 0;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "BpB", kwlist, &out_recovery_id, &out_compressed, &magic)) {
return NULL;
}
auto const result = kth_wallet_message_magic_to_recovery_id((uint8_t)out_recovery_id, (kth_bool_t)out_compressed, (uint8_t)magic);
return PyBool_FromLong((long)result);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C2 'kth_wallet_message_(recovery_id_to_magic|magic_to_recovery_id)\b'

Repository: k-nuth/py-native

Length of output: 741


🏁 Script executed:

#!/bin/bash
# Search for native function declarations/definitions
# First check header files in the repo
find . -type f \( -name "*.h" -o -name "*.hpp" \) | xargs rg -l "kth_wallet_message_(recovery_id_to_magic|magic_to_recovery_id)"

Repository: k-nuth/py-native

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check if there are any external dependencies or includes that define these functions
# Look for includes in the message.cpp file
head -50 src/wallet/message.cpp | grep -E "^#include"

Repository: k-nuth/py-native

Length of output: 255


🏁 Script executed:

#!/bin/bash
# Search for any documentation, comments, or usage patterns with these functions
rg -A5 -B5 "recovery_id_to_magic|magic_to_recovery_id" --type cpp --type c

Repository: k-nuth/py-native

Length of output: 4909


🏁 Script executed:

#!/bin/bash
# Look for kth/capi headers within the repository
find . -path "*/kth/capi*" -type f

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if there's a vendor directory, third_party, or similar with included headers
find . -name "*.h" -o -name "*.hpp" | grep -i capi

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check the repository structure and dependencies configuration
ls -la
cat README.md 2>/dev/null | head -30
cat setup.py 2>/dev/null | head -50
cat CMakeLists.txt 2>/dev/null | head -50

Repository: k-nuth/py-native

Length of output: 4635


🏁 Script executed:

#!/bin/bash
# Check the type stubs file for the function signatures
rg -A3 "wallet_message_recovery_id_to_magic|wallet_message_magic_to_recovery_id" kth_native.pyi

Repository: k-nuth/py-native

Length of output: 353


🏁 Script executed:

#!/bin/bash
# Look for any test files that show how these functions should be called
find . -name "*.py" -type f | xargs rg -l "recovery_id_to_magic|magic_to_recovery_id" 2>/dev/null

Repository: k-nuth/py-native

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check for any examples or documentation
find . -name "*.md" -o -name "*.rst" -o -name "*.txt" | xargs rg -l "recovery_id_to_magic|magic_to_recovery_id" 2>/dev/null

Repository: k-nuth/py-native

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Search for any conanfile or dependency configuration that might reveal the c-api version
cat conanfile.py | head -80

Repository: k-nuth/py-native

Length of output: 2967


🏁 Script executed:

#!/bin/bash
# Check if there's any information about the native library linkage or build artifacts
grep -r "kth_wallet_message_recovery_id_to_magic\|kth_wallet_message_magic_to_recovery_id" . --include="*.h" --include="*.hpp" 2>/dev/null

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check git history or comments for context on these functions
git log --oneline -n 20 -- src/wallet/message.cpp 2>/dev/null || echo "No git history"

Repository: k-nuth/py-native

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Try to find any documentation or examples from the kth project about these functions
# Check if there's a way to see what the staged headers look like
find . -path "*/kth/include/*" -name "*message*" -type f 2>/dev/null | head -5

Repository: k-nuth/py-native

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look for the git commit that synced with the C-API to understand what changed
git show 8942aeb --stat 2>/dev/null | head -50

Repository: k-nuth/py-native

Length of output: 2202


🏁 Script executed:

#!/bin/bash
# Check if there are any other similar out-parameter patterns in the codebase
# to understand how they're typically handled
rg "out_[a-zA-Z_]+\s*=" src/wallet/message.cpp -A2 -B2

Repository: k-nuth/py-native

Length of output: 1826


🏁 Script executed:

#!/bin/bash
# Look at how out_signature is handled in sign functions - might show the pattern
sed -n '40,120p' src/wallet/message.cpp

Repository: k-nuth/py-native

Length of output: 4580


🏁 Script executed:

#!/bin/bash
# Check if there's a native header file staged that shows the actual C-API signatures
find . -path "*/kth/include/*" -name "*.h" -type f 2>/dev/null | xargs grep -l "recovery_id_to_magic\|magic_to_recovery_id" 2>/dev/null

Repository: k-nuth/py-native

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Search for any Python tests that might show expected behavior
find tests -name "*.py" -type f 2>/dev/null | xargs cat 2>/dev/null | head -100

Repository: k-nuth/py-native

Length of output: 3764


Critical: recovery_id_to_magic / magic_to_recovery_id pass out-parameters by value — outputs cannot reach Python (or the code does not compile against the real native signature).

Both wrappers declare out_magic / out_recovery_id / out_compressed as local scalars, then pass them by value to the native call:

kth_wallet_message_recovery_id_to_magic((uint8_t)out_magic, (uint8_t)recovery_id, (kth_bool_t)compressed)
kth_wallet_message_magic_to_recovery_id((uint8_t)out_recovery_id, (kth_bool_t)out_compressed, (uint8_t)magic)

If the native API takes these as uint8_t* / kth_bool_t* (matching the pattern used by all other sign_message_* wrappers in the same file), (uint8_t)out_magic is not assignable to uint8_t* and compilation fails. If instead the native takes them by value, the wrappers expose meaningless kwargs and discard whatever the function was supposed to return.

Either way, this needs a generator-level fix: follow the sign_message_* pattern — accept buffers via "y#" format, create local structs, pass &out_* pointers to native, and return (bool, magic) / (bool, recovery_id, compressed) tuple.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/message.cpp` around lines 126 - 150, The wrappers
kth_py_native_wallet_message_recovery_id_to_magic and
kth_py_native_wallet_message_magic_to_recovery_id currently pass out-parameters
by value; change them to follow the sign_message_* pattern: parse out-parameter
buffers from Python using the "y#" (or appropriate buffer) format instead of
scalar B, allocate local uint8_t and kth_bool_t variables, pass their addresses
(&out_magic, &out_recovery_id, &out_compressed) to
kth_wallet_message_recovery_id_to_magic and
kth_wallet_message_magic_to_recovery_id (which expect uint8_t*/kth_bool_t*),
then build and return a Python tuple containing the boolean result plus the out
values (e.g. (bool, magic) and (bool, recovery_id, compressed)); update
PyArg_ParseTupleAndKeywords kwlists to match the buffer args and mirror the
sign_message_* implementations.

Comment thread src/wallet/mnemonic.cpp
Comment on lines +28 to +38
auto const result = kth_wallet_mnemonic_create_mnemonic((uint8_t const*)entropy_buf, (kth_size_t)entropy_size, lexicon_handle);
if (result == NULL) {
PyErr_SetString(PyExc_RuntimeError, "kth: NULL list returned");
return NULL;
}
PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CORE_STRING_LIST, NULL);
if (capsule == NULL) {
kth_core_string_list_destruct(result);
return NULL;
}
return capsule;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=cpp -C3 'KTH_PY_CAPSULE_CORE_STRING_LIST'
rg -nP --type=cpp -C2 'kth_core_string_list_(destruct|capsule_dtor)'

Repository: k-nuth/py-native

Length of output: 4152


🏁 Script executed:

# Search for destructor implementations
rg -nP --type=cpp 'kth_py_native.*dtor' -B 2 -A 8

# Also check for the pattern in coin_selection.cpp
fd coin_selection cpp | xargs cat -n

Repository: k-nuth/py-native

Length of output: 50482


🏁 Script executed:

# Search for coin_selection.cpp and the make_change_ratios function
rg -nP --type=cpp 'make_change_ratios|KTH_PY_CAPSULE_CORE_DOUBLE_LIST' -B 5 -A 10 | head -100

Repository: k-nuth/py-native

Length of output: 5999


Major: memory leaks — capsules for owned list resources created with NULL destructors.

kth_wallet_mnemonic_create_mnemonic (mnemonic.cpp) and kth_wallet_coin_selection_make_change_ratios (coin_selection.cpp) both allocate owned string/double lists. Wrapping them in capsules with NULL destructors means these lists and their contents are never released when the capsule is garbage-collected — every call leaks the allocation.

The error paths at mnemonic.cpp:35 and coin_selection.cpp:148 explicitly call kth_core_string_list_destruct and kth_core_double_list_destruct, confirming caller ownership. Unlike wallet_data.cpp (which uses kth_py_native_borrowed_parent_dtor with SetContext for borrowed views), these are genuine owned resources that require owning destructors.

Add owning destructor functions (e.g., kth_py_native_core_string_list_capsule_dtor and kth_py_native_core_double_list_capsule_dtor) following the existing pattern from ek_token.cpp / ec_private.cpp, and wire them into both capsule creations. Both are likely generated—consider a single template fix if code generation is involved.

🛠️ Suggested fix (mnemonic.cpp)
-    PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CORE_STRING_LIST, NULL);
+    PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CORE_STRING_LIST, kth_py_native_core_string_list_capsule_dtor);
🛠️ Suggested fix (coin_selection.cpp)
-    PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CORE_DOUBLE_LIST, NULL);
+    PyObject* capsule = PyCapsule_New((void*)result, KTH_PY_CAPSULE_CORE_DOUBLE_LIST, kth_py_native_core_double_list_capsule_dtor);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/mnemonic.cpp` around lines 28 - 38, The capsules wrapping owned
lists are created with NULL destructors causing leaks; add owning destructor
functions (e.g., kth_py_native_core_string_list_capsule_dtor and
kth_py_native_core_double_list_capsule_dtor) that call
kth_core_string_list_destruct and kth_core_double_list_destruct respectively
(follow the pattern used in ek_token.cpp / ec_private.cpp), and pass those
destructor functions into PyCapsule_New when creating the capsule in
kth_wallet_mnemonic_create_mnemonic (mnemonic.cpp) and in
kth_wallet_coin_selection_make_change_ratios (coin_selection.cpp) so the native
lists are freed when the capsule is garbage-collected.

Comment on lines +87 to +93
kth_py_native_wallet_stealth_receiver_destruct(PyObject* self, PyObject* py_arg0) {
PyObject* py_self = py_arg0;
kth_stealth_receiver_mut_t self_handle = (kth_stealth_receiver_mut_t)PyCapsule_GetPointer(py_self, KTH_PY_CAPSULE_WALLET_STEALTH_RECEIVER);
if (self_handle == NULL) return NULL;
kth_wallet_stealth_receiver_destruct(self_handle);
PyCapsule_SetName(py_self, "kth.destroyed");
Py_RETURN_NONE;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "stealth_receiver.cpp" -type f | head -5

Repository: k-nuth/py-native

Length of output: 94


🏁 Script executed:

# Get the total line count
wc -l src/wallet/stealth_receiver.cpp

Repository: k-nuth/py-native

Length of output: 96


🏁 Script executed:

# Read the critical sections - lines 87-93 and 105-123
sed -n '80,130p' src/wallet/stealth_receiver.cpp

Repository: k-nuth/py-native

Length of output: 2200


🏁 Script executed:

# Also search for kth_py_native_borrowed_parent_dtor usage in this file
rg "kth_py_native_borrowed_parent_dtor" src/wallet/stealth_receiver.cpp -A 3 -B 3

Repository: k-nuth/py-native

Length of output: 407


🏁 Script executed:

# Check for wallet_stealth_receiver_stealth_address and derive_private definitions
rg "wallet_stealth_receiver_stealth_address|derive_private" src/wallet/stealth_receiver.cpp -A 5 -B 2

Repository: k-nuth/py-native

Length of output: 2258


🏁 Script executed:

# Search for kth_py_native_borrowed_parent_dtor definition
rg "kth_py_native_borrowed_parent_dtor" . --type cpp --type h -B 5 -A 10 | head -80

Repository: k-nuth/py-native

Length of output: 5160


🏁 Script executed:

# Also check if there's a shared_utils or common header that defines this
fd -e h -e hpp . | xargs rg "borrowed_parent_dtor" 2>/dev/null | head -20

Repository: k-nuth/py-native

Length of output: 150


🏁 Script executed:

# Search for reference counting or parent validation mechanisms
rg "SetContext|GetContext" src/wallet/stealth_receiver.cpp -B 3 -A 3

Repository: k-nuth/py-native

Length of output: 383


🏁 Script executed:

# Check the full stealth_address function implementation (lines 105-123)
sed -n '105,123p' src/wallet/stealth_receiver.cpp

Repository: k-nuth/py-native

Length of output: 942


🏁 Script executed:

# Look for any guard checks that validate parent capsule state
rg "kth.destroyed|SetName" src/wallet/stealth_receiver.cpp -B 2 -A 2

Repository: k-nuth/py-native

Length of output: 545


🏁 Script executed:

# Find the implementation of kth_py_native_borrowed_parent_dtor
rg "kth_py_native_borrowed_parent_dtor" include/kth/py-native/utils.h -A 15

Repository: k-nuth/py-native

Length of output: 422


🏁 Script executed:

# Check the implementation in cpp file
fd -e cpp . | xargs rg "kth_py_native_borrowed_parent_dtor" -A 15 2>/dev/null | head -50

Repository: k-nuth/py-native

Length of output: 3225


🏁 Script executed:

# Look for the utils.cpp or utils implementation
fd -name "utils.cpp" -o -name "utils.h" | xargs cat 2>/dev/null | rg "borrowed_parent_dtor" -B 3 -A 15 | head -80

Repository: k-nuth/py-native

Length of output: 291


🏁 Script executed:

# Find all files that might contain the definition
find . -name "*.cpp" -o -name "*.h" | xargs rg "void kth_py_native_borrowed_parent_dtor" -A 15 2>/dev/null

Repository: k-nuth/py-native

Length of output: 1624


🏁 Script executed:

# Examine the stealth_address function lines more carefully
sed -n '102,130p' src/wallet/stealth_receiver.cpp

Repository: k-nuth/py-native

Length of output: 1270


🏁 Script executed:

# Check if there's any validation that prevents calling destruct on a receiver with active children
rg "stealth_address" src/wallet/stealth_receiver.cpp -B 5 -A 10 | head -60

Repository: k-nuth/py-native

Length of output: 2075


🏁 Script executed:

# Look at the derive_private function complete implementation
sed -n '140,165p' src/wallet/stealth_receiver.cpp

Repository: k-nuth/py-native

Length of output: 1594


🏁 Script executed:

# Get exact line numbers for stealth_address function
grep -n "kth_py_native_wallet_stealth_receiver_stealth_address" src/wallet/stealth_receiver.cpp

Repository: k-nuth/py-native

Length of output: 290


🏁 Script executed:

# Verify the full derive_private function to understand the output issue
sed -n '150,193p' src/wallet/stealth_receiver.cpp

Repository: k-nuth/py-native

Length of output: 2827


Don't expose a borrowed StealthAddress from an explicitly destructible receiver.

wallet_stealth_receiver_stealth_address() returns a capsule backed by kth_py_native_borrowed_parent_dtor, which increments the parent receiver's reference count. However, wallet_stealth_receiver_destruct() immediately frees the native receiver pointer and marks the parent capsule as destroyed. The child capsule still holds the freed pointer—the reference counting only keeps the parent Python object alive, not its native payload. This is a use-after-free vulnerability: any access to the child capsule after parent destruction dereferences freed memory. Return an owned StealthAddress copy instead of a borrowed capsule.

Additionally, wallet_stealth_receiver_derive_private() is non-functional: it parses out_private as immutable read-only bytes (y# format), copies input to a local stack buffer, and never writes the derived value back to the caller. The function should accept a writable bytes object like derive_address does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/stealth_receiver.cpp` around lines 87 - 93, The child capsule
returned by wallet_stealth_receiver_stealth_address currently uses
kth_py_native_borrowed_parent_dtor and thus borrows the native pointer from the
receiver, creating a use-after-free when
kth_py_native_wallet_stealth_receiver_destruct frees the receiver; change the
API to return an owned StealthAddress copy (allocate/clone a new native
StealthAddress and return a capsule whose destructor frees that owned copy)
instead of a borrowed capsule so the child capsule remains valid after destruct.
Also fix wallet_stealth_receiver_derive_private which currently parses
out_private as immutable (y#), copies into a stack buffer and never writes back:
accept a writable buffer (use the writable buffer API / buffer protocol like
writable bytes or a Py_buffer) and write the derived private bytes into the
provided output buffer (mirroring how derive_address handles writable output) so
the caller receives the derived value.

Comment on lines +151 to +177
kth_py_native_wallet_stealth_receiver_derive_private(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {(char*)"self", (char*)"out_private", (char*)"ephemeral_public", NULL};
PyObject* py_self = NULL;
char const* out_private_buf = NULL;
Py_ssize_t out_private_size = 0;
char const* ephemeral_public_buf = NULL;
Py_ssize_t ephemeral_public_size = 0;
if ( ! PyArg_ParseTupleAndKeywords(args, kwds, "Oy#y#", kwlist, &py_self, &out_private_buf, &out_private_size, &ephemeral_public_buf, &ephemeral_public_size)) {
return NULL;
}
kth_stealth_receiver_const_t self_handle = (kth_stealth_receiver_const_t)PyCapsule_GetPointer(py_self, KTH_PY_CAPSULE_WALLET_STEALTH_RECEIVER);
if (self_handle == NULL) return NULL;
if (out_private_size != (Py_ssize_t)KTH_BITCOIN_HASH_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte out_private, got %zd", (int)KTH_BITCOIN_HASH_SIZE, out_private_size);
return NULL;
}
kth_hash_t out_private;
memcpy(out_private.hash, out_private_buf, (size_t)KTH_BITCOIN_HASH_SIZE);
if (ephemeral_public_size != (Py_ssize_t)KTH_EC_COMPRESSED_SIZE) {
PyErr_Format(PyExc_ValueError, "expected %d-byte ephemeral_public, got %zd", (int)KTH_EC_COMPRESSED_SIZE, ephemeral_public_size);
return NULL;
}
kth_ec_compressed_t ephemeral_public;
memcpy(ephemeral_public.data, ephemeral_public_buf, (size_t)KTH_EC_COMPRESSED_SIZE);
auto const result = kth_wallet_stealth_receiver_derive_private(self_handle, &out_private, &ephemeral_public);
kth_core_secure_zero((void*)&out_private, sizeof(kth_hash_t));
return PyBool_FromLong((long)result);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

find . -name "stealth_receiver.cpp" -o -name "kth_native.pyi" 2>/dev/null | head -20

Repository: k-nuth/py-native

Length of output: 111


🏁 Script executed:

fd "stealth_receiver" --type f

Repository: k-nuth/py-native

Length of output: 140


🏁 Script executed:

fd "kth_native.pyi" --type f

Repository: k-nuth/py-native

Length of output: 75


🏁 Script executed:

cat -n src/wallet/stealth_receiver.cpp | sed -n '140,185p'

Repository: k-nuth/py-native

Length of output: 3294


🏁 Script executed:

cat kth_native.pyi | grep -A 10 "derive_private"

Repository: k-nuth/py-native

Length of output: 1260


🏁 Script executed:

cat include/kth/py-native/wallet/stealth_receiver.h

Repository: k-nuth/py-native

Length of output: 1292


🏁 Script executed:

rg "kth_wallet_stealth_receiver_derive_private" --type cpp --type h -B 5 -A 10

Repository: k-nuth/py-native

Length of output: 1767


🏁 Script executed:

rg "derive.*private" --type cpp --type h | grep -i stealth

Repository: k-nuth/py-native

Length of output: 684


🏁 Script executed:

rg "kth_.*_derive_" --type h -A 3 | head -50

Repository: k-nuth/py-native

Length of output: 1599


🏁 Script executed:

cat -n src/wallet/hd_private.cpp | sed -n '60,120p'

Repository: k-nuth/py-native

Length of output: 3090


🏁 Script executed:

rg "kth_py_native_wallet_hd_private_derive_private" -A 30

Repository: k-nuth/py-native

Length of output: 4692


🏁 Script executed:

cat kth_native.pyi | grep -A 2 "wallet_hd_private_derive_private"

Repository: k-nuth/py-native

Length of output: 302


🏁 Script executed:

cat -n src/wallet/stealth_receiver.cpp | sed -n '100,150p'

Repository: k-nuth/py-native

Length of output: 2935


🏁 Script executed:

rg "derive_address" src/wallet/stealth_receiver.cpp -B 5 -A 20

Repository: k-nuth/py-native

Length of output: 3560


🏁 Script executed:

git log --oneline src/wallet/stealth_receiver.cpp 2>/dev/null | head -20

Repository: k-nuth/py-native

Length of output: 124


derive_private never exposes the derived key to Python.

y# format accepts immutable bytes, so out_private is input-only. The native call fills a local kth_hash_t buffer at line 168, the buffer is wiped at line 176, and Python receives only a bool. The derived private key is unrecoverable. Return the derived 32-byte secret instead, or switch to a writable buffer API (y*) and update kth_native.pyi accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/stealth_receiver.cpp` around lines 151 - 177,
kth_py_native_wallet_stealth_receiver_derive_private currently parses
out_private as an immutable "y#" buffer and never exposes the derived key to
Python; change parsing to accept a writable buffer (use "y*" with a Py_buffer
for out_private), validate out_private_buf.len == KTH_BITCOIN_HASH_SIZE, pass
its buf to kth_wallet_stealth_receiver_derive_private so the native call writes
the key, then create a Python bytes object from that buffer to return (or copy
it into a new bytes object) before wiping the buffer with kth_core_secure_zero
and releasing the Py_buffer; also update kth_native.pyi to reflect the new
return type or writable-buffer API as appropriate.

Brings the Python bindings level with the C-API headers shipped
since the last py-native sync. The generator's full surface is
now emitted to this repo: every class that ships in `kth/capi/`
with a registered `py_root` has a matching `kth_py_native_*`
wrapper here. Two return shapes that the Python backend used to
reject — `pair<string, data_chunk>` and
`expected<tuple<list, ...>>` — are now bound, so
`cashaddr_decode` and `coin_selection::create_tx_template`
reach Python alongside everything else.

## New classes exposed to Python

* **chain** — `input_point`, plus the new `abla` /
  `abla_config` / `abla_state` opaque-handles and the
  `abla_*_validity` enums (BCH 2024 May Elastic Block
  Adjustment Algorithm).
* **vm** — `big_number`, `number`,
  `script_execution_context`.
* **wallet** — `bitcoin_uri`, `cashaddr` (encode + decode),
  `coin_selection` (incl. `create_tx_template` /
  `create_tx_template_default_ratios` /
  `create_token_split_tx_template`) + `coin_selection_result`,
  `ek_private` / `ek_public` / `ek_token` (BIP38),
  `encrypted_keys` (BIP38 free fns),
  `language` (BIP39 wordlists), `message` (BIP137),
  `mnemonic` (BIP39), `stealth_address` /
  `stealth_receiver` / `stealth_sender` (BIP63).

## Pre-existing classes that gained new methods

* `vm/program` — `pop_number` / `pop_big_number` /
  `pop_simple` / `top_number` / `top_big_number` (the
  BigInt opcode surface that landed on the C-API recently).
* `chain/output_point`, `wallet/wallet_data` — minor regen
  tweaks.

## Tests

Hand-written, patterned after the existing per-class test
files. One file per new class covering smoke + round-trip:

* `test_chain_input_point.py`
* `test_vm_big_number.py`
* `test_wallet_bitcoin_uri.py`
* `test_wallet_cashaddr.py` (covers the new
  pair-returning `decode`)
* `test_wallet_ek_private.py`
* `test_wallet_language.py`
* `test_wallet_message.py`
* `test_wallet_mnemonic.py`

## Plumbing

* `module.c` auto-emit picks up the new includes + 21
  `KTH_REGISTER_METHODS` lines.
* `setup.py` extended with the 21 new `.cpp` files under
  the existing chain / wallet / vm groups.
* `kth_native.pyi` type stubs refreshed.
* `capsule_names.h` — new capsule names for the new opaque
  handles.

## Out of scope

* `recovery_id_to_magic` / `magic_to_recovery_id` — the
  Python backend models their out-pointer scalar params as
  scalar inputs, so the wrappers don't write back through
  the pointers. Pinned as a follow-up; the bindings are in
  this PR, but no test covers them yet.
@fpelliccioni fpelliccioni force-pushed the sync/kth-0.83-full-regen branch from 8942aeb to 35404a4 Compare May 11, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant