Skip to content

Commit

Permalink
Merge pull request #73 from libsv/feature/brc-74
Browse files Browse the repository at this point in the history
feature: BRC-74 implementation of a BSV Universal Merkle Path format (BUMP)
  • Loading branch information
sirdeggen authored Oct 26, 2023
2 parents e0bb2d9 + 54550c2 commit 5d05fb3
Show file tree
Hide file tree
Showing 32 changed files with 4,149 additions and 191 deletions.
16 changes: 8 additions & 8 deletions block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"

"github.com/libsv/go-bt/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/libsv/go-bc"
)
Expand All @@ -24,8 +24,8 @@ func TestNewBlock(t *testing.T) {
blockBytes := "0000002043453154ad6d8209030ada359e07d2ce354cbed1f6169db497a5f2726e0bb51df5bc41a43429c7469dbb3501a186bf1f9238f9e886f84da057e7571c3472d12af33a1561ffff7f20010000000202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff05024c0b0101ffffffff0106270000000000002321033ac208f182e7fe982b1c25027ada05e6fc44590e3f862b0a8422eda03ea5951bac00000000020000000353d4f38490033f3baf11135175c011c61db6cb3e1d9c8d5579da464bd6d7500d000000004847304402205069ed8be3ea22953232328f4594b542655211ce103261ec9278900f8e4a7844022017baa239129970ab92dc4f3f18626954a298e179cc41457e94ea26232fa60de741feffffffd6db9360d48d9084e60d9e9e93ee187ec785768fc38a1826224cda54b436c198000000004847304402203a322b5c2145a8c6194f7575684cf877504a08e07c6718b633c1c7a88bfb71f3022079a87efe2bed70d886cd82f7c747b20a148c79f5adcaec1da05cc18df615fcee41feffffff07c023d3e3bc13b64025000002d2c565521b418562ae0e92e18553c5fafbc781010000006b483045022100abd8d9aed279921efe7be9fd9e24ff2e80b223106355a2e67ecb545cdfbfbf1002207c3861d13bbb08b4aa8e6d5f075f7505a70b98469c4b586c1674bd62b73cf8f2412102d86a9727d885baa389532bba48e37fc529c797939204c78d441a122b2f7a5c32feffffff02bd440f00000000001976a9142621c6863e947d83172bc677640d88cbe5b2477d88aca0860100000000001976a914b85524abf8202a961b847a3bd0bc89d3d4d41cc588ac4b0b0000"
b, err := bc.NewBlockFromStr(blockBytes)

assert.NoError(t, err)
assert.Equal(t, eb, b)
require.NoError(t, err)
require.Equal(t, eb, b)
}

func TestBlockString(t *testing.T) {
Expand All @@ -41,14 +41,14 @@ func TestBlockString(t *testing.T) {
Txs: txs,
}

assert.Equal(t, expectedBlock, b.String())
require.Equal(t, expectedBlock, b.String())
}

func TestBlockStringAndBytesMatch(t *testing.T) {
blockStr := "000000208340568a93304c2b327d901fde726e26825a753e9d9681697d60f13b5033691540dddb67dc3caf63b5ac5945e62eed5e7b328901c3bad1be775ca773152be5f8023d1561ffff7f20000000000302000000010000000000000000000000000000000000000000000000000000000000000000ffffffff05024e0b0101ffffffff01cc28000000000000232102af5e52d92723981deef3865309f04807a4cb16cc3da8270b203e482c43a370feac00000000020000000372545d8b76a366701abf79c5219a2f70748c2f888e933b82ada34ed070e66d2100000000494830450221009e8c1ec9c0bb567c47e153946c48dbb1c904d892dd149f92721d9fe87b816f1702207584a0fa85d39056a55685e2c7a1ed6f663b995670bc17fefc00b8ed781591d841feffffffef6f13ab6366f7a670869505630fdee12338ef12efbb223e223b44115f3c273100000000484730440220303ebd18633704633c3b92f261173fa833ca0376578e6d54c213d058c42c6716022077ec705a52337011cd7dd86ebcd207e613618b3da1252bae19355ad45cc04acd41feffffffad5cf4c165fde449155b4de8d1eee9f65e9bb66ff7665f4cb4788a38d665adcc010000006b483045022100ac2e344a9ec980b0c2625a5784c17e62ee59b674a146e6268ae56d49016b57e202202e2e7beb60d879148fdb3f0ed98b7b1148780bb31d82794cddc1c4a2f77d1ed5412102b691a69957cf30c1a7ceae9ba719d5f8891662623f0e797146446df73aa83872feffffff02a0860100000000001976a914b85524abf8202a961b847a3bd0bc89d3d4d41cc588acbd440f00000000001976a914fe88c4aeccc229c1bf9913e65fc6ff22f6c9d1fe88ac4d0b000002000000038bf51c82898c0f633f3bab38cdc737a4f666a3640c7128151d6d14bfa911aeb9000000004948304502210095cb2822a8ac066e074a06bf299fd4d2724f869e27e85b02365c2ba54da34e6902202191ffa313b9c4cf55d4893a18e99108d20720bafbbc7c5486238c1e502b254f41feffffffbbba0582b6dc50cce76a0b9d5e00e0cb3afa656db5000eeabad69c3c7b045b860000000049483045022100833865334ae594028a00460dd90575047cdfb9e40d3517051f4841a76035898e0220330e1321e99a59481513978d3fcd34db7b178c8f318176a0eccc0cdb308293a141feffffff5e6584b9ccc112673740ad8fe0f98db8b57585da611a727938fc6702c595827f000000006b483045022100e07f8411e6fd3fdc9ebc9360df6a18a45e49ce80f7e34f387930a16f07d3df6202206eba79ebe9e3760bdb21fa0bb10e4087a51bae88af8b16038d27b89256f9529e412103ba0acf181c9c111451fc5201b8008c33348b49f0b8337e6575312a39eb16852ffeffffff02bd440f00000000001976a914b7a6f23683c5570019094d61429c3c9cbe64533088aca0860100000000001976a914b85524abf8202a961b847a3bd0bc89d3d4d41cc588ac4d0b0000"
b, err := bc.NewBlockFromStr(blockStr)
assert.NoError(t, err)
assert.Equal(t, hex.EncodeToString(b.Bytes()), b.String())
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(b.Bytes()), b.String())
}

