Prepare tinywallet to serve as an out-of-process signing backend - #10
Conversation
Lets a host run transaction building somewhere other than its own binary —
specifically a loadable tinybus module — so the chain libraries that building
requires are absent from the host entirely, while key material never leaves it.
Four changes, in dependency order.
**`bitcoin` is no longer reachable from address validation or key derivation.**
It did three separable jobs here, and only one of them justified its weight:
it carries `secp256k1` and therefore a native C build, which every consumer
paid for even when all it wanted was to check an address.
- BIP-32 walk (`key/bip32.rs`) now delegates to `coins-bip32`, whose backend
is the pure-Rust `k256`. Still delegated, deliberately: a wrong derivation
returns a valid key for the wrong account, silently. `coins-bip32` is
already in the graph beneath `coins-bip39`, so this costs nothing.
- Address parsing (`address/btc.rs`) is now owned directly, over `bech32` and
the `bs58` this crate already had. Safe to own because the failure mode is
the opposite one: a wrong parser is caught by the first vector.
- PSBT build/sign keeps `bitcoin`, behind `tx` alone.
`key` no longer implies `tx`, so the profile a host needs — addresses plus
derivation — resolves with no `bitcoin` and no `secp256k1`: 68 crates against
78. The published BTC and EVM derivation vectors still produce byte-identical
addresses, which is the check that matters.
This found a real regression in the swap: `coins-bip32` increments BIP-32
depth unguarded, panicking in debug and **wrapping silently in release** —
deriving at the wrong depth — where `bitcoin`'s `Xpriv` returned
`MaximumDepthExceeded`. `key/bip32.rs` now bounds depth itself.
**`tinywallet::wire`** is the host/backend contract: outside every chain gate
and dependency-free beyond serde, so a host can take this crate with
`default-features = false`, share one definition, and link no chain library.
Same carve-out `tinydocs::spec` makes.
**Building and signing are separable** on all four chains. Each gains a pair —
digest/`sighashes` and `attach_signature(s)` — so a caller holding the key
elsewhere can sign the bytes and hand back only a signature. Every existing
`sign()` is rewritten to route through the same `attach_*`, because two copies
of an EIP-155 `v` computation or a witness layout are free to drift, and the
failure mode of that drift is a valid signature over the wrong transaction.
Equivalence tests compare both paths byte-for-byte, including a multi-input
Bitcoin spend where signature ordering matters.
**`tinywallet::eip712`** hashes EIP-712 typed data and the EIP-3009
authorization x402 signs, over `sha3` alone — no ABI encoder, no bignum, no
signer stack. Both published type hashes are pinned and re-derived from their
type strings in tests. This is what lets a host drop `ethers-core`.
Nothing here signs on a host's behalf and no new type carries key material.
271 tests pass; clippy is clean on the full, host, and bare profiles.
Co-authored-by: Medulla <medulla@tinyhumans.ai>
|
Warning Review limit reached
Next review available in: 30 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThis PR separates Bitcoin parsing and key-derivation dependencies, adds EIP-712 hashing and a serialized wire contract, and exposes split-signing APIs for Bitcoin, EVM, Solana, and Tron transactions. ChangesWallet signing contracts
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The ABI decoding function now returns an error instead of panicking when given an empty byte slice, ensuring robust handling of malformed or incomplete input data. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The `sha3` crate is now imported only inside the test-scoped `keccak` function, removing the top-level import and the workaround that silenced the unused-import warning in non-test builds. This makes the dependency on the hashing library explicit for test code only, since production uses the precomputed `TRANSFER_SELECTOR` constant. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Updated the test in `src/abi/test.rs` to properly verify that the ABI module returns an error when given an empty input, rather than silently succeeding. This ensures the validation logic is correctly tested and prevents false positives in the test suite. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a new `abi` feature that exposes the `tinywallet::abi` module for building ERC-20 `transfer` calldata. This feature is placed outside the `tx` gate because calldata construction is an input to transaction building, allowing hosts that build transactions elsewhere to use it without pulling in the full transaction dependency chain. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a workspace section to Cargo.toml that includes the tinywallet-module crate as a member while explicitly excluding the vendored tinybus submodule to prevent workspace conflicts. The tinybus submodule is also updated to a newer commit. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Bump the serde crate version from 1.0.197 to 1.0.200 in the tinywallet-module Cargo.toml to incorporate the latest bug fixes and improvements. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
When a user submits a wallet creation request with an empty name, the service now returns a validation error instead of proceeding with an invalid state. This prevents potential downstream issues and provides clearer feedback to the caller. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Removed an unused import from the tinywallet module's lib.rs file to eliminate a compiler warning about dead code. This cleanup keeps the codebase tidy without affecting any functionality. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a new test module to the service layer to enable unit testing of service functions. This establishes the testing infrastructure needed to verify service behavior and catch regressions. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the tinybus event-bus library, its macros crate, the tinybus-module abstraction, and the tinywallet-module crate to the workspace, along with all their transitive dependencies. This change also moves lint configuration from per-crate to workspace-level inheritance so that every member crate is held to the same standard. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
…ds and handle non-exhaustive en Adds the `bs58` crate to encode 64-byte Solana signatures directly, since the existing `address::solana::encode` only handles 32-byte addresses. Also adds wildcard arms to match statements on `TransactionSpec` and `Signature` enums, which are marked `#[non_exhaustive]`, so that future variants produce a clear error instead of a compilation failure or silent misbehavior. Fixes a type mismatch in EVM transaction construction by converting `nonce` and `gas_limit` from their source types to `u128`. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test now checks that the service correctly rejects invalid inputs by asserting the expected error response, ensuring the validation change is properly covered. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
…ce tests The tinywallet-module crate now depends on the bitcoin library with the secp-recovery feature enabled, because the service tests need to sign messages using the recoverable secp256k1 API exactly as the host would, ensuring the tests exercise the real cryptographic boundary rather than a simplified helper. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformat several long lines in the decimal_u128 function and test files to comply with the project's line-length conventions, wrapping them across multiple lines for improved readability without any change in behaviour. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/tx/evm.rs (1)
159-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
recovery_as_u64cannot fail here.
recovery_idis au8, soi32::from(recovery_id)is never negative and the negative-id branch is unreachable.u64::from(recovery_id)states the same thing without the fallible round trip.♻️ Proposed simplification
- let recovery = recovery_as_u64(i32::from(recovery_id))?; - let v = checked_v(recovery, self.chain_id)?; + let v = checked_v(u64::from(recovery_id), self.chain_id)?;Keep
recovery_as_u64if another caller still passes a rawi32.🤖 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/tx/evm.rs` around lines 159 - 160, In the code around recovery_id, replace the fallible recovery_as_u64(i32::from(recovery_id)) conversion with a direct u64 conversion from the u8 value, removing the unnecessary error propagation. Retain recovery_as_u64 only if another caller still requires conversion from a raw i32.src/tx/solana.rs (1)
370-381: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe assertion does not test what the name states.
message.len() > 32holds for any Solana message. It does not show that the signed payload is the message rather than a digest, and it does not exercise the pre-hashing mistake the comment describes.Sign a digest of the message and assert the result does not verify against
from. That failure is the property worth pinning.💚 Proposed test change
let transfer = transfer(); let message = transfer.message().unwrap(); - assert!( - message.len() > 32, - "a Solana message is the full serialized transaction, not a 32-byte digest" - ); + assert!(message.len() > 32, "a message is not a 32-byte digest"); + + use ed25519_dalek::{Signer as _, SigningKey, Signature, Verifier as _, VerifyingKey}; + use sha2::{Digest as _, Sha256}; + + let bytes: [u8; 32] = key().as_slice().try_into().unwrap(); + let signing = SigningKey::from_bytes(&bytes); + let digest: [u8; 32] = Sha256::digest(&message).into(); + let wrong = signing.sign(&digest).to_bytes(); + + let public = VerifyingKey::from_bytes(&crate::address::solana::decode(FROM).unwrap()).unwrap(); + assert!( + public.verify(&message, &Signature::from_bytes(&wrong)).is_err(), + "signing a digest must not produce a signature the network accepts" + );
sha2is already atxdependency, so no manifest change is needed.🤖 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/tx/solana.rs` around lines 370 - 381, Update the test the_signed_payload_is_the_message_itself_not_a_digest to hash the serialized message with the existing sha2 dependency, sign that digest using the transfer’s signing path, and assert the resulting signature does not verify against from. Remove the message-length assertion while preserving the test’s focus on rejecting pre-hashed payloads.src/tx/tron.rs (2)
120-135: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueConsider normalizing
shere, as the Bitcoin path does.
Transfer::attach_signaturesinsrc/tx/btc.rscallsnormalize_sat Line 323, with the reasoning that a host signing through a library that does not normalize still produces a broadcastable transaction.attach_signaturehere, and the EVM equivalent, place that burden on the host instead.src/wire/mod.rsLines 65-77 document the requirement, so the contract is stated, but the enforcement is asymmetric across the three secp256k1 chains.Either normalize in all three attachment paths or record in this doc comment why Bitcoin is the exception.
🤖 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/tx/tron.rs` around lines 120 - 135, Normalize the signature’s s component in attach_signature before constructing the 65-byte Tron output, matching the normalize_s behavior used by Bitcoin and the other secp256k1 attachment paths. Preserve the recovery_id validation and output layout, and ensure the doc comment reflects the enforced normalization contract if needed.
115-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
recompute_txidcan now be expressed throughdigest.Lines 44-47 repeat the same decode and SHA-256. The doc comment states the two are the same value by construction; routing one through the other makes that structural instead of relying on the test at
src/tx/test.rsLine 366.♻️ Proposed change to `recompute_txid`
pub fn recompute_txid(raw_data_hex: &str) -> Result<String> { - let raw = decode_hex(raw_data_hex)?; - Ok(hex_lower(&Sha256::digest(&raw))) + Ok(hex_lower(&digest(raw_data_hex)?)) }🤖 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/tx/tron.rs` around lines 115 - 118, Update recompute_txid to delegate to the existing digest function instead of independently decoding raw data and computing SHA-256. Preserve recompute_txid’s current return behavior while making the shared calculation structurally explicit.src/tx/btc.rs (3)
622-638: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
wronglikely fails the parse guard, not the address-mismatch guard.
[0x02u8; 33]is a prefix byte plus an x-coordinate that is almost certainly not on secp256k1.CompressedPublicKey::from_slicerejects it, socheck_controls_fromreturns before the address comparison. The test passes without covering the branch it targets.Derive a real key from another path, as
a_key_that_does_not_control_the_sender_is_rejecteddoes at Line 496, and use its compressed public key.💚 Proposed test change
- let wrong = [0x02u8; 33]; + let other = crate::key::derive(crate::Chain::Btc, VECTOR, "m/84'/0'/0'/0/1") + .unwrap() + .secret_bytes() + .to_vec(); + let wrong = { + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + let secret = SecretKey::from_slice(&other).unwrap(); + PublicKey::from_secret_key(&Secp256k1::new(), &secret).serialize() + };🤖 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/tx/btc.rs` around lines 622 - 638, Update a_public_key_that_does_not_control_the_sender_is_refused to derive a valid compressed public key from a different key path, following the setup used by a_key_that_does_not_control_the_sender_is_rejected. Replace the hard-coded wrong value while preserving both sighashes and attach_signatures assertions so they exercise the sender-address mismatch branch and return Error::Signing.
313-331: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winThe split attachment paths accept backend signatures without confirming they sign what this crate built. Each one-shot
signderives the key locally and refuses a key that does not control the sender. The matching attachment method assembles whatever bytes it is handed. The out-of-process backend this PR prepares for is exactly the caller that can get this wrong, and the result is a well-formed transaction the network rejects after the fee is committed.
src/tx/btc.rs#L313-L331: verify each compact signature against its corresponding BIP-143 sighash andpublic_keybefore building the witness, so a transposed or foreign signature fails locally.src/tx/solana.rs#L137-L153: verify the supplied 64-byte signature against the ed25519 key decoded fromself.from, restoring the guardsignapplies at Line 124.🤖 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/tx/btc.rs` around lines 313 - 331, The split attachment paths must validate backend signatures before attaching them. In src/tx/btc.rs:313-331, update the attachment loop around the witness construction to verify each compact signature against its corresponding BIP-143 sighash and public_key, rejecting transposed or foreign signatures before building the witness. In src/tx/solana.rs:137-153, verify the supplied 64-byte signature against the Ed25519 public key decoded from self.from, restoring the validation performed by sign; both sites should fail locally on invalid signatures.
221-236: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
signnow builds the transaction twice.
sighashescallsbuild, andattach_signaturescallsbuildagain. Eachbuildclones and sorts the whole UTXO set inselect_coins. The result is correct becausebuildis deterministic, but the work doubles for a wallet with many UTXOs.Consider adding a private helper that takes an already-built
(Transaction, Selection)and is shared by both public methods, leaving the public API unchanged.🤖 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/tx/btc.rs` around lines 221 - 236, Avoid rebuilding the transaction in sign by introducing a private helper that accepts the already-built (Transaction, Selection) result and performs signature attachment. Update sighashes and attach_signatures to reuse this helper or shared build output, while keeping their public APIs and transaction ordering behavior unchanged.
🤖 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 `@Cargo.toml`:
- Around line 105-108: Update the serde feature definition in Cargo.toml to also
enable serde’s derive feature, while preserving the existing dep:serde
dependency activation so Chain’s serde derives compile when the serde feature is
enabled.
In `@src/address/btc.rs`:
- Around line 207-246: Update the HRP validation in parse_bech32 to compare
case-insensitively, using bech32::hrp::BC or an equivalent ASCII-insensitive
comparison instead of direct string inequality. Preserve rejection of
non-mainnet HRPs while accepting valid uppercase and lowercase mainnet
addresses.
In `@src/wire/mod.rs`:
- Around line 120-145: Ensure SigningRequest and AttachRequest reject
inconsistent chain and transaction.kind combinations during request validation,
or remove the redundant chain field while preserving the API contract. Apply the
check to both request types and add rejection tests covering mismatched values
for each.
---
Nitpick comments:
In `@src/tx/btc.rs`:
- Around line 622-638: Update
a_public_key_that_does_not_control_the_sender_is_refused to derive a valid
compressed public key from a different key path, following the setup used by
a_key_that_does_not_control_the_sender_is_rejected. Replace the hard-coded wrong
value while preserving both sighashes and attach_signatures assertions so they
exercise the sender-address mismatch branch and return Error::Signing.
- Around line 313-331: The split attachment paths must validate backend
signatures before attaching them. In src/tx/btc.rs:313-331, update the
attachment loop around the witness construction to verify each compact signature
against its corresponding BIP-143 sighash and public_key, rejecting transposed
or foreign signatures before building the witness. In src/tx/solana.rs:137-153,
verify the supplied 64-byte signature against the Ed25519 public key decoded
from self.from, restoring the validation performed by sign; both sites should
fail locally on invalid signatures.
- Around line 221-236: Avoid rebuilding the transaction in sign by introducing a
private helper that accepts the already-built (Transaction, Selection) result
and performs signature attachment. Update sighashes and attach_signatures to
reuse this helper or shared build output, while keeping their public APIs and
transaction ordering behavior unchanged.
In `@src/tx/evm.rs`:
- Around line 159-160: In the code around recovery_id, replace the fallible
recovery_as_u64(i32::from(recovery_id)) conversion with a direct u64 conversion
from the u8 value, removing the unnecessary error propagation. Retain
recovery_as_u64 only if another caller still requires conversion from a raw i32.
In `@src/tx/solana.rs`:
- Around line 370-381: Update the test
the_signed_payload_is_the_message_itself_not_a_digest to hash the serialized
message with the existing sha2 dependency, sign that digest using the transfer’s
signing path, and assert the resulting signature does not verify against from.
Remove the message-length assertion while preserving the test’s focus on
rejecting pre-hashed payloads.
In `@src/tx/tron.rs`:
- Around line 120-135: Normalize the signature’s s component in attach_signature
before constructing the 65-byte Tron output, matching the normalize_s behavior
used by Bitcoin and the other secp256k1 attachment paths. Preserve the
recovery_id validation and output layout, and ensure the doc comment reflects
the enforced normalization contract if needed.
- Around line 115-118: Update recompute_txid to delegate to the existing digest
function instead of independently decoding raw data and computing SHA-256.
Preserve recompute_txid’s current return behavior while making the shared
calculation structurally explicit.
🪄 Autofix
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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a1bdeb89-0bfb-4e77-864e-2196a362f3d9
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
Cargo.tomlsrc/address/btc.rssrc/address/mod.rssrc/chain/mod.rssrc/eip712/mod.rssrc/eip712/test.rssrc/key/bip32.rssrc/key/btc.rssrc/key/evm.rssrc/key/test.rssrc/key/tron.rssrc/lib.rssrc/tx/btc.rssrc/tx/evm.rssrc/tx/solana.rssrc/tx/test.rssrc/tx/tron.rssrc/wire/mod.rssrc/wire/test.rs
Changed the `build_failed` function to take a reference to `tx::Error` instead of an owned value, and updated all call sites to pass `&e` instead of `e`. This avoids unnecessary cloning of error values when mapping errors in the transaction building pipeline. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The end-to-end test for the tinywallet module was accidentally removed during a previous refactor. This change restores the test to ensure the module's core functionality is validated in integration scenarios. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the ed25519-dalek and tokio crates as dependencies for the tinywallet-module crate. The ed25519-dalek library is needed to sign Solana transfers using ed25519 keys in end-to-end tests, while tokio provides the async runtime required to drive a real broker and loader during those tests. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the initial specification document for the tinybus module, defining its interface and behavior to guide implementation and future development. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a new workflow stage that builds native module bundles for eleven platform targets and creates a GitHub release with checksum verification. This enables automated distribution of compiled modules across Linux, macOS, and Windows architectures. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a new end-to-end test job that builds the TinyBus loadable module as a cdylib and runs the module_e2e test, which validates that signing through the dynamic loader matches in-process signing byte for byte. Fix the release workflow to also bump the version in the module crate's Cargo.toml and update the workspace lockfile, preventing a mismatch where the archive name would reflect the root crate version while the library inside reports an older version. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Vendored submodules contain third-party code whose coverage should not be enforced by this repository. The filter now skips files under the vendor directory to prevent false positives in the coverage gate. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds thirteen test cases that exercise every rejection path in the parser that was introduced when this module stopped delegating to the `bitcoin` crate. Each test targets a specific rule from BIP-173 or BIP-350, using the published test vectors where available, to ensure that a rejection that never fires is distinguishable from one that is wrong. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a unit test that exercises the defensive error paths in `decode_evm_address` with a short hex string and a non-hex character, verifying they return an error instead of panicking. The happy path with an unprefixed address is also tested to confirm the `0x` prefix is optional. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Added: the loadable module and its release pipelinePushed the second half. The PR now covers the whole tinywallet side of the port.
|
The MSRV job was building all workspace crates, but `tinywallet-module` is unpublished and requires a newer compiler due to its dependency on `tinybus`. Scoping the build to `tinywallet` prevents the module from forcing an unnecessarily high MSRV on the library's consumers. The module's documentation comment is also updated to point to the external spec file and clarify how to view the private service module's docs. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reworded the inline comment in the coverage threshold script to better explain why vendored submodule files are excluded from the coverage check, and added a note about the single-quoting constraint to prevent future confusion. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
…etection The address parser now dispatches on the structural shape of a bech32 string rather than matching against a hardcoded list of known human-readable parts. This allows any bech32-shaped address, including those from test networks or other chains, to be routed to the bech32 parser and reported as a wrong-network error instead of being misidentified as malformed base58. The change also replaces the manual `Hrp::parse` call in `encode_p2wpkh` with the constant `bech32::hrp::BC`, removing an unreachable error path. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds five test cases that exercise edge cases in Bitcoin address validation: unrecognised version bytes, wrong hash lengths, empty payloads, testnet P2SH addresses, and a Litecoin bech32 address that should produce a WrongNetwork error rather than a misleading base58 parse failure. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformatted the error mapping closure in `encode_p2wpkh` to use a single expression instead of a block, and removed unnecessary line breaks in the test for empty base58check payloads. These changes improve code consistency without altering behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
What
Lets a host run transaction building somewhere other than its own binary — specifically a loadable tinybus module — so the chain libraries that building requires are absent from the host entirely, while key material never leaves it.
This is the groundwork. The
tinywallet-modulecdylib itself follows in a second PR; nothing here depends on tinybus.Why
OpenHuman's
web3family carriesbitcoin,ethers-core,ethers-signersandcoins-bip39to sign four chains' transactions. Measured against the shipped product profile, moving signing out sheds 51 crates / 56 packages, including a native C build.The blocker was structural:
bitcoinhad two parents, OpenHuman and tinywallet, so cutting the host's direct edge shed nothing — tinywallet re-supplied it throughkey, which requiredbtc. Hence the first change below.The four changes
bitcoinis no longer reachable from address validation or key derivationIt did three separable jobs, and only one justified its weight — it carries
secp256k1and therefore a native C build, which every consumer paid for even when all it wanted was to check an address.coins-bip32(pure-Rustk256)coins-bip39, so it costs nothing.bech32+ the existingbs58bitcoin, behindtxalonekeyno longer impliestx, so the profile a host needs resolves with nobitcoinand nosecp256k1— 68 crates against 78. The published BTC and EVM derivation vectors still produce byte-identical addresses.Important
This found a real regression in the swap.
coins-bip32increments BIP-32 depth unguarded: it panics in debug and wraps silently in release, deriving at the wrong depth, wherebitcoin'sXprivreturnedMaximumDepthExceeded.key/bip32.rsnow bounds depth itself, with a test.tinywallet::wire— the host/backend contractOutside every chain gate and dependency-free beyond serde, so a host can take this crate with
default-features = false, share one definition of the contract, and link no chain library. The same carve-outtinydocs::specmakes.Building and signing are separable on all four chains
Each chain gains a pair — a digest (
sighashesfor Bitcoin, which needs one per input) andattach_signature(s).Every existing
sign()is rewritten to route through the sameattach_*. Two copies of an EIP-155vcomputation or a witness layout are free to drift, and the failure mode of that drift is a perfectly valid signature over the wrong transaction. Equivalence tests compare both paths byte-for-byte, including a multi-input Bitcoin spend where signature ordering matters.tinywallet::eip712EIP-712 typed data and the EIP-3009 authorization x402 signs, over
sha3alone — no ABI encoder, no bignum, no signer stack. Both published type hashes are pinned and re-derived from their type strings in tests, so pinning is safe rather than merely fast. This is what lets a host dropethers-core.Design note: keys stay with the host
SigningRequestcarries no secret, andAttachRequestre-sends the transaction fields rather than a handle — so a backend keeps no state between the two calls, and needs no store, no bounds, and no expiry for callers that never come back. Building is deterministic, so rebuilding reproduces the transaction the digests were computed over.The cost is two round trips instead of one. The alternative — shipping the mnemonic to the backend — is one round trip and sheds ~23 more crates, and was rejected: a loadable module shares the address space, but that is a reason not to widen what crosses the boundary, not a reason to stop caring.
Verification
pedantic,unwrap_used,missing_docs) on the full, host, and bare profiles.--no-default-featuresand--features wireboth build and test standalone.Compatibility
No public API is removed. Every existing
sign()keeps its signature and its bytes.Chaingains conditional serde derives. Thebtcfeature no longer pullsbitcoin— a consumer relying on that transitively must name it, which is the point.Summary by CodeRabbit
New Features
Bug Fixes