Skip to content

Commit

Permalink
Merge pull request #83 from libsv/fix/merkle-path-optimal
Browse files Browse the repository at this point in the history
[fix] optimize the BUMP calculation function to make it O(log(n))
  • Loading branch information
boecklim authored Mar 26, 2024
2 parents 4b39571 + ef52922 commit 186145d
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 72 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: latest
args: --deadline=480s --skip-dirs=vendor --tests
args: --tests
build:
strategy:
matrix:
Expand Down Expand Up @@ -54,6 +53,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
version: latest
args: release --clean
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ run-unit-tests-cover:
open file:///$(shell pwd)/cover.html

run-linter:
@golangci-lint run --deadline=480s --skip-dirs=vendor --tests
@golangci-lint run --skip-dirs=vendor --tests

install-linter:
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)bin v1.45.2
Expand Down
93 changes: 42 additions & 51 deletions bump.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,69 +219,60 @@ func (bump *BUMP) CalculateRootGivenTxid(txid string) (string, error) {

// NewBUMPFromMerkleTreeAndIndex with merkle tree we calculate the merkle path for a given transaction.
func NewBUMPFromMerkleTreeAndIndex(blockHeight uint64, merkleTree []*chainhash.Hash, txIndex uint64) (*BUMP, error) {
if len(merkleTree) == 0 {
return nil, errors.New("merkle tree is empty")
}

bump := &BUMP{
BlockHeight: blockHeight,
Path: [][]leaf{},
}
t := true

truePointer := true
txid := merkleTree[txIndex].String()
txidLeaf := leaf{Txid: &truePointer, Hash: &txid, Offset: &txIndex}

if len(merkleTree) == 1 {
// there is no merkle path to calculate
bump.Path = [][]leaf{{txidLeaf}}
return bump, nil
}

oddTxIndex := false

numOfTxids := (len(merkleTree) + 1) / 2
treeHeight := int(math.Log2(float64(numOfTxids)))
numOfHashes := numOfTxids
bump.Path = make([][]leaf, treeHeight)

if len(merkleTree) == 0 {
return nil, errors.New("merkle tree is empty")
}

// these are the offsets for the txid we're interested in.
offsets := make([]uint64, treeHeight)
for i := 0; i < treeHeight; i++ {
if txIndex>>uint64(i)&1 == 0 {
offsets[i] = txIndex>>uint64(i) + 1
levelOffset := 0
for height := 0; height < treeHeight; height++ {
offset := txIndex >> uint64(height)
if offset&1 == 0 {
// offset is even we need to use the hash to the right.
offset++
} else {
offsets[i] = txIndex>>uint64(i) - 1
// we need to use the hash to the left.
oddTxIndex = true
offset--
}
}

// if we have only one transaction in the block there is no merkle path to calculate
if len(merkleTree) != 1 {
// if our hash index is odd the next hash of the path is the previous element in the array otherwise the next element.
levelOffset := 0
for height := 0; height < treeHeight; height++ {
leaves := []leaf{}
bump.Path = append(bump.Path, leaves)
for offset := 0; offset < numOfHashes; offset++ {
o := uint64(offset)
// only include the hashes for the txid we're interested in.
if height == 0 {
if o != txIndex && o != offsets[height] {
continue
}
} else {
if o != offsets[height] {
continue
}
}
thisLeaf := leaf{Offset: &o}
hash := merkleTree[levelOffset+offset]
if hash.IsEqual(nil) {
thisLeaf.Duplicate = &t
} else {
sh := hash.String()
thisLeaf.Hash = &sh
if height == 0 && txIndex == o {
thisLeaf.Txid = &t
}
}
bump.Path[height] = append(bump.Path[height], thisLeaf)
}
levelOffset += numOfHashes
numOfHashes >>= 1
thisLeaf := leaf{Offset: &offset}
hash := merkleTree[levelOffset+int(offset)]
if hash.IsEqual(nil) {
thisLeaf.Duplicate = &truePointer
} else {
sh := hash.String()
thisLeaf.Hash = &sh
}
bump.Path[height] = []leaf{thisLeaf}
levelOffset += numOfHashes
numOfHashes >>= 1
}
if oddTxIndex {
// if the txIndex is odd we need to add the txid to the path.
bump.Path[0] = append(bump.Path[0], txidLeaf)
} else {
sh := merkleTree[0].String()
o := uint64(0)
bump.Path = [][]leaf{{leaf{Hash: &sh, Offset: &o, Txid: &t}}}
// otherwise prepend it.
bump.Path[0] = append([]leaf{txidLeaf}, bump.Path[0]...)
}

return bump, nil
Expand Down
33 changes: 32 additions & 1 deletion bump_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bc

import (
"crypto/rand"
"math"
"testing"

Expand Down Expand Up @@ -183,8 +184,38 @@ func TestOnlySpecifiedPathsStored(t *testing.T) {
totalHashes += len(level)
}
// number of levels plus the txid itself.
l := int(math.Log2(float64(len(blockTxExample)))) + 1
l := int(math.Ceil(math.Log2(float64(len(blockTxExample))))) + 1
require.Equal(t, l, totalHashes)
}

}

func BenchmarkNewBUMPFromMerkleTreeAndIndex(b *testing.B) {
transactions := 100000
// test how quickly we can calculate the BUMP Merkle Paths from a block of 100,000 random txids
chainHashBlock := make([]*chainhash.Hash, 0)
for i := 0; i < transactions; i++ {
bytes := make([]byte, 32)
_, _ = rand.Read(bytes)
hash, err := chainhash.NewHash(bytes)
if err != nil {
b.Fatal(err)
}
chainHashBlock = append(chainHashBlock, hash)
}
merkles := BuildMerkleTreeStoreChainHash(chainHashBlock)

b.ResetTimer()

for idx := 0; idx < transactions; idx++ {
bump, err := NewBUMPFromMerkleTreeAndIndex(850000, merkles, uint64(idx))
require.NoError(b, err)
totalHashes := 0
for _, level := range bump.Path {
totalHashes += len(level)
}
// number of levels plus the txid itself.
l := int(math.Ceil(math.Log2(float64(transactions)))) + 1
require.Equal(b, l, totalHashes)
}
}
Loading

0 comments on commit 186145d

Please sign in to comment.