From b8265555b69c1d11a3fa5229cb1310ca18bae464 Mon Sep 17 00:00:00 2001 From: vitalibalashka Date: Tue, 24 Oct 2023 16:11:52 +0200 Subject: [PATCH] feat: calculate and store BUMP in DB --- definitions.go | 2 + model_bump.go | 58 ++++++++++++++++++++++++++++ model_merkle_proof.go | 23 +++++++++++ model_merkle_proof_test.go | 68 +++++++++++++++++++++++++++++++++ model_sync_transactions.go | 3 ++ model_transactions.go | 78 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+) create mode 100644 model_bump.go diff --git a/definitions.go b/definitions.go index 0e2d874b..73931ccf 100644 --- a/definitions.go +++ b/definitions.go @@ -111,6 +111,8 @@ const ( xPubMetadataField = "xpub_metadata" blockHeightField = "block_height" blockHashField = "block_hash" + merkleProofField = "merkle_proof" + bumpField = "bump" // Universal statuses statusCanceled = "canceled" diff --git a/model_bump.go b/model_bump.go new file mode 100644 index 00000000..9d31e4d8 --- /dev/null +++ b/model_bump.go @@ -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 +} diff --git a/model_merkle_proof.go b/model_merkle_proof.go index ac1f958f..807509a2 100644 --- a/model_merkle_proof.go +++ b/model_merkle_proof.go @@ -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 +} diff --git a/model_merkle_proof_test.go b/model_merkle_proof_test.go index 53e7683c..8b877b4a 100644 --- a/model_merkle_proof_test.go +++ b/model_merkle_proof_test.go @@ -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) + }) +} diff --git a/model_sync_transactions.go b/model_sync_transactions.go index 68405eb3..2e27640c 100644 --- a/model_sync_transactions.go +++ b/model_sync_transactions.go @@ -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 diff --git a/model_transactions.go b/model_transactions.go index 1ae9f8b5..5d7aee23 100644 --- a/model_transactions.go +++ b/model_transactions.go @@ -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"` @@ -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) } @@ -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 @@ -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 +}