1 package wtxmgr
2 3 import (
4 "fmt"
5 "github.com/p9c/p9/pkg/amt"
6 7 "github.com/p9c/p9/pkg/chainhash"
8 "github.com/p9c/p9/pkg/walletdb"
9 )
10 11 // CreditRecord contains metadata regarding a transaction credit for a known transaction. Further details may be looked
12 // up by indexing a wire.MsgTx.TxOut with the Index field.
13 type CreditRecord struct {
14 Amount amt.Amount
15 Index uint32
16 Spent bool
17 Change bool
18 }
19 20 // DebitRecord contains metadata regarding a transaction debit for a known transaction. Further details may be looked up
21 // by indexing a wire.MsgTx.TxIn with the Index field.
22 type DebitRecord struct {
23 Amount amt.Amount
24 Index uint32
25 }
26 27 // TxDetails is intended to provide callers with access to rich details regarding a relevant transaction and which
28 // inputs and outputs are credit or debits.
29 type TxDetails struct {
30 TxRecord
31 Block BlockMeta
32 Credits []CreditRecord
33 Debits []DebitRecord
34 }
35 36 // minedTxDetails fetches the TxDetails for the mined transaction with hash txHash and the passed tx record key and
37 // value.
38 func (s *Store) minedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, recKey, recVal []byte) (
39 *TxDetails,
40 error,
41 ) {
42 var details TxDetails
43 // Parse transaction record k/v, lookup the full block record for the block time, and read all matching credits,
44 // debits.
45 e := readRawTxRecord(txHash, recVal, &details.TxRecord)
46 if e != nil {
47 return nil, e
48 }
49 e = readRawTxRecordBlock(recKey, &details.Block.Block)
50 if e != nil {
51 return nil, e
52 }
53 details.Block.Time, e = fetchBlockTime(ns, details.Block.Height)
54 if e != nil {
55 return nil, e
56 }
57 credIter := makeReadCreditIterator(ns, recKey)
58 for credIter.next() {
59 if int(credIter.elem.Index) >= len(details.MsgTx.TxOut) {
60 str := "saved credit index exceeds number of outputs"
61 return nil, storeError(ErrData, str, nil)
62 }
63 // The credit iterator does not record whether this credit was spent by an unmined transaction, so check that
64 // here.
65 if !credIter.elem.Spent {
66 k := canonicalOutPoint(txHash, credIter.elem.Index)
67 spent := existsRawUnminedInput(ns, k) != nil
68 credIter.elem.Spent = spent
69 }
70 details.Credits = append(details.Credits, credIter.elem)
71 }
72 if credIter.err != nil {
73 return nil, credIter.err
74 }
75 debIter := makeReadDebitIterator(ns, recKey)
76 for debIter.next() {
77 if int(debIter.elem.Index) >= len(details.MsgTx.TxIn) {
78 str := "saved debit index exceeds number of inputs"
79 return nil, storeError(ErrData, str, nil)
80 }
81 details.Debits = append(details.Debits, debIter.elem)
82 }
83 return &details, debIter.err
84 }
85 86 // unminedTxDetails fetches the TxDetails for the unmined transaction with the hash txHash and the passed unmined record
87 // value.
88 func (s *Store) unminedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, v []byte) (*TxDetails, error) {
89 details := TxDetails{
90 Block: BlockMeta{Block: Block{Height: -1}},
91 }
92 e := readRawTxRecord(txHash, v, &details.TxRecord)
93 if e != nil {
94 return nil, e
95 }
96 it := makeReadUnminedCreditIterator(ns, txHash)
97 for it.next() {
98 if int(it.elem.Index) >= len(details.MsgTx.TxOut) {
99 str := "saved credit index exceeds number of outputs"
100 return nil, storeError(ErrData, str, nil)
101 }
102 // Set the Spent field since this is not done by the iterator.
103 it.elem.Spent = existsRawUnminedInput(ns, it.ck) != nil
104 details.Credits = append(details.Credits, it.elem)
105 }
106 if it.err != nil {
107 return nil, it.err
108 }
109 // Debit records are not saved for unmined transactions. Instead, they must be looked up for each transaction input
110 // manually. There are two kinds of previous credits that may be debited by an unmined transaction: mined unspent
111 // outputs (which remain marked unspent even when spent by an unmined transaction), and credits from other unmined
112 // transactions. Both situations must be considered.
113 for i, output := range details.MsgTx.TxIn {
114 opKey := canonicalOutPoint(
115 &output.PreviousOutPoint.Hash,
116 output.PreviousOutPoint.Index,
117 )
118 credKey := existsRawUnspent(ns, opKey)
119 if credKey != nil {
120 v := existsRawCredit(ns, credKey)
121 amount, e := fetchRawCreditAmount(v)
122 if e != nil {
123 return nil, e
124 }
125 details.Debits = append(
126 details.Debits, DebitRecord{
127 Amount: amount,
128 Index: uint32(i),
129 },
130 )
131 continue
132 }
133 v := existsRawUnminedCredit(ns, opKey)
134 if v == nil {
135 continue
136 }
137 amount, e := fetchRawCreditAmount(v)
138 if e != nil {
139 return nil, e
140 }
141 details.Debits = append(
142 details.Debits, DebitRecord{
143 Amount: amount,
144 Index: uint32(i),
145 },
146 )
147 }
148 return &details, nil
149 }
150 151 // TxDetails looks up all recorded details regarding a transaction with some hash. In case of a hash collision, the most
152 // recent transaction with a matching hash is returned.
153 //
154 // Not finding a transaction with this hash is not an error. In this case, a nil TxDetails is returned.
155 func (s *Store) TxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash) (*TxDetails, error) {
156 // First, check whether there exists an unmined transaction with this hash. Use it if found.
157 v := existsRawUnmined(ns, txHash[:])
158 if v != nil {
159 return s.unminedTxDetails(ns, txHash, v)
160 }
161 // Otherwise, if there exists a mined transaction with this matching hash, skip over to the newest and begin
162 // fetching all details.
163 k, v := latestTxRecord(ns, txHash)
164 if v == nil {
165 // not found
166 return nil, nil
167 }
168 return s.minedTxDetails(ns, txHash, k, v)
169 }
170 171 // UniqueTxDetails looks up all recorded details for a transaction recorded mined in some particular block, or an
172 // unmined transaction if block is nil.
173 //
174 // Not finding a transaction with this hash from this block is not an error. In this case, a nil TxDetails is returned.
175 func (s *Store) UniqueTxDetails(
176 ns walletdb.ReadBucket, txHash *chainhash.Hash,
177 block *Block,
178 ) (*TxDetails, error) {
179 if block == nil {
180 v := existsRawUnmined(ns, txHash[:])
181 if v == nil {
182 return nil, nil
183 }
184 return s.unminedTxDetails(ns, txHash, v)
185 }
186 k, v := existsTxRecord(ns, txHash, block)
187 if v == nil {
188 return nil, nil
189 }
190 return s.minedTxDetails(ns, txHash, k, v)
191 }
192 193 // rangeUnminedTransactions executes the function f with TxDetails for every unmined transaction. f is not executed if
194 // no unmined transactions exist. DBError returns from f (if any) are propigated to the caller. Returns true (signaling
195 // breaking out of a RangeTransactions) iff f executes and returns true.
196 func (s *Store) rangeUnminedTransactions(
197 ns walletdb.ReadBucket,
198 f func([]TxDetails) (bool, error),
199 ) (bool, error) {
200 T.Ln("rangeUnminedTransactions")
201 var details []TxDetails
202 e := ns.NestedReadBucket(bucketUnmined).ForEach(
203 func(k, v []byte) (e error) {
204 // D.Ln("k", k, "v", v)
205 if len(k) < 32 {
206 str := fmt.Sprintf("%s: short key (expected %d bytes, read %d)", bucketUnmined, 32, len(k))
207 return storeError(ErrData, str, nil)
208 }
209 var txHash chainhash.Hash
210 copy(txHash[:], k)
211 detail, e := s.unminedTxDetails(ns, &txHash, v)
212 if e != nil {
213 return e
214 }
215 // Because the key was created while foreach-ing over the bucket, it should be impossible for unminedTxDetails
216 // to ever successfully return a nil details struct.
217 details = append(details, *detail)
218 return nil
219 },
220 )
221 if e == nil && len(details) > 0 {
222 return f(details)
223 }
224 return false, e
225 }
226 227 // rangeBlockTransactions executes the function f with TxDetails for every block between heights begin and end (reverse
228 // order when end > begin) until f returns true, or the transactions from block is processed. Returns true iff f
229 // executes and returns true.
230 func (s *Store) rangeBlockTransactions(
231 ns walletdb.ReadBucket, begin, end int32,
232 f func([]TxDetails) (bool, error),
233 ) (bool, error) {
234 T.Ln("rangeBlockTransactions", begin, end)
235 // Mempool height is considered a high bound.
236 if begin < 0 {
237 begin = int32(^uint32(0) >> 1)
238 }
239 if end < 0 {
240 end = int32(^uint32(0) >> 1)
241 }
242 T.Ln("begin", begin, "end", end)
243 var blockIter blockIterator
244 var advance func(*blockIterator) bool
245 if begin < end {
246 // Iterate in forwards order
247 blockIter = makeReadBlockIterator(ns, begin)
248 advance = func(it *blockIterator) bool {
249 if !it.next() {
250 D.Ln("end of blocks")
251 return false
252 }
253 return it.elem.Height <= end
254 }
255 } else {
256 // Iterate in backwards order, from begin -> end.
257 blockIter = makeReadBlockIterator(ns, begin)
258 advance = func(it *blockIterator) bool {
259 if !it.prev() {
260 return false
261 }
262 return end <= it.elem.Height
263 }
264 }
265 var details []TxDetails
266 for advance(&blockIter) {
267 block := &blockIter.elem
268 if cap(details) < len(block.transactions) {
269 details = make([]TxDetails, 0, len(block.transactions))
270 } else {
271 details = details[:0]
272 }
273 for _, txHash := range block.transactions {
274 k := keyTxRecord(&txHash, &block.Block)
275 v := existsRawTxRecord(ns, k)
276 if v == nil {
277 // T.F("missing transaction %v for block %v", txHash, block.Height)
278 // str := fmt.Sprintf("missing transaction %v for block %v", txHash, block.Height)
279 // return false, storeError(ErrData, str, nil)
280 // deleteTxRecord(ns, )
281 } else {
282 detail := TxDetails{
283 Block: BlockMeta{
284 Block: block.Block,
285 Time: block.Time,
286 },
287 }
288 e := readRawTxRecord(&txHash, v, &detail.TxRecord)
289 if e != nil {
290 return false, e
291 }
292 credIter := makeReadCreditIterator(ns, k)
293 for credIter.next() {
294 if int(credIter.elem.Index) >= len(detail.MsgTx.TxOut) {
295 str := "saved credit index exceeds number of outputs"
296 return false, storeError(ErrData, str, nil)
297 }
298 // The credit iterator does not record whether this credit was spent by an unmined transaction, so check
299 // that here.
300 if !credIter.elem.Spent {
301 k = canonicalOutPoint(&txHash, credIter.elem.Index)
302 spent := existsRawUnminedInput(ns, k) != nil
303 credIter.elem.Spent = spent
304 }
305 detail.Credits = append(detail.Credits, credIter.elem)
306 }
307 if credIter.err != nil {
308 return false, credIter.err
309 }
310 debIter := makeReadDebitIterator(ns, k)
311 for debIter.next() {
312 if int(debIter.elem.Index) >= len(detail.MsgTx.TxIn) {
313 str := "saved debit index exceeds number of inputs"
314 return false, storeError(ErrData, str, nil)
315 }
316 detail.Debits = append(detail.Debits, debIter.elem)
317 }
318 if debIter.err != nil {
319 return false, debIter.err
320 }
321 details = append(details, detail)
322 }
323 }
324 // Every block record must have at least one transaction, so it
325 // is safe to call f.
326 brk, e := f(details)
327 if e != nil || brk {
328 return brk, e
329 }
330 }
331 return false, blockIter.err
332 }
333 334 // RangeTransactions runs the function f on all transaction details between blocks on the best chain over the height
335 // range [begin,end]. The special height -1 may be used to also include unmined transactions. If the end height comes
336 // before the begin height, blocks are iterated in reverse order and unmined transactions (if any) are processed first.
337 //
338 // The function f may return an error which, if non-nil, is propagated to the caller. Additionally, a boolean return
339 // value allows exiting the function early without reading any additional transactions early when true.
340 //
341 // All calls to f are guaranteed to be passed a slice with more than zero elements. The slice may be reused for multiple
342 // blocks, so it is not safe to use it after the loop iteration it was acquired.
343 func (s *Store) RangeTransactions(
344 ns walletdb.ReadBucket, begin, end int32,
345 f func([]TxDetails) (bool, error),
346 ) error {
347 T.Ln("RangeTransactions")
348 var addedUnmined, brk bool
349 var e error
350 if begin < 0 {
351 brk, e = s.rangeUnminedTransactions(ns, f)
352 if e != nil || brk {
353 return e
354 }
355 addedUnmined = true
356 }
357 if brk, e = s.rangeBlockTransactions(ns, begin, end, f); E.Chk(e) {
358 }
359 if e == nil && !brk && !addedUnmined && end < 0 {
360 _, e = s.rangeUnminedTransactions(ns, f)
361 }
362 return e
363 }
364 365 // PreviousPkScripts returns a slice of previous output scripts for each credit output this transaction record debits
366 // from.
367 func (s *Store) PreviousPkScripts(ns walletdb.ReadBucket, rec *TxRecord, block *Block) ([][]byte, error) {
368 var pkScripts [][]byte
369 if block == nil {
370 for _, input := range rec.MsgTx.TxIn {
371 prevOut := &input.PreviousOutPoint
372 // Input may spend a previous unmined output, a mined output (which would still be marked unspent), or
373 // neither.
374 v := existsRawUnmined(ns, prevOut.Hash[:])
375 if v != nil {
376 // Ensure a credit exists for this unmined transaction before including the output script.
377 k := canonicalOutPoint(&prevOut.Hash, prevOut.Index)
378 if existsRawUnminedCredit(ns, k) == nil {
379 continue
380 }
381 pkScript, e := fetchRawTxRecordPkScript(
382 prevOut.Hash[:], v, prevOut.Index,
383 )
384 if e != nil {
385 return nil, e
386 }
387 pkScripts = append(pkScripts, pkScript)
388 continue
389 }
390 _, credKey := existsUnspent(ns, prevOut)
391 if credKey != nil {
392 k := extractRawCreditTxRecordKey(credKey)
393 v = existsRawTxRecord(ns, k)
394 pkScript, e := fetchRawTxRecordPkScript(
395 k, v,
396 prevOut.Index,
397 )
398 if e != nil {
399 return nil, e
400 }
401 pkScripts = append(pkScripts, pkScript)
402 continue
403 }
404 }
405 return pkScripts, nil
406 }
407 recKey := keyTxRecord(&rec.Hash, block)
408 it := makeReadDebitIterator(ns, recKey)
409 for it.next() {
410 credKey := extractRawDebitCreditKey(it.cv)
411 index := extractRawCreditIndex(credKey)
412 k := extractRawCreditTxRecordKey(credKey)
413 v := existsRawTxRecord(ns, k)
414 pkScript, e := fetchRawTxRecordPkScript(k, v, index)
415 if e != nil {
416 return nil, e
417 }
418 pkScripts = append(pkScripts, pkScript)
419 }
420 if it.err != nil {
421 return nil, it.err
422 }
423 return pkScripts, nil
424 }
425