Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Commit

Permalink
feat: calculate and store BUMP in DB
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalibalashka authored and witalij-4chain committed Oct 25, 2023
1 parent 9540f06 commit 15eb374
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 0 deletions.
2 changes: 2 additions & 0 deletions definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ const (
xPubMetadataField = "xpub_metadata"
blockHeightField = "block_height"
blockHashField = "block_hash"
merkleProofField = "merkle_proof"
bumpField = "bump"

// Universal statuses
statusCanceled = "canceled"
Expand Down
58 changes: 58 additions & 0 deletions model_bump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package bux

import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
"reflect"
)

// BUMP represents BUMP format
type BUMP struct {
BlockHeight uint64 `json:"blockHeight,string"`
Path []BUMPPathMap `json:"path"`
}

// BUMPPathMap represents map with pathes
type BUMPPathMap map[string]BUMPPathElement

// BUMPPathElement represents each BUMP path element
type BUMPPathElement struct {
Hash string `json:"hash,omitempty"`
TxId bool `json:"txid,omitempty"`
Duplicate bool `json:"duplicate,omitempty"`
}

// Scan scan value into Json, implements sql.Scanner interface
func (m *BUMP) Scan(value interface{}) error {
if value == nil {
return nil
}

xType := fmt.Sprintf("%T", value)
var byteValue []byte
if xType == ValueTypeString {
byteValue = []byte(value.(string))
} else {
byteValue = value.([]byte)
}
if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) {
return nil
}

return json.Unmarshal(byteValue, &m)
}

// Value return json value, implement driver.Valuer interface
func (m BUMP) Value() (driver.Value, error) {
if reflect.DeepEqual(m, BUMP{}) {
return nil, nil
}
marshal, err := json.Marshal(m)
if err != nil {
return nil, err
}

return string(marshal), nil
}
23 changes: 23 additions & 0 deletions model_merkle_proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,26 @@ func (m MerkleProof) Value() (driver.Value, error) {

return string(marshal), nil
}

func (m *MerkleProof) ToBUMP() BUMP {
bump := BUMP{}
height := len(m.Nodes)
if height == 0 {
return bump
}
path := make([]BUMPPathMap, 0)
txIdPath := make(BUMPPathMap, 2)
offset := m.Index
op := offsetPair(offset)
txIdPath[fmt.Sprint(offset)] = BUMPPathElement{Hash: m.TxOrID, TxId: true}
txIdPath[fmt.Sprint(op)] = BUMPPathElement{Hash: m.Nodes[0]}
path = append(path, txIdPath)
for i := 1; i < height; i++ {
p := make(BUMPPathMap, 1)
offset = parrentOffset(offset)
p[fmt.Sprint(offset)] = BUMPPathElement{Hash: m.Nodes[i]}
path = append(path, p)
}
bump.Path = path
return bump
}
68 changes: 68 additions & 0 deletions model_merkle_proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,71 @@ func TestMerkleProofModel_ToCompoundMerklePath(t *testing.T) {
assert.Nil(t, cmp)
})
}

