Skip to content

Bitcoin (BTC) Wallet Architecture

This document is the authoritative reference for the BTC module's wallet architecture within go-crypto-wallet's Clean Architecture + 3-wallet security model (Watch / Keygen / Sign).

It covers:

  • Wallet role assignment: all three wallets and their responsibilities
  • Clean Architecture boundary map: use case, port, and infrastructure boundaries per wallet
  • Use case assignments: which wallet owns which use cases
  • Keygen vs Sign wallet signing: how they differ in key source and signing scope
  • MuSig2 note: where MuSig2 nonce and partial signing fit

Related documents:


1. Wallet Roles for BTC

BTC supports both single-sig and multisig (P2WSH, MuSig2/P2TR). The Sign Wallet is required for multisig configurations.

WalletEnvironmentResponsibilities
WatchOnlineCreate unsigned PSBTs, import addresses/descriptors, broadcast finalized transactions, monitor confirmations
KeygenOffline (air-gapped)Generate HD keys, manage account keys, import co-signer pubkeys, create multisig/MuSig2 addresses, first signature on PSBT
SignOffline (air-gapped)Generate auth keys, export pubkeys (for Keygen multisig setup), additional signatures on PSBT

For single-sig: only Watch + Keygen are needed. Sign Wallet participates only in multisig flows. See docs/transaction-flow.md for the full single-sig vs multisig comparison.


2. Use Case Assignments per Wallet

Watch Wallet

Use CaseResponsibility
ImportAddressImport public addresses exported by Keygen; begin monitoring
ImportDescriptorImport output descriptors for descriptor wallet support
CreateTransactionSelect UTXOs, build unsigned PSBT, write to file, save to DB
SendTransactionFinalize and extract signed PSBT; broadcast via sendrawtransaction
MonitorTransactionPoll confirmation count; update status in DB; track balances
CreatePaymentRequestRecord outbound payment intent for later CreateTransaction
MuSig2AggregateAggregate MuSig2 partial signatures from all signers into final signature

Keygen Wallet

Use CaseResponsibility
GenerateSeedGenerate BIP-39 mnemonic; store encrypted seed
GenerateHDWalletDerive BIP-44/49/84/86 keys for all account types; store in DB
ImportPrivateKeyImport WIF keys into Bitcoin Core wallet
ImportFullPubkeyImport Sign wallet's extended public keys for multisig setup
CreateMultisigAddressCreate P2SH/P2WSH M-of-N multisig address from collected pubkeys
CreateMuSig2AddressCreate P2TR MuSig2 aggregated key address
ExportAddressExport addresses to file for Watch Wallet import
ExportDescriptorExport output descriptors for descriptor wallet
GenerateDescriptorCompute descriptor strings for all address types
SignTransactionRead unsigned PSBT; derive WIFs from account xpriv; add first signature; write partially-signed PSBT
MuSig2NonceGenerate MuSig2 session nonces (must not be reused)
MuSig2SignProduce MuSig2 partial signature for aggregation by Watch

Sign Wallet

Use CaseResponsibility
GenerateSeedGenerate BIP-39 mnemonic for auth keys
StoreSeedStore seed created externally
GenerateAuthKeyDerive auth keys at hardened BIP-44 paths
ImportPrivateKeyImport WIF auth keys into Bitcoin Core wallet
ExportFullPubkeyExport extended public keys for Keygen to create multisig address
SignTransactionRead partially-signed PSBT; derive WIF from auth xpriv; add subsequent signature; write updated PSBT
MuSig2NonceGenerate MuSig2 session nonces
MuSig2SignProduce MuSig2 partial signature

3. Architecture Boundary Map

The diagram shows the complete dependency graph across all layers. Read it as: each node depends only on nodes in layers below it (Clean Architecture dependency rule).


4. Port Interface Responsibilities

Each use case declares only the small interfaces it needs (Interface Segregation Principle). The full Bitcoiner interface implementation satisfies all of them implicitly via Go structural typing.

PortKey MethodsPrimary Users
ChainConfigProviderGetChainConf(), CoinTypeCode(), ConfirmationBlock()CreateTx, KeygenSignTx, SignSignTx, MonitorTx
UTXOProviderListUnspentByAccount(), GetUnspentListAddrs()CreateTx
RawTransactionCreatorCreateRawTransaction(), GetFee(), ToHex(), ToMsgTx()CreateTx
PSBTCreatorCreatePSBT()CreateTx
PSBTSignerParsePSBT(), SignPSBTWithKey(), WalletProcessPsbt()KeygenSignTx, SignSignTx, MuSig2Agg
PSBTFinalizerIsPSBTComplete(), FinalizePSBT(), ExtractTransaction()SendTx
TransactionSenderSendTransactionByHex()SendTx
TransactionMonitorGetTransactionByTxID()MonitorTx
BalanceCheckerGetBalanceByAccount()CreateTx, MonitorTx
AddressOperatorDecodeAddress(), GetAddressInfo(), GetAccount()CreateTx
AmountConverterFloatToAmount(), AmountToDecimal()CreateTx

5. Keygen vs Sign Wallet: SignTransaction Differences

Both wallets run a SignTransaction use case but they differ fundamentally in key source and signing scope.

