mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-10-14 00:59:33 +00:00

* [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.
340 lines
9.0 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|