Add GetUtxo. Mostly works.

This commit is contained in:
Alex 2017-05-04 22:37:37 -06:00 committed by Olaoluwa Osuntokun
parent ddc841924b
commit b549587296
2 changed files with 184 additions and 0 deletions

View file

@ -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()
}
}
}

View file

@ -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