Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(BUX-207): decode beef transaction #13

Merged
merged 25 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c44b045
feat: adds first version of beef decoding
wregulski Sep 21, 2023
b6464db
feat: adds test to decode whole cmpslice from stream
wregulski Sep 21, 2023
1e1ed77
feat: prepares decodebeeftx tests
wregulski Sep 21, 2023
9fd82c1
fix: removes copying of the slice and repetitive function
wregulski Sep 21, 2023
adfa2e7
fix: adds constant for consistency
wregulski Sep 21, 2023
8e470be
fix: removes removeLeadingBytes function and using reslicing instead
wregulski Sep 21, 2023
9e8da04
fix: swaps if else branches to cover happy paths first
wregulski Sep 22, 2023
cb8f24e
feat: moves processedTxData outside of inputsTxData
wregulski Sep 22, 2023
60082c9
fix: changes bb variable name to more descriptive - hex bytes
wregulski Sep 22, 2023
de0274d
fix: replaces errors.New to format error with data
wregulski Sep 22, 2023
45ad06b
fix: adds more descriptive error while failed to extract offset
wregulski Sep 22, 2023
452adba
fix: changes parameter name from expectedHeight to height
wregulski Sep 22, 2023
ed19517
fix: adds more descriptive error while extractPathMap fails with extr…
wregulski Sep 22, 2023
78302ec
fix: moves height check to the beginning of the func and adds its val…
wregulski Sep 22, 2023
6a975f0
fix: extends error message when there is not enough bytes to extract …
wregulski Sep 22, 2023
18f8f69
feat: moves marker and version bytes count consts to the top of the file
wregulski Sep 22, 2023
9c664de
fix: adjusts invalid beef format error to be more descriptive
wregulski Sep 22, 2023
b8d6c1b
fix: replaces previousHeight with currentHeight
wregulski Sep 22, 2023
1be0767
feat: introduces new const - hashbytescount to replace 32 bytes magic…
wregulski Sep 22, 2023
3dcf7cf
fix: adjusts error messages after refactoring
wregulski Sep 25, 2023
ca1ac96
feat: adds test cases only for the main method
wregulski Sep 25, 2023
b71e928
fix: markes bytes count as private
wregulski Sep 25, 2023
a0a57ed
feat: adds data to transaction
wregulski Sep 25, 2023
9337521
fix:removes unnecessary if
wregulski Sep 25, 2023
347cd46
feat: gets the result and tries to assert it to nil in unhappypaths
wregulski Sep 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions p2p_beef_tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package paymail

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

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

type CompoundMerklePath []map[string]uint64

type CMPSlice []CompoundMerklePath

const (
BEEFMarkerPart1 = 0xBE
BEEFMarkerPart2 = 0xEF
)

const (
HasNoCMP = 0x00
HasCMP = 0x01
)

const (
hashBytesCount = 32
markerBytesCount = 2
versionBytesCount = 2
)

type TxData struct {
Transaction *bt.Tx
PathIndex *bt.VarInt
}

type DecodedBEEF struct {
CMPSlice CMPSlice
InputsTxData []TxData
ProcessedTxData TxData
}

func DecodeBEEF(beefHex string) (*DecodedBEEF, error) {
beefBytes, err := extractBytesWithoutVersionAndMarker(beefHex)
if err != nil {
return nil, err
}

cmpSlice, remainingBytes, err := decodeCMPSliceFromStream(beefBytes)
if err != nil {
return nil, err
}

transactions, err := decodeTransactionsWithPathIndexes(remainingBytes)
if err != nil {
return nil, err
}

if len(transactions) < 2 {
return nil, errors.New("not enough transactions provided to decode BEEF")
}

// get the last transaction as the processed transaction - it should be the last one because of khan's ordering
processedTx := transactions[len(transactions)-1]

transactions = transactions[:len(transactions)-1]

return &DecodedBEEF{
CMPSlice: cmpSlice,
InputsTxData: transactions,
ProcessedTxData: processedTx,
}, nil
}

func decodeCMPSliceFromStream(hexBytes []byte) (CMPSlice, []byte, error) {
if len(hexBytes) == 0 {
return nil, nil, errors.New("cannot decode cmp slice from stream - no bytes provided")
}

nCMPs, bytesUsed := bt.NewVarIntFromBytes(hexBytes)
hexBytes = hexBytes[bytesUsed:]
arkadiuszos4chain marked this conversation as resolved.
Show resolved Hide resolved

var cmpPaths []CompoundMerklePath
for i := 0; i < int(nCMPs); i++ {
cmp, bytesUsedToDecodeCMP, err := NewCMPFromStream(hexBytes)
if err != nil {
return nil, nil, err
}

cmpPaths = append(cmpPaths, cmp)
hexBytes = hexBytes[bytesUsedToDecodeCMP:]
}

cmpSlice := CMPSlice(cmpPaths)

return cmpSlice, hexBytes, nil
}

