mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-04-30 12:41:59 +08:00
fix: sanity refactor.
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -46,3 +46,9 @@ type Event struct {
|
||||
BlockHash string
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
type MetaData struct {
|
||||
ExpectedTotalAmountDiscounted int64
|
||||
ReportedTotalAmount int64
|
||||
PaidTotalAmount int64
|
||||
}
|
||||
|
||||
51
modules/nodesale/internal/validator/delegate/validator.go
Normal file
51
modules/nodesale/internal/validator/delegate/validator.go
Normal 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
|
||||
}
|
||||
281
modules/nodesale/internal/validator/purchase/validator.go
Normal file
281
modules/nodesale/internal/validator/purchase/validator.go
Normal 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
|
||||
}
|
||||
96
modules/nodesale/internal/validator/validator.go
Normal file
96
modules/nodesale/internal/validator/validator.go
Normal 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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user