-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #73 from libsv/feature/brc-74
feature: BRC-74 implementation of a BSV Universal Merkle Path format (BUMP)
- Loading branch information
Showing
32 changed files
with
4,149 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.