fix: sanity refactor.

This commit is contained in:
Waris Aiemworawutikul
2024-07-19 19:05:06 +07:00
parent 38cf4c95a5
commit 28c42c02b7
8 changed files with 509 additions and 276 deletions

View File

@@ -1,54 +1,34 @@
package nodesale
import (
"bytes"
"context"
"encoding/hex"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
delegatevalidator "github.com/gaze-network/indexer-network/modules/nodesale/internal/validator/delegate"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/samber/lo"
)
func (p *Processor) processDelegate(ctx context.Context, qtx datagateway.NodesaleDataGatewayWithTx, block *types.Block, event nodesaleEvent) error {
valid := true
// valid := true
validator := delegatevalidator.New()
delegate := event.eventMessage.Delegate
nodeIds := make([]int32, len(delegate.NodeIDs))
for i, id := range delegate.NodeIDs {
nodeIds[i] = int32(id)
}
nodes, err := qtx.GetNodes(ctx, datagateway.GetNodesParams{
SaleBlock: int64(delegate.DeployID.Block),
SaleTxIndex: int32(delegate.DeployID.TxIndex),
NodeIds: nodeIds,
})
_, nodes, err := validator.NodesExist(ctx, qtx, delegate.DeployID, delegate.NodeIDs)
if err != nil {
return errors.Wrap(err, "Failed to get nodes")
return errors.Wrap(err, "cannot connect to datagateway")
}
if len(nodeIds) != len(nodes) {
valid = false
}
if valid {
for _, node := range nodes {
OwnerPublicKeyBytes, err := hex.DecodeString(node.OwnerPublicKey)
if err != nil {
valid = false
break
}
OwnerPublicKey, err := btcec.ParsePubKey(OwnerPublicKeyBytes)
if err != nil {
valid = false
break
}
xOnlyOwnerPublicKey := btcec.ToSerialized(OwnerPublicKey).SchnorrSerialized()
xOnlyTxPubKey := btcec.ToSerialized(event.txPubkey).SchnorrSerialized()
if !bytes.Equal(xOnlyOwnerPublicKey[:], xOnlyTxPubKey[:]) {
valid = false
break
}
for _, node := range nodes {
valid, err := validator.EqualXonlyPublicKey(node.OwnerPublicKey, event.txPubkey)
if err != nil {
logger.DebugContext(ctx, "Invalid public key", slogx.Error(err))
}
if !valid {
break
}
}
@@ -61,7 +41,7 @@ func (p *Processor) processDelegate(ctx context.Context, qtx datagateway.Nodesal
BlockTimestamp: block.Header.Timestamp,
BlockHash: event.transaction.BlockHash.String(),
BlockHeight: event.transaction.BlockHeight,
Valid: valid,
Valid: validator.Valid,
WalletAddress: p.pubkeyToPkHashAddress(event.txPubkey).EncodeAddress(),
Metadata: []byte("{}"),
})
@@ -69,7 +49,8 @@ func (p *Processor) processDelegate(ctx context.Context, qtx datagateway.Nodesal
return errors.Wrap(err, "Failed to insert event")
}
if valid {
if validator.Valid {
nodeIds := lo.Map(delegate.NodeIDs, func(item uint32, index int) int32 { return int32(item) })
_, err = qtx.SetDelegates(ctx, datagateway.SetDelegatesParams{
SaleBlock: int64(delegate.DeployID.Block),
SaleTxIndex: int32(delegate.DeployID.TxIndex),

View File

@@ -1,47 +1,26 @@
package nodesale
import (
"bytes"
"context"
"encoding/hex"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"google.golang.org/protobuf/encoding/protojson"
)
func (p *Processor) processDeploy(ctx context.Context, qtx datagateway.NodesaleDataGatewayWithTx, block *types.Block, event nodesaleEvent) error {
valid := true
deploy := event.eventMessage.Deploy
sellerPubKeyBytes, err := hex.DecodeString(deploy.SellerPublicKey)
validator := validator.New()
_, err := validator.EqualXonlyPublicKey(deploy.SellerPublicKey, event.txPubkey)
if err != nil {
valid = false
}
if valid {
sellerPubKey, err := btcec.ParsePubKey(sellerPubKeyBytes)
if err != nil {
valid = false
}
xOnlySellerPubKey := btcec.ToSerialized(sellerPubKey).SchnorrSerialized()
xOnlyTxPubKey := btcec.ToSerialized(event.txPubkey).SchnorrSerialized()
if valid && !bytes.Equal(xOnlySellerPubKey[:], xOnlyTxPubKey[:]) {
valid = false
}
}
tiers := make([][]byte, len(deploy.Tiers))
for i, tier := range deploy.Tiers {
tierJson, err := protojson.Marshal(tier)
if err != nil {
return errors.Wrap(err, "Failed to parse tiers to json")
}
tiers[i] = tierJson
logger.DebugContext(ctx, "Invalid public key", slogx.Error(err))
}
err = qtx.AddEvent(ctx, datagateway.AddEventParams{
@@ -53,14 +32,22 @@ func (p *Processor) processDeploy(ctx context.Context, qtx datagateway.NodesaleD
BlockTimestamp: block.Header.Timestamp,
BlockHash: event.transaction.BlockHash.String(),
BlockHeight: event.transaction.BlockHeight,
Valid: valid,
Valid: validator.Valid,
WalletAddress: p.pubkeyToPkHashAddress(event.txPubkey).EncodeAddress(),
Metadata: []byte("{}"),
})
if err != nil {
return errors.Wrap(err, "Failed to insert event")
}
if valid {
if validator.Valid {
tiers := make([][]byte, len(deploy.Tiers))
for i, tier := range deploy.Tiers {
tierJson, err := protojson.Marshal(tier)
if err != nil {
return errors.Wrap(err, "Failed to parse tiers to json")
}
tiers[i] = tierJson
}
err = qtx.AddNodesale(ctx, datagateway.AddNodesaleParams{
BlockHeight: event.transaction.BlockHeight,
TxIndex: int32(event.transaction.Index),

View File

@@ -14,7 +14,11 @@ func TestDeployInvalid(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
prvKey, _ := btcec.NewPrivateKey()
prvKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
strangerKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
strangerPubkeyHex := hex.EncodeToString(strangerKey.PubKey().SerializeCompressed())
sellerWallet := p.pubkeyToPkHashAddress(prvKey.PubKey())
message := &protobuf.NodeSaleEvent{
Action: protobuf.Action_ACTION_DEPLOY,
@@ -34,7 +38,7 @@ func TestDeployInvalid(t *testing.T) {
MaxPerAddress: 100,
},
},
SellerPublicKey: "0102030405",
SellerPublicKey: strangerPubkeyHex,
MaxPerAddress: 100,
MaxDiscountPercentage: 50,
SellerWallet: sellerWallet.EncodeAddress(),

View File

@@ -46,3 +46,9 @@ type Event struct {
BlockHash string
Metadata []byte
}
type MetaData struct {
ExpectedTotalAmountDiscounted int64
ReportedTotalAmount int64
PaidTotalAmount int64
}

View File

@@ -0,0 +1,51 @@
package delegate
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"github.com/samber/lo"
)
type DelegateValidator struct {
validator.Validator
}
func New() *DelegateValidator {
return &DelegateValidator{
Validator: validator.Validator{Valid: true},
}
}
func (v *DelegateValidator) NodesExist(
ctx context.Context,
qtx datagateway.NodesaleDataGatewayWithTx,
deployId *protobuf.ActionID,
nodeIds []uint32,
) (bool, []entity.Node, error) {
if !v.Valid {
return false, nil, nil
}
nodes, err := qtx.GetNodes(ctx, datagateway.GetNodesParams{
SaleBlock: int64(deployId.Block),
SaleTxIndex: int32(deployId.TxIndex),
NodeIds: lo.Map(nodeIds, func(item uint32, index int) int32 { return int32(item) }),
})
if err != nil {
v.Valid = false
return v.Valid, nil, errors.Wrap(err, "Failed to get nodes")
}
if len(nodeIds) != len(nodes) {
v.Valid = false
return v.Valid, nil, nil
}
v.Valid = true
return v.Valid, nodes, nil
}

View File

@@ -0,0 +1,281 @@
package purchasevalidator
import (
"bytes"
"context"
"encoding/hex"
"slices"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type PurchaseValidator struct {
validator.Validator
}
func New() *PurchaseValidator {
return &PurchaseValidator{
Validator: validator.Validator{Valid: true},
}
}
func (v *PurchaseValidator) NodeSaleExists(ctx context.Context, qtx datagateway.NodesaleDataGatewayWithTx, payload *protobuf.PurchasePayload) (bool, *entity.NodeSale, error) {
if !v.Valid {
return false, nil, nil
}
// check node existed
deploys, err := qtx.GetNodesale(ctx, datagateway.GetNodesaleParams{
BlockHeight: int64(payload.DeployID.Block),
TxIndex: int32(payload.DeployID.TxIndex),
})
if err != nil {
v.Valid = false
return v.Valid, nil, errors.Wrap(err, "Failed to Get nodesale")
}
if len(deploys) < 1 {
v.Valid = false
return v.Valid, nil, nil
} else {
v.Valid = true
return v.Valid, &deploys[0], nil
}
}
func (v *PurchaseValidator) ValidTimestamp(deploy *entity.NodeSale, timestamp time.Time) bool {
if !v.Valid {
return false
}
if timestamp.Before(deploy.StartsAt) ||
timestamp.After(deploy.EndsAt) {
v.Valid = false
return v.Valid
}
v.Valid = true
return v.Valid
}
func (v *PurchaseValidator) WithinTimeoutBlock(payload *protobuf.PurchasePayload, blockHeight uint64) bool {
if !v.Valid {
return false
}
if payload.TimeOutBlock < blockHeight {
v.Valid = false
return v.Valid
}
v.Valid = true
return v.Valid
}
func (v *PurchaseValidator) VerifySignature(purchase *protobuf.ActionPurchase, deploy *entity.NodeSale) (bool, error) {
if !v.Valid {
return false, nil
}
payload := purchase.Payload
payloadBytes, _ := proto.Marshal(payload)
signatureBytes, _ := hex.DecodeString(purchase.SellerSignature)
signature, err := ecdsa.ParseSignature(signatureBytes)
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "cannot parse signature")
}
hash := chainhash.DoubleHashB(payloadBytes)
pubkeyBytes, _ := hex.DecodeString(deploy.SellerPublicKey)
pubKey, _ := btcec.ParsePubKey(pubkeyBytes)
verified := signature.Verify(hash[:], pubKey)
if !verified {
v.Valid = false
return v.Valid, nil
}
v.Valid = true
return v.Valid, nil
}
func (v *PurchaseValidator) ValidTiers(
payload *protobuf.PurchasePayload,
deploy *entity.NodeSale,
) (bool, []protobuf.Tier, []uint32, map[uint32]int32, error) {
if !v.Valid {
return false, nil, nil, nil, nil
}
tiers := make([]protobuf.Tier, len(deploy.Tiers))
buyingTiersCount := make([]uint32, len(tiers))
nodeIdToTier := make(map[uint32]int32)
for i, tierJson := range deploy.Tiers {
tier := &tiers[i]
err := protojson.Unmarshal(tierJson, tier)
if err != nil {
v.Valid = false
return v.Valid, nil, nil, nil, errors.Wrap(err, "Failed to decode tiers json")
}
}
slices.Sort(payload.NodeIDs)
var currentTier int32 = -1
var tierSum uint32 = 0
for _, nodeId := range payload.NodeIDs {
for nodeId >= tierSum && currentTier < int32(len(tiers)-1) {
currentTier++
tierSum += tiers[currentTier].Limit
}
if nodeId < tierSum {
buyingTiersCount[currentTier]++
nodeIdToTier[nodeId] = currentTier
} else {
v.Valid = false
return false, nil, nil, nil, nil
}
}
v.Valid = true
return v.Valid, tiers, buyingTiersCount, nodeIdToTier, nil
}
func (v *PurchaseValidator) ValidUnpurchasedNodes(
ctx context.Context,
qtx datagateway.NodesaleDataGatewayWithTx,
payload *protobuf.PurchasePayload,
) (bool, error) {
if !v.Valid {
return false, nil
}
// valid unpurchased node ID
nodeIds := make([]int32, len(payload.NodeIDs))
for i, id := range payload.NodeIDs {
nodeIds[i] = int32(id)
}
nodes, err := qtx.GetNodes(ctx, datagateway.GetNodesParams{
SaleBlock: int64(payload.DeployID.Block),
SaleTxIndex: int32(payload.DeployID.TxIndex),
NodeIds: nodeIds,
})
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "Failed to Get nodes")
}
if len(nodes) > 0 {
v.Valid = false
return false, nil
}
v.Valid = true
return true, nil
}
func (v *PurchaseValidator) ValidPaidAmount(
payload *protobuf.PurchasePayload,
deploy *entity.NodeSale,
txOuts []*types.TxOut,
tiers []protobuf.Tier,
buyingTiersCount []uint32,
network *chaincfg.Params,
) (bool, *entity.MetaData, error) {
if !v.Valid {
return false, nil, nil
}
sellerAddr, err := btcutil.DecodeAddress(deploy.SellerWallet, network) // default to mainnet
if err != nil {
v.Valid = false
return v.Valid, nil, errors.Wrap(err, "Cannot decode Sellerwallet")
}
var txPaid int64 = 0
meta := entity.MetaData{}
// get total amount paid to seller
for _, txOut := range txOuts {
_, txOutAddrs, _, _ := txscript.ExtractPkScriptAddrs(txOut.PkScript, network)
if len(txOutAddrs) == 1 && bytes.Equal(
[]byte(sellerAddr.EncodeAddress()),
[]byte(txOutAddrs[0].EncodeAddress()),
) {
txPaid += txOut.Value
}
}
meta.PaidTotalAmount = txPaid
meta.ReportedTotalAmount = payload.TotalAmountSat
// total amount paid is greater than report paid
if txPaid < payload.TotalAmountSat {
v.Valid = false
return v.Valid, nil, nil
}
// calculate total price
var totalPrice int64 = 0
for i := 0; i < len(tiers); i++ {
totalPrice += int64(buyingTiersCount[i] * tiers[i].PriceSat)
}
// report paid is greater than max discounted total price
maxDiscounted := totalPrice * (100 - int64(deploy.MaxDiscountPercentage))
decimal := maxDiscounted % 100
maxDiscounted /= 100
if decimal%100 >= 50 {
maxDiscounted++
}
meta.ExpectedTotalAmountDiscounted = maxDiscounted
if payload.TotalAmountSat < maxDiscounted {
v.Valid = false
return v.Valid, nil, nil
}
v.Valid = true
return v.Valid, &meta, nil
}
func (v *PurchaseValidator) WithinLimit(
ctx context.Context,
qtx datagateway.NodesaleDataGatewayWithTx,
payload *protobuf.PurchasePayload,
deploy *entity.NodeSale,
tiers []protobuf.Tier,
buyingTiersCount []uint32,
) (bool, error) {
if !v.Valid {
return false, nil
}
// check node limit
// get all selled by seller and owned by buyer
buyerOwnedNodes, err := qtx.GetNodesByOwner(ctx, datagateway.GetNodesByOwnerParams{
SaleBlock: deploy.BlockHeight,
SaleTxIndex: deploy.TxIndex,
OwnerPublicKey: payload.BuyerPublicKey,
})
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "Failed to GetNodesByOwner")
}
if len(buyerOwnedNodes)+len(payload.NodeIDs) > int(deploy.MaxPerAddress) {
v.Valid = false
return v.Valid, nil
}
// check limit
// count each tiers
// check limited for each tier
ownedTiersCount := make([]uint32, len(tiers))
for _, node := range buyerOwnedNodes {
ownedTiersCount[node.TierIndex]++
}
for i := 0; i < len(tiers); i++ {
if ownedTiersCount[i]+buyingTiersCount[i] > tiers[i].MaxPerAddress {
v.Valid = false
return v.Valid, nil
}
}
v.Valid = true
return v.Valid, nil
}

View File

@@ -0,0 +1,96 @@
package validator
import (
"bytes"
"context"
"encoding/hex"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
)
type Validator struct {
Valid bool
}
func New() *Validator {
return &Validator{
Valid: true,
}
}
func EqualXonlyPublicKey(valid bool, target string, expected *btcec.PublicKey) (bool, error) {
if !valid {
return false, nil
}
targetBytes, err := hex.DecodeString(target)
if err != nil {
return false, errors.Wrap(err, "cannot decode hexstring")
}
targetPubKey, err := btcec.ParsePubKey(targetBytes)
if err != nil {
return false, errors.Wrap(err, "cannot parse public key")
}
xOnlyTargetPubKey := btcec.ToSerialized(targetPubKey).SchnorrSerialized()
xOnlyExpectedPubKey := btcec.ToSerialized(expected).SchnorrSerialized()
return bytes.Equal(xOnlyTargetPubKey[:], xOnlyExpectedPubKey[:]), nil
}
func (v *Validator) EqualXonlyPublicKey(target string, expected *btcec.PublicKey) (bool, error) {
if !v.Valid {
return false, nil
}
targetBytes, err := hex.DecodeString(target)
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "cannot decode hexstring")
}
targetPubKey, err := btcec.ParsePubKey(targetBytes)
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "cannot parse public key")
}
xOnlyTargetPubKey := btcec.ToSerialized(targetPubKey).SchnorrSerialized()
xOnlyExpectedPubKey := btcec.ToSerialized(expected).SchnorrSerialized()
v.Valid = bytes.Equal(xOnlyTargetPubKey[:], xOnlyExpectedPubKey[:])
return v.Valid, nil
}
func (v *Validator) NodesExist(
ctx context.Context,
qtx datagateway.NodesaleDataGatewayWithTx,
deployId *protobuf.ActionID,
nodeIds []uint32,
) (bool, error) {
if !v.Valid {
return false, nil
}
nodeIdsInt32 := make([]int32, len(nodeIds))
for i, id := range nodeIds {
nodeIdsInt32[i] = int32(id)
}
nodes, err := qtx.GetNodes(ctx, datagateway.GetNodesParams{
SaleBlock: int64(deployId.Block),
SaleTxIndex: int32(deployId.TxIndex),
NodeIds: nodeIdsInt32,
})
if err != nil {
v.Valid = false
return v.Valid, errors.Wrap(err, "Failed to get nodes")
}
if len(nodeIds) != len(nodes) {
v.Valid = false
return v.Valid, nil
}
v.Valid = true
return v.Valid, nil
}

View File

@@ -1,238 +1,65 @@
package nodesale
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"slices"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/core/types"
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
purchasevalidator "github.com/gaze-network/indexer-network/modules/nodesale/internal/validator/purchase"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
)
type metaData struct {
ExpectedTotalAmountDiscounted int64
ReportedTotalAmount int64
PaidTotalAmount int64
}
func (p *Processor) processPurchase(ctx context.Context, qtx datagateway.NodesaleDataGatewayWithTx, block *types.Block, event nodesaleEvent) error {
valid := true
purchase := event.eventMessage.Purchase
payload := purchase.Payload
buyerPubkeyBytes, err := hex.DecodeString(payload.BuyerPublicKey)
validator := purchasevalidator.New()
_, err := validator.EqualXonlyPublicKey(payload.BuyerPublicKey, event.txPubkey)
if err != nil {
valid = false
logger.DebugContext(ctx, "Invalid public key", slogx.Error(err))
}
if valid {
buyerPubkey, err := btcec.ParsePubKey(buyerPubkeyBytes)
if err != nil {
valid = false
}
xOnlyBuyerPubkey := btcec.ToSerialized(buyerPubkey).SchnorrSerialized()
xOnlyTxPubKey := btcec.ToSerialized(event.txPubkey).SchnorrSerialized()
if valid && !bytes.Equal(xOnlyBuyerPubkey[:], xOnlyTxPubKey[:]) {
valid = false
}
_, deploy, err := validator.NodeSaleExists(ctx, qtx, payload)
if err != nil {
return errors.Wrap(err, "Cannot connect to datagateway")
}
var deploy *entity.NodeSale
if valid {
// check node existed
deploys, err := qtx.GetNodesale(ctx, datagateway.GetNodesaleParams{
BlockHeight: int64(payload.DeployID.Block),
TxIndex: int32(payload.DeployID.TxIndex),
})
if err != nil {
return errors.Wrap(err, "Failed to Get nodesale")
}
if len(deploys) < 1 {
valid = false
} else {
deploy = &deploys[0]
}
validator.ValidTimestamp(deploy, block.Header.Timestamp)
validator.WithinTimeoutBlock(payload, uint64(event.transaction.BlockHeight))
_, err = validator.VerifySignature(purchase, deploy)
if err != nil {
logger.DebugContext(ctx, "incorrect Signature format", slogx.Error(err))
}
if valid {
// check timestamp
timestamp := block.Header.Timestamp
if timestamp.Before(deploy.StartsAt) ||
timestamp.After(deploy.EndsAt) {
valid = false
}
_, tiers, buyingTiersCount, nodeIdToTier, err := validator.ValidTiers(payload, deploy)
if err != nil {
logger.DebugContext(ctx, "invalid nodesale tiers data", slogx.Error(err))
}
if valid {
if payload.TimeOutBlock < uint64(event.transaction.BlockHeight) {
valid = false
}
_, err = validator.ValidUnpurchasedNodes(ctx, qtx, payload)
if err != nil {
return errors.Wrap(err, "cannot connect to datagateway")
}
if valid {
// verified signature
payloadBytes, _ := proto.Marshal(payload)
signatureBytes, _ := hex.DecodeString(purchase.SellerSignature)
signature, err := ecdsa.ParseSignature(signatureBytes)
if err != nil {
valid = false
}
if valid {
hash := chainhash.DoubleHashB(payloadBytes)
pubkeyBytes, _ := hex.DecodeString(deploy.SellerPublicKey)
pubKey, _ := btcec.ParsePubKey(pubkeyBytes)
verified := signature.Verify(hash[:], pubKey)
if !verified {
valid = false
}
}
_, meta, err := validator.ValidPaidAmount(payload, deploy, event.transaction.TxOut, tiers, buyingTiersCount, p.network.ChainParams())
if err != nil {
logger.DebugContext(ctx, "Invalid seller address", slogx.Error(err))
}
var tiers []protobuf.Tier
var buyingTiersCount []uint32
nodeIdToTier := make(map[uint32]int32, 1)
if valid {
// valid nodeID tier
tiers = make([]protobuf.Tier, len(deploy.Tiers))
for i, tierJson := range deploy.Tiers {
tier := &tiers[i]
err := protojson.Unmarshal(tierJson, tier)
if err != nil {
return errors.Wrap(err, "Failed to decode tiers json")
}
}
slices.Sort(payload.NodeIDs)
buyingTiersCount = make([]uint32, len(tiers))
var currentTier int32 = -1
var tierSum uint32 = 0
for _, nodeId := range payload.NodeIDs {
for nodeId >= tierSum && currentTier < int32(len(tiers)-1) {
currentTier++
tierSum += tiers[currentTier].Limit
}
if nodeId < tierSum {
buyingTiersCount[currentTier]++
nodeIdToTier[nodeId] = currentTier
} else {
valid = false
}
}
}
if valid {
// valid unpurchased node ID
nodeIds := make([]int32, len(payload.NodeIDs))
for i, id := range payload.NodeIDs {
nodeIds[i] = int32(id)
}
nodes, err := qtx.GetNodes(ctx, datagateway.GetNodesParams{
SaleBlock: int64(payload.DeployID.Block),
SaleTxIndex: int32(payload.DeployID.TxIndex),
NodeIds: nodeIds,
})
if err != nil {
return errors.Wrap(err, "Failed to Get nodes")
}
if len(nodes) > 0 {
valid = false
}
}
var sellerAddr btcutil.Address
if valid {
sellerAddr, err = btcutil.DecodeAddress(deploy.SellerWallet, p.network.ChainParams())
if err != nil {
valid = false
}
}
var txPaid int64 = 0
meta := metaData{}
if valid {
// get total amount paid to seller
for _, txOut := range event.transaction.TxOut {
_, txOutAddrs, _, _ := txscript.ExtractPkScriptAddrs(txOut.PkScript, p.network.ChainParams())
if len(txOutAddrs) == 1 && bytes.Equal(
[]byte(sellerAddr.EncodeAddress()),
[]byte(txOutAddrs[0].EncodeAddress()),
) {
txPaid += txOut.Value
}
}
meta.PaidTotalAmount = txPaid
meta.ReportedTotalAmount = payload.TotalAmountSat
// total amount paid is greater than report paid
if txPaid < payload.TotalAmountSat {
valid = false
}
// calculate total price
var totalPrice int64 = 0
for i := 0; i < len(tiers); i++ {
totalPrice += int64(buyingTiersCount[i] * tiers[i].PriceSat)
}
// report paid is greater than max discounted total price
maxDiscounted := totalPrice * (100 - int64(deploy.MaxDiscountPercentage))
decimal := maxDiscounted % 100
maxDiscounted /= 100
if decimal%100 >= 50 {
maxDiscounted++
}
meta.ExpectedTotalAmountDiscounted = maxDiscounted
if payload.TotalAmountSat < maxDiscounted {
valid = false
}
}
var buyerOwnedNodes []entity.Node
if valid {
var err error
// check node limit
// get all selled by seller and owned by buyer
buyerOwnedNodes, err = qtx.GetNodesByOwner(ctx, datagateway.GetNodesByOwnerParams{
SaleBlock: deploy.BlockHeight,
SaleTxIndex: deploy.TxIndex,
OwnerPublicKey: payload.BuyerPublicKey,
})
if err != nil {
return errors.Wrap(err, "Failed to GetNodesByOwner")
}
if len(buyerOwnedNodes)+len(payload.NodeIDs) > int(deploy.MaxPerAddress) {
valid = false
}
}
if valid {
// check limit
// count each tiers
// check limited for each tier
ownedTiersCount := make([]uint32, len(tiers))
for _, node := range buyerOwnedNodes {
ownedTiersCount[node.TierIndex]++
}
for i := 0; i < len(tiers); i++ {
if ownedTiersCount[i]+buyingTiersCount[i] > tiers[i].MaxPerAddress {
valid = false
break
}
}
_, err = validator.WithinLimit(ctx, qtx, payload, deploy, tiers, buyingTiersCount)
if err != nil {
return errors.Wrap(err, "Cannot connect to datagateway")
}
metaDataBytes, _ := json.Marshal(meta)
if meta == nil {
metaDataBytes = []byte("{}")
}
err = qtx.AddEvent(ctx, datagateway.AddEventParams{
TxHash: event.transaction.TxHash.String(),
@@ -243,7 +70,7 @@ func (p *Processor) processPurchase(ctx context.Context, qtx datagateway.Nodesal
BlockTimestamp: block.Header.Timestamp,
BlockHash: event.transaction.BlockHash.String(),
BlockHeight: event.transaction.BlockHeight,
Valid: valid,
Valid: validator.Valid,
WalletAddress: p.pubkeyToPkHashAddress(event.txPubkey).EncodeAddress(),
Metadata: metaDataBytes,
})
@@ -251,7 +78,7 @@ func (p *Processor) processPurchase(ctx context.Context, qtx datagateway.Nodesal
return errors.Wrap(err, "Failed to insert event")
}
if valid {
if validator.Valid {
// add to node
for _, nodeId := range payload.NodeIDs {
err := qtx.AddNode(ctx, datagateway.AddNodeParams{