OSDN Git Service

clean the account utxo struct (#221)
[bytom/bytom.git] / blockchain / wallet / indexer.go
1 package wallet
2
3 import (
4         "encoding/json"
5         "fmt"
6
7         log "github.com/sirupsen/logrus"
8         "github.com/tendermint/tmlibs/db"
9
10         "github.com/bytom/blockchain/account"
11         "github.com/bytom/blockchain/query"
12         "github.com/bytom/crypto/sha3pool"
13         "github.com/bytom/errors"
14         "github.com/bytom/protocol/bc"
15         "github.com/bytom/protocol/bc/legacy"
16 )
17
18 type rawOutput struct {
19         OutputID bc.Hash
20         bc.AssetAmount
21         ControlProgram []byte
22         txHash         bc.Hash
23         outputIndex    uint32
24         sourceID       bc.Hash
25         sourcePos      uint64
26         refData        bc.Hash
27 }
28
29 type accountOutput struct {
30         rawOutput
31         AccountID string
32         Address   string
33         keyIndex  uint64
34         change    bool
35 }
36
37 const (
38         //TxPreFix is wallet database transactions prefix
39         TxPreFix = "TXS:"
40 )
41
42 func calcAnnotatedKey(blockHeight uint64, position uint32) []byte {
43         return []byte(fmt.Sprintf("%s%016x%08x", TxPreFix, blockHeight, position))
44 }
45
46 func calcDeletePreFix(blockHeight uint64) []byte {
47         return []byte(fmt.Sprintf("%s%016x", TxPreFix, blockHeight))
48 }
49
50 //deleteTransaction delete transactions when orphan block rollback
51 func (w *Wallet) deleteTransactions(batch db.Batch, height uint64, b *legacy.Block) {
52         txIter := w.DB.IteratorPrefix(calcDeletePreFix(height))
53         defer txIter.Release()
54
55         for txIter.Next() {
56                 batch.Delete(txIter.Key())
57         }
58 }
59
60 //ReverseAccountUTXOs process the invalid blocks when orphan block rollback
61 func (w *Wallet) reverseAccountUTXOs(batch db.Batch, b *legacy.Block) {
62         var err error
63
64         //unknow how many spent and retire outputs
65         reverseOuts := make([]*rawOutput, 0)
66
67         //handle spent UTXOs
68         for _, tx := range b.Transactions {
69                 for _, inpID := range tx.Tx.InputIDs {
70                         //spend and retire
71                         sp, err := tx.Spend(inpID)
72                         if err != nil {
73                                 continue
74                         }
75
76                         resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
77                         if !ok {
78                                 continue
79                         }
80
81                         out := &rawOutput{
82                                 OutputID:       *sp.SpentOutputId,
83                                 AssetAmount:    *resOut.Source.Value,
84                                 ControlProgram: resOut.ControlProgram.Code,
85                                 txHash:         tx.ID,
86                                 sourceID:       *resOut.Source.Ref,
87                                 sourcePos:      resOut.Source.Position,
88                                 refData:        *resOut.Data,
89                         }
90                         reverseOuts = append(reverseOuts, out)
91                 }
92         }
93
94         accOuts := loadAccountInfo(reverseOuts, w)
95         if err = upsertConfirmedAccountOutputs(accOuts, b, batch, w); err != nil {
96                 log.WithField("err", err).Error("reversing account spent and retire outputs")
97                 return
98         }
99
100         //handle new UTXOs
101         for _, tx := range b.Transactions {
102                 for j := range tx.Outputs {
103                         resOutID := tx.ResultIds[j]
104                         if _, ok := tx.Entries[*resOutID].(*bc.Output); !ok {
105                                 //retirement
106                                 continue
107                         }
108                         //delete new UTXOs
109                         batch.Delete(account.UTXOKey(*resOutID))
110                 }
111         }
112 }
113
114 //indexTransactions saves all annotated transactions to the database.
115 func (w *Wallet) indexTransactions(batch db.Batch, b *legacy.Block) error {
116         annotatedTxs := filterAccountTxs(b, w)
117         annotateTxsAsset(annotatedTxs, w.DB)
118         annotateTxsAccount(annotatedTxs, w.DB)
119
120         for pos, tx := range annotatedTxs {
121                 rawTx, err := json.Marshal(tx)
122                 if err != nil {
123                         return errors.Wrap(err, "inserting annotated_txs to db")
124                 }
125
126                 batch.Set(calcAnnotatedKey(b.Height, uint32(pos)), rawTx)
127         }
128         return nil
129 }
130
131 //buildAccountUTXOs process valid blocks to build account unspent outputs db
132 func (w *Wallet) buildAccountUTXOs(batch db.Batch, b *legacy.Block) {
133         var err error
134
135         //handle spent UTXOs
136         delOutputIDs := prevoutDBKeys(b.Transactions...)
137         for _, delOutputID := range delOutputIDs {
138                 batch.Delete(account.UTXOKey(delOutputID))
139         }
140
141         //handle new UTXOs
142         outs := make([]*rawOutput, 0, len(b.Transactions))
143         for _, tx := range b.Transactions {
144                 for j, out := range tx.Outputs {
145                         resOutID := tx.ResultIds[j]
146                         resOut, ok := tx.Entries[*resOutID].(*bc.Output)
147                         if !ok {
148                                 continue
149                         }
150                         out := &rawOutput{
151                                 OutputID:       *tx.OutputID(j),
152                                 AssetAmount:    out.AssetAmount,
153                                 ControlProgram: out.ControlProgram,
154                                 txHash:         tx.ID,
155                                 outputIndex:    uint32(j),
156                                 sourceID:       *resOut.Source.Ref,
157                                 sourcePos:      resOut.Source.Position,
158                                 refData:        *resOut.Data,
159                         }
160                         outs = append(outs, out)
161                 }
162         }
163         accOuts := loadAccountInfo(outs, w)
164
165         if err = upsertConfirmedAccountOutputs(accOuts, b, batch, w); err != nil {
166                 log.WithField("err", err).Error("building new account outputs")
167                 return
168         }
169 }
170
171 func prevoutDBKeys(txs ...*legacy.Tx) (outputIDs []bc.Hash) {
172         for _, tx := range txs {
173                 for _, inpID := range tx.Tx.InputIDs {
174                         if sp, err := tx.Spend(inpID); err == nil {
175                                 outputIDs = append(outputIDs, *sp.SpentOutputId)
176                         }
177                 }
178         }
179         return
180 }
181
182 // loadAccountInfo turns a set of output IDs into a set of
183 // outputs by adding account annotations.  Outputs that can't be
184 // annotated are excluded from the result.
185 func loadAccountInfo(outs []*rawOutput, w *Wallet) []*accountOutput {
186         outsByScript := make(map[string][]*rawOutput, len(outs))
187         for _, out := range outs {
188                 scriptStr := string(out.ControlProgram)
189                 outsByScript[scriptStr] = append(outsByScript[scriptStr], out)
190         }
191
192         result := make([]*accountOutput, 0, len(outs))
193         cp := account.CtrlProgram{}
194
195         var hash [32]byte
196         for s := range outsByScript {
197                 sha3pool.Sum256(hash[:], []byte(s))
198                 bytes := w.DB.Get(account.CPKey(hash))
199                 if bytes == nil {
200                         continue
201                 }
202
203                 err := json.Unmarshal(bytes, &cp)
204                 if err != nil {
205                         continue
206                 }
207
208                 isExist := w.DB.Get(account.Key(cp.AccountID))
209                 if isExist == nil {
210                         continue
211                 }
212
213                 for _, out := range outsByScript[s] {
214                         newOut := &accountOutput{
215                                 rawOutput: *out,
216                                 AccountID: cp.AccountID,
217                                 Address:   cp.Address,
218                                 keyIndex:  cp.KeyIndex,
219                                 change:    cp.Change,
220                         }
221                         result = append(result, newOut)
222                 }
223         }
224
225         return result
226 }
227
228 // upsertConfirmedAccountOutputs records the account data for confirmed utxos.
229 // If the account utxo already exists (because it's from a local tx), the
230 // block confirmation data will in the row will be updated.
231 func upsertConfirmedAccountOutputs(outs []*accountOutput, block *legacy.Block, batch db.Batch, w *Wallet) error {
232         u := &account.UTXO{}
233         for _, out := range outs {
234                 u = &account.UTXO{
235                         OutputID:            out.OutputID,
236                         SourceID:            out.sourceID,
237                         AssetID:             *out.AssetId,
238                         Amount:              out.Amount,
239                         SourcePos:           out.sourcePos,
240                         ControlProgram:      out.ControlProgram,
241                         RefDataHash:         out.refData,
242                         ControlProgramIndex: out.keyIndex,
243                         AccountID:           out.AccountID,
244                         Address:             out.Address,
245                 }
246
247                 data, err := json.Marshal(u)
248                 if err != nil {
249                         return errors.Wrap(err, "failed marshal accountutxo")
250                 }
251                 batch.Set(account.UTXOKey(out.OutputID), data)
252         }
253         return nil
254 }
255
256 // filt related and build the fully annotated transactions.
257 func filterAccountTxs(b *legacy.Block, w *Wallet) []*query.AnnotatedTx {
258         annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
259         for pos, tx := range b.Transactions {
260                 local := false
261                 for _, v := range tx.Outputs {
262                         var hash [32]byte
263
264                         sha3pool.Sum256(hash[:], v.ControlProgram)
265                         if bytes := w.DB.Get(account.CPKey(hash)); bytes != nil {
266                                 annotatedTxs = append(annotatedTxs, buildAnnotatedTransaction(tx, b, uint32(pos)))
267                                 local = true
268                                 break
269                         }
270                 }
271
272                 if local == true {
273                         continue
274                 }
275
276                 for _, v := range tx.Inputs {
277                         outid, err := v.SpentOutputID()
278                         if err != nil {
279                                 continue
280                         }
281                         if bytes := w.DB.Get(account.UTXOKey(outid)); bytes != nil {
282                                 annotatedTxs = append(annotatedTxs, buildAnnotatedTransaction(tx, b, uint32(pos)))
283                                 break
284                         }
285                 }
286         }
287
288         return annotatedTxs
289 }