AspectKeygen SignTransactionSign SignTransaction
Key repositoryBTCAccountKeyRepositorier (account keys)AuthAccountKeyRepositorier (auth keys)
Key storageaccount_key table: xpriv for deposit/payment/stored accountsauth_account_key table: xpriv for auth1/auth2/etc
Signing orderFirst signer (signature 1 of M)Subsequent signer (signature 2..M of M)
WIF derivationMultiple WIFs for multi-input PSBTs (all inputs belong to account)Single WIF per call (one auth key per Sign wallet)
RPC pathSupports WalletProcessPsbt (descriptor wallet mode)Offline only via SignPSBTWithKey
Wallet trackingNo wtype/authType trackingStores wtype and authType (which Sign wallet is this?)
Output: done checkUnsignedCount == 0IsComplete from PSBT status

Keygen SignTransaction Flow

KeygenSignTxUseCase.Sign(filePath)

    ├── Read unsigned PSBT file (TransactionFileRepo)

    ├── Path A: Descriptor-based derivation (recommended)
    │     Load accountXpriv from DB (BTCAccountKeyRepository)
    │     Parse PSBT → extract BIP-32 derivation paths per input
    │     Derive child WIF at each path from accountXpriv
    │     SignPSBTWithKey(psbt, wifs[]) → (signedPSBT, isComplete)

    ├── Path B: Stored WIF keys
    │     Load WIF keys directly from DB
    │     SignPSBTWithKey(psbt, wifs[])

    └── Path C: RPC descriptor wallet
          WalletProcessPsbt(psbt) → (signedPSBT, isComplete)

    → Write partially-signed PSBT file
    → Return: { FilePath, IsDone, SignedCount, UnsignedCount }

Sign Wallet SignTransaction Flow

SignSignTxUseCase.Sign(filePath)

    ├── Read partially-signed PSBT file (TransactionFileRepo)

    ├── Load auth xpriv from DB (AuthAccountKeyRepository)
    │     Keyed by: (walletType, authType, accountIndex)

    ├── Parse PSBT → extract BIP-32 derivation path for this auth key
    │     Derive single child WIF from auth xpriv
    │     SignPSBTWithKey(psbt, [wif]) → (signedPSBT, isComplete)

    └── Write updated PSBT file

    → Return: { SignedData, IsComplete, NextFilePath }

6. PSBT File Exchange Convention

Files are named to encode signing progress:

{action}_{txID}_{type}_{signedCount}_{timestamp}.psbt

Examples:
  payment_5_unsigned_0_1534466246366489473.psbt   ← Watch creates
  payment_5_unsigned_1_1534466246366489473.psbt   ← Keygen signs (1st)
  payment_5_signed_2_1534466246366489473.psbt     ← Sign1 signs (2nd, complete for 2-of-3)

The signedCount in the filename is the count of signers who have signed so far. When signedCount reaches the required threshold M, the type field changes from unsigned to signed.


7. MuSig2 Notes

MuSig2 (BIP-327) is a Schnorr signature aggregation scheme for P2TR outputs that appears as single-sig on-chain.

Both Keygen and Sign wallets participate in MuSig2:

StepWalletUse Case
Address creationKeygenCreateMuSig2Address — aggregate all signers' pubkeys
Nonce generationKeygen + Sign (each)MuSig2Nonce — fresh nonces per session
Partial signingKeygen + Sign (each)MuSig2Sign — partial Schnorr signature
AggregationWatchMuSig2Aggregate — combine partial sigs into final Schnorr sig

Critical: MuSig2 nonces must never be reused. Reuse leaks the private key. See docs/chains/btc/musig2/security.md for details.


8. Directory Layout

internal/
├── domain/bitcoin/
│   └── types.go                           # BTCAccountKey, WalletKey, domain types

├── application/
│   ├── ports/api/btc/
│   │   ├── interface.go                   # Monolithic Bitcoiner (~50 methods)
│   │   └── interfaces_small.go            # ISP-compliant small interfaces
│   │
│   └── usecase/
│       ├── watch/btc/
│       │   ├── create_transaction.go
│       │   ├── send_transaction.go
│       │   ├── monitor_transaction.go
│       │   ├── import_address.go
│       │   ├── import_descriptor.go
│       │   ├── musig2_aggregate.go
│       │   └── create_payment_request.go
│       ├── keygen/btc/
│       │   ├── sign_transaction.go        # First PSBT signature (account keys)
│       │   ├── create_multisig_address.go
│       │   ├── create_musig2_address.go
│       │   ├── import_private_key.go
│       │   ├── import_fullpubkey.go
│       │   ├── export_address.go
│       │   ├── export_descriptor.go
│       │   ├── generate_descriptor.go
│       │   ├── musig2_nonce.go
│       │   └── musig2_sign.go
│       └── sign/btc/
│           ├── sign_transaction.go        # Subsequent PSBT signatures (auth keys)
│           ├── export_fullpubkey.go
│           ├── import_private_key.go
│           ├── musig2_nonce.go
│           └── musig2_sign.go

└── infrastructure/
    ├── api/bitcoin/btc/
    │   └── bitcoin.go                     # Bitcoin struct (implements all small interfaces)
    └── interface-adapters/wallet/btc/
        ├── watch.go                       # BTCWatch: wires all Watch use cases
        ├── keygen.go                      # BTCKeygen: wires all Keygen use cases incl. SignTx
        └── sign.go                        # BTCSign: wires all Sign use cases incl. SignTx

Document Version: 1.0 Last Updated: 2026-02-24 Maintainer: go-crypto-wallet team