github.com/btcsuite/btcd@v0.24.0/integration/rpctest/memwallet.go (about) 1 // Copyright (c) 2016-2017 The btcsuite developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package rpctest 6 7 import ( 8 "bytes" 9 "encoding/binary" 10 "fmt" 11 "sync" 12 13 "github.com/btcsuite/btcd/blockchain" 14 "github.com/btcsuite/btcd/btcec/v2" 15 "github.com/btcsuite/btcd/btcutil" 16 "github.com/btcsuite/btcd/btcutil/hdkeychain" 17 "github.com/btcsuite/btcd/chaincfg" 18 "github.com/btcsuite/btcd/chaincfg/chainhash" 19 "github.com/btcsuite/btcd/rpcclient" 20 "github.com/btcsuite/btcd/txscript" 21 "github.com/btcsuite/btcd/wire" 22 ) 23 24 var ( 25 // hdSeed is the BIP 32 seed used by the memWallet to initialize it's 26 // HD root key. This value is hard coded in order to ensure 27 // deterministic behavior across test runs. 28 hdSeed = [chainhash.HashSize]byte{ 29 0x79, 0xa6, 0x1a, 0xdb, 0xc6, 0xe5, 0xa2, 0xe1, 30 0x39, 0xd2, 0x71, 0x3a, 0x54, 0x6e, 0xc7, 0xc8, 31 0x75, 0x63, 0x2e, 0x75, 0xf1, 0xdf, 0x9c, 0x3f, 32 0xa6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 33 } 34 ) 35 36 // utxo represents an unspent output spendable by the memWallet. The maturity 37 // height of the transaction is recorded in order to properly observe the 38 // maturity period of direct coinbase outputs. 39 type utxo struct { 40 pkScript []byte 41 value btcutil.Amount 42 keyIndex uint32 43 maturityHeight int32 44 isLocked bool 45 } 46 47 // isMature returns true if the target utxo is considered "mature" at the 48 // passed block height. Otherwise, false is returned. 49 func (u *utxo) isMature(height int32) bool { 50 return height >= u.maturityHeight 51 } 52 53 // chainUpdate encapsulates an update to the current main chain. This struct is 54 // used to sync up the memWallet each time a new block is connected to the main 55 // chain. 56 type chainUpdate struct { 57 blockHeight int32 58 filteredTxns []*btcutil.Tx 59 isConnect bool // True if connect, false if disconnect 60 } 61 62 // undoEntry is functionally the opposite of a chainUpdate. An undoEntry is 63 // created for each new block received, then stored in a log in order to 64 // properly handle block re-orgs. 65 type undoEntry struct { 66 utxosDestroyed map[wire.OutPoint]*utxo 67 utxosCreated []wire.OutPoint 68 } 69 70 // memWallet is a simple in-memory wallet whose purpose is to provide basic 71 // wallet functionality to the harness. The wallet uses a hard-coded HD key 72 // hierarchy which promotes reproducibility between harness test runs. 73 type memWallet struct { 74 coinbaseKey *btcec.PrivateKey 75 coinbaseAddr btcutil.Address 76 77 // hdRoot is the root master private key for the wallet. 78 hdRoot *hdkeychain.ExtendedKey 79 80 // hdIndex is the next available key index offset from the hdRoot. 81 hdIndex uint32 82 83 // currentHeight is the latest height the wallet is known to be synced 84 // to. 85 currentHeight int32 86 87 // addrs tracks all addresses belonging to the wallet. The addresses 88 // are indexed by their keypath from the hdRoot. 89 addrs map[uint32]btcutil.Address 90 91 // utxos is the set of utxos spendable by the wallet. 92 utxos map[wire.OutPoint]*utxo 93 94 // reorgJournal is a map storing an undo entry for each new block 95 // received. Once a block is disconnected, the undo entry for the 96 // particular height is evaluated, thereby rewinding the effect of the 97 // disconnected block on the wallet's set of spendable utxos. 98 reorgJournal map[int32]*undoEntry 99 100 chainUpdates []*chainUpdate 101 chainUpdateSignal chan struct{} 102 chainMtx sync.Mutex 103 104 net *chaincfg.Params 105 106 rpc *rpcclient.Client 107 108 sync.RWMutex 109 } 110 111 // newMemWallet creates and returns a fully initialized instance of the 112 // memWallet given a particular blockchain's parameters. 113 func newMemWallet(net *chaincfg.Params, harnessID uint32) (*memWallet, error) { 114 // The wallet's final HD seed is: hdSeed || harnessID. This method 115 // ensures that each harness instance uses a deterministic root seed 116 // based on its harness ID. 117 var harnessHDSeed [chainhash.HashSize + 4]byte 118 copy(harnessHDSeed[:], hdSeed[:]) 119 binary.BigEndian.PutUint32(harnessHDSeed[:chainhash.HashSize], harnessID) 120 121 hdRoot, err := hdkeychain.NewMaster(harnessHDSeed[:], net) 122 if err != nil { 123 return nil, nil 124 } 125 126 // The first child key from the hd root is reserved as the coinbase 127 // generation address. 128 coinbaseChild, err := hdRoot.Derive(0) 129 if err != nil { 130 return nil, err 131 } 132 coinbaseKey, err := coinbaseChild.ECPrivKey() 133 if err != nil { 134 return nil, err 135 } 136 137 coinbaseAddr, err := keyToAddr(coinbaseKey, net) 138 if err != nil { 139 return nil, err 140 } 141 142 // Track the coinbase generation address to ensure we properly track 143 // newly generated bitcoin we can spend. 144 addrs := make(map[uint32]btcutil.Address) 145 addrs[0] = coinbaseAddr 146 147 return &memWallet{ 148 net: net, 149 coinbaseKey: coinbaseKey, 150 coinbaseAddr: coinbaseAddr, 151 hdIndex: 1, 152 hdRoot: hdRoot, 153 addrs: addrs, 154 utxos: make(map[wire.OutPoint]*utxo), 155 chainUpdateSignal: make(chan struct{}), 156 reorgJournal: make(map[int32]*undoEntry), 157 }, nil 158 } 159 160 // Start launches all goroutines required for the wallet to function properly. 161 func (m *memWallet) Start() { 162 go m.chainSyncer() 163 } 164 165 // SyncedHeight returns the height the wallet is known to be synced to. 166 // 167 // This function is safe for concurrent access. 168 func (m *memWallet) SyncedHeight() int32 { 169 m.RLock() 170 defer m.RUnlock() 171 return m.currentHeight 172 } 173 174 // SetRPCClient saves the passed rpc connection to btcd as the wallet's 175 // personal rpc connection. 176 func (m *memWallet) SetRPCClient(rpcClient *rpcclient.Client) { 177 m.rpc = rpcClient 178 } 179 180 // IngestBlock is a call-back which is to be triggered each time a new block is 181 // connected to the main chain. It queues the update for the chain syncer, 182 // calling the private version in sequential order. 183 func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) { 184 // Append this new chain update to the end of the queue of new chain 185 // updates. 186 m.chainMtx.Lock() 187 m.chainUpdates = append(m.chainUpdates, &chainUpdate{height, 188 filteredTxns, true}) 189 m.chainMtx.Unlock() 190 191 // Launch a goroutine to signal the chainSyncer that a new update is 192 // available. We do this in a new goroutine in order to avoid blocking 193 // the main loop of the rpc client. 194 go func() { 195 m.chainUpdateSignal <- struct{}{} 196 }() 197 } 198 199 // ingestBlock updates the wallet's internal utxo state based on the outputs 200 // created and destroyed within each block. 201 func (m *memWallet) ingestBlock(update *chainUpdate) { 202 // Update the latest synced height, then process each filtered 203 // transaction in the block creating and destroying utxos within 204 // the wallet as a result. 205 m.currentHeight = update.blockHeight 206 undo := &undoEntry{ 207 utxosDestroyed: make(map[wire.OutPoint]*utxo), 208 } 209 for _, tx := range update.filteredTxns { 210 mtx := tx.MsgTx() 211 isCoinbase := blockchain.IsCoinBaseTx(mtx) 212 txHash := mtx.TxHash() 213 m.evalOutputs(mtx.TxOut, &txHash, isCoinbase, undo) 214 m.evalInputs(mtx.TxIn, undo) 215 } 216 217 // Finally, record the undo entry for this block so we can 218 // properly update our internal state in response to the block 219 // being re-org'd from the main chain. 220 m.reorgJournal[update.blockHeight] = undo 221 } 222 223 // chainSyncer is a goroutine dedicated to processing new blocks in order to 224 // keep the wallet's utxo state up to date. 225 // 226 // NOTE: This MUST be run as a goroutine. 227 func (m *memWallet) chainSyncer() { 228 var update *chainUpdate 229 230 for range m.chainUpdateSignal { 231 // A new update is available, so pop the new chain update from 232 // the front of the update queue. 233 m.chainMtx.Lock() 234 update = m.chainUpdates[0] 235 m.chainUpdates[0] = nil // Set to nil to prevent GC leak. 236 m.chainUpdates = m.chainUpdates[1:] 237 m.chainMtx.Unlock() 238 239 m.Lock() 240 if update.isConnect { 241 m.ingestBlock(update) 242 } else { 243 m.unwindBlock(update) 244 } 245 m.Unlock() 246 } 247 } 248 249 // evalOutputs evaluates each of the passed outputs, creating a new matching 250 // utxo within the wallet if we're able to spend the output. 251 func (m *memWallet) evalOutputs(outputs []*wire.TxOut, txHash *chainhash.Hash, 252 isCoinbase bool, undo *undoEntry) { 253 254 for i, output := range outputs { 255 pkScript := output.PkScript 256 257 // Scan all the addresses we currently control to see if the 258 // output is paying to us. 259 for keyIndex, addr := range m.addrs { 260 pkHash := addr.ScriptAddress() 261 if !bytes.Contains(pkScript, pkHash) { 262 continue 263 } 264 265 // If this is a coinbase output, then we mark the 266 // maturity height at the proper block height in the 267 // future. 268 var maturityHeight int32 269 if isCoinbase { 270 maturityHeight = m.currentHeight + int32(m.net.CoinbaseMaturity) 271 } 272 273 op := wire.OutPoint{Hash: *txHash, Index: uint32(i)} 274 m.utxos[op] = &utxo{ 275 value: btcutil.Amount(output.Value), 276 keyIndex: keyIndex, 277 maturityHeight: maturityHeight, 278 pkScript: pkScript, 279 } 280 undo.utxosCreated = append(undo.utxosCreated, op) 281 } 282 } 283 } 284 285 // evalInputs scans all the passed inputs, destroying any utxos within the 286 // wallet which are spent by an input. 287 func (m *memWallet) evalInputs(inputs []*wire.TxIn, undo *undoEntry) { 288 for _, txIn := range inputs { 289 op := txIn.PreviousOutPoint 290 oldUtxo, ok := m.utxos[op] 291 if !ok { 292 continue 293 } 294 295 undo.utxosDestroyed[op] = oldUtxo 296 delete(m.utxos, op) 297 } 298 } 299 300 // UnwindBlock is a call-back which is to be executed each time a block is 301 // disconnected from the main chain. It queues the update for the chain syncer, 302 // calling the private version in sequential order. 303 func (m *memWallet) UnwindBlock(height int32, header *wire.BlockHeader) { 304 // Append this new chain update to the end of the queue of new chain 305 // updates. 306 m.chainMtx.Lock() 307 m.chainUpdates = append(m.chainUpdates, &chainUpdate{height, 308 nil, false}) 309 m.chainMtx.Unlock() 310 311 // Launch a goroutine to signal the chainSyncer that a new update is 312 // available. We do this in a new goroutine in order to avoid blocking 313 // the main loop of the rpc client. 314 go func() { 315 m.chainUpdateSignal <- struct{}{} 316 }() 317 } 318 319 // unwindBlock undoes the effect that a particular block had on the wallet's 320 // internal utxo state. 321 func (m *memWallet) unwindBlock(update *chainUpdate) { 322 undo := m.reorgJournal[update.blockHeight] 323 324 for _, utxo := range undo.utxosCreated { 325 delete(m.utxos, utxo) 326 } 327 328 for outPoint, utxo := range undo.utxosDestroyed { 329 m.utxos[outPoint] = utxo 330 } 331 332 delete(m.reorgJournal, update.blockHeight) 333 } 334 335 // newAddress returns a new address from the wallet's hd key chain. It also 336 // loads the address into the RPC client's transaction filter to ensure any 337 // transactions that involve it are delivered via the notifications. 338 func (m *memWallet) newAddress() (btcutil.Address, error) { 339 index := m.hdIndex 340 341 childKey, err := m.hdRoot.Derive(index) 342 if err != nil { 343 return nil, err 344 } 345 privKey, err := childKey.ECPrivKey() 346 if err != nil { 347 return nil, err 348 } 349 350 addr, err := keyToAddr(privKey, m.net) 351 if err != nil { 352 return nil, err 353 } 354 355 err = m.rpc.LoadTxFilter(false, []btcutil.Address{addr}, nil) 356 if err != nil { 357 return nil, err 358 } 359 360 m.addrs[index] = addr 361 362 m.hdIndex++ 363 364 return addr, nil 365 } 366 367 // NewAddress returns a fresh address spendable by the wallet. 368 // 369 // This function is safe for concurrent access. 370 func (m *memWallet) NewAddress() (btcutil.Address, error) { 371 m.Lock() 372 defer m.Unlock() 373 374 return m.newAddress() 375 } 376 377 // fundTx attempts to fund a transaction sending amt bitcoin. The coins are 378 // selected such that the final amount spent pays enough fees as dictated by the 379 // passed fee rate. The passed fee rate should be expressed in 380 // satoshis-per-byte. The transaction being funded can optionally include a 381 // change output indicated by the change boolean. 382 // 383 // NOTE: The memWallet's mutex must be held when this function is called. 384 func (m *memWallet) fundTx(tx *wire.MsgTx, amt btcutil.Amount, 385 feeRate btcutil.Amount, change bool) error { 386 387 const ( 388 // spendSize is the largest number of bytes of a sigScript 389 // which spends a p2pkh output: OP_DATA_73 <sig> OP_DATA_33 <pubkey> 390 spendSize = 1 + 73 + 1 + 33 391 ) 392 393 var ( 394 amtSelected btcutil.Amount 395 txSize int 396 ) 397 398 for outPoint, utxo := range m.utxos { 399 // Skip any outputs that are still currently immature or are 400 // currently locked. 401 if !utxo.isMature(m.currentHeight) || utxo.isLocked { 402 continue 403 } 404 405 amtSelected += utxo.value 406 407 // Add the selected output to the transaction, updating the 408 // current tx size while accounting for the size of the future 409 // sigScript. 410 tx.AddTxIn(wire.NewTxIn(&outPoint, nil, nil)) 411 txSize = tx.SerializeSize() + spendSize*len(tx.TxIn) 412 413 // Calculate the fee required for the txn at this point 414 // observing the specified fee rate. If we don't have enough 415 // coins from he current amount selected to pay the fee, then 416 // continue to grab more coins. 417 reqFee := btcutil.Amount(txSize * int(feeRate)) 418 if amtSelected-reqFee < amt { 419 continue 420 } 421 422 // If we have any change left over and we should create a change 423 // output, then add an additional output to the transaction 424 // reserved for it. 425 changeVal := amtSelected - amt - reqFee 426 if changeVal > 0 && change { 427 addr, err := m.newAddress() 428 if err != nil { 429 return err 430 } 431 pkScript, err := txscript.PayToAddrScript(addr) 432 if err != nil { 433 return err 434 } 435 changeOutput := &wire.TxOut{ 436 Value: int64(changeVal), 437 PkScript: pkScript, 438 } 439 tx.AddTxOut(changeOutput) 440 } 441 442 return nil 443 } 444 445 // If we've reached this point, then coin selection failed due to an 446 // insufficient amount of coins. 447 return fmt.Errorf("not enough funds for coin selection") 448 } 449 450 // SendOutputs creates, then sends a transaction paying to the specified output 451 // while observing the passed fee rate. The passed fee rate should be expressed 452 // in satoshis-per-byte. 453 func (m *memWallet) SendOutputs(outputs []*wire.TxOut, 454 feeRate btcutil.Amount) (*chainhash.Hash, error) { 455 456 tx, err := m.CreateTransaction(outputs, feeRate, true) 457 if err != nil { 458 return nil, err 459 } 460 461 return m.rpc.SendRawTransaction(tx, true) 462 } 463 464 // SendOutputsWithoutChange creates and sends a transaction that pays to the 465 // specified outputs while observing the passed fee rate and ignoring a change 466 // output. The passed fee rate should be expressed in sat/b. 467 func (m *memWallet) SendOutputsWithoutChange(outputs []*wire.TxOut, 468 feeRate btcutil.Amount) (*chainhash.Hash, error) { 469 470 tx, err := m.CreateTransaction(outputs, feeRate, false) 471 if err != nil { 472 return nil, err 473 } 474 475 return m.rpc.SendRawTransaction(tx, true) 476 } 477 478 // CreateTransaction returns a fully signed transaction paying to the specified 479 // outputs while observing the desired fee rate. The passed fee rate should be 480 // expressed in satoshis-per-byte. The transaction being created can optionally 481 // include a change output indicated by the change boolean. 482 // 483 // This function is safe for concurrent access. 484 func (m *memWallet) CreateTransaction(outputs []*wire.TxOut, 485 feeRate btcutil.Amount, change bool) (*wire.MsgTx, error) { 486 487 m.Lock() 488 defer m.Unlock() 489 490 tx := wire.NewMsgTx(wire.TxVersion) 491 492 // Tally up the total amount to be sent in order to perform coin 493 // selection shortly below. 494 var outputAmt btcutil.Amount 495 for _, output := range outputs { 496 outputAmt += btcutil.Amount(output.Value) 497 tx.AddTxOut(output) 498 } 499 500 // Attempt to fund the transaction with spendable utxos. 501 if err := m.fundTx(tx, outputAmt, feeRate, change); err != nil { 502 return nil, err 503 } 504 505 // Populate all the selected inputs with valid sigScript for spending. 506 // Along the way record all outputs being spent in order to avoid a 507 // potential double spend. 508 spentOutputs := make([]*utxo, 0, len(tx.TxIn)) 509 for i, txIn := range tx.TxIn { 510 outPoint := txIn.PreviousOutPoint 511 utxo := m.utxos[outPoint] 512 513 extendedKey, err := m.hdRoot.Derive(utxo.keyIndex) 514 if err != nil { 515 return nil, err 516 } 517 518 privKeyOld, err := extendedKey.ECPrivKey() 519 if err != nil { 520 return nil, err 521 } 522 523 privKey, _ := btcec.PrivKeyFromBytes(privKeyOld.Serialize()) 524 525 sigScript, err := txscript.SignatureScript(tx, i, utxo.pkScript, 526 txscript.SigHashAll, privKey, true) 527 if err != nil { 528 return nil, err 529 } 530 531 txIn.SignatureScript = sigScript 532 533 spentOutputs = append(spentOutputs, utxo) 534 } 535 536 // As these outputs are now being spent by this newly created 537 // transaction, mark the outputs are "locked". This action ensures 538 // these outputs won't be double spent by any subsequent transactions. 539 // These locked outputs can be freed via a call to UnlockOutputs. 540 for _, utxo := range spentOutputs { 541 utxo.isLocked = true 542 } 543 544 return tx, nil 545 } 546 547 // UnlockOutputs unlocks any outputs which were previously locked due to 548 // being selected to fund a transaction via the CreateTransaction method. 549 // 550 // This function is safe for concurrent access. 551 func (m *memWallet) UnlockOutputs(inputs []*wire.TxIn) { 552 m.Lock() 553 defer m.Unlock() 554 555 for _, input := range inputs { 556 utxo, ok := m.utxos[input.PreviousOutPoint] 557 if !ok { 558 continue 559 } 560 561 utxo.isLocked = false 562 } 563 } 564 565 // ConfirmedBalance returns the confirmed balance of the wallet. 566 // 567 // This function is safe for concurrent access. 568 func (m *memWallet) ConfirmedBalance() btcutil.Amount { 569 m.RLock() 570 defer m.RUnlock() 571 572 var balance btcutil.Amount 573 for _, utxo := range m.utxos { 574 // Prevent any immature or locked outputs from contributing to 575 // the wallet's total confirmed balance. 576 if !utxo.isMature(m.currentHeight) || utxo.isLocked { 577 continue 578 } 579 580 balance += utxo.value 581 } 582 583 return balance 584 } 585 586 // keyToAddr maps the passed private to corresponding p2pkh address. 587 func keyToAddr(key *btcec.PrivateKey, net *chaincfg.Params) (btcutil.Address, error) { 588 serializedKey := key.PubKey().SerializeCompressed() 589 pubKeyAddr, err := btcutil.NewAddressPubKey(serializedKey, net) 590 if err != nil { 591 return nil, err 592 } 593 return pubKeyAddr.AddressPubKeyHash(), nil 594 }