github.com/lbryio/lbcd@v0.22.119/claimtrie/node/node.go (about)

     1  package node
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"sort"
     7  
     8  	"github.com/lbryio/lbcd/claimtrie/change"
     9  	"github.com/lbryio/lbcd/claimtrie/param"
    10  )
    11  
    12  type Node struct {
    13  	BestClaim   *Claim    // The claim that has most effective amount at the current height.
    14  	TakenOverAt int32     // The height at when the current BestClaim took over.
    15  	Claims      ClaimList // List of all Claims.
    16  	Supports    ClaimList // List of all Supports, including orphaned ones.
    17  	SupportSums map[string]int64
    18  }
    19  
    20  // New returns a new node.
    21  func New() *Node {
    22  	return &Node{SupportSums: map[string]int64{}}
    23  }
    24  
    25  func (n *Node) HasActiveBestClaim() bool {
    26  	return n.BestClaim != nil && n.BestClaim.Status == Activated
    27  }
    28  
    29  func (n *Node) ApplyChange(chg change.Change, delay int32) error {
    30  
    31  	visibleAt := chg.VisibleHeight
    32  	if visibleAt <= 0 {
    33  		visibleAt = chg.Height
    34  	}
    35  
    36  	switch chg.Type {
    37  	case change.AddClaim:
    38  		c := &Claim{
    39  			OutPoint: chg.OutPoint,
    40  			Amount:   chg.Amount,
    41  			ClaimID:  chg.ClaimID,
    42  			// CreatedAt:  chg.Height,
    43  			AcceptedAt: chg.Height,
    44  			ActiveAt:   chg.Height + delay,
    45  			VisibleAt:  visibleAt,
    46  			Sequence:   int32(len(n.Claims)),
    47  		}
    48  		// old := n.Claims.find(byOut(chg.OutPoint)) // TODO: remove this after proving ResetHeight works
    49  		// if old != nil {
    50  		// return errors.Errorf("CONFLICT WITH EXISTING TXO! Name: %s, Height: %d", chg.Name, chg.Height)
    51  		// }
    52  		n.Claims = append(n.Claims, c)
    53  
    54  	case change.SpendClaim:
    55  		c := n.Claims.find(byOut(chg.OutPoint))
    56  		if c != nil {
    57  			c.setStatus(Deactivated)
    58  		} else {
    59  			LogOnce(fmt.Sprintf("Spending claim but missing existing claim with TXO %s, "+
    60  				"Name: %s, ID: %s", chg.OutPoint, chg.Name, chg.ClaimID))
    61  		}
    62  		// apparently it's legit to be absent in the map:
    63  		// 'two' at 481100, 36a719a156a1df178531f3c712b8b37f8e7cc3b36eea532df961229d936272a1:0
    64  
    65  	case change.UpdateClaim:
    66  		// Find and remove the claim, which has just been spent.
    67  		c := n.Claims.find(byID(chg.ClaimID))
    68  		if c != nil && c.Status == Deactivated {
    69  
    70  			// Keep its ID, which was generated from the spent claim.
    71  			// And update the rest of properties.
    72  			c.setOutPoint(chg.OutPoint).SetAmt(chg.Amount)
    73  			c.setStatus(Accepted) // it was Deactivated in the spend (but we only activate at the end of the block)
    74  			// that's because the old code would put all insertions into the "queue" that was processed at block's end
    75  
    76  			// This forces us to be newer, which may in an unintentional takeover if there's an older one.
    77  			// TODO: reconsider these updates in future hard forks.
    78  			c.setAccepted(chg.Height)
    79  			c.setActiveAt(chg.Height + delay)
    80  
    81  		} else {
    82  			LogOnce(fmt.Sprintf("Updating claim but missing existing claim with ID %s", chg.ClaimID))
    83  		}
    84  	case change.AddSupport:
    85  		n.Supports = append(n.Supports, &Claim{
    86  			OutPoint:   chg.OutPoint,
    87  			Amount:     chg.Amount,
    88  			ClaimID:    chg.ClaimID,
    89  			AcceptedAt: chg.Height,
    90  			ActiveAt:   chg.Height + delay,
    91  			VisibleAt:  visibleAt,
    92  		})
    93  
    94  	case change.SpendSupport:
    95  		s := n.Supports.find(byOut(chg.OutPoint))
    96  		if s != nil {
    97  			if s.Status == Activated {
    98  				n.SupportSums[s.ClaimID.Key()] -= s.Amount
    99  			}
   100  			// TODO: we could do without this Deactivated flag if we set expiration instead
   101  			// That would eliminate the above Sum update.
   102  			// We would also need to track the update situation, though, but that could be done locally.
   103  			s.setStatus(Deactivated)
   104  		} else {
   105  			LogOnce(fmt.Sprintf("Spending support but missing existing claim with TXO %s, "+
   106  				"Name: %s, ID: %s", chg.OutPoint, chg.Name, chg.ClaimID))
   107  		}
   108  	}
   109  	return nil
   110  }
   111  
   112  // AdjustTo activates claims and computes takeovers until it reaches the specified height.
   113  func (n *Node) AdjustTo(height, maxHeight int32, name []byte) {
   114  	changed := n.handleExpiredAndActivated(height) > 0
   115  	n.updateTakeoverHeight(height, name, changed)
   116  	if maxHeight > height {
   117  		for h := n.NextUpdate(); h <= maxHeight; h = n.NextUpdate() {
   118  			changed = n.handleExpiredAndActivated(h) > 0
   119  			n.updateTakeoverHeight(h, name, changed)
   120  			height = h
   121  		}
   122  	}
   123  }
   124  
   125  func (n *Node) updateTakeoverHeight(height int32, name []byte, refindBest bool) {
   126  
   127  	candidate := n.BestClaim
   128  	if refindBest {
   129  		candidate = n.findBestClaim() // so expensive...
   130  	}
   131  
   132  	hasCandidate := candidate != nil
   133  	hasCurrentWinner := n.HasActiveBestClaim()
   134  
   135  	takeoverHappening := !hasCandidate || !hasCurrentWinner || candidate.ClaimID != n.BestClaim.ClaimID
   136  
   137  	if takeoverHappening {
   138  		if n.activateAllClaims(height) > 0 {
   139  			candidate = n.findBestClaim()
   140  		}
   141  	}
   142  
   143  	if !takeoverHappening && height < param.ActiveParams.MaxRemovalWorkaroundHeight {
   144  		// This is a super ugly hack to work around bug in old code.
   145  		// The bug: un/support a name then update it. This will cause its takeover height to be reset to current.
   146  		// This is because the old code would add to the cache without setting block originals when dealing in supports.
   147  		_, takeoverHappening = param.TakeoverWorkarounds[fmt.Sprintf("%d_%s", height, name)] // TODO: ditch the fmt call
   148  	}
   149  
   150  	if takeoverHappening {
   151  		n.TakenOverAt = height
   152  		n.BestClaim = candidate
   153  	}
   154  }
   155  
   156  func (n *Node) handleExpiredAndActivated(height int32) int {
   157  
   158  	ot := param.ActiveParams.OriginalClaimExpirationTime
   159  	et := param.ActiveParams.ExtendedClaimExpirationTime
   160  	fk := param.ActiveParams.ExtendedClaimExpirationForkHeight
   161  	expiresAt := func(c *Claim) int32 {
   162  		if c.AcceptedAt+ot > fk {
   163  			return c.AcceptedAt + et
   164  		}
   165  		return c.AcceptedAt + ot
   166  	}
   167  
   168  	changes := 0
   169  	update := func(items ClaimList, sums map[string]int64) ClaimList {
   170  		for i := 0; i < len(items); i++ {
   171  			c := items[i]
   172  			if c.Status == Accepted && c.ActiveAt <= height && c.VisibleAt <= height {
   173  				c.setStatus(Activated)
   174  				changes++
   175  				if sums != nil {
   176  					sums[c.ClaimID.Key()] += c.Amount
   177  				}
   178  			}
   179  			if c.Status == Deactivated || expiresAt(c) <= height {
   180  				if i < len(items)-1 {
   181  					items[i] = items[len(items)-1]
   182  					i--
   183  				}
   184  				items = items[:len(items)-1]
   185  				changes++
   186  				if sums != nil && c.Status != Deactivated {
   187  					sums[c.ClaimID.Key()] -= c.Amount
   188  				}
   189  			}
   190  		}
   191  		return items
   192  	}
   193  	n.Claims = update(n.Claims, nil)
   194  	n.Supports = update(n.Supports, n.SupportSums)
   195  	return changes
   196  }
   197  
   198  // NextUpdate returns the nearest height in the future that the node should
   199  // be refreshed due to changes of claims or supports.
   200  func (n Node) NextUpdate() int32 {
   201  
   202  	ot := param.ActiveParams.OriginalClaimExpirationTime
   203  	et := param.ActiveParams.ExtendedClaimExpirationTime
   204  	fk := param.ActiveParams.ExtendedClaimExpirationForkHeight
   205  	expiresAt := func(c *Claim) int32 {
   206  		if c.AcceptedAt+ot > fk {
   207  			return c.AcceptedAt + et
   208  		}
   209  		return c.AcceptedAt + ot
   210  	}
   211  
   212  	next := int32(math.MaxInt32)
   213  
   214  	for _, c := range n.Claims {
   215  		ea := expiresAt(c)
   216  		if ea < next {
   217  			next = ea
   218  		}
   219  		// if we're not active, we need to go to activeAt unless we're still invisible there
   220  		if c.Status == Accepted {
   221  			min := c.ActiveAt
   222  			if c.VisibleAt > min {
   223  				min = c.VisibleAt
   224  			}
   225  			if min < next {
   226  				next = min
   227  			}
   228  		}
   229  	}
   230  
   231  	for _, s := range n.Supports {
   232  		es := expiresAt(s)
   233  		if es < next {
   234  			next = es
   235  		}
   236  		if s.Status == Accepted {
   237  			min := s.ActiveAt
   238  			if s.VisibleAt > min {
   239  				min = s.VisibleAt
   240  			}
   241  			if min < next {
   242  				next = min
   243  			}
   244  		}
   245  	}
   246  
   247  	return next
   248  }
   249  
   250  func (n Node) findBestClaim() *Claim {
   251  
   252  	// WARNING: this method is called billions of times.
   253  	// if we just had some easy way to know that our best claim was the first one in the list...
   254  	// or it may be faster to cache effective amount in the db at some point.
   255  
   256  	var best *Claim
   257  	var bestAmount int64
   258  	for _, candidate := range n.Claims {
   259  
   260  		// not using switch here for performance reasons
   261  		if candidate.Status != Activated {
   262  			continue
   263  		}
   264  
   265  		if best == nil {
   266  			best = candidate
   267  			continue
   268  		}
   269  
   270  		candidateAmount := candidate.Amount + n.SupportSums[candidate.ClaimID.Key()]
   271  		if bestAmount <= 0 {
   272  			bestAmount = best.Amount + n.SupportSums[best.ClaimID.Key()]
   273  		}
   274  
   275  		switch {
   276  		case candidateAmount > bestAmount:
   277  			best = candidate
   278  			bestAmount = candidateAmount
   279  		case candidateAmount < bestAmount:
   280  			continue
   281  		case candidate.AcceptedAt < best.AcceptedAt:
   282  			best = candidate
   283  			bestAmount = candidateAmount
   284  		case candidate.AcceptedAt > best.AcceptedAt:
   285  			continue
   286  		case OutPointLess(candidate.OutPoint, best.OutPoint):
   287  			best = candidate
   288  			bestAmount = candidateAmount
   289  		}
   290  	}
   291  
   292  	return best
   293  }
   294  
   295  func (n *Node) activateAllClaims(height int32) int {
   296  	count := 0
   297  	for _, c := range n.Claims {
   298  		if c.Status == Accepted && c.ActiveAt > height && c.VisibleAt <= height {
   299  			c.setActiveAt(height) // don't necessarily need to change this number?
   300  			c.setStatus(Activated)
   301  			count++
   302  		}
   303  	}
   304  
   305  	for _, s := range n.Supports {
   306  		if s.Status == Accepted && s.ActiveAt > height && s.VisibleAt <= height {
   307  			s.setActiveAt(height) // don't necessarily need to change this number?
   308  			s.setStatus(Activated)
   309  			count++
   310  			n.SupportSums[s.ClaimID.Key()] += s.Amount
   311  		}
   312  	}
   313  	return count
   314  }
   315  
   316  func (n *Node) SortClaimsByBid() {
   317  
   318  	// purposefully sorting by descent via func parameter order:
   319  	sort.Slice(n.Claims, func(j, i int) bool {
   320  		// SupportSums only include active values; do the same for amount. No active claim will have a zero amount
   321  		iAmount := n.SupportSums[n.Claims[i].ClaimID.Key()]
   322  		if n.Claims[i].Status == Activated {
   323  			iAmount += n.Claims[i].Amount
   324  		}
   325  		jAmount := n.SupportSums[n.Claims[j].ClaimID.Key()]
   326  		if n.Claims[j].Status == Activated {
   327  			jAmount += n.Claims[j].Amount
   328  		}
   329  		switch {
   330  		case iAmount < jAmount:
   331  			return true
   332  		case iAmount > jAmount:
   333  			return false
   334  		case n.Claims[i].AcceptedAt > n.Claims[j].AcceptedAt:
   335  			return true
   336  		case n.Claims[i].AcceptedAt < n.Claims[j].AcceptedAt:
   337  			return false
   338  		}
   339  		return OutPointLess(n.Claims[j].OutPoint, n.Claims[i].OutPoint)
   340  	})
   341  }
   342  
   343  func (n *Node) Clone() *Node {
   344  	clone := New()
   345  	if n.SupportSums != nil {
   346  		clone.SupportSums = map[string]int64{}
   347  		for key, value := range n.SupportSums {
   348  			clone.SupportSums[key] = value
   349  		}
   350  	}
   351  	clone.Supports = make(ClaimList, len(n.Supports))
   352  	for i, support := range n.Supports {
   353  		clone.Supports[i] = &Claim{}
   354  		*clone.Supports[i] = *support
   355  	}
   356  	clone.Claims = make(ClaimList, len(n.Claims))
   357  	for i, claim := range n.Claims {
   358  		clone.Claims[i] = &Claim{}
   359  		*clone.Claims[i] = *claim
   360  	}
   361  	clone.TakenOverAt = n.TakenOverAt
   362  	if n.BestClaim != nil {
   363  		clone.BestClaim = clone.Claims.find(byID(n.BestClaim.ClaimID))
   364  	}
   365  	return clone
   366  }