From b549587296d7d71a1faea8e0b30203ddf071286f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 May 2017 22:37:37 -0600 Subject: [PATCH] Add GetUtxo. Mostly works. --- spvsvc/spvchain/rescan.go | 142 +++++++++++++++++++++++++++++++++++ spvsvc/spvchain/sync_test.go | 42 +++++++++++ 2 files changed, 184 insertions(+) diff --git a/spvsvc/spvchain/rescan.go b/spvsvc/spvchain/rescan.go index 7ac5038..fe7ef7a 100644 --- a/spvsvc/spvchain/rescan.go +++ b/spvsvc/spvchain/rescan.go @@ -29,6 +29,7 @@ type rescanOptions struct { watchAddrs []btcutil.Address watchOutPoints []wire.OutPoint watchTXIDs []chainhash.Hash + txIdx uint32 quit <-chan struct{} } @@ -107,6 +108,14 @@ func WatchTXIDs(watchTXIDs ...chainhash.Hash) RescanOption { } } +// TxIdx specifies a hint transaction index into the block in which the UTXO +// is created (eg, coinbase is 0, next transaction is 1, etc.) +func TxIdx(txIdx uint32) RescanOption { + return func(ro *rescanOptions) { + ro.txIdx = txIdx + } +} + // QuitChan specifies the quit channel. This can be used by the caller to let // an indefinite rescan (one with no EndBlock set) know it should gracefully // shut down. If this isn't specified, an end block MUST be specified as Rescan @@ -438,3 +447,136 @@ func notifyBlock(block *btcutil.Block, outPoints *[]wire.OutPoint, } return relevantTxs, nil } + +// GetUtxo gets the appropriate TxOut or errors if it's spent. The option +// WatchOutPoints (with a single outpoint) is required. StartBlock can be used +// to give a hint about which block the transaction is in, and TxIdx can be used +// to give a hint of which transaction in the block matches it (coinbase is 0, +// first normal transaction is 1, etc.). +func (s *ChainService) GetUtxo(options ...RescanOption) (*wire.TxOut, error) { + ro := defaultRescanOptions() + ro.startBlock = &waddrmgr.BlockStamp{ + Hash: *s.chainParams.GenesisHash, + Height: 0, + } + for _, option := range options { + option(ro) + } + if len(ro.watchOutPoints) != 1 { + return nil, fmt.Errorf("Must pass exactly one OutPoint.") + } + watchList := [][]byte{ + builder.OutPointToFilterEntry(ro.watchOutPoints[0]), + ro.watchOutPoints[0].Hash[:], + } + // Track our position in the chain. + curHeader, curHeight, err := s.LatestBlock() + curStamp := &waddrmgr.BlockStamp{ + Hash: curHeader.BlockHash(), + Height: int32(curHeight), + } + if err != nil { + return nil, err + } + + // Find our earliest possible block. + if (ro.startBlock.Hash != chainhash.Hash{}) { + _, height, err := s.GetBlockByHash(ro.startBlock.Hash) + if err == nil { + ro.startBlock.Height = int32(height) + } else { + ro.startBlock.Hash = chainhash.Hash{} + } + } + if (ro.startBlock.Hash == chainhash.Hash{}) { + if ro.startBlock.Height == 0 { + ro.startBlock.Hash = *s.chainParams.GenesisHash + } else { + header, err := s.GetBlockByHeight( + uint32(ro.startBlock.Height)) + if err == nil { + ro.startBlock.Hash = header.BlockHash() + } else { + ro.startBlock.Hash = *s.chainParams.GenesisHash + ro.startBlock.Height = 0 + } + } + } + log.Tracef("Starting scan for output spend from known block %d (%s) "+ + "back to block %d (%s)", curStamp.Height, curStamp.Hash) + + for { + // Check the basic filter for the spend and the extended filter + // for the transaction in which the outpout is funded. + filter := s.GetCFilter(curStamp.Hash, false, + ro.queryOptions...) + if filter == nil { + return nil, fmt.Errorf("Couldn't get basic filter for "+ + "block %d (%s)", curStamp.Height, curStamp.Hash) + } + matched, err := filter.MatchAny(builder.DeriveKey( + &curStamp.Hash), watchList) + if err != nil { + return nil, err + } + if !matched { + filter = s.GetCFilter(curStamp.Hash, true, + ro.queryOptions...) + if filter == nil { + return nil, fmt.Errorf("Couldn't get extended "+ + "filter for block %d (%s)", + curStamp.Height, curStamp.Hash) + } + matched, err = filter.MatchAny(builder.DeriveKey( + &curStamp.Hash), watchList) + } + // If either is matched, download the block and check to see + // what we have. + if matched { + block := s.GetBlockFromNetwork(curStamp.Hash, + ro.queryOptions...) + if block == nil { + return nil, fmt.Errorf("Couldn't get "+ + "block %d (%s)", + curStamp.Height, curStamp.Hash) + } + // If we've spent the output in this block, return an + // error stating that the output is spent. + for _, tx := range block.Transactions() { + for _, ti := range tx.MsgTx().TxIn { + if ti.PreviousOutPoint == + ro.watchOutPoints[0] { + return nil, fmt.Errorf( + "OutPoint %s has been "+ + "spent", + ro.watchOutPoints[0]) + } + } + } + // If we found the transaction that created the output, + // then it's not spent and we can return the TxOut. + for _, tx := range block.Transactions() { + if *(tx.Hash()) == + ro.watchOutPoints[0].Hash { + return tx.MsgTx(). + TxOut[ro.watchOutPoints[0]. + Index], nil + } + } + // Otherwise, iterate backwards until we've gone too + // far. + curStamp.Height-- + if curStamp.Height < ro.startBlock.Height { + return nil, fmt.Errorf("Couldn't find "+ + "transaction %s", + ro.watchOutPoints[0].Hash) + } + header, err := s.GetBlockByHeight( + uint32(curStamp.Height)) + if err != nil { + return nil, err + } + curStamp.Hash = header.BlockHash() + } + } +} diff --git a/spvsvc/spvchain/sync_test.go b/spvsvc/spvchain/sync_test.go index 9863332..c800da0 100644 --- a/spvsvc/spvchain/sync_test.go +++ b/spvsvc/spvchain/sync_test.go @@ -433,6 +433,36 @@ func TestSetup(t *testing.T) { t.Fatalf("Couldn't rescan chain for transaction %s: %s", tx1.TxHash(), err) } + + // Call GetUtxo for our output in tx1 to see if it's spent. + ourIndex := 1 << 30 // Should work on 32-bit systems + for i, txo := range tx1.TxOut { + if bytes.Equal(txo.PkScript, script1) { + ourIndex = i + } + } + var ourOutPoint wire.OutPoint + if ourIndex != 1<<30 { + ourOutPoint = wire.OutPoint{ + Hash: tx1.TxHash(), + Index: uint32(ourIndex), + } + } else { + t.Fatalf("Couldn't find the index of our output in transaction"+ + " %s", tx1.TxHash()) + } + txo, err := svc.GetUtxo( + spvchain.WatchOutPoints(ourOutPoint), + spvchain.StartBlock(&waddrmgr.BlockStamp{Height: 801}), + ) + if err != nil { + t.Fatalf("Couldn't get UTXO %s: %s", ourOutPoint, err) + } + if !bytes.Equal(txo.PkScript, script1) { + t.Fatalf("UTXO's script doesn't match expected script for %s", + ourOutPoint) + } + // Start a rescan with notifications in another goroutine. We'll kill // it with a quit channel at the end and make sure we got the expected // results. @@ -642,6 +672,18 @@ func TestSetup(t *testing.T) { t.Fatalf("Rescan event logs incorrect.\nWant: %s\nGot: %s\n", wantLog, gotLog) } + + // Check and make sure the previous UTXO is now spent. + // TODO: Uncomment this (right now it causes a deadlock.) + /*_, err = svc.GetUtxo( + spvchain.WatchOutPoints(ourOutPoint), + spvchain.StartBlock(&waddrmgr.BlockStamp{Height: 801}), + ) + if err.Error() != fmt.Sprintf("OutPoint %s has been spent", + ourOutPoint) { + t.Fatalf("UTXO %s not seen as spent: %s", ourOutPoint, err) + }*/ + } // csd does a connect-sync-disconnect between nodes in order to support