func NewCMPFromStream(hexBytes []byte) (CompoundMerklePath, int, error) {
dorzepowski marked this conversation as resolved.
Show resolved Hide resolved
height, bytesUsed, err := extractHeight(hexBytes)
if err != nil {
return nil, 0, err
}
hexBytes = hexBytes[bytesUsed:]

var cmp CompoundMerklePath
currentHeight := height
bytesUsedToDecodeCMP := bytesUsed

for currentHeight >= 0 {
var pathMap map[string]uint64

pathMap, bytesUsed, err = extractPathMap(hexBytes, currentHeight)
if err != nil {
return nil, 0, err
}

cmp = append(cmp, pathMap)
hexBytes = hexBytes[bytesUsed:]

currentHeight--
bytesUsedToDecodeCMP += bytesUsed
}

return cmp, bytesUsedToDecodeCMP, nil
}

func decodeTransactionsWithPathIndexes(bytes []byte) ([]TxData, error) {
nTransactions, offset := bt.NewVarIntFromBytes(bytes)
bytes = bytes[offset:]

var transactions []TxData

for i := 0; i < int(nTransactions); i++ {
tx, offset, err := bt.NewTxFromStream(bytes)
if err != nil {
return nil, err
}
bytes = bytes[offset:]

var pathIndex *bt.VarInt
if bytes[0] == HasCMP {
value, offset := bt.NewVarIntFromBytes(bytes[1:])
pathIndex = &value
bytes = bytes[1+offset:]
} else if bytes[0] == HasNoCMP {
bytes = bytes[1:]
} else {
return nil, fmt.Errorf("invalid HasCMP flag for transaction at index %d", i)
}

transactions = append(transactions, TxData{
Transaction: tx,
PathIndex: pathIndex,
})
}

return transactions, nil
}

func extractHeight(hexBytes []byte) (int, int, error) {
if len(hexBytes) < 1 {
return 0, 0, errors.New("insufficient bytes to extract height of compount merkle path")
}
height := int(hexBytes[0])
if height > 64 {
return 0, 0, errors.New("height exceeds maximum allowed value of 64")
}
return height, 1, nil
}

func extractPathMap(hexBytes []byte, height int) (map[string]uint64, int, error) {
if len(hexBytes) < 1 {
return nil, 0, fmt.Errorf("insufficient bytes to extract Compound Merkle Path at height %d", height)
}

nLeaves, nLeavesBytesUsed := bt.NewVarIntFromBytes(hexBytes)
bytesUsed := nLeavesBytesUsed
var pathMap = make(map[string]uint64)

for i := 0; i < int(nLeaves); i++ {
if len(hexBytes[bytesUsed:]) < 1 {
return nil, 0, fmt.Errorf("insufficient bytes to extract index %d leaf of %d leaves at %d height", i, int(nLeaves), height)
}

offsetValue, offsetBytesUsed := bt.NewVarIntFromBytes(hexBytes[bytesUsed:])
bytesUsed += offsetBytesUsed

if len(hexBytes[bytesUsed:]) < hashBytesCount {
return nil, 0, fmt.Errorf("insufficient bytes to extract hash of path with offset %d at height %d", offsetValue, height)
}

hash := hex.EncodeToString(hexBytes[bytesUsed : bytesUsed+hashBytesCount])
bytesUsed += hashBytesCount

pathMap[hash] = uint64(offsetValue)
}

return pathMap, bytesUsed, nil
}

func extractBytesWithoutVersionAndMarker(hexStream string) ([]byte, error) {
bytes, err := hex.DecodeString(hexStream)
if err != nil {
return nil, errors.New("invalid beef hex stream")
}
if len(bytes) < 4 {
return nil, errors.New("invalid beef hex stream")
}

// removes version bytes
bytes = bytes[versionBytesCount:]
err = validateMarker(bytes)
if err != nil {
return nil, err
}

// removes marker bytes
bytes = bytes[markerBytesCount:]

return bytes, nil
}

func validateMarker(bytes []byte) error {
if bytes[0] != BEEFMarkerPart1 || bytes[1] != BEEFMarkerPart2 {
return errors.New("invalid format of transaction, BEEF marker not found")
}

return nil
}
Loading
Loading