github.com/lbryio/lbcd@v0.22.119/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/lbryio/lbcd/blockchain" 14 "github.com/lbryio/lbcd/btcec" 15 "github.com/lbryio/lbcd/chaincfg" 16 "github.com/lbryio/lbcd/chaincfg/chainhash" 17 "github.com/lbryio/lbcd/rpcclient" 18 "github.com/lbryio/lbcd/txscript" 19 "github.com/lbryio/lbcd/wire" 20 btcutil "github.com/lbryio/lbcutil" 21 "github.com/lbryio/lbcutil/hdkeychain" 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 coinbaseAddr, err := keyToAddr(coinbaseKey, net) 137 if err != nil { 138 return nil, err 139 } 140 141 // Track the coinbase generation address to ensure we properly track 142 // newly generated bitcoin we can spend. 143 addrs := make(map[uint32]btcutil.Address) 144 addrs[0] = coinbaseAddr 145 146 return &memWallet{ 147 net: net, 148 coinbaseKey: coinbaseKey, 149 coinbaseAddr: coinbaseAddr, 150 hdIndex: 1, 151 hdRoot: hdRoot, 152 addrs: addrs, 153 utxos: make(map[wire.OutPoint]*utxo), 154 chainUpdateSignal: make(chan struct{}), 155 reorgJournal: make(map[int32]*undoEntry), 156 }, nil 157 } 158 159 // Start launches all goroutines required for the wallet to function properly. 160 func (m *memWallet) Start() { 161 go m.chainSyncer() 162 } 163 164 // SyncedHeight returns the height the wallet is known to be synced to. 165 // 166 // This function is safe for concurrent access. 167 func (m *memWallet) SyncedHeight() int32 { 168 m.RLock() 169 defer m.RUnlock() 170 return m.currentHeight 171 } 172 173 // SetRPCClient saves the passed rpc connection to btcd as the wallet's 174 // personal rpc connection. 175 func (m *memWallet) SetRPCClient(rpcClient *rpcclient.Client) { 176 m.rpc = rpcClient 177 } 178 179 // IngestBlock is a call-back which is to be triggered each time a new block is 180 // connected to the main chain. It queues the update for the chain syncer, 181 // calling the private version in sequential order. 182 func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) { 183 // Append this new chain update to the end of the queue of new chain 184 // updates. 185 m.chainMtx.Lock() 186 m.chainUpdates = append(m.chainUpdates, &chainUpdate{height, 187 filteredTxns, true}) 188 m.chainMtx.Unlock() 189 190 // Launch a goroutine to signal the chainSyncer that a new update is 191 // available. We do this in a new goroutine in order to avoid blocking 192 // the main loop of the rpc client. 193 go func() { 194 m.chainUpdateSignal <- struct{}{} 195 }() 196 } 197 198 // ingestBlock updates the wallet's internal utxo state based on the outputs 199 // created and destroyed within each block. 200 func (m *memWallet) ingestBlock(update *chainUpdate) { 201 // Update the latest synced height, then process each filtered 202 // transaction in the block creating and destroying utxos within 203 // the wallet as a result. 204 m.currentHeight = update.blockHeight 205 undo := &undoEntry{ 206 utxosDestroyed: make(map[wire.OutPoint]*utxo), 207 } 208 for _, tx := range update.filteredTxns { 209 mtx := tx.MsgTx() 210 isCoinbase := blockchain.IsCoinBaseTx(mtx) 211 txHash := mtx.TxHash() 212 m.evalOutputs(mtx.TxOut, &txHash, isCoinbase, undo) 213 m.evalInputs(mtx.TxIn, undo) 214 } 215 216 // Finally, record the undo entry for this block so we can 217 // properly update our internal state in response to the block 218 // being re-org'd from the main chain. 219 m.reorgJournal[update.blockHeight] = undo 220 } 221 222 // chainSyncer is a goroutine dedicated to processing new blocks in order to 223 // keep the wallet's utxo state up to date. 224 // 225 // NOTE: This MUST be run as a goroutine. 226 func (m *memWallet) chainSyncer() { 227 var update *chainUpdate 228 229 for range m.chainUpdateSignal { 230 // A new update is available, so pop the new chain update from 231 // the front of the update queue. 232 m.chainMtx.Lock() 233 update = m.chainUpdates[0] 234 m.chainUpdates[0] = nil // Set to nil to prevent GC leak. 235 m.chainUpdates = m.chainUpdates[1:] 236 m.chainMtx.Unlock() 237 238 m.Lock() 239 if update.isConnect { 240 m.ingestBlock(update) 241 } else { 242 m.unwindBlock(update) 243 } 244 m.Unlock() 245 } 246 } 247 248 // evalOutputs evaluates each of the passed outputs, creating a new matching 249 // utxo within the wallet if we're able to spend the output. 250 func (m *memWallet) evalOutputs(outputs []*wire.TxOut, txHash *chainhash.Hash, 251 isCoinbase bool, undo *undoEntry) { 252 253 for i, output := range outputs { 254 pkScript := output.PkScript 255 256 // Scan all the addresses we currently control to see if the 257 // output is paying to us. 258 for keyIndex, addr := range m.addrs { 259 pkHash := addr.ScriptAddress() 260 if !bytes.Contains(pkScript, pkHash) { 261 continue 262 } 263 264 // If this is a coinbase output, then we mark the 265 // maturity height at the proper block height in the 266 // future. 267 var maturityHeight int32 268 if isCoinbase { 269 maturityHeight = m.currentHeight + int32(m.net.CoinbaseMaturity) 270 } 271 272 op := wire.OutPoint{Hash: *txHash, Index: uint32(i)} 273 m.utxos[op] = &utxo{ 274 value: btcutil.Amount(output.Value), 275 keyIndex: keyIndex, 276 maturityHeight: maturityHeight, 277 pkScript: pkScript, 278 } 279 undo.utxosCreated = append(undo.utxosCreated, op) 280 } 281 } 282 } 283 284 // evalInputs scans all the passed inputs, destroying any utxos within the 285 // wallet which are spent by an input. 286 func (m *memWallet) evalInputs(inputs []*wire.TxIn, undo *undoEntry) { 287 for _, txIn := range inputs { 288 op := txIn.PreviousOutPoint 289 oldUtxo, ok := m.utxos[op] 290 if !ok { 291 continue 292 } 293 294 undo.utxosDestroyed[op] = oldUtxo 295 delete(m.utxos, op) 296 } 297 } 298 299 // UnwindBlock is a call-back which is to be executed each time a block is 300 // disconnected from the main chain. It queues the update for the chain syncer, 301 // calling the private version in sequential order. 302 func (m *memWallet) UnwindBlock(height int32, header *wire.BlockHeader) { 303 // Append this new chain update to the end of the queue of new chain 304 // updates. 305 m.chainMtx.Lock() 306 m.chainUpdates = append(m.chainUpdates, &chainUpdate{height, 307 nil, false}) 308 m.chainMtx.Unlock() 309 310 // Launch a goroutine to signal the chainSyncer that a new update is 311 // available. We do this in a new goroutine in order to avoid blocking 312 // the main loop of the rpc client. 313 go func() { 314 m.chainUpdateSignal <- struct{}{} 315 }() 316 } 317 318 // unwindBlock undoes the effect that a particular block had on the wallet's 319 // internal utxo state. 320 func (m *memWallet) unwindBlock(update *chainUpdate) { 321 undo := m.reorgJournal[update.blockHeight] 322 323 for _, utxo := range undo.utxosCreated { 324 delete(m.utxos, utxo) 325 } 326 327 for outPoint, utxo := range undo.utxosDestroyed { 328 m.utxos[outPoint] = utxo 329 } 330 331 delete(m.reorgJournal, update.blockHeight) 332 } 333 334 // newAddress returns a new address from the wallet's hd key chain. It also 335 // loads the address into the RPC client's transaction filter to ensure any 336 // transactions that involve it are delivered via the notifications. 337 func (m *memWallet) newAddress() (btcutil.Address, error) { 338 index := m.hdIndex 339 340 childKey, err := m.hdRoot.Derive(index) 341 if err != nil { 342 return nil, err 343 } 344 privKey, err := childKey.ECPrivKey() 345 if err != nil { 346 return nil, err 347 } 348 349 addr, err := keyToAddr(privKey, m.net) 350 if err != nil { 351 return nil, err 352 } 353 354 err = m.rpc.LoadTxFilter(false, []btcutil.Address{addr}, nil) 355 if err != nil { 356 return nil, err 357 } 358 359 m.addrs[index] = addr 360 361 m.hdIndex++ 362 363 return addr, nil 364 } 365 366 // NewAddress returns a fresh address spendable by the wallet. 367 // 368 // This function is safe for concurrent access. 369 func (m *memWallet) NewAddress() (btcutil.Address, error) { 370 m.Lock() 371 defer m.Unlock() 372 373 return m.newAddress() 374 } 375 376 // fundTx attempts to fund a transaction sending amt bitcoin. The coins are 377 // selected such that the final amount spent pays enough fees as dictated by the 378 // passed fee rate. The passed fee rate should be expressed in 379 // satoshis-per-byte. The transaction being funded can optionally include a 380 // change output indicated by the change boolean. 381 // 382 // NOTE: The memWallet's mutex must be held when this function is called. 383 func (m *memWallet) fundTx(tx *wire.MsgTx, amt btcutil.Amount, 384 feeRate btcutil.Amount, change bool) error { 385 386 const ( 387 // spendSize is the largest number of bytes of a sigScript 388 // which spends a p2pkh output: OP_DATA_73 <sig> OP_DATA_33 <pubkey> 389 spendSize = 1 + 73 + 1 + 33 390 ) 391 392 var ( 393 amtSelected btcutil.Amount 394 txSize int 395 ) 396 397 for outPoint, utxo := range m.utxos { 398 // Skip any outputs that are still currently immature or are 399 // currently locked. 400 if !utxo.isMature(m.currentHeight) || utxo.isLocked { 401 continue 402 } 403 404 amtSelected += utxo.value 405 406 // Add the selected output to the transaction, updating the 407 // current tx size while accounting for the size of the future 408 // sigScript. 409 tx.AddTxIn(wire.NewTxIn(&outPoint, nil, nil)) 410 txSize = tx.SerializeSize() + spendSize*len(tx.TxIn) 411 412 // Calculate the fee required for the txn at this point 413 // observing the specified fee rate. If we don't have enough 414 // coins from he current amount selected to pay the fee, then 415 // continue to grab more coins. 416 reqFee := btcutil.Amount(txSize * int(feeRate)) 417 if amtSelected-reqFee < amt { 418 continue 419 } 420 421 // If we have any change left over and we should create a change 422 // output, then add an additional output to the transaction 423 // reserved for it. 424 changeVal := amtSelected - amt - reqFee 425 if changeVal > 0 && change { 426 addr, err := m.newAddress() 427 if err != nil { 428 return err 429 } 430 pkScript, err := txscript.PayToAddrScript(addr) 431 if err != nil { 432 return err 433 } 434 changeOutput := &wire.TxOut{ 435 Value: int64(changeVal), 436 PkScript: pkScript, 437 } 438 tx.AddTxOut(changeOutput) 439 } 440 441 return nil 442 } 443 444 // If we've reached this point, then coin selection failed due to an 445 // insufficient amount of coins. 446 return fmt.Errorf("not enough funds for coin selection") 447 } 448 449 // SendOutputs creates, then sends a transaction paying to the specified output 450 // while observing the passed fee rate. The passed fee rate should be expressed 451 // in satoshis-per-byte. 452 func (m *memWallet) SendOutputs(outputs []*wire.TxOut, 453 feeRate btcutil.Amount) (*chainhash.Hash, error) { 454 455 tx, err := m.CreateTransaction(outputs, feeRate, true) 456 if err != nil { 457 return nil, err 458 } 459 460 return m.rpc.SendRawTransaction(tx, true) 461 } 462 463 // SendOutputsWithoutChange creates and sends a transaction that pays to the 464 // specified outputs while observing the passed fee rate and ignoring a change 465 // output. The passed fee rate should be expressed in sat/b. 466 func (m *memWallet) SendOutputsWithoutChange(outputs []*wire.TxOut, 467 feeRate btcutil.Amount) (*chainhash.Hash, error) { 468 469 tx, err := m.CreateTransaction(outputs, feeRate, false) 470 if err != nil { 471 return nil, err 472 } 473 474 return m.rpc.SendRawTransaction(tx, true) 475 } 476 477 // CreateTransaction returns a fully signed transaction paying to the specified 478 // outputs while observing the desired fee rate. The passed fee rate should be 479 // expressed in satoshis-per-byte. The transaction being created can optionally 480 // include a change output indicated by the change boolean. 481 // 482 // This function is safe for concurrent access. 483 func (m *memWallet) CreateTransaction(outputs []*wire.TxOut, 484 feeRate btcutil.Amount, change bool) (*wire.MsgTx, error) { 485 486 m.Lock() 487 defer m.Unlock() 488 489 tx := wire.NewMsgTx(wire.TxVersion) 490 491 // Tally up the total amount to be sent in order to perform coin 492 // selection shortly below. 493 var outputAmt btcutil.Amount 494 for _, output := range outputs { 495 outputAmt += btcutil.Amount(output.Value) 496 tx.AddTxOut(output) 497 } 498 499 // Attempt to fund the transaction with spendable utxos. 500 if err := m.fundTx(tx, outputAmt, feeRate, change); err != nil { 501 return nil, err 502 } 503 504 // Populate all the selected inputs with valid sigScript for spending. 505 // Along the way record all outputs being spent in order to avoid a 506 // potential double spend. 507 spentOutputs := make([]*utxo, 0, len(tx.TxIn)) 508 for i, txIn := range tx.TxIn { 509 outPoint := txIn.PreviousOutPoint 510 utxo := m.utxos[outPoint] 511 512 extendedKey, err := m.hdRoot.Derive(utxo.keyIndex) 513 if err != nil { 514 return nil, err 515 } 516 517 privKey, err := extendedKey.ECPrivKey() 518 if err != nil { 519 return nil, err 520 } 521 522 sigScript, err := txscript.SignatureScript(tx, i, utxo.pkScript, 523 txscript.SigHashAll, privKey, true) 524 if err != nil { 525 return nil, err 526 } 527 528 txIn.SignatureScript = sigScript 529 530 spentOutputs = append(spentOutputs, utxo) 531 } 532 533 // As these outputs are now being spent by this newly created 534 // transaction, mark the outputs are "locked". This action ensures 535 // these outputs won't be double spent by any subsequent transactions. 536 // These locked outputs can be freed via a call to UnlockOutputs. 537 for _, utxo := range spentOutputs { 538 utxo.isLocked = true 539 } 540 541 return tx, nil 542 } 543 544 // UnlockOutputs unlocks any outputs which were previously locked due to 545 // being selected to fund a transaction via the CreateTransaction method. 546 // 547 // This function is safe for concurrent access. 548 func (m *memWallet) UnlockOutputs(inputs []*wire.TxIn) { 549 m.Lock() 550 defer m.Unlock() 551 552 for _, input := range inputs { 553 utxo, ok := m.utxos[input.PreviousOutPoint] 554 if !ok { 555 continue 556 } 557 558 utxo.isLocked = false 559 } 560 } 561 562 // ConfirmedBalance returns the confirmed balance of the wallet. 563 // 564 // This function is safe for concurrent access. 565 func (m *memWallet) ConfirmedBalance() btcutil.Amount { 566 m.RLock() 567 defer m.RUnlock() 568 569 var balance btcutil.Amount 570 for _, utxo := range m.utxos { 571 // Prevent any immature or locked outputs from contributing to 572 // the wallet's total confirmed balance. 573 if !utxo.isMature(m.currentHeight) || utxo.isLocked { 574 continue 575 } 576 577 balance += utxo.value 578 } 579 580 return balance 581 } 582 583 // keyToAddr maps the passed private to corresponding p2pkh address. 584 func keyToAddr(key *btcec.PrivateKey, net *chaincfg.Params) (btcutil.Address, error) { 585 serializedKey := key.PubKey().SerializeCompressed() 586 pubKeyAddr, err := btcutil.NewAddressPubKey(serializedKey, net) 587 if err != nil { 588 return nil, err 589 } 590 return pubKeyAddr.AddressPubKeyHash(), nil 591 }