Skip to content

fix: privy sign v & tuple no start with#17

Merged
Will-Guan merged 1 commit intomainfrom
fix/privy-sign-v
Apr 1, 2026
Merged

fix: privy sign v & tuple no start with#17
Will-Guan merged 1 commit intomainfrom
fix/privy-sign-v

Conversation

@gummy789j
Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

Code Review Audit Report

PR Overview

Property Value
Branch fix/privy-sign-vmain
Commit 820bace — "fix: privy sign v & tuple no start with"
Files Changed 3
Lines Added 6
Lines Removed 3

Files Changed

File Changes
packages/python/src/agent_wallet/core/adapters/privy.py 1 line changed
packages/python/src/agent_wallet/core/providers/config_provider.py 1 line changed
packages/python/tests/test_privy_adapter.py 5 lines changed (+4 / -1)

Change Summary

1. Privy TRON Signature V-Byte Fix (privy.py)

The _tron_sign_hash method previously appended the raw recovery ID (0 or 1) as the v-byte. This was incorrect: the TronWeb verifyMessage convention requires the v-byte to be offset by 27 (i.e., 0 → 0x1b, 1 → 0x1c). The fix adds + 27 to align with this convention and match the TypeScript implementation.

Before: return (sig_hex + f"{v:02x}").lower()
After: return (sig_hex + f"{v + 27:02x}").lower()

2. Cache Eviction Bug Fix in remove_wallet (config_provider.py)

The remove_wallet method was attempting to call .startswith() on a tuple cache key, which would raise an AttributeError at runtime. The cache dictionary self._wallets uses keys of type tuple[str, WalletType, str | None], not strings. The fix correctly accesses the first element of the tuple with cache_key[0] for comparison.

Before: if not cache_key.startswith(f"{wallet_id}:")
After: if cache_key[0] != wallet_id

3. Test Update to Match V-Byte Fix (test_privy_adapter.py)

The test for test_tron_sign_message_uses_raw_sign_and_appends_v was previously asserting signature == sig.hex(), which expected the raw tronpy signature output (including its own v-byte encoding). The updated test correctly decodes the v-byte from tronpy's output and re-encodes it as v + 27 to match the corrected production code.


Detailed Findings

[CRITICAL-01] Cache Eviction Code Would Raise AttributeError at Runtime

Property Value
Severity Critical
Category Correctness
File packages/python/src/agent_wallet/core/providers/config_provider.py : Lines 99-103

Description: The original code called .startswith() on a tuple value, which is a method that exists only on strings. The _wallets dictionary uses tuple[str, WalletType, str | None] keys (as declared on line 46 and constructed on line 138). Any call to remove_wallet would unconditionally raise AttributeError: 'tuple' object has no attribute 'startswith', meaning the wallet cache was never evicted and the wallet entry deletion path was broken in all cases.

Code (original — now fixed):

self._wallets = {
    cache_key: wallet
    for cache_key, wallet in self._wallets.items()
    if not cache_key.startswith(f"{wallet_id}:")  # AttributeError: tuple has no .startswith
}

Fixed code:

self._wallets = {
    cache_key: wallet
    for cache_key, wallet in self._wallets.items()
    if cache_key[0] != wallet_id
}

Recommendation: The fix is correct. The first element of the cache key tuple (cache_key[0]) is always the wallet_id string, so this comparison is semantically equivalent to what was intended. No further changes needed.


[CRITICAL-02] Incorrect TRON Signature V-Byte Produces Unverifiable Signatures

Property Value
Severity Critical
Category Correctness
File packages/python/src/agent_wallet/core/adapters/privy.py : Lines 151-152

Description: The _tron_sign_hash method was appending the raw ECDSA recovery ID (0 or 1) as the final byte of the signature hex. However, TronWeb's signature convention (used by verifyMessage and other on-chain verifiers) expects the v-byte to be recovery_id + 27 (i.e., 0x1b or 0x1c). Signatures produced without this offset would fail verification in any TRON ecosystem tool, making the Privy TRON signing path completely non-functional for real-world use.

The TypeScript counterpart already implemented this correctly: const vHex = (v + 27).toString(16).padStart(2, '0') (TS file line 158).

Code (original — now fixed):

v = _recover_tron_v(sig_bytes, digest, await self.get_address())
return (sig_hex + f"{v:02x}").lower()

Fixed code:

v = _recover_tron_v(sig_bytes, digest, await self.get_address())
return (sig_hex + f"{v + 27:02x}").lower()

Recommendation: The fix is correct and aligns the Python implementation with the TypeScript implementation. No further changes needed.


