kaspad/cmd/txgen/txloop.go
stasatdaglabs 266e471941 [NOD-190] Implement Confirmations counting algorithm (#312)
* [NOD-192] Add method to compute confirmations of a single transaction (#306)

* [NOD-192] Implemented txConfirmations.

* [NOD-192] Renamed acceptedBy -> acceptingBlock and ConfirmationsByHash -> BlockConfirmationsByHash.

* [NOD-194 + NOD-199] Update all JSON-RPC methods to use new methods for computing confirmations + Remove the x1.5 factor when counting confirmations in txgen (#309)

* [NOD-194] Connected JSON-RPC commands with new confirmations logic.

* [NOD-194] Fixed failing tests.

* [NOD-194] Removed x1.5 from isTxMatured.

* [NOD-194] Made isTxMatured panic if it receives nil confirmations.

* [NOD-194] Added isInMempool to RPC methods that require it.

* [NOD-194] Fixed a typo.

* [NOD-194] Made the declaration of isInMempool more clear.

* [NOD-194] Removed some unnecessary complexity from isTxMatured.

* [NOD-193] Update Tx-Index to accomodate correct Confirmations structure (#308)

* [NOD-193] Uploaded BlockID to be uint64 in txIndex and addrIndex.

* [NOD-193] Removed the inclusion of current block transactions to txsAcceptanceData.

* [NOD-193] Implemented writing to the tx index txs with the virtual as the accepting block.

* [NOD-193] Added test for txs accepted by the virtual block.

* [NOD-193] Removed the requirement for subnetwork registry transactions to be accepted.

* [NOD-194] Made in-memory the txsAcceptedByVirtual part of txIndex.

* [NOD-193] Optimized txsAcceptedByVirtual initialization.

* [NOD-193] Fixed weird loop in txsAcceptedByVirtual initialization.

* [NOD-190] Fixed merge errors.
2019-05-29 13:09:16 +03:00

340 lines
9.0 KiB
Go

package main
import (
"bytes"
"encoding/hex"
"fmt"
"math/rand"
"sync/atomic"
"time"
"github.com/daglabs/btcd/rpcclient"
"github.com/daglabs/btcd/txscript"
"github.com/daglabs/btcd/util/daghash"
"github.com/daglabs/btcd/wire"
)
// utxo represents an unspent output spendable by the memWallet. The maturity
// height of the transaction is recorded in order to properly observe the
// maturity period of direct coinbase outputs.
type utxo struct {
txOut *wire.TxOut
isLocked bool
}
var (
random = rand.New(rand.NewSource(time.Now().UnixNano()))
utxos map[wire.OutPoint]*utxo
pkScript []byte
spentTxs map[daghash.TxID]bool
)
const (
// Those constants should be updated, when monetary policy changed
minSpendableAmount uint64 = 10000
minTxFee uint64 = 3000
)
func isDust(value uint64) bool {
return value < minSpendableAmount+minTxFee
}
// evalOutputs evaluates each of the passed outputs, creating a new matching
// utxo within the wallet if we're able to spend the output.
func evalOutputs(outputs []*wire.TxOut, txID *daghash.TxID) {
for i, output := range outputs {
if isDust(output.Value) {
continue
}
op := wire.OutPoint{TxID: *txID, Index: uint32(i)}
utxos[op] = &utxo{txOut: output}
}
}
// evalInputs scans all the passed inputs, deleting any utxos within the
// wallet which are spent by an input.
func evalInputs(inputs []*wire.TxIn) {
for _, txIn := range inputs {
op := txIn.PreviousOutPoint
if _, ok := utxos[op]; ok {
delete(utxos, op)
}
}
}
func utxosFunds() uint64 {
var funds uint64
for _, utxo := range utxos {
if utxo.isLocked {
continue
}
funds += utxo.txOut.Value
}
return funds
}
func isTxMatured(tx *wire.MsgTx, confirmations uint64) bool {
if !tx.IsBlockReward() {
return confirmations >= 1
}
return confirmations >= activeNetParams.BlockRewardMaturity
}
// DumpTx logs out transaction with given header
func DumpTx(header string, tx *wire.MsgTx) {
log.Info(header)
log.Infof("\tInputs:")
for i, txIn := range tx.TxIn {
asm, _ := txscript.DisasmString(txIn.SignatureScript)
log.Infof("\t\t%d: PreviousOutPoint: %v, SignatureScript: %s",
i, txIn.PreviousOutPoint, asm)
}
log.Infof("\tOutputs:")
for i, txOut := range tx.TxOut {
asm, _ := txscript.DisasmString(txOut.PkScript)
log.Infof("\t\t%d: Value: %d, PkScript: %s", i, txOut.Value, asm)
}
}
func fetchAndPopulateUtxos(client *rpcclient.Client) (funds uint64, exit bool, err error) {
skipCount := 0
for atomic.LoadInt32(&isRunning) == 1 {
arr, err := client.SearchRawTransactionsVerbose(p2pkhAddress, skipCount, 1000, true, false, nil)
if err != nil {
log.Infof("No spandable transactions found and SearchRawTransactionsVerbose failed: %s", err)
funds := utxosFunds()
if !isDust(funds) {
// we have something to spend
log.Infof("We have enough funds to generate transactions: %d", funds)
return funds, false, nil
}
log.Infof("Sleeping 30 sec...")
for i := 0; i < 30; i++ {
time.Sleep(time.Second)
if atomic.LoadInt32(&isRunning) != 1 {
return 0, true, nil
}
}
skipCount = 0
continue
}
receivedCount := len(arr)
skipCount += receivedCount
log.Infof("Received %d transactions", receivedCount)
for _, searchResult := range arr {
txBytes, err := hex.DecodeString(searchResult.Hex)
if err != nil {
log.Warnf("Failed to decode transactions bytes: %s", err)
continue
}
txID, err := daghash.NewTxIDFromStr(searchResult.TxID)
if err != nil {
log.Warnf("Failed to decode transaction ID: %s", err)
continue
}
var tx wire.MsgTx
rbuf := bytes.NewReader(txBytes)
err = tx.Deserialize(rbuf)
if err != nil {
log.Warnf("Failed to deserialize transaction: %s", err)
continue
}
if spentTxs[*txID] {
continue
}
if isTxMatured(&tx, *searchResult.Confirmations) {
spentTxs[*txID] = true
evalOutputs(tx.TxOut, txID)
evalInputs(tx.TxIn)
}
}
}
return 0, true, nil
}
// fundTx attempts to fund a transaction sending amount bitcoin. The coins are
// selected such that the final amount spent pays enough fees as dictated by
// the passed fee rate. The passed fee rate should be expressed in
// satoshis-per-byte.
func fundTx(tx *wire.MsgTx, amount uint64, feeRate uint64) (uint64, error) {
const (
// spendSize is the largest number of bytes of a sigScript
// which spends a p2pkh output: OP_DATA_73 <sig> OP_DATA_33 <pubkey>
spendSize = 1 + 73 + 1 + 33
)
var (
amountSelected uint64
txSize int
)
for outPoint, utxo := range utxos {
if utxo.isLocked {
continue
}
amountSelected += utxo.txOut.Value
// Add the selected output to the transaction, updating the
// current tx size while accounting for the size of the future
// sigScript.
tx.AddTxIn(wire.NewTxIn(&outPoint, nil))
txSize = tx.SerializeSize() + spendSize*len(tx.TxIn)
// Calculate the fee required for the txn at this point
// observing the specified fee rate. If we don't have enough
// coins from he current amount selected to pay the fee, then
// continue to grab more coins.
reqFee := uint64(txSize) * feeRate
if amountSelected-reqFee < amount {
continue
}
// If we have any change left over, then add an additional
// output to the transaction reserved for change.
changeVal := amountSelected - amount - reqFee
if changeVal > 0 {
changeOutput := &wire.TxOut{
Value: changeVal,
PkScript: pkScript,
}
tx.AddTxOut(changeOutput)
}
return reqFee, nil
}
// If we've reached this point, then coin selection failed due to an
// insufficient amount of coins.
return 0, fmt.Errorf("not enough funds for coin selection")
}
// signTxAndLockSpentUtxo signs new transaction and locks spentutxo
func signTxAndLockSpentUtxo(tx *wire.MsgTx) error {
// Populate all the selected inputs with valid sigScript for spending.
// Along the way record all outputs being spent in order to avoid a
// potential double spend.
spentOutputs := make([]*utxo, 0, len(tx.TxIn))
for i, txIn := range tx.TxIn {
outPoint := txIn.PreviousOutPoint
utxo := utxos[outPoint]
txOut := utxo.txOut
sigScript, err := txscript.SignatureScript(tx, i, txOut.PkScript,
txscript.SigHashAll, privateKey, true)
if err != nil {
log.Warnf("Failed to sign transaction: %s", err)
return err
}
txIn.SignatureScript = sigScript
spentOutputs = append(spentOutputs, utxo)
}
// As these outputs are now being spent by this newly created
// transaction, mark the outputs are "locked". This action ensures
// these outputs won't be double spent by any subsequent transactions.
// These locked outputs can be freed via a call to UnlockOutputs.
for _, utxo := range spentOutputs {
utxo.isLocked = true
}
return nil
}
// createTransaction returns a fully signed transaction paying to the specified
// outputs while observing the desired fee rate. The passed fee rate should be
// expressed in satoshis-per-byte.
func createTransaction(outputs []*wire.TxOut, feeRate uint64) (*wire.MsgTx, uint64, error) {
tx := wire.NewNativeMsgTx(wire.TxVersion, nil, nil)
// Tally up the total amount to be sent in order to perform coin
// selection shortly below.
var outputAmount uint64
for _, output := range outputs {
outputAmount += output.Value
tx.AddTxOut(output)
}
// Attempt to fund the transaction with spendable utxos.
fees, err := fundTx(tx, outputAmount, feeRate)
if err != nil {
return nil, 0, err
}
err = signTxAndLockSpentUtxo(tx)
if err != nil {
return nil, 0, err
}
return tx, fees, nil
}
// txLoop performs main loop of transaction generation
func txLoop(clients []*rpcclient.Client) {
clientsCount := int64(len(clients))
utxos = make(map[wire.OutPoint]*utxo)
spentTxs = make(map[daghash.TxID]bool)
var err error
pkScript, err = txscript.PayToAddrScript(p2pkhAddress)
if err != nil {
log.Warnf("Failed to generate pkscript to address: %s", err)
return
}
for atomic.LoadInt32(&isRunning) == 1 {
funds, exit, err := fetchAndPopulateUtxos(clients[0])
if exit {
return
}
if err != nil {
log.Warnf("fetchAndPopulateUtxos failed: %s", err)
continue
}
if isDust(funds) {
log.Warnf("fetchAndPopulateUtxos returned not enough funds")
continue
}
log.Infof("UTXO funds after population %d", funds)
for !isDust(funds) {
amount := minSpendableAmount + uint64(random.Int63n(int64(minSpendableAmount*4)))
if amount > funds-minTxFee {
amount = funds - minTxFee
}
output := wire.NewTxOut(amount, pkScript)
tx, fees, err := createTransaction([]*wire.TxOut{output}, 10)
if err != nil {
log.Warnf("Failed to create transaction (output value %d, funds %d): %s",
amount, funds, err)
continue
}
log.Infof("Created transaction %s: amount %d, fees %d", tx.TxID(), amount, fees)
funds = utxosFunds()
log.Infof("Remaining funds: %d", funds)
var currentClient *rpcclient.Client
if clientsCount == 1 {
currentClient = clients[0]
} else {
currentClient = clients[random.Int63n(clientsCount)]
}
_, err = currentClient.SendRawTransaction(tx, true)
if err != nil {
log.Warnf("Failed to send transaction: %s", err)
continue
}
}
}
}