func TestBlockInvalid(t *testing.T) {
Expand All @@ -69,8 +69,8 @@ func TestBlockInvalid(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, err := bc.NewBlockFromStr(test.expectedBlock)
assert.Error(t, err)
assert.EqualError(t, err, test.expErr.Error())
require.Error(t, err)
require.EqualError(t, err, test.expErr.Error())
})
}
}
45 changes: 23 additions & 22 deletions blockheader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/libsv/go-bc"
)
Expand All @@ -33,8 +34,8 @@ func TestNewBlockHeader(t *testing.T) {
headerBytes := "0000002074a17794e7890e9124d87e122b7f67b9d707dcb6c5b9d542b22eff3d13054678e9d8afa92026c2c0873524b18cbf2479720a8471952770c847d9ec8e1e939dfc1f593460ffff7f2000000000"
bh, err := bc.NewBlockHeaderFromStr(headerBytes)

assert.NoError(t, err)
assert.Equal(t, ebh, bh)
require.NoError(t, err)
require.Equal(t, ebh, bh)
}

func TestBlockHeaderString(t *testing.T) {
Expand All @@ -58,14 +59,14 @@ func TestBlockHeaderString(t *testing.T) {
Nonce: 0,
}

assert.Equal(t, expectedHeader, bh.String())
require.Equal(t, expectedHeader, bh.String())
}

func TestBlockHeaderStringAndBytesMatch(t *testing.T) {
headerStr := "0000002074a17794e7890e9124d87e122b7f67b9d707dcb6c5b9d542b22eff3d13054678e9d8afa92026c2c0873524b18cbf2479720a8471952770c847d9ec8e1e939dfc1f593460ffff7f2000000000"
bh, err := bc.NewBlockHeaderFromStr(headerStr)
assert.NoError(t, err)
assert.Equal(t, hex.EncodeToString(bh.Bytes()), bh.String())
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(bh.Bytes()), bh.String())
}

func TestBlockHeaderInvalid(t *testing.T) {
Expand All @@ -90,8 +91,8 @@ func TestBlockHeaderInvalid(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, err := bc.NewBlockHeaderFromStr(test.expectedHeader)
assert.Error(t, err)
assert.EqualError(t, err, test.expErr.Error())
require.Error(t, err)
require.EqualError(t, err, test.expErr.Error())
})
}
}
Expand All @@ -101,33 +102,33 @@ func TestExtractMerkleRootFromBlockHeader(t *testing.T) {

merkleRoot, err := bc.ExtractMerkleRootFromBlockHeader(header)

assert.NoError(t, err)
assert.Equal(t, merkleRoot, "96cbb75fd2ef98e4309eebc8a54d2386333d936ded2a0f3e06c23a91bb612f70")
require.NoError(t, err)
require.Equal(t, "96cbb75fd2ef98e4309eebc8a54d2386333d936ded2a0f3e06c23a91bb612f70", merkleRoot)
}

func TestEncodeAndDecodeBlockHeader(t *testing.T) {
// the genesis block
genesisHex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"
genesis, err := bc.NewBlockHeaderFromStr(genesisHex)
assert.NoError(t, err)
assert.Equal(t, genesisHex, genesis.String())
require.NoError(t, err)
require.Equal(t, genesisHex, genesis.String())
}

