diff --git a/beef_tx_test.go b/beef_tx_test.go index 16cb0d0f..313e401a 100644 --- a/beef_tx_test.go +++ b/beef_tx_test.go @@ -10,23 +10,7 @@ import ( ) func Test_ToBeefHex(t *testing.T) { - t.Run("all parents txs are already mined", func(t *testing.T) { - // given - ctx, client, deferMe := initSimpleTestCase(t) - defer deferMe() - - ancestorTx := addGrandpaTx(ctx, t, client) - minedParentTx := createTxWithDraft(ctx, t, client, ancestorTx, true) - - newTx := createTxWithDraft(ctx, t, client, minedParentTx, false) - - // when - hex, err := ToBeefHex(ctx, newTx) - - // then - assert.NoError(t, err) - assert.NotEmpty(t, hex) - }) + // TOOD: prepare tests in BUX-168 t.Run("some parents txs are not mined yet", func(t *testing.T) { // Error expected! this should be changed in the future. right now the test case has been written to make sure the system doesn't panic in such a situation @@ -64,7 +48,7 @@ func addGrandpaTx(ctx context.Context, t *testing.T, client ClientInterface) *Tr }, } grandpaTx.MerkleProof = MerkleProof(grandpaTxMp) - grandpaTx.BUMP = grandpaTx.MerkleProof.ToBUMP() + grandpaTx.BUMP = grandpaTx.MerkleProof.ToBUMP(grandpaTx.BlockHeight) err := grandpaTx.Save(ctx) require.NoError(t, err) diff --git a/db_model_transactions.go b/db_model_transactions.go index 130edd07..54f63215 100644 --- a/db_model_transactions.go +++ b/db_model_transactions.go @@ -223,8 +223,7 @@ func (m *Transaction) migrateBUMP() error { return err } for _, tx := range txs { - bump := tx.MerkleProof.ToBUMP() - bump.BlockHeight = tx.BlockHeight + bump := tx.MerkleProof.ToBUMP(tx.BlockHeight) tx.BUMP = bump _ = tx.Save(ctx) } diff --git a/go.mod b/go.mod index cbbe9e5e..c33c1230 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/99designs/gqlgen v0.17.40 github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/bitcoin-sv/go-broadcast-client v0.9.0 - github.com/bitcoin-sv/go-paymail v0.6.0 + github.com/bitcoin-sv/go-paymail v0.7.0 github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 github.com/bitcoinschema/go-map v0.1.0 github.com/centrifugal/centrifuge-go v0.10.2 diff --git a/go.sum b/go.sum index 05e5c89c..0a2986a0 100644 --- a/go.sum +++ b/go.sum @@ -57,10 +57,8 @@ github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/aws/aws-sdk-go v1.43.45 h1:2708Bj4uV+ym62MOtBnErm/CDX61C4mFe9V2gXy1caE= github.com/bitcoin-sv/go-broadcast-client v0.9.0 h1:6oR1th7TppFtWxcfCHFvEM0XnwTvuO4yTx1tg3rlOIc= github.com/bitcoin-sv/go-broadcast-client v0.9.0/go.mod h1:hami7qUkK0eoZolDXeo4tOLD16qbDXuZRV7BQ1RDzaE= -github.com/bitcoin-sv/go-paymail v0.5.1 h1:tng1CWRfTwQeN8+kNOSzJOMFzAtsxlGpdOX5uoyT+fk= -github.com/bitcoin-sv/go-paymail v0.5.1/go.mod h1:i0mTFBj3hfKEZ1tJUgUfV38b3jJVFgyeIBGR0c9lqOI= -github.com/bitcoin-sv/go-paymail v0.6.0 h1:dmEkERsKSmr+csriIoRuG68Gz+nPZ7Yp1ShQrO0RRl8= -github.com/bitcoin-sv/go-paymail v0.6.0/go.mod h1:i0mTFBj3hfKEZ1tJUgUfV38b3jJVFgyeIBGR0c9lqOI= +github.com/bitcoin-sv/go-paymail v0.7.0 h1:QcHsWp+2kgxBsiwM2LGevaNO0PodAk4L5bHABI9flX0= +github.com/bitcoin-sv/go-paymail v0.7.0/go.mod h1:i0mTFBj3hfKEZ1tJUgUfV38b3jJVFgyeIBGR0c9lqOI= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 h1:Sgh5Eb746Zck/46rFDrZZEXZWyO53fMuWYhNoZa1tck= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5/go.mod h1:JjO1ivfZv6vhK0uAXzyH08AAHlzNMAfnyK1Fiv9r4ZA= github.com/bitcoinschema/go-bob v0.4.0 h1:adsAEboLQCg0D6e9vwcJUJEJScszsouAYCYu35UAiGo= @@ -137,8 +135,6 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis_rate/v9 v9.1.2 h1:H0l5VzoAtOE6ydd38j8MCq3ABlGLnvvbA1xDSVVCHgQ= github.com/go-redis/redis_rate/v9 v9.1.2/go.mod h1:oam2de2apSgRG8aJzwJddXbNu91Iyz1m8IKJE2vpvlQ= -github.com/go-resty/resty/v2 v2.9.1 h1:PIgGx4VrHvag0juCJ4dDv3MiFRlDmP0vicBucwf+gLM= -github.com/go-resty/resty/v2 v2.9.1/go.mod h1:4/GYJVjh9nhkhGR6AUNW3XhpDYNUr+Uvy9gV/VGZIy4= github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -281,8 +277,6 @@ github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libsv/go-bc v0.1.19 h1:6N1umu8NV6RCUb+e+FNhv+nCjObN45aqPOEs/uzGAAU= -github.com/libsv/go-bc v0.1.19/go.mod h1:l6epTfcakN8YKId/hrpUzlu1QeT3ODF1MI3DeYhG1O8= github.com/libsv/go-bc v0.1.20 h1:NMH7knygmk8slZcBzoIYLut8DjRNgh8103Md3FzG+W0= github.com/libsv/go-bc v0.1.20/go.mod h1:l6epTfcakN8YKId/hrpUzlu1QeT3ODF1MI3DeYhG1O8= github.com/libsv/go-bk v0.1.6 h1:c9CiT5+64HRDbzxPl1v/oiFmbvWZTuUYqywCf+MBs/c= @@ -460,7 +454,6 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -538,7 +531,6 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -613,7 +605,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -621,7 +612,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -639,8 +629,7 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/model_bump.go b/model_bump.go index 41772230..059fb1cc 100644 --- a/model_bump.go +++ b/model_bump.go @@ -10,13 +10,14 @@ import ( "reflect" "sort" + "github.com/libsv/go-bc" "github.com/libsv/go-bt/v2" ) const maxBumpHeight = 64 // BUMPs represents a slice of BUMPs - BSV Unified Merkle Paths -type BUMPs []BUMP +type BUMPs []*BUMP // BUMP represents BUMP (BSV Unified Merkle Path) format type BUMP struct { @@ -29,43 +30,64 @@ type BUMP struct { // BUMPLeaf represents each BUMP path element type BUMPLeaf struct { Offset uint64 `json:"offset,string"` - Hash string `json:"hash"` + Hash string `json:"hash,omitempty"` TxID bool `json:"txid,omitempty"` Duplicate bool `json:"duplicate,omitempty"` } // CalculateMergedBUMP calculates Merged BUMP from a slice of Merkle Proofs -func CalculateMergedBUMP(mp []MerkleProof) (BUMP, error) { - bump := BUMP{} - - if len(mp) == 0 || mp == nil { - return bump, nil +func CalculateMergedBUMP(bumps []BUMP) (*BUMP, error) { + if len(bumps) == 0 || bumps == nil { + return nil, nil } - height := len(mp[0].Nodes) - if height > maxBumpHeight { - return bump, + blockHeight := bumps[0].BlockHeight + bumpHeight := len(bumps[0].Path) + if bumpHeight > maxBumpHeight { + return nil, fmt.Errorf("BUMP cannot be higher than %d", maxBumpHeight) } - for _, m := range mp { - if height != len(m.Nodes) { - return bump, + for _, b := range bumps { + if bumpHeight != len(b.Path) { + return nil, errors.New("Merged BUMP cannot be obtained from Merkle Proofs of different heights") } + if b.BlockHeight != blockHeight { + return nil, + errors.New("BUMPs have different block heights. Cannot merge BUMPs from different blocks") + } + if len(b.Path) == 0 { + return nil, + errors.New("Empty BUMP given") + } } - bump.Path = make([][]BUMPLeaf, height) - bump.allNodes = make([]map[uint64]bool, height) + bump := BUMP{BlockHeight: blockHeight} + bump.Path = make([][]BUMPLeaf, bumpHeight) + bump.allNodes = make([]map[uint64]bool, bumpHeight) for i := range bump.allNodes { bump.allNodes[i] = make(map[uint64]bool, 0) } - for _, m := range mp { - bumpToAdd := m.ToBUMP() - err := bump.add(bumpToAdd) + merkleRoot, err := bumps[0].calculateMerkleRoot() + if err != nil { + return nil, err + } + + for _, b := range bumps { + mr, err := b.calculateMerkleRoot() if err != nil { - return BUMP{}, err + return nil, err + } + + if merkleRoot != mr { + return nil, errors.New("BUMPs have different merkle roots") + } + + err = bump.add(b) + if err != nil { + return nil, err } } @@ -75,7 +97,7 @@ func CalculateMergedBUMP(mp []MerkleProof) (BUMP, error) { }) } - return bump, nil + return &bump, nil } func (bump *BUMP) add(b BUMP) error { @@ -104,6 +126,97 @@ func (bump *BUMP) add(b BUMP) error { return nil } +func (b *BUMP) calculateMerkleRoot() (string, error) { + merkleRoot := "" + + for _, bumpPathElement := range b.Path[0] { + if bumpPathElement.TxID { + calcMerkleRoot, err := calculateMerkleRoot(bumpPathElement, b) + if err != nil { + return "", err + } + + if merkleRoot == "" { + merkleRoot = calcMerkleRoot + continue + } + + if calcMerkleRoot != merkleRoot { + return "", errors.New("different merkle roots for the same block") + } + } + } + return merkleRoot, nil +} + +// calculateMerkleRoots will calculate one merkle root for tx in the BUMPLeaf +func calculateMerkleRoot(baseLeaf BUMPLeaf, bump *BUMP) (string, error) { + calculatedHash := baseLeaf.Hash + offset := baseLeaf.Offset + + for _, bLevel := range bump.Path { + newOffset := getOffsetPair(offset) + leafInPair := findLeafByOffset(newOffset, bLevel) + if leafInPair == nil { + return "", errors.New("could not find pair") + } + + leftNode, rightNode := prepareNodes(baseLeaf, offset, *leafInPair, newOffset) + + str, err := bc.MerkleTreeParentStr(leftNode, rightNode) + if err != nil { + return "", err + } + calculatedHash = str + + offset = offset / 2 + + baseLeaf = BUMPLeaf{ + Hash: calculatedHash, + Offset: offset, + } + } + + return calculatedHash, nil +} + +func findLeafByOffset(offset uint64, bumpLeaves []BUMPLeaf) *BUMPLeaf { + for _, bumpTx := range bumpLeaves { + if bumpTx.Offset == offset { + return &bumpTx + } + } + return nil +} + +func getOffsetPair(offset uint64) uint64 { + if offset%2 == 0 { + return offset + 1 + } + return offset - 1 +} + +func prepareNodes(baseLeaf BUMPLeaf, offset uint64, leafInPair BUMPLeaf, newOffset uint64) (string, string) { + var baseLeafHash, pairLeafHash string + + if baseLeaf.Duplicate { + baseLeafHash = leafInPair.Hash + } else { + baseLeafHash = baseLeaf.Hash + } + + if leafInPair.Duplicate { + pairLeafHash = baseLeaf.Hash + } else { + pairLeafHash = leafInPair.Hash + } + + if newOffset > offset { + return baseLeafHash, pairLeafHash + } + return pairLeafHash, baseLeafHash +} + // Bytes returns BUMPs bytes func (bumps *BUMPs) Bytes() []byte { var buff bytes.Buffer diff --git a/model_bump_test.go b/model_bump_test.go index a0dca28d..64e91567 100644 --- a/model_bump_test.go +++ b/model_bump_test.go @@ -10,45 +10,74 @@ import ( func TestBUMPModel_CalculateBUMP(t *testing.T) { t.Parallel() - t.Run("Single Merkle Proof", func(t *testing.T) { + t.Run("Single BUMP", func(t *testing.T) { // given - merkleProofs := []MerkleProof{ + bumps := []BUMP{ { - Index: 1, - TxOrID: "txId", - Nodes: []string{"node0", "node1", "node2", "node3"}, + BlockHeight: 0, + Path: [][]BUMPLeaf{ + { + { + Offset: 0, + Hash: "123b00", // this has to be a valid hex now + }, + { + Offset: 1, + Hash: "123b", + TxID: true, + }, + }, + { + { + Offset: 1, + Hash: "123b01", + }, + }, + { + { + Offset: 1, + Hash: "123b02", + }, + }, + { + { + Offset: 1, + Hash: "123b03", + }, + }, + }, }, } - expectedBUMP := BUMP{ + expectedBUMP := &BUMP{ BlockHeight: 0, Path: [][]BUMPLeaf{ { { Offset: 0, - Hash: "node0", + Hash: "123b00", }, { Offset: 1, - Hash: "txId", + Hash: "123b", TxID: true, }, }, { { Offset: 1, - Hash: "node1", + Hash: "123b01", }, }, { { Offset: 1, - Hash: "node2", + Hash: "123b02", }, }, { { Offset: 1, - Hash: "node3", + Hash: "123b03", }, }, }, @@ -70,260 +99,299 @@ func TestBUMPModel_CalculateBUMP(t *testing.T) { } // when - bump, err := CalculateMergedBUMP(merkleProofs) + bump, err := CalculateMergedBUMP(bumps) // then assert.NoError(t, err) assert.Equal(t, expectedBUMP, bump) }) - t.Run("Slice of Merkle Proofs", func(t *testing.T) { + t.Run("Paired Transactions", func(t *testing.T) { // given - merkleProofs := []MerkleProof{ - { - Index: 2, - TxOrID: "txId1", - Nodes: []string{"D", "AB", "EFGH", "IJKLMNOP"}, - }, + bumps := []BUMP{ { - Index: 7, - TxOrID: "txId2", - Nodes: []string{"G", "EF", "ABCD", "IJKLMNOP"}, - }, - { - Index: 13, - TxOrID: "txId3", - Nodes: []string{"M", "OP", "IJKL", "ABCDEFGH"}, - }, - } - expectedBUMP := BUMP{ - BlockHeight: 0, - Path: [][]BUMPLeaf{ - { - { - Offset: 2, - Hash: "txId1", - TxID: true, - }, + BlockHeight: 0, + Path: [][]BUMPLeaf{ { - Offset: 3, - Hash: "D", + { + Offset: 8, + Hash: "123b09", + TxID: true, + }, + { + Offset: 9, + Hash: "123b10", + }, }, { - Offset: 6, - Hash: "G", + { + Offset: 5, + Hash: "123b1112", + }, }, { - Offset: 7, - Hash: "txId2", - TxID: true, + { + Offset: 3, + Hash: "123b13141516", + }, }, { - Offset: 12, - Hash: "M", + { + Offset: 0, + Hash: "123b0102030405060708", + }, }, + }, + }, + { + BlockHeight: 0, + Path: [][]BUMPLeaf{ { - Offset: 13, - Hash: "txId3", - TxID: true, + { + Offset: 8, + Hash: "123b09", + }, + { + Offset: 9, + Hash: "123b10", + TxID: true, + }, }, - }, - { { - Offset: 0, - Hash: "AB", + { + Offset: 5, + Hash: "123b1112", + }, }, { - Offset: 2, - Hash: "EF", + { + Offset: 3, + Hash: "123b13141516", + }, }, { - Offset: 7, - Hash: "OP", + { + Offset: 0, + Hash: "123b0102030405060708", + }, }, }, + }, + } + expectedBUMP := &BUMP{ + BlockHeight: 0, + Path: [][]BUMPLeaf{ { { - Offset: 0, - Hash: "ABCD", + Offset: 8, + Hash: "123b09", + TxID: true, }, { - Offset: 1, - Hash: "EFGH", + Offset: 9, + Hash: "123b10", + TxID: true, }, + }, + { { - Offset: 2, - Hash: "IJKL", + Offset: 5, + Hash: "123b1112", }, }, { { - Offset: 0, - Hash: "ABCDEFGH", + Offset: 3, + Hash: "123b13141516", }, + }, + { { - Offset: 1, - Hash: "IJKLMNOP", + Offset: 0, + Hash: "123b0102030405060708", }, }, }, allNodes: []map[uint64]bool{ { - 2: true, - 3: true, - 6: true, - 7: true, - 12: true, - 13: true, + 8: true, + 9: true, }, { - 0: true, - 2: true, - 7: true, + 5: true, }, { - 0: true, - 1: true, - 2: true, + 3: true, }, { 0: true, - 1: true, }, }, } // when - bump, err := CalculateMergedBUMP(merkleProofs) + bump, err := CalculateMergedBUMP(bumps) // then assert.NoError(t, err) assert.Equal(t, expectedBUMP, bump) }) - t.Run("Paired Transactions", func(t *testing.T) { + t.Run("Different sizes of BUMPs", func(t *testing.T) { // given - merkleProofs := []MerkleProof{ - { - Index: 8, - TxOrID: "I", - Nodes: []string{"J", "KL", "MNOP", "ABCDEFGH"}, - }, + bumps := []BUMP{ { - Index: 9, - TxOrID: "J", - Nodes: []string{"I", "KL", "MNOP", "ABCDEFGH"}, - }, - } - expectedBUMP := BUMP{ - BlockHeight: 0, - Path: [][]BUMPLeaf{ - { + BlockHeight: 0, + Path: [][]BUMPLeaf{ { - Offset: 8, - Hash: "I", - TxID: true, + { + Offset: 8, + Hash: "123b09", + TxID: true, + }, + { + Offset: 9, + Hash: "123b10", + }, }, { - Offset: 9, - Hash: "J", - TxID: true, + { + Offset: 5, + Hash: "123b1112", + }, }, - }, - { { - Offset: 5, - Hash: "KL", + { + Offset: 3, + Hash: "123b13141516", + }, }, - }, - { { - Offset: 3, - Hash: "MNOP", + { + Offset: 0, + Hash: "123b0102030405060708", + }, }, }, - { + }, + { + BlockHeight: 0, + Path: [][]BUMPLeaf{ { - Offset: 0, - Hash: "ABCDEFGH", + { + Offset: 8, + Hash: "123b09", + }, + { + Offset: 9, + Hash: "123b10", + TxID: true, + }, + }, + { + { + Offset: 5, + Hash: "123b1112", + }, + }, + { + { + Offset: 3, + Hash: "123b0102030405060708", + }, }, - }, - }, - allNodes: []map[uint64]bool{ - { - 8: true, - 9: true, - }, - { - 5: true, - }, - { - 3: true, - }, - { - 0: true, }, }, } // when - bump, err := CalculateMergedBUMP(merkleProofs) + bump, err := CalculateMergedBUMP(bumps) // then - assert.NoError(t, err) - assert.Equal(t, expectedBUMP, bump) + assert.Error(t, err) + assert.Nil(t, bump) }) - t.Run("Different sizes of Merkle Proofs", func(t *testing.T) { + t.Run("BUMPs with different block heights", func(t *testing.T) { // given - merkleProofs := []MerkleProof{ + bumps := []BUMP{ { - Index: 8, - TxOrID: "I", - Nodes: []string{"J", "KL", "MNOP", "ABCDEFGH"}, + BlockHeight: 0, + Path: [][]BUMPLeaf{ + { + { + Offset: 8, + Hash: "123b09", + TxID: true, + }, + { + Offset: 9, + Hash: "123b10", + }, + }, + { + { + Offset: 5, + Hash: "123b1112", + }, + }, + }, }, { - Index: 9, - TxOrID: "J", - Nodes: []string{"I", "KL", "MNOP"}, + BlockHeight: 100, + Path: [][]BUMPLeaf{ + { + { + Offset: 8, + Hash: "123b09", + }, + { + Offset: 9, + Hash: "123b10", + TxID: true, + }, + }, + { + { + Offset: 5, + Hash: "123b1112", + }, + }, + }, }, } // when - bump, err := CalculateMergedBUMP(merkleProofs) + bump, err := CalculateMergedBUMP(bumps) // then assert.Error(t, err) - assert.Equal(t, bump, BUMP{}) + assert.Nil(t, bump) }) - t.Run("Empty slice of Merkle Proofs", func(t *testing.T) { + t.Run("Empty slice of BUMPS", func(t *testing.T) { // given - merkleProof := []MerkleProof{} + bumps := []BUMP{} // when - bump, err := CalculateMergedBUMP(merkleProof) + bump, err := CalculateMergedBUMP(bumps) // then assert.NoError(t, err) - assert.Equal(t, bump, BUMP{}) + assert.Nil(t, bump) }) - t.Run("Slice of empty Merkle Proofs", func(t *testing.T) { + t.Run("Slice of empty BUMPS", func(t *testing.T) { // given - merkleProofs := []MerkleProof{ + bumps := []BUMP{ {}, {}, {}, } // when - bump, err := CalculateMergedBUMP(merkleProofs) + bump, err := CalculateMergedBUMP(bumps) // then - assert.NoError(t, err) - assert.Equal(t, bump, BUMP{ - BlockHeight: 0, - Path: [][]BUMPLeaf{}, - allNodes: []map[uint64]bool{}, - }) + assert.Error(t, err) + assert.Nil(t, bump) }) } @@ -522,7 +590,7 @@ func TestBUMPModel_CalculateMergedBUMPAndHex(t *testing.T) { }, }, } - expectedBUMP := BUMP{ + expectedBUMP := &BUMP{ BlockHeight: 0, Path: [][]BUMPLeaf{ { @@ -727,7 +795,11 @@ func TestBUMPModel_CalculateMergedBUMPAndHex(t *testing.T) { "3d2388f114e6f627fd9dd632e72502699e419338bed5022840f4176e1731f715" // when - bump, err := CalculateMergedBUMP(merkleProof) + bumps := make([]BUMP, 0) + for _, mp := range merkleProof { + bumps = append(bumps, mp.ToBUMP(0)) + } + bump, err := CalculateMergedBUMP(bumps) actualHex := bump.Hex() // then diff --git a/model_draft_transactions.go b/model_draft_transactions.go index 114266fc..f82f79c9 100644 --- a/model_draft_transactions.go +++ b/model_draft_transactions.go @@ -380,7 +380,7 @@ func (m *DraftTransaction) createTransactionHex(ctx context.Context) (err error) // final sanity check inputValue := uint64(0) usedUtxos := make([]string, 0) - merkleProofs := make(map[uint64][]MerkleProof) + bumps := make(map[uint64][]BUMP) for _, input := range m.Configuration.Inputs { // check whether an utxo was used twice, this is not valid if utils.StringInSlice(input.Utxo.ID, usedUtxos) { @@ -392,8 +392,8 @@ func (m *DraftTransaction) createTransactionHex(ctx context.Context) (err error) if err != nil { return err } - if tx.MerkleProof.TxOrID != "" { - merkleProofs[tx.BlockHeight] = append(merkleProofs[tx.BlockHeight], tx.MerkleProof) + if len(tx.BUMP.Path) != 0 { + bumps[tx.BlockHeight] = append(bumps[tx.BlockHeight], tx.BUMP) } } outputValue := uint64(0) @@ -410,10 +410,13 @@ func (m *DraftTransaction) createTransactionHex(ctx context.Context) (err error) if inputValue-outputValue != m.Configuration.Fee { return ErrTransactionFeeInvalid } - for _, v := range merkleProofs { - bump, err := CalculateMergedBUMP(v) + for _, b := range bumps { + bump, err := CalculateMergedBUMP(b) if err != nil { - return err + return fmt.Errorf("Error while calculating Merged BUMP: %s", err.Error()) + } + if bump == nil { + continue } m.BUMPs = append(m.BUMPs, bump) } diff --git a/model_merkle_proof.go b/model_merkle_proof.go index 075e8167..fb351181 100644 --- a/model_merkle_proof.go +++ b/model_merkle_proof.go @@ -58,8 +58,8 @@ func (m MerkleProof) Value() (driver.Value, error) { } // ToBUMP transform Merkle Proof to BUMP -func (m *MerkleProof) ToBUMP() BUMP { - bump := BUMP{} +func (m *MerkleProof) ToBUMP(blockHeight uint64) BUMP { + bump := BUMP{BlockHeight: blockHeight} height := len(m.Nodes) if height == 0 { @@ -79,7 +79,11 @@ func (m *MerkleProof) ToBUMP() BUMP { } txIDPath2 := BUMPLeaf{ Offset: offsetPair(offset), - Hash: m.Nodes[0], + } + if m.Nodes[0] != "*" { + txIDPath2.Hash = m.Nodes[0] + } else { + txIDPath2.Duplicate = true } if offset < pairOffset { @@ -94,10 +98,17 @@ func (m *MerkleProof) ToBUMP() BUMP { for i := 1; i < height; i++ { p := make([]BUMPLeaf, 0) offset = parentOffset(offset) - p = append(p, BUMPLeaf{ - Offset: offset, - Hash: m.Nodes[i], - }) + + leaf := BUMPLeaf{Offset: offset} + + isDuplicate := m.Nodes[i] == "*" + if !isDuplicate { + leaf.Hash = m.Nodes[i] + } else { + leaf.Duplicate = true + } + + p = append(p, leaf) path = append(path, p) } bump.Path = path diff --git a/model_merkle_proof_test.go b/model_merkle_proof_test.go index 83d45938..19d8a737 100644 --- a/model_merkle_proof_test.go +++ b/model_merkle_proof_test.go @@ -11,12 +11,15 @@ func TestMerkleProofModel_ToBUMP(t *testing.T) { t.Parallel() t.Run("Valid Merkle Proof #1", func(t *testing.T) { + // given + blockHeight := uint64(0) mp := MerkleProof{ Index: 1, TxOrID: "txId", Nodes: []string{"node0", "node1", "node2", "node3"}, } expectedBUMP := BUMP{ + BlockHeight: blockHeight, Path: [][]BUMPLeaf{ { {Offset: 0, Hash: "node0"}, @@ -33,17 +36,24 @@ func TestMerkleProofModel_ToBUMP(t *testing.T) { }, }, } - actualBUMP := mp.ToBUMP() + + // when + actualBUMP := mp.ToBUMP(blockHeight) + + // then assert.Equal(t, expectedBUMP, actualBUMP) }) t.Run("Valid Merkle Proof #2", func(t *testing.T) { + // given + blockHeight := uint64(0) mp := MerkleProof{ Index: 14, TxOrID: "txId", Nodes: []string{"node0", "node1", "node2", "node3", "node4"}, } expectedBUMP := BUMP{ + BlockHeight: blockHeight, Path: [][]BUMPLeaf{ { {Offset: 14, Hash: "txId", TxID: true}, @@ -63,13 +73,55 @@ func TestMerkleProofModel_ToBUMP(t *testing.T) { }, }, } - actualBUMP := mp.ToBUMP() + + // when + actualBUMP := mp.ToBUMP(blockHeight) + + // then + assert.Equal(t, expectedBUMP, actualBUMP) + }) + + t.Run("Valid Merkle Proof #3 - with *", func(t *testing.T) { + // given + blockHeight := uint64(0) + mp := MerkleProof{ + Index: 14, + TxOrID: "txId", + Nodes: []string{"*", "node1", "node2", "node3", "node4"}, + } + expectedBUMP := BUMP{ + BlockHeight: blockHeight, + Path: [][]BUMPLeaf{ + { + {Offset: 14, Hash: "txId", TxID: true}, + {Offset: 15, Duplicate: true}, + }, + { + {Offset: 6, Hash: "node1"}, + }, + { + {Offset: 2, Hash: "node2"}, + }, + { + {Offset: 0, Hash: "node3"}, + }, + { + {Offset: 1, Hash: "node4"}, + }, + }, + } + + // when + actualBUMP := mp.ToBUMP(blockHeight) + + // then assert.Equal(t, expectedBUMP, actualBUMP) }) t.Run("Empty Merkle Proof", func(t *testing.T) { + blockHeight := uint64(0) mp := MerkleProof{} - actualBUMP := mp.ToBUMP() - assert.Equal(t, BUMP{}, actualBUMP) + actualBUMP := mp.ToBUMP(blockHeight) + assert.Equal(t, BUMP{BlockHeight: blockHeight}, actualBUMP) }) } diff --git a/model_transactions.go b/model_transactions.go index 281d5ebe..0e149c57 100644 --- a/model_transactions.go +++ b/model_transactions.go @@ -234,8 +234,7 @@ func (m *Transaction) setMerkleRoot(txInfo *chainstate.TransactionInfo) { mp := MerkleProof(*txInfo.MerkleProof) m.MerkleProof = mp - bump := mp.ToBUMP() - bump.BlockHeight = uint64(txInfo.BlockHeight) + bump := mp.ToBUMP(uint64(txInfo.BlockHeight)) m.BUMP = bump } }