[MINOR-01] Test Assertion Logic Depends on External tronpy Signature Format

Property Value
Severity Minor
Category Testing
File packages/python/tests/test_privy_adapter.py : Lines 189-192

Description: The updated test derives the expected v-byte by extracting the last two hex characters of sig.hex() and adding 27. This approach couples the test logic to tronpy's internal signature format (its output must contain a 1-byte v suffix as the final 2 hex characters). If tronpy's sign_msg output format ever changes, the test could give misleading results. A more robust approach would directly verify that the output ends with either 1b or 1c (the only valid TronWeb v-bytes).

Code:

tronpy_v = int(sig.hex()[-2:], 16)
expected = raw_sig + f"{tronpy_v + 27:02x}"
assert signature == expected

Recommendation: Consider supplementing the assertion with an explicit check that the final byte is one of the two valid values:

assert signature[-2:] in ("1b", "1c"), f"Expected TronWeb v-byte 0x1b or 0x1c, got {signature[-2:]}"

This makes the intent self-documenting and adds defense against unexpected v-byte values.


[MINOR-02] Missing Test Coverage for remove_wallet Cache Eviction

Property Value
Severity Minor
Category Testing
File packages/python/tests/test_registry.py : Lines 259-271

Description: The existing test_remove_wallet_deletes_secret_and_clears_active test does not call get_wallet before calling remove_wallet, so self._wallets is empty during the eviction loop. The bug (calling .startswith() on a tuple) would never have been exercised by the test suite. A test that pre-populates the wallet cache before calling remove_wallet would have caught this regression and will guard against future regressions to cache eviction logic.

Recommendation: Add a test that calls get_wallet (to populate the cache) before remove_wallet, then verifies that the cached entry is no longer present and that a subsequent get_wallet call creates a fresh instance.


[SUGGESTION-01] TypeScript Config Provider Uses String Keys — Structural Divergence

Property Value
Severity Suggestion
Category Docs / Maintainability
File packages/typescript/src/core/providers/config-provider.ts : Line 115

Description: The TypeScript implementation uses string-keyed cache entries (key.startsWith(...)) while the Python implementation uses tuple-keyed entries. The structural divergence between the two implementations allowed the Python bug to persist undetected. The CLAUDE.md notes that changes to core logic should typically be applied to both languages. While both implementations are now individually correct, documenting the key-type difference (or unifying the approach) would aid future maintainers.

Recommendation: No code change required, but a comment in config_provider.py near the _wallets declaration noting that keys are tuples (not strings as in the TS counterpart) would prevent future authors from copying the TS string-based cache invalidation pattern incorrectly.


Positive Observations

  1. Symmetry with TypeScript fixed. The Python _tron_sign_hash now matches the TypeScript tronSignHash implementation exactly in its v-byte encoding (v + 27). The dual-language codebase is now consistent on this critical signing convention.

  2. Bug fix is minimal and targeted. Both fixes change exactly one operator or expression without restructuring surrounding logic, minimizing the risk of introducing new issues.

  3. Test was updated in the same commit. The test for the v-byte fix was updated alongside the fix, ensuring the test suite reflects the new expected behavior and prevents regression.

  4. Clear commit message. The commit message "fix: privy sign v & tuple no start with" accurately describes both changes, making the intent clear in the git history.

  5. _recover_tron_v helper design. The helper cleanly separates recovery-ID derivation from v-byte formatting, which made the bug easy to isolate and fix.


Checklist Results

Category Result Notes
A. Correctness & Logic PASS (after fix) Two critical bugs corrected
B. Security PASS No security regressions; no credentials introduced
C. Performance PASS No performance-relevant changes
D. Code Quality PASS Small, focused changes with clear naming
E. Testing PARTIAL Test updated for privy fix; cache eviction still lacks coverage
F. Documentation & Maintainability PARTIAL Structural divergence from TS not documented

Review Verdict

Approve (with suggestions)

Both changes are correct bug fixes that resolve critical runtime failures. The TRON v-byte fix corrects signatures that would have been unverifiable by any TRON ecosystem tool. The cache eviction fix corrects code that would have unconditionally raised AttributeError whenever remove_wallet was called on a provider with any cached wallet entries.

The test updates are adequate for the immediate changes. Two minor gaps remain:

  • No test exercises the cache eviction path (the bug could have been caught by an existing test with a minor addition).
  • The test assertion logic for the v-byte could be made more self-documenting.

Neither gap blocks merging, but the cache eviction test gap should be addressed in a follow-up to prevent regression.

@Will-Guan Will-Guan merged commit 41419a9 into main Apr 1, 2026
6 checks passed
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.

2 participants