func TestVerifyBlockHeader(t *testing.T) {
// the genesis block
genesisHex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"
header, err := hex.DecodeString(genesisHex)
assert.NoError(t, err)
require.NoError(t, err)
genesis, err := bc.NewBlockHeaderFromBytes(header)
assert.NoError(t, err)
require.NoError(t, err)

assert.Equal(t, genesisHex, genesis.String())
require.Equal(t, genesisHex, genesis.String())
assert.True(t, genesis.Valid())

// change one letter
header[0] = 222
genesisInvalid, err := bc.NewBlockHeaderFromBytes(header)
assert.NoError(t, err)
require.NoError(t, err)
assert.False(t, genesisInvalid.Valid())
}

Expand Down Expand Up @@ -186,8 +187,8 @@ func TestBlockHeader_MarshalJSON(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
bhj, err := json.MarshalIndent(test.bh, "", "\t")
assert.NoError(t, err)
assert.Equal(t, test.expJSON, string(bhj))
require.NoError(t, err)
require.Equal(t, test.expJSON, string(bhj))
})
}
}
Expand Down Expand Up @@ -232,15 +233,15 @@ func TestBlockHeader_UnmarshalJSON(t *testing.T) {
t.Run(name, func(t *testing.T) {
b, err := json.Marshal(test.bh)
if test.err != nil {
assert.Error(t, err)
assert.EqualError(t, err, test.err.Error())
require.Error(t, err)
require.EqualError(t, err, test.err.Error())
return
}
assert.NoError(t, err)
require.NoError(t, err)

var bh *bc.BlockHeader
assert.NoError(t, json.Unmarshal(b, &bh))
assert.Equal(t, test.bh.String(), bh.String())
require.NoError(t, json.Unmarshal(b, &bh))
require.Equal(t, test.bh.String(), bh.String())
})
}
}
187 changes: 187 additions & 0 deletions bump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package bc

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"

"github.com/libsv/go-bt/v2"
)

// BUMP data model json format according to BRC-74.
type BUMP struct {
BlockHeight uint32 `json:"blockHeight"`
Path [][]leaf `json:"path"`
}

// It should be written such that the internal bytes are kept for calculations.
// and the JSON is generated from the internal struct to an external format.
// leaf represents a leaf in the Merkle tree.
type leaf struct {
Offset uint64 `json:"offset,omitempty"`
Hash string `json:"hash,omitempty"`
Txid *bool `json:"txid,omitempty"`
Duplicate *bool `json:"duplicate,omitempty"`
}

// NewBUMPFromBytes creates a new BUMP from a byte slice.
func NewBUMPFromBytes(bytes []byte) (*BUMP, error) {
bump := &BUMP{}

// first bytes are the block height.
var skip int
index, size := bt.NewVarIntFromBytes(bytes[skip:])
skip += size
bump.BlockHeight = uint32(index)

// Next byte is the tree height.
treeHeight := uint(bytes[skip])
skip++

// We expect tree height levels.
bump.Path = make([][]leaf, treeHeight)

for lv := uint(0); lv < treeHeight; lv++ {
// For each level we parse a bunch of nLeaves.
n, size := bt.NewVarIntFromBytes(bytes[skip:])
skip += size
nLeavesAtThisHeight := uint64(n)
bump.Path[lv] = make([]leaf, nLeavesAtThisHeight)
for lf := uint64(0); lf < nLeavesAtThisHeight; lf++ {
// For each leaf we parse the offset, hash, txid and duplicate.
offset, size := bt.NewVarIntFromBytes(bytes[skip:])
skip += size
var l leaf
l.Offset = uint64(offset)
flags := bytes[skip]
skip++
var dup bool
var txid bool
dup = flags&1 > 0
txid = flags&2 > 0
if dup {
l.Duplicate = &dup
}
if txid {
l.Txid = &txid
}
l.Hash = StringFromBytesReverse(bytes[skip : skip+32])
skip += 32
bump.Path[lv][lf] = l
}
}

// Sort each of the levels by the offset for consistency.
for _, level := range bump.Path {
sort.Slice(level, func(i, j int) bool {
return level[i].Offset < level[j].Offset
})
}

return bump, nil
}

// NewBUMPFromStr creates a BUMP from hex string.
func NewBUMPFromStr(str string) (*BUMP, error) {
bytes, err := hex.DecodeString(str)
if err != nil {
return nil, err
}
return NewBUMPFromBytes(bytes)
}

// NewBUMPFromJSON creates a BUMP from a JSON string.
func NewBUMPFromJSON(jsonStr string) (*BUMP, error) {
bump := &BUMP{}
err := json.Unmarshal([]byte(jsonStr), bump)
if err != nil {
return nil, err
}
return bump, nil
}

