mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00
[NOD-495] Remove txgen to separate repository
This commit is contained in:
parent
07651e51c8
commit
97240a108e
@ -1,45 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/rpcclient"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/kaspanet/kaspad/wire"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type txgenClient struct {
|
||||
*rpcclient.Client
|
||||
onBlockAdded chan *blockAddedMsg
|
||||
}
|
||||
|
||||
type blockAddedMsg struct {
|
||||
chainHeight uint64
|
||||
header *wire.BlockHeader
|
||||
txs []*util.Tx
|
||||
}
|
||||
|
||||
func newTxgenClient(connCfg *rpcclient.ConnConfig) (*txgenClient, error) {
|
||||
client := &txgenClient{
|
||||
onBlockAdded: make(chan *blockAddedMsg),
|
||||
}
|
||||
notificationHandlers := &rpcclient.NotificationHandlers{
|
||||
OnFilteredBlockAdded: func(height uint64, header *wire.BlockHeader,
|
||||
txs []*util.Tx) {
|
||||
client.onBlockAdded <- &blockAddedMsg{
|
||||
chainHeight: height,
|
||||
header: header,
|
||||
txs: txs,
|
||||
}
|
||||
},
|
||||
}
|
||||
var err error
|
||||
client.Client, err = rpcclient.New(connCfg, notificationHandlers)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("Error connecting to address %s: %s", connCfg.Host, err)
|
||||
}
|
||||
|
||||
if err = client.NotifyBlocks(); err != nil {
|
||||
return nil, errors.Errorf("Error while registering client %s for block notifications: %s", client.Host(), err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/pkg/errors"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLogFilename = "txgen.log"
|
||||
defaultErrLogFilename = "txgen_err.log"
|
||||
)
|
||||
|
||||
var (
|
||||
// Default configuration options
|
||||
defaultHomeDir = util.AppDataDir("txgen", false)
|
||||
defaultLogFile = filepath.Join(defaultHomeDir, defaultLogFilename)
|
||||
defaultErrLogFile = filepath.Join(defaultHomeDir, defaultErrLogFilename)
|
||||
defaultTargetNumberOfOutputs uint64 = 1
|
||||
defaultTargetNumberOfInputs uint64 = 1
|
||||
)
|
||||
|
||||
type configFlags struct {
|
||||
Address string `long:"address" description:"An address to a JSON-RPC endpoints" required:"true"`
|
||||
PrivateKey string `long:"private-key" description:"Private key" required:"true"`
|
||||
SecondaryAddress string `long:"secondary-address" description:"An address that gets paid once per txgen run"`
|
||||
CertificatePath string `long:"cert" description:"Path to certificate accepted by JSON-RPC endpoint"`
|
||||
DisableTLS bool `long:"notls" description:"Disable TLS"`
|
||||
TxInterval uint64 `long:"tx-interval" description:"Transaction emission interval (in milliseconds)"`
|
||||
TargetNumberOfOutputs uint64 `long:"num-outputs" description:"Target number of transaction outputs (with some randomization)"`
|
||||
TargetNumberOfInputs uint64 `long:"num-inputs" description:"Target number of transaction inputs (with some randomization)"`
|
||||
AveragePayloadSize uint64 `long:"payload-size" description:"Average size of transaction payload"`
|
||||
AverageGasFraction float64 `long:"gas-fraction" description:"The average portion of gas from the gas limit"`
|
||||
AverageFeeRate float64 `long:"fee-rate" description:"Average coins per gram fee rate"`
|
||||
}
|
||||
|
||||
func parseConfig() (*configFlags, error) {
|
||||
cfg := &configFlags{}
|
||||
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
|
||||
_, err := parser.Parse()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.CertificatePath == "" && !cfg.DisableTLS {
|
||||
return nil, errors.New("--notls has to be disabled if --cert is used")
|
||||
}
|
||||
|
||||
if cfg.CertificatePath != "" && cfg.DisableTLS {
|
||||
return nil, errors.New("--cert should be omitted if --notls is used")
|
||||
}
|
||||
|
||||
if cfg.AverageGasFraction >= 1 || cfg.AverageGasFraction < 0 {
|
||||
return nil, errors.New("--gas-fraction should be between 0 and 1")
|
||||
}
|
||||
|
||||
if cfg.TargetNumberOfOutputs < 0 {
|
||||
return nil, errors.New("--num-outputs should be positive")
|
||||
}
|
||||
|
||||
if cfg.TargetNumberOfInputs < 0 {
|
||||
return nil, errors.New("--num-inputs should be positive")
|
||||
}
|
||||
|
||||
if cfg.TargetNumberOfOutputs == 0 {
|
||||
cfg.TargetNumberOfOutputs = defaultTargetNumberOfOutputs
|
||||
}
|
||||
|
||||
if cfg.TargetNumberOfInputs == 0 {
|
||||
cfg.TargetNumberOfInputs = defaultTargetNumberOfInputs
|
||||
}
|
||||
|
||||
initLog(defaultLogFile, defaultErrLogFile)
|
||||
|
||||
return cfg, nil
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/rpcclient"
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func connectToServer(cfg *configFlags) (*txgenClient, error) {
|
||||
var cert []byte
|
||||
if !cfg.DisableTLS {
|
||||
var err error
|
||||
cert, err = ioutil.ReadFile(cfg.CertificatePath)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("Error reading certificates file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
connCfg := &rpcclient.ConnConfig{
|
||||
Host: cfg.Address,
|
||||
Endpoint: "ws",
|
||||
User: "user",
|
||||
Pass: "pass",
|
||||
DisableTLS: cfg.DisableTLS,
|
||||
}
|
||||
|
||||
if !cfg.DisableTLS {
|
||||
connCfg.Certificates = cert
|
||||
}
|
||||
|
||||
client, err := newTxgenClient(connCfg)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("Error connecting to address %s: %s", cfg.Address, err)
|
||||
}
|
||||
|
||||
log.Infof("Connected to server %s", cfg.Address)
|
||||
|
||||
return client, nil
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.13-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
WORKDIR /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
RUN apk add --no-cache curl git
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cd cmd/txgen && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o txgen .
|
||||
|
||||
# --- multistage docker build: stage #2: runtime image
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
|
||||
COPY --from=build /go/src/github.com/kaspanet/kaspad/cmd/txgen/txgen /app/
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["/app/txgen"]
|
@ -1,9 +0,0 @@
|
||||
1. To build docker image invoke following command from btcd root directory:
|
||||
docker build -t txgen -f ./cmd/txgen/docker/Dockerfile .
|
||||
|
||||
2. To run:
|
||||
a. create folder ~/.btcd/txgen with the following files:
|
||||
rpc.cert - certificate file that all rpc nodes accept
|
||||
addresses - list of node addresses in the format [hostname]:[port]. One node per line
|
||||
b. run:
|
||||
docker run -v ~/.btcd:/root/.btcd -t txgen
|
@ -1,27 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/logs"
|
||||
"github.com/kaspanet/kaspad/util/panics"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
backendLog = logs.NewBackend()
|
||||
log = backendLog.Logger("TXGN")
|
||||
spawn = panics.GoroutineWrapperFunc(log)
|
||||
)
|
||||
|
||||
func initLog(logFile, errLogFile string) {
|
||||
err := backendLog.AddLogFile(logFile, logs.LevelTrace)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", logFile, logs.LevelTrace, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = backendLog.AddLogFile(errLogFile, logs.LevelWarn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", errLogFile, logs.LevelWarn, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/dagconfig"
|
||||
"github.com/kaspanet/kaspad/ecc"
|
||||
"github.com/kaspanet/kaspad/signal"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/kaspanet/kaspad/util/base58"
|
||||
"github.com/kaspanet/kaspad/util/panics"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
activeNetParams = &dagconfig.DevNetParams
|
||||
p2pkhAddress util.Address
|
||||
secondaryAddress util.Address
|
||||
privateKey *ecc.PrivateKey
|
||||
)
|
||||
|
||||
// privateKeyToP2pkhAddress generates p2pkh address from private key.
|
||||
func privateKeyToP2pkhAddress(key *ecc.PrivateKey, net *dagconfig.Params) (util.Address, error) {
|
||||
return util.NewAddressPubKeyHashFromPublicKey(key.PubKey().SerializeCompressed(), net.Prefix)
|
||||
}
|
||||
|
||||
func main() {
|
||||
defer panics.HandlePanic(log, nil, nil)
|
||||
|
||||
cfg, err := parseConfig()
|
||||
if err != nil {
|
||||
panic(errors.Errorf("Error parsing command-line arguments: %s", err))
|
||||
}
|
||||
|
||||
privateKeyBytes := base58.Decode(cfg.PrivateKey)
|
||||
privateKey, _ = ecc.PrivKeyFromBytes(ecc.S256(), privateKeyBytes)
|
||||
|
||||
p2pkhAddress, err = privateKeyToP2pkhAddress(privateKey, activeNetParams)
|
||||
if err != nil {
|
||||
panic(errors.Errorf("Failed to get P2PKH address from private key: %s", err))
|
||||
}
|
||||
|
||||
log.Infof("P2PKH address for private key: %s\n", p2pkhAddress)
|
||||
|
||||
if cfg.SecondaryAddress != "" {
|
||||
secondaryAddress, err = util.DecodeAddress(cfg.SecondaryAddress, activeNetParams.Prefix)
|
||||
if err != nil {
|
||||
panic(errors.Errorf("Failed to decode secondary address %s: %s", cfg.SecondaryAddress, err))
|
||||
}
|
||||
}
|
||||
|
||||
client, err := connectToServer(cfg)
|
||||
if err != nil {
|
||||
panic(errors.Errorf("Error connecting to servers: %s", err))
|
||||
}
|
||||
defer disconnect(client)
|
||||
|
||||
spawn(func() {
|
||||
err := txLoop(client, cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
interrupt := signal.InterruptListener()
|
||||
<-interrupt
|
||||
}
|
||||
|
||||
func disconnect(client *txgenClient) {
|
||||
log.Infof("Disconnecting client")
|
||||
client.Disconnect()
|
||||
}
|
@ -1,528 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"github.com/pkg/errors"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/blockdag"
|
||||
"github.com/kaspanet/kaspad/rpcmodel"
|
||||
"github.com/kaspanet/kaspad/txscript"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/kaspanet/kaspad/util/daghash"
|
||||
"github.com/kaspanet/kaspad/util/subnetworkid"
|
||||
"github.com/kaspanet/kaspad/wire"
|
||||
)
|
||||
|
||||
const (
|
||||
// Those constants should be updated, when monetary policy changed
|
||||
minSpendableAmount uint64 = 10000
|
||||
maxSpendableAmount uint64 = 5 * minSpendableAmount
|
||||
minTxFee uint64 = 3000
|
||||
|
||||
// spendSize is the largest number of bytes of a sigScript
|
||||
// which spends a p2pkh output: OP_DATA_73 <sig> OP_DATA_33 <pubkey>
|
||||
spendSize uint64 = 1 + 73 + 1 + 33
|
||||
// Value 8 bytes + serialized varint size for the length of ScriptPubKey +
|
||||
// ScriptPubKey bytes.
|
||||
outputSize uint64 = 8 + 1 + 25
|
||||
|
||||
txLifeSpan = 1000
|
||||
requiredConfirmations = 10
|
||||
approximateConfirmationsForCoinbaseMaturity = 150
|
||||
searchRawTransactionResultCount = 1000
|
||||
searchRawTransactionMaxResults = 5000
|
||||
txMaxQueueLength = 10000
|
||||
maxResendDepth = 500
|
||||
minSecondaryTxAmount = 100000000
|
||||
)
|
||||
|
||||
type walletTransaction struct {
|
||||
tx *util.Tx
|
||||
chainHeight uint64
|
||||
checkConfirmationCountdown uint64
|
||||
confirmed bool
|
||||
}
|
||||
|
||||
type utxoSet map[wire.Outpoint]*wire.TxOut
|
||||
|
||||
func isDust(value uint64) bool {
|
||||
return value < minSpendableAmount+minTxFee
|
||||
}
|
||||
|
||||
var (
|
||||
random = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
primaryScriptPubKey []byte
|
||||
secondaryScriptPubKey []byte
|
||||
sentToSecondaryAddress bool
|
||||
)
|
||||
|
||||
// txLoop performs main loop of transaction generation
|
||||
func txLoop(client *txgenClient, cfg *configFlags) error {
|
||||
filterAddresses := []util.Address{p2pkhAddress}
|
||||
var err error
|
||||
primaryScriptPubKey, err = txscript.PayToAddrScript(p2pkhAddress)
|
||||
if err != nil {
|
||||
return errors.Errorf("failed to generate primaryScriptPubKey to address: %s", err)
|
||||
}
|
||||
|
||||
if secondaryAddress != nil {
|
||||
secondaryScriptPubKey, err = txscript.PayToAddrScript(secondaryAddress)
|
||||
if err != nil {
|
||||
return errors.Errorf("failed to generate primaryScriptPubKey to address: %s", err)
|
||||
}
|
||||
|
||||
filterAddresses = append(filterAddresses, secondaryAddress)
|
||||
}
|
||||
|
||||
err = client.LoadTxFilter(true, filterAddresses, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gasLimitMap := make(map[subnetworkid.SubnetworkID]uint64)
|
||||
gasLimitMap[*subnetworkid.SubnetworkIDNative] = 0
|
||||
|
||||
walletUTXOSet, walletTxs, err := getInitialUTXOSetAndWalletTxs(client, gasLimitMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txChan := make(chan *wire.MsgTx, txMaxQueueLength)
|
||||
spawn(func() {
|
||||
err := sendTransactionLoop(client, cfg.TxInterval, txChan)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
for blockAdded := range client.onBlockAdded {
|
||||
log.Infof("Block %s Added with %d relevant transactions", blockAdded.header.BlockHash(), len(blockAdded.txs))
|
||||
err := updateSubnetworks(blockAdded.txs, gasLimitMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateWalletTxs(blockAdded, walletTxs)
|
||||
err = enqueueTransactions(client, blockAdded, walletUTXOSet, walletTxs, txChan, cfg, gasLimitMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSubnetworks(txs []*util.Tx, gasLimitMap map[subnetworkid.SubnetworkID]uint64) error {
|
||||
for _, tx := range txs {
|
||||
msgTx := tx.MsgTx()
|
||||
if msgTx.SubnetworkID.IsEqual(subnetworkid.SubnetworkIDRegistry) {
|
||||
subnetworkID, err := blockdag.TxToSubnetworkID(msgTx)
|
||||
if err != nil {
|
||||
return errors.Errorf("could not build subnetwork ID: %s", err)
|
||||
}
|
||||
gasLimit := blockdag.ExtractGasLimit(msgTx)
|
||||
log.Infof("Found subnetwork %s with gas limit %d", subnetworkID, gasLimit)
|
||||
gasLimitMap[*subnetworkID] = gasLimit
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendTransactionLoop(client *txgenClient, interval uint64, txChan chan *wire.MsgTx) error {
|
||||
var ticker *time.Ticker
|
||||
if interval != 0 {
|
||||
ticker = time.NewTicker(time.Duration(interval) * time.Millisecond)
|
||||
}
|
||||
for tx := range txChan {
|
||||
_, err := client.SendRawTransaction(tx, true)
|
||||
log.Infof("Sending tx %s to subnetwork %s with %d inputs, %d outputs, %d payload size and %d gas", tx.TxID(), tx.SubnetworkID, len(tx.TxIn), len(tx.TxOut), len(tx.Payload), tx.Gas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ticker != nil {
|
||||
<-ticker.C
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getInitialUTXOSetAndWalletTxs(client *txgenClient, gasLimitMap map[subnetworkid.SubnetworkID]uint64) (utxoSet, map[daghash.TxID]*walletTransaction, error) {
|
||||
walletUTXOSet := make(utxoSet)
|
||||
walletTxs := make(map[daghash.TxID]*walletTransaction)
|
||||
|
||||
initialTxs, err := collectTransactions(client, gasLimitMap)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Add all of the confirmed transaction outputs to the UTXO.
|
||||
for _, initialTx := range initialTxs {
|
||||
if initialTx.confirmed {
|
||||
addTxOutsToUTXOSet(walletUTXOSet, initialTx.tx.MsgTx())
|
||||
}
|
||||
}
|
||||
|
||||
for _, initialTx := range initialTxs {
|
||||
// Remove all of the previous outpoints from the UTXO.
|
||||
// The previous outpoints are removed for unconfirmed
|
||||
// transactions as well, to avoid potential
|
||||
// double spends.
|
||||
removeTxInsFromUTXOSet(walletUTXOSet, initialTx.tx.MsgTx())
|
||||
|
||||
// Add unconfirmed transactions to walletTxs, so we can
|
||||
// add their outputs to the UTXO when they are confirmed.
|
||||
if !initialTx.confirmed {
|
||||
walletTxs[*initialTx.tx.ID()] = initialTx
|
||||
}
|
||||
}
|
||||
|
||||
return walletUTXOSet, walletTxs, nil
|
||||
}
|
||||
|
||||
func updateWalletTxs(blockAdded *blockAddedMsg, walletTxs map[daghash.TxID]*walletTransaction) {
|
||||
for txID, walletTx := range walletTxs {
|
||||
if walletTx.checkConfirmationCountdown > 0 && walletTx.chainHeight < blockAdded.chainHeight {
|
||||
walletTx.checkConfirmationCountdown--
|
||||
}
|
||||
|
||||
// Delete old confirmed transactions to save memory
|
||||
if walletTx.confirmed && walletTx.chainHeight+txLifeSpan < blockAdded.chainHeight {
|
||||
delete(walletTxs, txID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tx := range blockAdded.txs {
|
||||
if _, ok := walletTxs[*tx.ID()]; !ok {
|
||||
walletTxs[*tx.ID()] = &walletTransaction{
|
||||
tx: tx,
|
||||
chainHeight: blockAdded.chainHeight,
|
||||
checkConfirmationCountdown: requiredConfirmations,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func randomWithAverageTarget(target float64) float64 {
|
||||
randomFraction := random.Float64()
|
||||
return randomFraction * target * 2
|
||||
}
|
||||
|
||||
func randomIntegerWithAverageTarget(target uint64, allowZero bool) uint64 {
|
||||
randomNum := randomWithAverageTarget(float64(target))
|
||||
if !allowZero && randomNum < 1 {
|
||||
randomNum = 1
|
||||
}
|
||||
return uint64(math.Round(randomNum))
|
||||
}
|
||||
|
||||
func createRandomTxFromFunds(walletUTXOSet utxoSet, cfg *configFlags, gasLimitMap map[subnetworkid.SubnetworkID]uint64, funds uint64) (tx *wire.MsgTx, isSecondaryAddress bool, err error) {
|
||||
if secondaryScriptPubKey != nil && !sentToSecondaryAddress && funds > minSecondaryTxAmount {
|
||||
tx, err = createTx(walletUTXOSet, minSecondaryTxAmount, cfg.AverageFeeRate, 1, 1, subnetworkid.SubnetworkIDNative, 0, 0, secondaryScriptPubKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return tx, true, nil
|
||||
}
|
||||
|
||||
payloadSize := uint64(0)
|
||||
gas := uint64(0)
|
||||
|
||||
// In Go map iteration is randomized, so if we want
|
||||
// to choose a random element from a map we can
|
||||
// just take the first iterated element.
|
||||
chosenSubnetwork := subnetworkid.SubnetworkIDNative
|
||||
chosenGasLimit := uint64(0)
|
||||
for subnetworkID, gasLimit := range gasLimitMap {
|
||||
chosenSubnetwork = &subnetworkID
|
||||
chosenGasLimit = gasLimit
|
||||
break
|
||||
}
|
||||
|
||||
if !chosenSubnetwork.IsEqual(subnetworkid.SubnetworkIDNative) {
|
||||
payloadSize = randomIntegerWithAverageTarget(cfg.AveragePayloadSize, true)
|
||||
gas = randomIntegerWithAverageTarget(uint64(float64(chosenGasLimit)*cfg.AverageGasFraction), true)
|
||||
if gas > chosenGasLimit {
|
||||
gas = chosenGasLimit
|
||||
}
|
||||
}
|
||||
|
||||
targetNumberOfOutputs := randomIntegerWithAverageTarget(cfg.TargetNumberOfOutputs, false)
|
||||
targetNumberOfInputs := randomIntegerWithAverageTarget(cfg.TargetNumberOfInputs, false)
|
||||
|
||||
feeRate := randomWithAverageTarget(cfg.AverageFeeRate)
|
||||
|
||||
amount := minSpendableAmount + uint64(random.Int63n(int64(maxSpendableAmount-minSpendableAmount)))
|
||||
amount *= targetNumberOfOutputs
|
||||
if amount > funds-minTxFee {
|
||||
amount = funds - minTxFee
|
||||
}
|
||||
tx, err = createTx(walletUTXOSet, amount, feeRate, targetNumberOfOutputs, targetNumberOfInputs, chosenSubnetwork, payloadSize, gas, primaryScriptPubKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return tx, true, nil
|
||||
}
|
||||
|
||||
func enqueueTransactions(client *txgenClient, blockAdded *blockAddedMsg, walletUTXOSet utxoSet, walletTxs map[daghash.TxID]*walletTransaction,
|
||||
txChan chan *wire.MsgTx, cfg *configFlags, gasLimitMap map[subnetworkid.SubnetworkID]uint64) error {
|
||||
if err := applyConfirmedTransactionsAndResendNonAccepted(client, walletTxs, walletUTXOSet, blockAdded.chainHeight, txChan); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for funds := calcUTXOSetFunds(walletUTXOSet); !isDust(funds); funds = calcUTXOSetFunds(walletUTXOSet) {
|
||||
tx, isSecondaryAddress, err := createRandomTxFromFunds(walletUTXOSet, cfg, gasLimitMap, funds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
txChan <- tx
|
||||
if isSecondaryAddress {
|
||||
sentToSecondaryAddress = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTx(walletUTXOSet utxoSet, minAmount uint64, feeRate float64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64,
|
||||
subnetworkdID *subnetworkid.SubnetworkID, payloadSize uint64, gas uint64, scriptPubKey []byte) (*wire.MsgTx, error) {
|
||||
var tx *wire.MsgTx
|
||||
if subnetworkdID.IsEqual(subnetworkid.SubnetworkIDNative) {
|
||||
tx = wire.NewNativeMsgTx(wire.TxVersion, nil, nil)
|
||||
} else {
|
||||
payload := make([]byte, payloadSize)
|
||||
tx = wire.NewSubnetworkMsgTx(wire.TxVersion, nil, nil, subnetworkdID, gas, payload)
|
||||
}
|
||||
|
||||
// Attempt to fund the transaction with spendable utxos.
|
||||
funds, err := fundTx(walletUTXOSet, tx, minAmount, feeRate, targetNumberOfOutputs, targetNumberOfInputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxNumOuts := funds / minSpendableAmount
|
||||
numOuts := targetNumberOfOutputs
|
||||
if numOuts > maxNumOuts {
|
||||
numOuts = maxNumOuts
|
||||
}
|
||||
|
||||
fee := calcFee(tx, feeRate, numOuts, walletUTXOSet)
|
||||
funds -= fee
|
||||
|
||||
for i := uint64(0); i < numOuts; i++ {
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: funds / numOuts,
|
||||
ScriptPubKey: scriptPubKey,
|
||||
})
|
||||
}
|
||||
|
||||
err = signTx(walletUTXOSet, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removeTxInsFromUTXOSet(walletUTXOSet, tx)
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// signTx signs a transaction
|
||||
func signTx(walletUTXOSet utxoSet, tx *wire.MsgTx) error {
|
||||
for i, txIn := range tx.TxIn {
|
||||
outpoint := txIn.PreviousOutpoint
|
||||
prevOut := walletUTXOSet[outpoint]
|
||||
|
||||
sigScript, err := txscript.SignatureScript(tx, i, prevOut.ScriptPubKey,
|
||||
txscript.SigHashAll, privateKey, true)
|
||||
if err != nil {
|
||||
return errors.Errorf("Failed to sign transaction: %s", err)
|
||||
}
|
||||
|
||||
txIn.SignatureScript = sigScript
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64, feeRate float64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64) (uint64, error) {
|
||||
|
||||
amountSelected := uint64(0)
|
||||
isTxFunded := false
|
||||
|
||||
for outpoint, output := range walletUTXOSet {
|
||||
amountSelected += output.Value
|
||||
|
||||
// Add the selected output to the transaction
|
||||
tx.AddTxIn(wire.NewTxIn(&outpoint, nil))
|
||||
|
||||
// Check if transaction has enough funds. If we don't have enough
|
||||
// coins from he current amount selected to pay the fee, or we have
|
||||
// less inputs then the targeted amount, continue to grab more coins.
|
||||
isTxFunded = isFunded(tx, feeRate, targetNumberOfOutputs, amountSelected, amount, walletUTXOSet)
|
||||
if uint64(len(tx.TxIn)) >= targetNumberOfInputs && isTxFunded {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isTxFunded {
|
||||
return 0, errors.Errorf("not enough funds for coin selection")
|
||||
}
|
||||
|
||||
return amountSelected, nil
|
||||
}
|
||||
|
||||
// isFunded checks if the transaction has enough funds to cover the fee
|
||||
// required for the txn.
|
||||
func isFunded(tx *wire.MsgTx, feeRate float64, targetNumberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) bool {
|
||||
reqFee := calcFee(tx, feeRate, targetNumberOfOutputs, walletUTXOSet)
|
||||
return amountSelected > reqFee && amountSelected-reqFee >= targetAmount
|
||||
}
|
||||
|
||||
func calcFee(msgTx *wire.MsgTx, feeRate float64, numberOfOutputs uint64, walletUTXOSet utxoSet) uint64 {
|
||||
txMass := calcTxMass(msgTx, walletUTXOSet)
|
||||
txMassWithOutputs := txMass + outputsTotalSize(numberOfOutputs)
|
||||
reqFee := uint64(float64(txMassWithOutputs) * feeRate)
|
||||
if reqFee < minTxFee {
|
||||
return minTxFee
|
||||
}
|
||||
return reqFee
|
||||
}
|
||||
|
||||
func outputsTotalSize(numberOfOutputs uint64) uint64 {
|
||||
return numberOfOutputs*outputSize + uint64(wire.VarIntSerializeSize(numberOfOutputs))
|
||||
}
|
||||
|
||||
func calcTxMass(msgTx *wire.MsgTx, walletUTXOSet utxoSet) uint64 {
|
||||
previousScriptPubKeys := getPreviousScriptPubKeys(msgTx, walletUTXOSet)
|
||||
return blockdag.CalcTxMass(util.NewTx(msgTx), previousScriptPubKeys)
|
||||
}
|
||||
|
||||
func getPreviousScriptPubKeys(msgTx *wire.MsgTx, walletUTXOSet utxoSet) [][]byte {
|
||||
previousScriptPubKeys := make([][]byte, len(msgTx.TxIn))
|
||||
for i, txIn := range msgTx.TxIn {
|
||||
outpoint := txIn.PreviousOutpoint
|
||||
prevOut := walletUTXOSet[outpoint]
|
||||
previousScriptPubKeys[i] = prevOut.ScriptPubKey
|
||||
}
|
||||
return previousScriptPubKeys
|
||||
}
|
||||
|
||||
func applyConfirmedTransactionsAndResendNonAccepted(client *txgenClient, walletTxs map[daghash.TxID]*walletTransaction, walletUTXOSet utxoSet,
|
||||
blockChainHeight uint64, txChan chan *wire.MsgTx) error {
|
||||
for txID, walletTx := range walletTxs {
|
||||
if !walletTx.confirmed && walletTx.checkConfirmationCountdown == 0 {
|
||||
txResult, err := client.GetRawTransactionVerbose(&txID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgTx := walletTx.tx.MsgTx()
|
||||
if isTxMatured(msgTx, *txResult.Confirmations) {
|
||||
walletTx.confirmed = true
|
||||
addTxOutsToUTXOSet(walletUTXOSet, msgTx)
|
||||
} else if !msgTx.IsCoinBase() && *txResult.Confirmations == 0 && !txResult.IsInMempool && blockChainHeight > walletTx.chainHeight+maxResendDepth {
|
||||
log.Infof("Transaction %s was not accepted in the DAG. Resending", txID)
|
||||
txChan <- msgTx
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeTxInsFromUTXOSet(walletUTXOSet utxoSet, tx *wire.MsgTx) {
|
||||
for _, txIn := range tx.TxIn {
|
||||
delete(walletUTXOSet, txIn.PreviousOutpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func addTxOutsToUTXOSet(walletUTXOSet utxoSet, tx *wire.MsgTx) {
|
||||
for i, txOut := range tx.TxOut {
|
||||
if bytes.Equal(txOut.ScriptPubKey, primaryScriptPubKey) {
|
||||
outpoint := wire.Outpoint{TxID: *tx.TxID(), Index: uint32(i)}
|
||||
walletUTXOSet[outpoint] = txOut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTxMatured(tx *wire.MsgTx, confirmations uint64) bool {
|
||||
if !tx.IsCoinBase() {
|
||||
return confirmations >= requiredConfirmations
|
||||
}
|
||||
return confirmations >= approximateConfirmationsForCoinbaseMaturity
|
||||
}
|
||||
|
||||
func calcUTXOSetFunds(walletUTXOSet utxoSet) uint64 {
|
||||
var funds uint64
|
||||
for _, output := range walletUTXOSet {
|
||||
funds += output.Value
|
||||
}
|
||||
return funds
|
||||
}
|
||||
|
||||
func collectTransactions(client *txgenClient, gasLimitMap map[subnetworkid.SubnetworkID]uint64) (map[daghash.TxID]*walletTransaction, error) {
|
||||
registryTxs := make([]*util.Tx, 0)
|
||||
walletTxs := make(map[daghash.TxID]*walletTransaction)
|
||||
skip := 0
|
||||
for skip < searchRawTransactionMaxResults {
|
||||
results, err := client.SearchRawTransactionsVerbose(p2pkhAddress, skip, searchRawTransactionResultCount, true, true, nil)
|
||||
if err != nil {
|
||||
// Break when there are no further txs
|
||||
if rpcError, ok := err.(*rpcmodel.RPCError); ok && rpcError.Code == rpcmodel.ErrRPCNoTxInfo {
|
||||
break
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
// Mempool transactions and red block transactions bring about unnecessary complexity, so
|
||||
// simply don't bother processing them
|
||||
if result.IsInMempool || *result.Confirmations == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tx, err := parseRawTransactionResult(result)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to process SearchRawTransactionResult: %s", err)
|
||||
}
|
||||
if tx == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
txID := tx.TxID()
|
||||
utilTx := util.NewTx(tx)
|
||||
|
||||
if existingTx, ok := walletTxs[*txID]; !ok || !existingTx.confirmed {
|
||||
walletTxs[*txID] = &walletTransaction{
|
||||
tx: utilTx,
|
||||
checkConfirmationCountdown: requiredConfirmations,
|
||||
confirmed: isTxMatured(tx, *result.Confirmations),
|
||||
}
|
||||
}
|
||||
|
||||
if tx.SubnetworkID.IsEqual(subnetworkid.SubnetworkIDRegistry) {
|
||||
registryTxs = append(registryTxs, utilTx)
|
||||
}
|
||||
}
|
||||
|
||||
skip += searchRawTransactionResultCount
|
||||
}
|
||||
err := updateSubnetworks(registryTxs, gasLimitMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return walletTxs, nil
|
||||
}
|
||||
|
||||
func parseRawTransactionResult(result *rpcmodel.SearchRawTransactionsResult) (*wire.MsgTx, error) {
|
||||
txBytes, err := hex.DecodeString(result.Hex)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to decode transaction bytes: %s", err)
|
||||
}
|
||||
var tx wire.MsgTx
|
||||
reader := bytes.NewReader(txBytes)
|
||||
err = tx.Deserialize(reader)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to deserialize transaction: %s", err)
|
||||
}
|
||||
return &tx, nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user