github.com/nebulouslabs/sia@v1.3.7/modules/transactionpool/update.go (about) 1 package transactionpool 2 3 import ( 4 "bytes" 5 "fmt" 6 "sort" 7 8 "github.com/NebulousLabs/Sia/crypto" 9 "github.com/NebulousLabs/Sia/modules" 10 "github.com/NebulousLabs/Sia/types" 11 ) 12 13 // findSets takes a bunch of transactions (presumably from a block) and finds 14 // all of the separate transaction sets within it. Set does not check for 15 // conflicts. 16 // 17 // The algorithm goes through one transaction at a time. All of the outputs of 18 // that transaction are added to the objMap, pointing to the transaction to 19 // indicate that the transaction contains those outputs. The transaction is 20 // assigned an integer id (each transaction will have a unique id) and added to 21 // the txMap. 22 // 23 // The transaction's inputs are then checked against the objMap to see if there 24 // are any parents of the transaction in the graph. If there are, the 25 // transaction is added to the parent set instead of its own set. If not, the 26 // transaction is added as its own set. 27 // 28 // The forwards map contains a list of ints indicating when a transaction has 29 // been merged with a set. When a transaction gets merged with a parent set, its 30 // integer id gets added to the forwards map, indicating that the transaction is 31 // no longer in its own set, but instead has been merged with other sets. 32 // 33 // Some transactions will have parents from multiple distinct sets. If a 34 // transaction has parents in multiple distinct sets, those sets get merged 35 // together and the transaction gets added to the result. One of the sets is 36 // nominated (arbitrarily) as the official set, and the integer id of the other 37 // set and the new transaction get forwarded to the official set. 38 // 39 // TODO: Set merging currently occurs any time that there is a child. But 40 // really, it should only occur if the child increases the average fee value of 41 // the set that it is merging with (which it will if and only if it has a higher 42 // average fee than that set). If the child has multiple parent sets, it should 43 // be compared with the parent set that has the lowest fee value. Then, after it 44 // is merged with that parent, the result should be merged with the next 45 // lowest-fee parent set if and only if the new set has a higher average fee 46 // than the parent set. And this continues until either all of the sets have 47 // been merged, or until the remaining parent sets have higher values. 48 func findSets(ts []types.Transaction) [][]types.Transaction { 49 // txMap marks what set each transaction is in. If two sets get combined, 50 // this number will not be updated. The 'forwards' map defined further on 51 // will help to discover which sets have been combined. 52 txMap := make(map[types.TransactionID]int) 53 setMap := make(map[int][]types.Transaction) 54 objMap := make(map[ObjectID]types.TransactionID) 55 forwards := make(map[int]int) 56 57 // Define a function to follow and collapse any update chain. 58 forward := func(prev int) (ret int) { 59 ret = prev 60 next, exists := forwards[prev] 61 for exists { 62 ret = next 63 forwards[prev] = next // collapse the forwards function to prevent quadratic runtime of findSets. 64 next, exists = forwards[next] 65 } 66 return ret 67 } 68 69 // Add the transactions to the setup one-by-one, merging them as they belong 70 // to a set. 71 for i, t := range ts { 72 // Check if the inputs depend on any previous transaction outputs. 73 tid := t.ID() 74 parentSets := make(map[int]struct{}) 75 for _, obj := range t.SiacoinInputs { 76 txid, exists := objMap[ObjectID(obj.ParentID)] 77 if exists { 78 parentSet := forward(txMap[txid]) 79 parentSets[parentSet] = struct{}{} 80 } 81 } 82 for _, obj := range t.FileContractRevisions { 83 txid, exists := objMap[ObjectID(obj.ParentID)] 84 if exists { 85 parentSet := forward(txMap[txid]) 86 parentSets[parentSet] = struct{}{} 87 } 88 } 89 for _, obj := range t.StorageProofs { 90 txid, exists := objMap[ObjectID(obj.ParentID)] 91 if exists { 92 parentSet := forward(txMap[txid]) 93 parentSets[parentSet] = struct{}{} 94 } 95 } 96 for _, obj := range t.SiafundInputs { 97 txid, exists := objMap[ObjectID(obj.ParentID)] 98 if exists { 99 parentSet := forward(txMap[txid]) 100 parentSets[parentSet] = struct{}{} 101 } 102 } 103 104 // Determine the new counter for this transaction. 105 if len(parentSets) == 0 { 106 // No parent sets. Make a new set for this transaction. 107 txMap[tid] = i 108 setMap[i] = []types.Transaction{t} 109 // Don't need to add anything for the file contract outputs, storage 110 // proof outputs, siafund claim outputs; these outputs are not 111 // allowed to be spent until 50 confirmations. 112 } else { 113 // There are parent sets, pick one as the base and then merge the 114 // rest into it. 115 parentsSlice := make([]int, 0, len(parentSets)) 116 for j := range parentSets { 117 parentsSlice = append(parentsSlice, j) 118 } 119 base := parentsSlice[0] 120 txMap[tid] = base 121 for _, j := range parentsSlice[1:] { 122 // Forward any future transactions pointing at this set to the 123 // base set. 124 forwards[j] = base 125 // Combine the transactions in this set with the transactions in 126 // the base set. 127 setMap[base] = append(setMap[base], setMap[j]...) 128 // Delete this set map, it has been merged with the base set. 129 delete(setMap, j) 130 } 131 // Add this transaction to the base set. 132 setMap[base] = append(setMap[base], t) 133 } 134 135 // Mark this transaction's outputs as potential inputs to future 136 // transactions. 137 for j := range t.SiacoinOutputs { 138 scoid := t.SiacoinOutputID(uint64(j)) 139 objMap[ObjectID(scoid)] = tid 140 } 141 for j := range t.FileContracts { 142 fcid := t.FileContractID(uint64(j)) 143 objMap[ObjectID(fcid)] = tid 144 } 145 for j := range t.FileContractRevisions { 146 fcid := t.FileContractRevisions[j].ParentID 147 objMap[ObjectID(fcid)] = tid 148 } 149 for j := range t.SiafundOutputs { 150 sfoid := t.SiafundOutputID(uint64(j)) 151 objMap[ObjectID(sfoid)] = tid 152 } 153 } 154 155 // Compile the final group of sets. 156 ret := make([][]types.Transaction, 0, len(setMap)) 157 for _, set := range setMap { 158 ret = append(ret, set) 159 } 160 return ret 161 } 162 163 // purge removes all transactions from the transaction pool. 164 func (tp *TransactionPool) purge() { 165 tp.knownObjects = make(map[ObjectID]TransactionSetID) 166 tp.transactionSets = make(map[TransactionSetID][]types.Transaction) 167 tp.transactionSetDiffs = make(map[TransactionSetID]*modules.ConsensusChange) 168 tp.transactionListSize = 0 169 } 170 171 // ProcessConsensusChange gets called to inform the transaction pool of changes 172 // to the consensus set. 173 func (tp *TransactionPool) ProcessConsensusChange(cc modules.ConsensusChange) { 174 tp.mu.Lock() 175 176 tp.log.Printf("CCID %v (height %v): %v applied blocks, %v reverted blocks", crypto.Hash(cc.ID).String()[:8], tp.blockHeight, len(cc.AppliedBlocks), len(cc.RevertedBlocks)) 177 178 // Get the recent block ID for a sanity check that the consensus change is 179 // being provided to us correctly. 180 resetSanityCheck := false 181 recentID, err := tp.getRecentBlockID(tp.dbTx) 182 if err == errNilRecentBlock { 183 // This almost certainly means that the database hasn't been initialized 184 // yet with a recent block, meaning the user was previously running 185 // v1.3.1 or earlier. 186 tp.log.Println("NOTE: Upgrading tpool database to support consensus change verification.") 187 resetSanityCheck = true 188 } else if err != nil { 189 tp.log.Critical("ERROR: Could not access recentID from tpool:", err) 190 } 191 192 // Update the database of confirmed transactions. 193 for _, block := range cc.RevertedBlocks { 194 // Sanity check - the id of each reverted block should match the recent 195 // parent id. 196 if block.ID() != recentID && !resetSanityCheck { 197 panic(fmt.Sprintf("Consensus change series appears to be inconsistent - we are reverting the wrong block. bid: %v recent: %v", block.ID(), recentID)) 198 } 199 recentID = block.ParentID 200 201 if tp.blockHeight > 0 || block.ID() != types.GenesisID { 202 tp.blockHeight-- 203 } 204 for _, txn := range block.Transactions { 205 err := tp.deleteTransaction(tp.dbTx, txn.ID()) 206 if err != nil { 207 tp.log.Println("ERROR: could not delete a transaction:", err) 208 } 209 } 210 211 // Pull the transactions out of the fee summary. For estimating only 212 // over 10 blocks, it is extremely likely that there will be more 213 // applied blocks than reverted blocks, and if there aren't (a height 214 // decreasing reorg), there will be more than 10 applied blocks. 215 if len(tp.recentMedians) > 0 { 216 // Strip out all of the transactions in this block. 217 tp.recentMedians = tp.recentMedians[:len(tp.recentMedians)-1] 218 } 219 } 220 for _, block := range cc.AppliedBlocks { 221 // Sanity check - the parent id of each block should match the current 222 // block id. 223 if block.ParentID != recentID && !resetSanityCheck { 224 panic(fmt.Sprintf("Consensus change series appears to be inconsistent - we are applying the wrong block. pid: %v recent: %v", block.ParentID, recentID)) 225 } 226 recentID = block.ID() 227 228 if tp.blockHeight > 0 || block.ID() != types.GenesisID { 229 tp.blockHeight++ 230 } 231 for _, txn := range block.Transactions { 232 err := tp.putTransaction(tp.dbTx, txn.ID()) 233 if err != nil { 234 tp.log.Println("ERROR: could not add a transaction:", err) 235 } 236 } 237 238 // Find the median transaction fee for this block. 239 type feeSummary struct { 240 fee types.Currency 241 size int 242 } 243 var fees []feeSummary 244 var totalSize int 245 txnSets := findSets(block.Transactions) 246 for _, set := range txnSets { 247 // Compile the fees for this set. 248 var feeSum types.Currency 249 var sizeSum int 250 b := new(bytes.Buffer) 251 for _, txn := range set { 252 txn.MarshalSia(b) 253 sizeSum += b.Len() 254 b.Reset() 255 for _, fee := range txn.MinerFees { 256 feeSum = feeSum.Add(fee) 257 } 258 } 259 feeAvg := feeSum.Div64(uint64(sizeSum)) 260 fees = append(fees, feeSummary{ 261 fee: feeAvg, 262 size: sizeSum, 263 }) 264 totalSize += sizeSum 265 } 266 // Add an extra zero-fee tranasction for any unused block space. 267 remaining := int(types.BlockSizeLimit) - totalSize 268 fees = append(fees, feeSummary{ 269 fee: types.ZeroCurrency, 270 size: remaining, // fine if remaining is zero. 271 }) 272 // Sort the fees by value and then scroll until the median. 273 sort.Slice(fees, func(i, j int) bool { 274 return fees[i].fee.Cmp(fees[j].fee) < 0 275 }) 276 var progress int 277 for i := range fees { 278 progress += fees[i].size 279 // Instead of grabbing the full median, look at the 75%-ile. It's 280 // going to be cheaper than the 50%-ile, but it still got into a 281 // block. 282 if uint64(progress) > types.BlockSizeLimit/4 { 283 tp.recentMedians = append(tp.recentMedians, fees[i].fee) 284 break 285 } 286 } 287 288 // If there are more than 10 blocks recorded in the txnsPerBlock, strip 289 // off the oldest blocks. 290 for len(tp.recentMedians) > blockFeeEstimationDepth { 291 tp.recentMedians = tp.recentMedians[1:] 292 } 293 } 294 // Grab the median of the recent medians. Copy to a new slice so the sorting 295 // doesn't screw up the slice. 296 safeMedians := make([]types.Currency, len(tp.recentMedians)) 297 copy(safeMedians, tp.recentMedians) 298 sort.Slice(safeMedians, func(i, j int) bool { 299 return safeMedians[i].Cmp(safeMedians[j]) < 0 300 }) 301 tp.recentMedianFee = safeMedians[len(safeMedians)/2] 302 303 // Update all the on-disk structures. 304 err = tp.putRecentConsensusChange(tp.dbTx, cc.ID) 305 if err != nil { 306 tp.log.Println("ERROR: could not update the recent consensus change:", err) 307 } 308 err = tp.putRecentBlockID(tp.dbTx, recentID) 309 if err != nil { 310 tp.log.Println("ERROR: could not store recent block id:", err) 311 } 312 err = tp.putBlockHeight(tp.dbTx, tp.blockHeight) 313 if err != nil { 314 tp.log.Println("ERROR: could not update the block height:", err) 315 } 316 err = tp.putFeeMedian(tp.dbTx, medianPersist{ 317 RecentMedians: tp.recentMedians, 318 RecentMedianFee: tp.recentMedianFee, 319 }) 320 if err != nil { 321 tp.log.Println("ERROR: could not update the transaction pool median fee information:", err) 322 } 323 324 // Scan the applied blocks for transactions that got accepted. This will 325 // help to determine which transactions to remove from the transaction 326 // pool. Having this list enables both efficiency improvements and helps to 327 // clean out transactions with no dependencies, such as arbitrary data 328 // transactions from the host. 329 txids := make(map[types.TransactionID]struct{}) 330 for _, block := range cc.AppliedBlocks { 331 for _, txn := range block.Transactions { 332 txids[txn.ID()] = struct{}{} 333 } 334 } 335 336 // Save all of the current unconfirmed transaction sets into a list. 337 var unconfirmedSets [][]types.Transaction 338 for _, tSet := range tp.transactionSets { 339 // Compile a new transaction set the removes all transactions duplicated 340 // in the block. Though mostly handled by the dependency manager in the 341 // transaction pool, this should both improve efficiency and will strip 342 // out duplicate transactions with no dependencies (arbitrary data only 343 // transactions) 344 var newTSet []types.Transaction 345 for _, txn := range tSet { 346 _, exists := txids[txn.ID()] 347 if !exists { 348 newTSet = append(newTSet, txn) 349 } 350 } 351 unconfirmedSets = append(unconfirmedSets, newTSet) 352 } 353 354 // Purge the transaction pool. Some of the transactions sets may be invalid 355 // after the consensus change. 356 tp.purge() 357 358 // prune transactions older than maxTxnAge. 359 for i, tSet := range unconfirmedSets { 360 var validTxns []types.Transaction 361 for _, txn := range tSet { 362 seenHeight, seen := tp.transactionHeights[txn.ID()] 363 if tp.blockHeight-seenHeight <= maxTxnAge || !seen { 364 validTxns = append(validTxns, txn) 365 } else { 366 delete(tp.transactionHeights, txn.ID()) 367 } 368 } 369 unconfirmedSets[i] = validTxns 370 } 371 372 // Scan through the reverted blocks and re-add any transactions that got 373 // reverted to the tpool. 374 for i := len(cc.RevertedBlocks) - 1; i >= 0; i-- { 375 block := cc.RevertedBlocks[i] 376 for _, txn := range block.Transactions { 377 // Check whether this transaction has already be re-added to the 378 // consensus set by the applied blocks. 379 _, exists := txids[txn.ID()] 380 if exists { 381 continue 382 } 383 384 // Try adding the transaction back into the transaction pool. 385 tp.acceptTransactionSet([]types.Transaction{txn}, cc.TryTransactionSet) // Error is ignored. 386 } 387 } 388 389 // Add all of the unconfirmed transaction sets back to the transaction 390 // pool. The ones that are invalid will throw an error and will not be 391 // re-added. 392 // 393 // Accepting a transaction set requires locking the consensus set (to check 394 // validity). But, ProcessConsensusChange is only called when the consensus 395 // set is already locked, causing a deadlock problem. Therefore, 396 // transactions are readded to the pool in a goroutine, so that this 397 // function can finish and consensus can unlock. The tpool lock is held 398 // however until the goroutine completes. 399 // 400 // Which means that no other modules can require a tpool lock when 401 // processing consensus changes. Overall, the locking is pretty fragile and 402 // more rules need to be put in place. 403 for _, set := range unconfirmedSets { 404 for _, txn := range set { 405 err := tp.acceptTransactionSet([]types.Transaction{txn}, cc.TryTransactionSet) 406 if err != nil { 407 // The transaction is no longer valid, delete it from the 408 // heights map to prevent a memory leak. 409 delete(tp.transactionHeights, txn.ID()) 410 } 411 } 412 } 413 414 // Inform subscribers that an update has executed. 415 tp.mu.Demote() 416 tp.updateSubscribersTransactions() 417 tp.mu.DemotedUnlock() 418 } 419 420 // PurgeTransactionPool deletes all transactions from the transaction pool. 421 func (tp *TransactionPool) PurgeTransactionPool() { 422 tp.mu.Lock() 423 tp.purge() 424 tp.mu.Unlock() 425 }