// Bytes encodes a BUMP as a slice of bytes. BUMP Binary Format according to BRC-74 https://brc.dev/74
func (bump *BUMP) Bytes() ([]byte, error) {
bytes := []byte{}
bytes = append(bytes, bt.VarInt(bump.BlockHeight).Bytes()...)
treeHeight := len(bump.Path)
bytes = append(bytes, byte(treeHeight))
for level := 0; level < treeHeight; level++ {
nLeaves := len(bump.Path[level])
bytes = append(bytes, bt.VarInt(nLeaves).Bytes()...)
for _, leaf := range bump.Path[level] {
bytes = append(bytes, bt.VarInt(leaf.Offset).Bytes()...)
flags := byte(0)
if leaf.Duplicate != nil {
flags |= 1
}
if leaf.Txid != nil {
flags |= 2
}
bytes = append(bytes, flags)
if (flags & 1) == 0 {
bytes = append(bytes, BytesFromStringReverse(leaf.Hash)...)
}
}
}
return bytes, nil
}

func (bump *BUMP) String() (string, error) {
bytes, err := bump.Bytes()
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}

// CalculateRootGivenTxid calculates the root of the Merkle tree given a txid.
func (bump *BUMP) CalculateRootGivenTxid(txid string) (string, error) {
// Find the index of the txid at the lowest level of the Merkle tree
var index uint64
txidFound := false
for _, l := range bump.Path[0] {
if l.Hash == txid {
txidFound = true
index = l.Offset
break
}
}
if !txidFound {
return "", errors.New("The BUMP does not contain the txid: " + txid)
}

// Calculate the root using the index as a way to determine which direction to concatenate.
workingHash := BytesFromStringReverse(txid)
for height, leaves := range bump.Path {
offset := (index >> height) ^ 1
var leafAtThisLevel leaf
offsetFound := false
for _, l := range leaves {
if l.Offset == offset {
offsetFound = true
leafAtThisLevel = l
break
}
}
if !offsetFound {
return "", fmt.Errorf("We do not have a hash for this index at height: %v", height)
}

var digest []byte
if leafAtThisLevel.Duplicate != nil {
digest = append(workingHash, workingHash...)
} else {
leafBytes := BytesFromStringReverse(leafAtThisLevel.Hash)
if (offset % 2) != 0 {
digest = append(workingHash, leafBytes...)
} else {
digest = append(leafBytes, workingHash...)
}
}
workingHash = Sha256Sha256(digest)
}
return StringFromBytesReverse(workingHash), nil
}
39 changes: 39 additions & 0 deletions bump_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package bc

import (
"testing"

"github.com/stretchr/testify/require"
)

const (
jsonExample = `{"blockHeight":814435,"path":[[{"offset":20,"hash":"0dc75b4efeeddb95d8ee98ded75d781fcf95d35f9d88f7f1ce54a77a0c7c50fe"},{"offset":21,"txid":true,"hash":"3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac"}],[{"offset":11,"hash":"5745cf28cd3a31703f611fb80b5a080da55acefa4c6977b21917d1ef95f34fbc"}],[{"offset":4,"hash":"522a096a1a6d3b64a4289ab456134158d8443f2c3b8ed8618bd2b842912d4b57"}],[{"offset":3,"hash":"191c70d2ecb477f90716d602f4e39f2f81f686f8f4230c255d1b534dc85fa051"}],[{"offset":0,"hash":"1f487b8cd3b11472c56617227e7e8509b44054f2a796f33c52c28fd5291578fd"}],[{"offset":1,"hash":"5ecc0ad4f24b5d8c7e6ec5669dc1d45fcb3405d8ce13c0860f66a35ef442f562"}],[{"offset":1,"hash":"31631241c8124bc5a9531c160bfddb6fcff3729f4e652b10d57cfd3618e921b1"}]]}`

hexExample = `fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331`
rootExample = `bb6f640cc4ee56bf38eb5a1969ac0c16caa2d3d202b22bf3735d10eec0ca6e00`
txidExample = `3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac`
)

func TestNewBUMPFromStr(t *testing.T) {
bump, err := NewBUMPFromStr(hexExample)
require.NoError(t, err)
str, err := bump.String()
require.NoError(t, err)
require.Equal(t, hexExample, str)
}

func TestNewBUMPFromJson(t *testing.T) {
jBump, err := NewBUMPFromJSON(jsonExample)
require.NoError(t, err)
jStr, err := jBump.String()
require.NoError(t, err)
require.Equal(t, hexExample, jStr)
}

func TestCalculateRootGivenTxid(t *testing.T) {
bump, err := NewBUMPFromJSON(jsonExample)
require.NoError(t, err)
root, err := bump.CalculateRootGivenTxid(txidExample)
require.NoError(t, err)
require.Equal(t, rootExample, root)
}
Loading

0 comments on commit 5d05fb3

Please sign in to comment.