// TestMerkleProofModel_ToBUMP will test the method ToBUMP()
func TestMerkleProofModel_ToBUMP(t *testing.T) {
t.Parallel()

t.Run("Valid Merkle Proof #1", func(t *testing.T) {
mp := MerkleProof{
Index: 1,
TxOrID: "txId",
Nodes: []string{"node0", "node1", "node2", "node3"},
}
expectedBUMP := BUMP{
Path: []BUMPPathMap{
{
"0": BUMPPathElement{Hash: "node0"},
"1": BUMPPathElement{Hash: "txId", TxId: true},
},
{
"1": BUMPPathElement{Hash: "node1"},
},
{
"1": BUMPPathElement{Hash: "node2"},
},
{
"1": BUMPPathElement{Hash: "node3"},
},
},
}
actualBUMP := mp.ToBUMP()
assert.Equal(t, expectedBUMP, actualBUMP)
})

t.Run("Valid Merkle Proof #2", func(t *testing.T) {
mp := MerkleProof{
Index: 14,
TxOrID: "txId",
Nodes: []string{"node0", "node1", "node2", "node3", "node4"},
}
expectedBUMP := BUMP{
Path: []BUMPPathMap{
{
"14": BUMPPathElement{Hash: "txId", TxId: true},
"15": BUMPPathElement{Hash: "node0"},
},
{
"6": BUMPPathElement{Hash: "node1"},
},
{
"2": BUMPPathElement{Hash: "node2"},
},
{
"0": BUMPPathElement{Hash: "node3"},
},
{
"1": BUMPPathElement{Hash: "node4"},
},
},
}
actualBUMP := mp.ToBUMP()
assert.Equal(t, expectedBUMP, actualBUMP)
})

t.Run("Empty Merkle Proof", func(t *testing.T) {
mp := MerkleProof{}
actualBUMP := mp.ToBUMP()
assert.Equal(t, BUMP{}, actualBUMP)
})
}
3 changes: 3 additions & 0 deletions model_sync_transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,9 @@ func processSyncTransaction(ctx context.Context, syncTx *SyncTransaction, transa
transaction.BlockHash = txInfo.BlockHash
transaction.BlockHeight = uint64(txInfo.BlockHeight)
transaction.MerkleProof = MerkleProof(*txInfo.MerkleProof)
bump := transaction.MerkleProof.ToBUMP()
bump.BlockHeight = transaction.BlockHeight
transaction.BUMP = bump

// Create status message
message := "transaction was found on-chain by " + chainstate.ProviderBroadcastClient
Expand Down
78 changes: 78 additions & 0 deletions model_transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Transaction struct {
XpubMetadata XpubMetadata `json:"-" toml:"xpub_metadata" gorm:"<-;type:json;xpub_id specific metadata" bson:"xpub_metadata,omitempty"`
XpubOutputValue XpubOutputValue `json:"-" toml:"xpub_output_value" gorm:"<-;type:json;xpub_id specific value" bson:"xpub_output_value,omitempty"`
MerkleProof MerkleProof `json:"merkle_proof" toml:"merkle_proof" yaml:"merkle_proof" gorm:"<-;type:text;comment:Merkle Proof payload from mAPI" bson:"merkle_proof,omitempty"`
BUMP BUMP `json:"bump" toml:"bump" yaml:"bump" gorm:"<-;type:text;comment:BSV Unified Merkle Path (BUMP) Format" bson:"bump,omitempty"`

// Virtual Fields
OutputValue int64 `json:"output_value" toml:"-" yaml:"-" gorm:"-" bson:"-,omitempty"`
Expand Down Expand Up @@ -832,6 +833,11 @@ func (m *Transaction) Migrate(client datastore.ClientInterface) error {
return err
}
}

err := m.migrateBUMP()
if err != nil {
return err
}

return client.IndexMetadata(tableName, xPubMetadataField)
}
Expand Down Expand Up @@ -892,6 +898,21 @@ func (m *Transaction) migrateMySQL(client datastore.ClientInterface, tableName s
return nil
}

func (m *Transaction) migrateBUMP() error {
ctx := context.Background()
txs, err := getTransactionsToCalculateBUMP(ctx, nil, WithClient(m.client))
if err != nil {
return err
}
for _, tx := range txs {
bump := tx.MerkleProof.ToBUMP()
bump.BlockHeight = tx.BlockHeight
tx.BUMP = bump
tx.Save(ctx)
}
return nil
}

// hasOneKnownDestination will check if the transaction has at least one known destination
//
// This is used to validate if an external transaction should be recorded into the engine
Expand Down Expand Up @@ -1001,3 +1022,60 @@ func processTransaction(ctx context.Context, transaction *Transaction) error {

return transaction.Save(ctx)
}

// getTransactionsByConditions will get the sync transactions to migrate
func getTransactionsByConditions(ctx context.Context, conditions map[string]interface{},
queryParams *datastore.QueryParams, opts ...ModelOps,
) ([]*Transaction, error) {
if queryParams == nil {
queryParams = &datastore.QueryParams{
OrderByField: createdAtField,
SortDirection: datastore.SortAsc,
}
} else if queryParams.OrderByField == "" || queryParams.SortDirection == "" {
queryParams.OrderByField = createdAtField
queryParams.SortDirection = datastore.SortAsc
}

// Get the records
var models []Transaction
if err := getModels(
ctx, NewBaseModel(ModelNameEmpty, opts...).Client().Datastore(),
&models, conditions, queryParams, defaultDatabaseReadTimeout,
); err != nil {
if errors.Is(err, datastore.ErrNoResults) {
return nil, nil
}
return nil, err
}

// Loop and enrich
txs := make([]*Transaction, 0)
for index := range models {
models[index].enrich(ModelTransaction, opts...)
txs = append(txs, &models[index])
}

return txs, nil
}

// getTransactionsToMigrateMerklePath will get the transactions where bump should be calculated
func getTransactionsToCalculateBUMP(ctx context.Context, queryParams *datastore.QueryParams,
opts ...ModelOps,
) ([]*Transaction, error) {
// Get the records by status
txs, err := getTransactionsByConditions(
ctx,
map[string]interface{}{
bumpField: nil,
merkleProofField: map[string]interface{}{
"$exists": true,
},
},
queryParams, opts...,
)
if err != nil {
return nil, err
}
return txs, nil
}

0 comments on commit 15eb374

Please sign in to comment.