From d3419ea77589fa0e4ea71b2ca3f76c76e1366021 Mon Sep 17 00:00:00 2001 From: Tzu-Jung Lee Date: Mon, 9 Jul 2018 11:34:12 -0700 Subject: [PATCH] cleanup: refactor, bugfix, and comment. --- claim/claim.go | 109 +++++++++----------- claimnode/node.go | 219 +++++++++++++++++++---------------------- claimnode/node_test.go | 119 +++++++++++++--------- claimnode/ui.go | 86 ++++++++++++++++ claimtrie.go | 215 ++++++++++++++++++++++++---------------- cmd/claimtrie/main.go | 28 ++++-- memento/memento.go | 14 ++- trie/stage.go | 7 +- 8 files changed, 470 insertions(+), 327 deletions(-) create mode 100644 claimnode/ui.go diff --git a/claim/claim.go b/claim/claim.go index a295b26..4b3345a 100644 --- a/claim/claim.go +++ b/claim/claim.go @@ -1,7 +1,6 @@ package claim import ( - "bytes" "encoding/json" "fmt" @@ -14,88 +13,74 @@ type Amount int64 // Height ... type Height int64 -var dbg bool +// Seq is a strictly increasing sequence number determine relative order between Claims and Supports. +type Seq uint64 // Claim ... type Claim struct { - wire.OutPoint - + OutPoint wire.OutPoint ID ID Amt Amount EffAmt Amount Accepted Height ActiveAt Height + + // TODO: Get rid of this. Implement ordered map in upper layer. + Seq Seq } func (c *Claim) String() string { - if dbg { - w := bytes.NewBuffer(nil) - fmt.Fprintf(w, "C%-3d amt: %2d, effamt: %v, accepted: %2d, active: %2d, id: %s", c.Index, c.Amt, c.EffAmt, c.Accepted, c.ActiveAt, c.ID) - return w.String() - } - b, err := json.MarshalIndent(c, "", " ") - if err != nil { - fmt.Printf("can't marshal, err :%s", err) - } - return string(b) -} - -// Support ... -type Support struct { - wire.OutPoint - - Amt Amount - Accepted Height - ActiveAt Height - - SupportedID ID + return fmt.Sprintf("C-%-68s amt: %-3d effamt: %-3d accepted: %-3d active: %-3d id: %s", + c.OutPoint, c.Amt, c.EffAmt, c.Accepted, c.ActiveAt, c.ID) } // MarshalJSON customizes the representation of JSON. func (c *Claim) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { - OutPoint string - ID string - Amount Amount - EffectiveAmount Amount - Accepted Height - ActiveAt Height + OutPoint string + ID string + Amount Amount + EffAmount Amount + Accepted Height + ActiveAt Height }{ - OutPoint: c.OutPoint.String(), - ID: c.ID.String(), - Amount: c.Amt, - EffectiveAmount: c.EffAmt, - Accepted: c.Accepted, - ActiveAt: c.ActiveAt, + OutPoint: c.OutPoint.String(), + ID: c.ID.String(), + Amount: c.Amt, + EffAmount: c.EffAmt, + Accepted: c.Accepted, + ActiveAt: c.ActiveAt, }) } -// MarshalJSON ... -func (s *Support) MarshalJSON() ([]byte, error) { - return json.Marshal(&struct { - OutPoint string - SupportedClaimID string - Amount Amount - Accepted Height - ActiveAt Height - }{ - OutPoint: s.OutPoint.String(), - SupportedClaimID: s.SupportedID.String(), - Amount: s.Amt, - Accepted: s.Accepted, - ActiveAt: s.ActiveAt, - }) +// Support ... +type Support struct { + OutPoint wire.OutPoint + ClaimID ID + Amt Amount + Accepted Height + ActiveAt Height + Seq Seq } func (s *Support) String() string { - if dbg { - w := bytes.NewBuffer(nil) - fmt.Fprintf(w, "S%-3d amt: %2d, accepted: %2d, active: %2d, id: %s", s.Index, s.Amt, s.Accepted, s.ActiveAt, s.SupportedID) - return w.String() - } - b, err := json.MarshalIndent(s, "", " ") - if err != nil { - fmt.Printf("can't marshal, err :%s", err) - } - return string(b) + return fmt.Sprintf("S-%-68s amt: %-3d accepted: %-3d active: %-3d id: %s", + s.OutPoint, s.Amt, s.Accepted, s.ActiveAt, s.ClaimID) +} + +// MarshalJSON customizes the representation of JSON. +func (s *Support) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + OutPoint string + ClaimID string + Amount Amount + Accepted Height + ActiveAt Height + }{ + OutPoint: s.OutPoint.String(), + ClaimID: s.ClaimID.String(), + Amount: s.Amt, + Accepted: s.Accepted, + ActiveAt: s.ActiveAt, + }) } diff --git a/claimnode/node.go b/claimnode/node.go index 3cadeed..ae6167c 100644 --- a/claimnode/node.go +++ b/claimnode/node.go @@ -1,11 +1,8 @@ package claimnode import ( - "bytes" "crypto/sha256" "encoding/binary" - "encoding/json" - "fmt" "math" "strconv" @@ -16,94 +13,84 @@ import ( "github.com/lbryio/claimtrie/memento" ) -var dbg bool - // Node ... type Node struct { - memento.Memento - + mem memento.Memento height claim.Height bestClaims map[claim.Height]*claim.Claim - claims map[wire.OutPoint]*claim.Claim - supports map[wire.OutPoint]*claim.Support + + // To ensure the Claims and Supports are totally ordered, we assign a + // strictly increasing seq to each Claim or Support added to the node. + seq claim.Seq + claims map[wire.OutPoint]*claim.Claim + supports map[wire.OutPoint]*claim.Support + + updateNext bool } -// NewNode ... +// NewNode returns a new Node. func NewNode() *Node { return &Node{ - Memento: memento.Memento{}, + mem: memento.Memento{}, bestClaims: map[claim.Height]*claim.Claim{0: nil}, claims: map[wire.OutPoint]*claim.Claim{}, supports: map[wire.OutPoint]*claim.Support{}, } } -// Height ... +// Height returns the current height. func (n *Node) Height() claim.Height { return n.height } -// BestClaim ... +// BestClaim returns the best claim at the current height. func (n *Node) BestClaim() *claim.Claim { - var latest claim.Height - for k := range n.bestClaims { - if k > latest { - latest = k - } - } - return n.bestClaims[latest] + c, _ := BestClaimAt(n, n.height) + return c } -// Tookover ... +// Tookover returns the height at which current best claim tookover. func (n *Node) Tookover() claim.Height { - var latest claim.Height - for k := range n.bestClaims { - if k > latest { - latest = k - } - } - return latest + _, since := BestClaimAt(n, n.height) + return since } -// IncrementBlock ... -func (n *Node) IncrementBlock(h claim.Height) error { - if h < 0 { - return ErrInvalidHeight - } - for i := claim.Height(0); i < h; i++ { +// AdjustTo increments or decrements current height until it reaches the specific height. +func (n *Node) AdjustTo(h claim.Height) error { + for n.height < h { n.height++ n.processBlock() - n.Commit() + n.mem.Commit() + } + for n.height > h { + n.height-- + n.mem.Rollback() } return nil } -// DecrementBlock ... -func (n *Node) DecrementBlock(h claim.Height) error { - if h < 0 { - return ErrInvalidHeight - } - for i := claim.Height(0); i < h; i++ { - n.height-- - n.Rollback() - } +// Reset ... +func (n *Node) Reset() error { + n.mem.RollbackUncommited() return nil } // AddClaim ... func (n *Node) AddClaim(op wire.OutPoint, amt claim.Amount) (*claim.Claim, error) { + n.seq++ c := &claim.Claim{ OutPoint: op, ID: claim.NewID(op), Amt: amt, Accepted: n.height + 1, ActiveAt: n.height + 1, + Seq: n.seq, } if n.BestClaim() != nil { c.ActiveAt = calActiveHeight(c.Accepted, c.Accepted, n.Tookover()) } - n.Execute(cmdAddClaim{node: n, claim: c}) + n.mem.Execute(cmdAddClaim{node: n, claim: c}) return c, nil } @@ -113,24 +100,27 @@ func (n *Node) RemoveClaim(op wire.OutPoint) error { if !ok { return ErrNotFound } - n.Execute(cmdRemoveClaim{node: n, claim: c}) + n.mem.Execute(cmdRemoveClaim{node: n, claim: c}) if n.BestClaim() != c { return nil } - n.Execute(updateNodeBestClaim{node: n, height: n.Tookover(), old: c, new: nil}) + n.mem.Execute(updateNodeBestClaim{node: n, height: n.Tookover(), old: c, new: nil}) n.updateActiveHeights() + n.updateNext = true return nil } // AddSupport ... func (n *Node) AddSupport(op wire.OutPoint, amt claim.Amount, supported claim.ID) (*claim.Support, error) { + n.seq++ s := &claim.Support{ - OutPoint: op, - Amt: amt, - SupportedID: supported, - Accepted: n.height + 1, - ActiveAt: n.height + 1, + OutPoint: op, + Amt: amt, + ClaimID: supported, + Accepted: n.height + 1, + ActiveAt: n.height + 1, + Seq: n.seq, } if n.BestClaim() == nil || n.BestClaim().OutPoint != op { s.ActiveAt = calActiveHeight(s.Accepted, s.Accepted, n.Tookover()) @@ -140,7 +130,7 @@ func (n *Node) AddSupport(op wire.OutPoint, amt claim.Amount, supported claim.ID if c.ID != supported { continue } - n.Execute(cmdAddSupport{node: n, support: s}) + n.mem.Execute(cmdAddSupport{node: n, support: s}) return s, nil } @@ -154,12 +144,18 @@ func (n *Node) RemoveSupport(op wire.OutPoint) error { if !ok { return ErrNotFound } - n.Execute(cmdRemoveSupport{node: n, support: s}) + n.mem.Execute(cmdRemoveSupport{node: n, support: s}) return nil } -// FindNextUpdateHeights ... -func (n *Node) FindNextUpdateHeights() claim.Height { +// FindNextUpdateHeight returns the smallest height in the future that the the state of the node might change. +// If no such height exists, the current height of the node is returned. +func (n *Node) FindNextUpdateHeight() claim.Height { + if n.updateNext { + n.updateNext = false + return n.height + 1 + } + next := claim.Height(math.MaxInt64) for _, v := range n.claims { if v.ActiveAt > n.height && v.ActiveAt < next { @@ -187,63 +183,14 @@ func (n *Node) Hash() chainhash.Hash { // MarshalJSON customizes JSON marshaling of the Node. func (n *Node) MarshalJSON() ([]byte, error) { - c := make([]*claim.Claim, 0, len(n.claims)) - for _, v := range n.claims { - c = append(c, v) - } - s := make([]*claim.Support, 0, len(n.supports)) - for _, v := range n.supports { - s = append(s, v) - } - return json.Marshal(&struct { - Height claim.Height - Hash string - Tookover claim.Height - BestClaim *claim.Claim - Claims []*claim.Claim - Supports []*claim.Support - }{ - Height: n.height, - Hash: n.Hash().String(), - Tookover: n.Tookover(), - BestClaim: n.BestClaim(), - Claims: c, - Supports: s, - }) + return toJSON(n) } // String implements Stringer interface. func (n *Node) String() string { - if dbg { - w := bytes.NewBuffer(nil) - fmt.Fprintf(w, "H: %2d BestClaims: ", n.height) - for k, v := range n.bestClaims { - if v == nil { - fmt.Fprintf(w, "{%d, nil}, ", k) - continue - } - fmt.Fprintf(w, "{%d, %d}, ", k, v.Index) - } - fmt.Fprintf(w, "\n") - for _, v := range n.claims { - fmt.Fprintf(w, "\n %v", v) - if v == n.BestClaim() { - fmt.Fprintf(w, " ") - } - } - for _, v := range n.supports { - fmt.Fprintf(w, "\n %v", v) - } - fmt.Fprintf(w, "\n") - return w.String() - - } - b, err := json.MarshalIndent(n, "", " ") - if err != nil { - panic("can't marshal Node") - } - return string(b) + return toString(n) } + func (n *Node) updateEffectiveAmounts() { for _, c := range n.claims { c.EffAmt = c.Amt @@ -252,7 +199,7 @@ func (n *Node) updateEffectiveAmounts() { continue } for _, s := range n.supports { - if s.ActiveAt > n.height || s.SupportedID != c.ID { + if s.ActiveAt > n.height || s.ClaimID != c.ID { continue } c.EffAmt += s.Amt @@ -263,40 +210,80 @@ func (n *Node) updateEffectiveAmounts() { func (n *Node) updateActiveHeights() { for _, v := range n.claims { if old, new := v.ActiveAt, calActiveHeight(v.Accepted, n.height, n.height); old != new { - n.Execute(cmdUpdateClaimActiveHeight{claim: v, old: old, new: new}) + n.mem.Execute(cmdUpdateClaimActiveHeight{claim: v, old: old, new: new}) } } for _, v := range n.supports { if old, new := v.ActiveAt, calActiveHeight(v.Accepted, n.height, n.height); old != new { - n.Execute(cmdUpdateSupportActiveHeight{support: v, old: old, new: new}) + n.mem.Execute(cmdUpdateSupportActiveHeight{support: v, old: old, new: new}) } } } + func (n *Node) processBlock() { for { + n.updateEffectiveAmounts() candidate := findCandiadte(n) if n.BestClaim() == candidate { return } - n.Execute(updateNodeBestClaim{node: n, height: n.height, old: n.bestClaims[n.height], new: candidate}) + n.mem.Execute(updateNodeBestClaim{node: n, height: n.height, old: n.bestClaims[n.height], new: candidate}) n.updateActiveHeights() } } func findCandiadte(n *Node) *claim.Claim { - n.updateEffectiveAmounts() var candidate *claim.Claim for _, v := range n.claims { - if v.ActiveAt > n.height { + switch { + case v.ActiveAt > n.height: continue - } - if candidate == nil || v.EffAmt > candidate.EffAmt { + case candidate == nil: + candidate = v + case v.EffAmt > candidate.EffAmt: + candidate = v + case v.EffAmt == candidate.EffAmt && v.Seq < candidate.Seq: candidate = v } } return candidate } +// BestClaimAt returns the BestClaim at specified Height along with the height when the claim tookover. +func BestClaimAt(n *Node, at claim.Height) (best *claim.Claim, since claim.Height) { + var latest claim.Height + for k := range n.bestClaims { + if k > at { + continue + } + if k > latest { + latest = k + } + } + return n.bestClaims[latest], latest +} + +// clone copies (deeply) the contents (except memento) of src to dst. +func clone(dst, src *Node) { + dst.height = src.height + for k, v := range src.bestClaims { + if v == nil { + dst.bestClaims[k] = nil + continue + } + dup := *v + dst.bestClaims[k] = &dup + } + for k, v := range src.claims { + dup := *v + dst.claims[k] = &dup + } + for k, v := range src.supports { + dup := *v + dst.supports[k] = &dup + } +} + func calNodeHash(op wire.OutPoint, tookover claim.Height) chainhash.Hash { txHash := chainhash.DoubleHashH(op.Hash[:]) diff --git a/claimnode/node_test.go b/claimnode/node_test.go index 030ead6..3111e1e 100644 --- a/claimnode/node_test.go +++ b/claimnode/node_test.go @@ -76,10 +76,10 @@ func Test_History1(t *testing.T) { // no competing bids test1 := func() { c1, _ = n.AddClaim(*newOutPoint(1), 1) - n.IncrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c1, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } @@ -87,49 +87,49 @@ func Test_History1(t *testing.T) { test2 := func() { n.AddClaim(*newOutPoint(2), 1) c3, _ = n.AddClaim(*newOutPoint(3), 2) - n.IncrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c3, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } // make two claims , one older test3 := func() { c4, _ = n.AddClaim(*newOutPoint(4), 1) - n.IncrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c4, n.BestClaim()) n.AddClaim(*newOutPoint(5), 1) - n.IncrementBlock(1) + n.AdjustTo(2) assert.Equal(t, c4, n.BestClaim()) - n.IncrementBlock(1) + n.AdjustTo(3) assert.Equal(t, c4, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(2) assert.Equal(t, c4, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c4, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } // check claim takeover test4 := func() { c6, _ = n.AddClaim(*newOutPoint(6), 1) - n.IncrementBlock(10) + n.AdjustTo(10) assert.Equal(t, c6, n.BestClaim()) c7, _ = n.AddClaim(*newOutPoint(7), 2) - n.IncrementBlock(1) + n.AdjustTo(11) assert.Equal(t, c6, n.BestClaim()) - n.IncrementBlock(10) + n.AdjustTo(21) assert.Equal(t, c7, n.BestClaim()) - n.DecrementBlock(10) + n.AdjustTo(11) assert.Equal(t, c6, n.BestClaim()) - n.DecrementBlock(10) + n.AdjustTo(1) assert.Equal(t, c6, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } @@ -137,51 +137,51 @@ func Test_History1(t *testing.T) { test5 := func() { c1, _ = n.AddClaim(*newOutPoint(1), 2) c2, _ = n.AddClaim(*newOutPoint(2), 1) - n.IncrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c1, n.BestClaim()) n.RemoveClaim(c1.OutPoint) - n.IncrementBlock(1) + n.AdjustTo(2) assert.Equal(t, c2, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c1, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } // spending winning claim will make inactive claim winner test6 := func() { c3, _ = n.AddClaim(*newOutPoint(3), 2) - n.IncrementBlock(10) + n.AdjustTo(10) assert.Equal(t, c3, n.BestClaim()) c4, _ = n.AddClaim(*newOutPoint(4), 2) - n.IncrementBlock(1) + n.AdjustTo(11) assert.Equal(t, c3, n.BestClaim()) n.RemoveClaim(c3.OutPoint) - n.IncrementBlock(1) + n.AdjustTo(12) assert.Equal(t, c4, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(11) assert.Equal(t, c3, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(10) assert.Equal(t, c3, n.BestClaim()) - n.DecrementBlock(10) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } // spending winning claim will empty out claim trie test7 := func() { c5, _ = n.AddClaim(*newOutPoint(5), 2) - n.IncrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c5, n.BestClaim()) n.RemoveClaim(c5.OutPoint) - n.IncrementBlock(1) + n.AdjustTo(2) assert.NotEqual(t, c5, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c5, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } @@ -191,49 +191,49 @@ func Test_History1(t *testing.T) { c2, _ = n.AddClaim(*newOutPoint(2), 1) s1, _ = n.AddSupport(*newOutPoint(11), 1, c1.ID) s2, _ = n.AddSupport(*newOutPoint(12), 10, c2.ID) - n.IncrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c2, n.BestClaim()) assert.Equal(t, claim.Amount(11), n.BestClaim().EffAmt) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } // check support delay test9 := func() { c3, _ = n.AddClaim(*newOutPoint(3), 1) c4, _ = n.AddClaim(*newOutPoint(4), 2) - n.IncrementBlock(10) + n.AdjustTo(10) assert.Equal(t, c4, n.BestClaim()) assert.Equal(t, claim.Amount(2), n.BestClaim().EffAmt) s4, _ = n.AddSupport(*newOutPoint(14), 10, c3.ID) - n.IncrementBlock(10) + n.AdjustTo(20) assert.Equal(t, c4, n.BestClaim()) assert.Equal(t, claim.Amount(2), n.BestClaim().EffAmt) - n.IncrementBlock(1) + n.AdjustTo(21) assert.Equal(t, c3, n.BestClaim()) assert.Equal(t, claim.Amount(11), n.BestClaim().EffAmt) - n.DecrementBlock(1) + n.AdjustTo(20) assert.Equal(t, c4, n.BestClaim()) assert.Equal(t, claim.Amount(2), n.BestClaim().EffAmt) - n.DecrementBlock(10) + n.AdjustTo(10) assert.Equal(t, c4, n.BestClaim()) assert.Equal(t, claim.Amount(2), n.BestClaim().EffAmt) - n.DecrementBlock(10) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } // supporting and abandoing on the same block will cause it to crash test10 := func() { c1, _ = n.AddClaim(*newOutPoint(1), 2) - n.IncrementBlock(1) + n.AdjustTo(1) s1, _ = n.AddSupport(*newOutPoint(11), 1, c1.ID) n.RemoveClaim(c1.OutPoint) - n.IncrementBlock(1) + n.AdjustTo(2) assert.NotEqual(t, c1, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c1, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } @@ -241,20 +241,44 @@ func Test_History1(t *testing.T) { test11 := func() { c1, _ = n.AddClaim(*newOutPoint(1), 2) s1, _ = n.AddSupport(*newOutPoint(11), 1, c1.ID) - n.IncrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c1, n.BestClaim()) //abandoning a support and abandoing claim on the same block will cause it to crash n.RemoveClaim(c1.OutPoint) n.RemoveSupport(s1.OutPoint) - n.IncrementBlock(1) + n.AdjustTo(2) assert.Nil(t, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(1) assert.Equal(t, c1, n.BestClaim()) - n.DecrementBlock(1) + n.AdjustTo(0) assert.Nil(t, n.BestClaim()) } + test12 := func() { + c1, _ = n.AddClaim(*newOutPoint(1), 3) + c2, _ = n.AddClaim(*newOutPoint(2), 2) + n.AdjustTo(10) + // c1 tookover since 1 + assert.Equal(t, c1, n.BestClaim()) + + // C3 will takeover at 11 + 11 - 1 = 21 + c3, _ = n.AddClaim(*newOutPoint(3), 5) + s1, _ = n.AddSupport(*newOutPoint(11), 2, c2.ID) + + n.AdjustTo(20) + assert.Equal(t, c1, n.BestClaim()) + + n.AdjustTo(21) + assert.Equal(t, c3, n.BestClaim()) + + n.RemoveClaim(c3.OutPoint) + n.AdjustTo(22) + + // c2 (3+4) should bid over c1(5) at 21 + assert.Equal(t, c2, n.BestClaim()) + + } tests := []func(){ test1, test2, @@ -267,9 +291,10 @@ func Test_History1(t *testing.T) { test9, test10, test11, + test12, } for _, tt := range tests { tt() } - _ = []func(){test1, test2, test3, test4, test5, test6, test7, test8, test9, test10, test11} + _ = []func(){test1, test2, test3, test4, test5, test6, test7, test8, test9, test10, test12} } diff --git a/claimnode/ui.go b/claimnode/ui.go new file mode 100644 index 0000000..1cd07f3 --- /dev/null +++ b/claimnode/ui.go @@ -0,0 +1,86 @@ +package claimnode + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "sort" + + "github.com/lbryio/claimtrie/claim" +) + +func sortedBestClaims(n *Node) []string { + var s []string + for i := claim.Height(0); i <= n.Tookover(); i++ { + v, ok := n.bestClaims[i] + if !ok || v == nil { + continue + } + s = append(s, fmt.Sprintf("{%d, %d}, ", i, v.OutPoint.Index)) + } + return s +} + +func sortedClaims(n *Node) []*claim.Claim { + c := make([]*claim.Claim, 0, len(n.claims)) + for _, v := range n.claims { + c = append(c, v) + } + sort.Slice(c, func(i, j int) bool { return c[i].Seq < c[j].Seq }) + return c +} + +func sortedSupports(n *Node) []*claim.Support { + s := make([]*claim.Support, 0, len(n.supports)) + for _, v := range n.supports { + s = append(s, v) + } + sort.Slice(s, func(i, j int) bool { return s[i].Seq < s[j].Seq }) + return s +} + +func export(n *Node) interface{} { + return &struct { + Height claim.Height + Hash string + BestClaims []string + BestClaim *claim.Claim + Claims []*claim.Claim + Supports []*claim.Support + }{ + Height: n.height, + Hash: n.Hash().String(), + BestClaims: sortedBestClaims(n), + BestClaim: n.BestClaim(), + Claims: sortedClaims(n), + Supports: sortedSupports(n), + } +} + +func toString(n *Node) string { + ui := ` Height {{.Height}}, {{.Hash}} BestClaims: {{range .BestClaims}}{{.}}{{end}} + {{$best := .BestClaim}} +{{- if .Claims}} + {{range .Claims -}} + {{.}} {{if (CMP . $best)}} {{end}} + {{end}} +{{- end}} +{{- if .Supports}} + {{range .Supports}}{{.}} + {{end}} +{{- end}}` + + w := bytes.NewBuffer(nil) + t := template.Must(template.New("").Funcs(template.FuncMap{ + "CMP": func(a, b *claim.Claim) bool { return a == b }, + }).Parse(ui)) + if err := t.Execute(w, export(n)); err != nil { + fmt.Printf("can't execute template, err: %s\n", err) + } + return w.String() +} + +func toJSON(n *Node) ([]byte, error) { + return json.Marshal(export(n)) +} diff --git a/claimtrie.go b/claimtrie.go index 62ad35b..2db390c 100644 --- a/claimtrie.go +++ b/claimtrie.go @@ -1,8 +1,6 @@ package claimtrie import ( - "fmt" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -16,7 +14,7 @@ import ( type ClaimTrie struct { // The highest block number commited to the ClaimTrie. - bestBlock claim.Height + height claim.Height // Immutable linear history. head *trie.Commit @@ -24,8 +22,16 @@ type ClaimTrie struct { // An overlay supporting Copy-on-Write to the current tip commit. stg *trie.Stage - // pending keeps track update for future block height. - pending map[claim.Height][]string + // todos tracks pending updates for future block height. + // + // A claim or support has a dynamic active peroid (ActiveAt, ExipresAt). + // This makes the state of each node dynamic as the ClaimTrie increases/decreases its height. + // Instead of polling every node for updates everytime ClaimTrie changes, the node is evaluated + // for the nearest future height it may change the states, and add that height to the todos. + // + // When a ClaimTrie at height h1 is committed with h2, the pending updates from todos (h1, h2] + // will be applied to bring the nodes up to date. + todos map[claim.Height][]string } // CommitMeta implements trie.CommitMeta with commit-specific metadata. @@ -37,71 +43,57 @@ type CommitMeta struct { func New() *ClaimTrie { mt := trie.New() return &ClaimTrie{ - head: trie.NewCommit(nil, CommitMeta{0}, mt), - stg: trie.NewStage(mt), - pending: map[claim.Height][]string{}, + head: trie.NewCommit(nil, CommitMeta{0}, mt), + stg: trie.NewStage(mt), + todos: map[claim.Height][]string{}, } } -func updateStageNode(stg *trie.Stage, name string, modifier func(n *claimnode.Node) error) error { - v, err := stg.Get(trie.Key(name)) - if err != nil && err != trie.ErrKeyNotFound { - return err - } - var n *claimnode.Node - if v == nil { - n = claimnode.NewNode() - } else { - n = v.(*claimnode.Node) - } - if err = modifier(n); err != nil { - return err - } - return stg.Update(trie.Key(name), n) +// Height returns the highest height of blocks commited to the ClaimTrie. +func (ct *ClaimTrie) Height() claim.Height { + return ct.height +} + +// Head returns the tip commit in the commit database. +func (ct *ClaimTrie) Head() *trie.Commit { + return ct.head } // AddClaim adds a Claim to the Stage of ClaimTrie. func (ct *ClaimTrie) AddClaim(name string, op wire.OutPoint, amt claim.Amount) error { - return updateStageNode(ct.stg, name, func(n *claimnode.Node) error { - if err := n.IncrementBlock(claim.Height(ct.bestBlock) - n.Height()); err != nil { - return err - } - _, err := n.AddClaim(op, claim.Amount(amt)) - next := ct.bestBlock + 1 - ct.pending[next] = append(ct.pending[next], name) + modifier := func(n *claimnode.Node) error { + _, err := n.AddClaim(op, amt) return err - }) + } + return updateNode(ct, ct.height, name, modifier) } // AddSupport adds a Support to the Stage of ClaimTrie. func (ct *ClaimTrie) AddSupport(name string, op wire.OutPoint, amt claim.Amount, supported claim.ID) error { - return updateStageNode(ct.stg, name, func(n *claimnode.Node) error { + modifier := func(n *claimnode.Node) error { _, err := n.AddSupport(op, amt, supported) - next := ct.bestBlock + 1 - ct.pending[next] = append(ct.pending[next], name) return err - }) + } + return updateNode(ct, ct.height, name, modifier) } // SpendClaim removes a Claim in the Stage. func (ct *ClaimTrie) SpendClaim(name string, op wire.OutPoint) error { - return updateStageNode(ct.stg, name, func(n *claimnode.Node) error { - next := ct.bestBlock + 1 - ct.pending[next] = append(ct.pending[next], name) + modifier := func(n *claimnode.Node) error { return n.RemoveClaim(op) - }) + } + return updateNode(ct, ct.height, name, modifier) } // SpendSupport removes a Support in the Stage. func (ct *ClaimTrie) SpendSupport(name string, op wire.OutPoint) error { - return updateStageNode(ct.stg, name, func(n *claimnode.Node) error { - next := ct.bestBlock + 1 - ct.pending[next] = append(ct.pending[next], name) + modifier := func(n *claimnode.Node) error { return n.RemoveSupport(op) - }) + } + return updateNode(ct, ct.height, name, modifier) } -// Traverse visits Nodes in the Stage of the ClaimTrie. +// Traverse visits Nodes in the Stage. func (ct *ClaimTrie) Traverse(visit trie.Visit, update, valueOnly bool) error { return ct.stg.Traverse(visit, update, valueOnly) } @@ -111,72 +103,121 @@ func (ct *ClaimTrie) MerkleHash() chainhash.Hash { return ct.stg.MerkleHash() } -// BestBlock returns the highest height of blocks commited to the ClaimTrie. -func (ct *ClaimTrie) BestBlock() claim.Height { - return ct.bestBlock -} - -// Commit commits the current Stage into commit database, and updates the BestBlock with the associated height. -// The height must be higher than the current BestBlock, or ErrInvalidHeight is returned. +// Commit commits the current Stage into commit database. +// If h is lower than the current height, ErrInvalidHeight is returned. +// +// As Stage can be always cleanly reset to a specific commited snapshot, +// any error occurred during the commit would leave the Stage partially updated +// so the caller can inspect the status if interested. +// +// Changes to the ClaimTrie status, such as height or todos, are all or nothing. func (ct *ClaimTrie) Commit(h claim.Height) error { - if h <= ct.bestBlock { + + // Already caught up. + if h <= ct.height { return ErrInvalidHeight } - for i := claim.Height(ct.bestBlock) + 1; i <= h; i++ { - for _, prefix := range ct.pending[i] { - // Brings the value node to date. - catchup := func(n *claimnode.Node) error { - if err := n.IncrementBlock(i - n.Height()); err != nil { - return err - } - - // After the update, the node may subscribe to another pending update. - if next := n.FindNextUpdateHeights(); next > i { - fmt.Printf("Subscribe pendings for %v to future Height at %d\n", prefix, next) - ct.pending[next] = append(ct.pending[next], prefix) - } - return nil - } - - // Update the node with the catchup modifier, and clear the Merkle Hash along the way. - if err := updateStageNode(ct.stg, prefix, catchup); err != nil { + // Apply pending updates in todos (ct.Height, h]. + // Note that ct.Height is excluded while h is included. + for i := ct.height + 1; i <= h; i++ { + for _, name := range ct.todos[i] { + // dummy modifier to have the node brought up to date. + modifier := func(n *claimnode.Node) error { return nil } + if err := updateNode(ct, i, name, modifier); err != nil { return err } } - delete(ct.pending, i) } commit, err := ct.stg.Commit(ct.head, CommitMeta{Height: h}) if err != nil { return err } + + // No more errors. Change the ClaimTrie status. ct.head = commit - ct.bestBlock = h + for i := ct.height + 1; i <= h; i++ { + delete(ct.todos, i) + } + ct.height = h return nil } // Reset reverts the Stage to a specified commit by height. func (ct *ClaimTrie) Reset(h claim.Height) error { - visit := func(prefix trie.Key, value trie.Value) error { + if h > ct.height { + return ErrInvalidHeight + } + + // Find the most recent commit that is equal or earlier than h. + commit := ct.head + for commit != nil { + if commit.Meta.(CommitMeta).Height <= h { + break + } + commit = commit.Prev + } + + // The commit history is not deep enough. + if commit == nil { + return ErrInvalidHeight + } + + // Drop (rollback) any uncommited change, and adjust to the specified height. + rollback := func(prefix trie.Key, value trie.Value) error { n := value.(*claimnode.Node) - return n.DecrementBlock(n.Height() - claim.Height(h)) + n.Reset() + return n.AdjustTo(h) } - if err := ct.stg.Traverse(visit, true, true); err != nil { - return err + if err := ct.stg.Traverse(rollback, true, true); err != nil { + // Rollback a node to a known state can't go wrong. + // It's a programming error, and can't recover. + panic(err) } - for commit := ct.head; commit != nil; commit = commit.Prev { - meta := commit.Meta.(CommitMeta) - if meta.Height <= h { - ct.head = commit - ct.bestBlock = h - ct.stg = trie.NewStage(commit.MerkleTrie) - return nil + + // Update ClaimTrie status + ct.head = commit + ct.height = h + for k := range ct.todos { + if k >= h { + delete(ct.todos, k) } } - return ErrInvalidHeight + ct.stg = trie.NewStage(commit.MerkleTrie) + return nil } -// Head returns the current tip commit in the commit database. -func (ct *ClaimTrie) Head() *trie.Commit { - return ct.head +// updateNode implements a get-modify-set sequence to the node associated with name. +// After the modifier is applied, the node is evaluated for how soon in the +// nearest future change. And register it, if any, to the todos for the next updateNode. +func updateNode(ct *ClaimTrie, h claim.Height, name string, modifier func(n *claimnode.Node) error) error { + + // Get the node from the Stage, or create one if it did not exist yet. + v, err := ct.stg.Get(trie.Key(name)) + if err == trie.ErrKeyNotFound { + v = claimnode.NewNode() + } else if err != nil { + return err + } + + n := v.(*claimnode.Node) + + // Bring the node state up to date. + if err = n.AdjustTo(h); err != nil { + return err + } + + // Apply the modifier on the node. + if err = modifier(n); err != nil { + return err + } + + // Register pending update, if any, for future height. + next := n.FindNextUpdateHeight() + if next > h { + ct.todos[next] = append(ct.todos[next], name) + } + + // Store the modified value back to the Stage, clearing out all the Merkle Hash on the path. + return ct.stg.Update(trie.Key(name), n) } diff --git a/cmd/claimtrie/main.go b/cmd/claimtrie/main.go index 6a9b8cc..815074a 100644 --- a/cmd/claimtrie/main.go +++ b/cmd/claimtrie/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" "crypto/rand" + "encoding/json" "fmt" "math/big" "os" @@ -26,7 +27,8 @@ var ( flagHeight = cli.Int64Flag{Name: "height, ht", Usage: "Height"} flagName = cli.StringFlag{Name: "name, n", Value: "Hello", Usage: "Name"} flagID = cli.StringFlag{Name: "id", Usage: "Claim ID"} - flagOutPoint = cli.StringFlag{Name: "outpoint, op", Usage: "Outpoint. (HASH:INDEX) "} + flagOutPoint = cli.StringFlag{Name: "outpoint, op", Usage: "Outpoint. (HASH:INDEX)"} + flagJSON = cli.BoolFlag{Name: "json, j", Usage: "Show Claim / Support in JSON format."} ) var ( @@ -40,7 +42,6 @@ func main() { app.Usage = "A CLI tool for ClaimTrie" app.Version = "0.0.1" app.Action = cli.ShowAppHelp - app.Commands = []cli.Command{ { Name: "add-claim", @@ -75,7 +76,7 @@ func main() { Aliases: []string{"s"}, Usage: "Show the Key-Value pairs of the Stage or specified commit. (links nodes are showed if -a is also specified)", Action: cmdShow, - Flags: []cli.Flag{flagAll, flagHeight}, + Flags: []cli.Flag{flagAll, flagJSON, flagHeight}, }, { Name: "merkle", @@ -154,7 +155,7 @@ func cmdAddClaim(c *cli.Context) error { // height := claim.Height(c.Int64("height")) // if !c.IsSet("height") { - // height = ct.BestBlock() + // height = ct.Height() // } outPoint, err := newOutPoint(c.String("outpoint")) @@ -176,7 +177,7 @@ func cmdAddSupport(c *cli.Context) error { // height := claim.Height(c.Int64("height")) // if !c.IsSet("height") { - // height = ct.BestBlock() + // height = ct.Height() // } outPoint, err := newOutPoint(c.String("outpoint")) @@ -215,14 +216,23 @@ func cmdShow(c *cli.Context) error { fmt.Printf("%-8s:\n", prefix) return nil } - fmt.Printf("%-8s: %v\n", prefix, val) + if !c.IsSet("json") { + fmt.Printf("%-8s: %v\n", prefix, val) + return nil + } + b, err := json.MarshalIndent(val, "", " ") + if err != nil { + return err + } + fmt.Printf("%-8s: %s\n", prefix, b) return nil } - height := claim.Height(c.Int64("height")) + if !c.IsSet("height") { - fmt.Printf("\n", ct.BestBlock()) + fmt.Printf("\n", ct.Height()) return ct.Traverse(dump, false, !c.Bool("all")) } + height := claim.Height(c.Int64("height")) fmt.Printf("NOTE: peeking to the past is broken for now. Try RESET command instead\n") for commit := ct.Head(); commit != nil; commit = commit.Prev { meta := commit.Meta.(claimtrie.CommitMeta) @@ -243,7 +253,7 @@ func cmdMerkle(c *cli.Context) error { func cmdCommit(c *cli.Context) error { height := claim.Height(c.Int64("height")) if !c.IsSet("height") { - height = ct.BestBlock() + 1 + height = ct.Height() + 1 } return ct.Commit(height) } diff --git a/memento/memento.go b/memento/memento.go index afd3fbe..dceef75 100644 --- a/memento/memento.go +++ b/memento/memento.go @@ -28,9 +28,19 @@ func (m *Memento) Commit() { // Rollback ... func (m *Memento) Rollback() { - cmds := m.stack[len(m.stack)-1] - m.stack = m.stack[:len(m.stack)-1] + n := len(m.stack) + cmds := m.stack[n-1] + m.stack = m.stack[:n-1] + m.cmds = nil for i := len(cmds) - 1; i >= 0; i-- { cmds[i].Undo() } } + +// RollbackUncommited ... +func (m *Memento) RollbackUncommited() { + for i := len(m.cmds) - 1; i >= 0; i-- { + m.cmds[i].Undo() + } + m.cmds = nil +} diff --git a/trie/stage.go b/trie/stage.go index 665777f..e2ecf88 100644 --- a/trie/stage.go +++ b/trie/stage.go @@ -22,6 +22,7 @@ func (s *Stage) Update(key Key, val Value) error { defer s.mu.Unlock() n := s.root + n.hash = nil for _, k := range key { org := n.links[k] n.links[k] = newNode(nil) @@ -31,10 +32,8 @@ func (s *Stage) Update(key Key, val Value) error { n.hash = nil n = n.links[k] } - if n.value != val { - n.value = val - n.hash = nil - } + n.value = val + n.hash = nil return nil }