[NOD-66] Transaction generator (#247)

* [NOD-66] Created TX generator

* [NOD-66] Created transaction generator

* [NOD-66] Improved TX generator against double spend. Created genaddr utility. Refactored

* [NOD-66] Save chenges before branch switch

* [NOD-66] Use log package instead of fmt

* [NOD-66] Fixed/restored docker files

* [NOD-66] Changed according to new WithLock/NoLock convention
This commit is contained in:
Evgeny Khirin 2019-04-21 15:05:03 +03:00 committed by GitHub
parent a3735da12a
commit daa4481282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 602 additions and 19 deletions

View File

@ -346,12 +346,18 @@ type SequenceLock struct {
//
// This function is safe for concurrent access.
func (dag *BlockDAG) CalcSequenceLock(tx *util.Tx, utxoSet UTXOSet, mempool bool) (*SequenceLock, error) {
dag.dagLock.Lock()
defer dag.dagLock.Unlock()
dag.dagLock.RLock()
defer dag.dagLock.RUnlock()
return dag.calcSequenceLock(dag.selectedTip(), utxoSet, tx, mempool)
}
// CalcSequenceLockNoLock is lock free version of CalcSequenceLockWithLock
// This function is unsafe for concurrent access.
func (dag *BlockDAG) CalcSequenceLockNoLock(tx *util.Tx, utxoSet UTXOSet, mempool bool) (*SequenceLock, error) {
return dag.calcSequenceLock(dag.selectedTip(), utxoSet, tx, mempool)
}
// calcSequenceLock computes the relative lock-times for the passed
// transaction. See the exported version, CalcSequenceLock for further details.
//

View File

@ -219,7 +219,7 @@ func TestHaveBlock(t *testing.T) {
}
// TestCalcSequenceLock tests the LockTimeToSequence function, and the
// CalcSequenceLock method of a Chain instance. The tests exercise several
// CalcSequenceLock method of a DAG instance. The tests exercise several
// combinations of inputs to the CalcSequenceLock function in order to ensure
// the returned SequenceLocks are correct for each test instance.
func TestCalcSequenceLock(t *testing.T) {

29
cmd/genaddr/genaddr.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"fmt"
"os"
"github.com/daglabs/btcd/btcec"
"github.com/daglabs/btcd/dagconfig"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/util/base58"
)
func main() {
activeNetParams := &dagconfig.DevNetParams
privateKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to generate private key: %s", err)
os.Exit(1)
}
fmt.Printf("\nPrivate key (base-58): %s\n", base58.Encode(privateKey.Serialize()))
serializedKey := privateKey.PubKey().SerializeCompressed()
pubKeyAddr, err := util.NewAddressPubKey(serializedKey, activeNetParams.Prefix)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to generate public key address: %s", err)
os.Exit(1)
}
addr := pubKeyAddr.AddressPubKeyHash()
fmt.Printf("Public key: %s\n\n", addr)
}

22
cmd/txgen/addresslist.go Normal file
View File

@ -0,0 +1,22 @@
package main
import (
"bufio"
"os"
)
func getAddressList(cfg *config) ([]string, error) {
file, err := os.Open(cfg.AddressListPath)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
addressList := []string{}
for scanner.Scan() {
addressList = append(addressList, scanner.Text())
}
return addressList, nil
}

34
cmd/txgen/config.go Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"errors"
"github.com/jessevdk/go-flags"
)
type config struct {
AddressListPath string `long:"addresslist" description:"Path to a list of nodes' JSON-RPC endpoints" required:"true"`
PrivateKey string `long:"private-key" description:"Private key" required:"true"`
CertificatePath string `long:"cert" description:"Path to certificate accepted by JSON-RPC endpoint"`
DisableTLS bool `long:"notls" description:"Disable TLS"`
}
func parseConfig() (*config, error) {
cfg := &config{}
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")
}
return cfg, nil
}

47
cmd/txgen/connect.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"github.com/daglabs/btcd/rpcclient"
)
func connectToServers(cfg *config, addressList []string) ([]*rpcclient.Client, error) {
clients := make([]*rpcclient.Client, len(addressList))
var cert []byte
if !cfg.DisableTLS {
var err error
cert, err = ioutil.ReadFile(cfg.CertificatePath)
if err != nil {
return nil, fmt.Errorf("Error reading certificates file: %s", err)
}
}
for i, address := range addressList {
connCfg := &rpcclient.ConnConfig{
Host: address,
Endpoint: "ws",
User: "user",
Pass: "pass",
DisableTLS: cfg.DisableTLS,
}
if !cfg.DisableTLS {
connCfg.Certificates = cert
}
client, err := rpcclient.New(connCfg, nil)
if err != nil {
return nil, fmt.Errorf("Error connecting to address %s: %s", address, err)
}
clients[i] = client
log.Printf("Connected to server %s", address)
}
return clients, nil
}

View File

@ -0,0 +1,33 @@
# -- multistage docker build: stage #1: build stage
FROM golang:1.12-alpine AS build
RUN mkdir -p /go/src/github.com/daglabs/btcd
WORKDIR /go/src/github.com/daglabs/btcd
RUN apk add --no-cache curl git
# GO111MODULE=on forces Go to use the go-module system
# TODO: remove this once Go 1.13 is released
ENV GO111MODULE=on
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/daglabs/btcd/cmd/txgen/txgen /app/
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/app/txgen"]

9
cmd/txgen/docker/README Normal file
View File

@ -0,0 +1,9 @@
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

85
cmd/txgen/main.go Normal file
View File

@ -0,0 +1,85 @@
package main
import (
"log"
"runtime/debug"
"sync/atomic"
"github.com/daglabs/btcd/btcec"
"github.com/daglabs/btcd/dagconfig"
"github.com/daglabs/btcd/rpcclient"
"github.com/daglabs/btcd/signal"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/util/base58"
)
var (
isRunning int32
activeNetParams *dagconfig.Params = &dagconfig.DevNetParams
p2pkhAddress util.Address
privateKey *btcec.PrivateKey
)
// privateKeyToP2pkhAddress generates p2pkh address from private key.
func privateKeyToP2pkhAddress(key *btcec.PrivateKey, net *dagconfig.Params) (util.Address, error) {
serializedKey := key.PubKey().SerializeCompressed()
pubKeyAddr, err := util.NewAddressPubKey(serializedKey, net.Prefix)
if err != nil {
return nil, err
}
return pubKeyAddr.AddressPubKeyHash(), nil
}
func main() {
defer handlePanic()
cfg, err := parseConfig()
if err != nil {
log.Panicf("Error parsing command-line arguments: %s", err)
}
privateKeyBytes := base58.Decode(cfg.PrivateKey)
privateKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), privateKeyBytes)
p2pkhAddress, err = privateKeyToP2pkhAddress(privateKey, activeNetParams)
if err != nil {
log.Panicf("Failed to get P2PKH address from private key: %s", err)
}
log.Printf("P2PKH address for private key: %s\n", p2pkhAddress)
addressList, err := getAddressList(cfg)
if err != nil {
log.Panicf("Couldn't load address list: %s", err)
}
clients, err := connectToServers(cfg, addressList)
if err != nil {
log.Panicf("Error connecting to servers: %s", err)
}
defer disconnect(clients)
atomic.StoreInt32(&isRunning, 1)
go txLoop(clients)
interrupt := signal.InterruptListener()
<-interrupt
atomic.StoreInt32(&isRunning, 0)
}
func disconnect(clients []*rpcclient.Client) {
log.Printf("Disconnecting clients")
for _, client := range clients {
client.Disconnect()
}
}
func handlePanic() {
err := recover()
if err != nil {
log.Printf("Fatal error: %s", err)
log.Printf("Stack trace: %s", debug.Stack())
}
}

320
cmd/txgen/txloop.go Normal file
View File

@ -0,0 +1,320 @@
package main
import (
"bytes"
"encoding/hex"
"fmt"
"log"
"math/rand"
"sync/atomic"
"time"
"github.com/daglabs/btcd/dagconfig/daghash"
"github.com/daglabs/btcd/mempool"
"github.com/daglabs/btcd/rpcclient"
"github.com/daglabs/btcd/txscript"
"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
minRelayTxFee uint64 = uint64(mempool.DefaultMinRelayTxFee)
)
func isDust(value uint64) bool {
return value < minSpendableAmount+minRelayTxFee
}
// 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 >= uint64(float64(activeNetParams.BlockRewardMaturity)*1.5)
}
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.Printf("No spandable transactions found and SearchRawTransactionsVerbose failed: %s", err)
funds := utxosFunds()
if !isDust(funds) {
// we have something to spend
log.Printf("We have enough funds to generate transactions: %d", funds)
return funds, false, nil
}
log.Printf("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.Printf("Received %d transactions", receivedCount)
for _, searchResult := range arr {
txBytes, err := hex.DecodeString(searchResult.Hex)
if err != nil {
log.Printf("Failed to decode transactions bytes: %s", err)
continue
}
txID, err := daghash.NewTxIDFromStr(searchResult.TxID)
if err != nil {
log.Printf("Failed to decode transaction ID: %s", err)
continue
}
var tx wire.MsgTx
rbuf := bytes.NewReader(txBytes)
err = tx.Deserialize(rbuf)
if err != nil {
log.Printf("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.Printf("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)
pkScript, err := txscript.PayToAddrScript(p2pkhAddress)
if err != nil {
log.Printf("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.Printf("fetchAndPopulateUtxos failed: %s", err)
continue
}
if isDust(funds) {
log.Printf("fetchAndPopulateUtxos returned not enough funds")
continue
}
log.Printf("UTXO funds after population %d", funds)
for !isDust(funds) {
amount := minSpendableAmount + uint64(random.Int63n(int64(minSpendableAmount*4)))
output := wire.NewTxOut(amount, pkScript)
tx, fees, err := createTransaction([]*wire.TxOut{output}, 10)
if err != nil {
log.Printf("Failed to create transaction (output value %d, funds %d): %s",
amount, funds, err)
continue
}
log.Printf("Created transaction %s: amount %d, fees %d", tx.TxID(), amount, fees)
funds = utxosFunds()
log.Printf("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.Printf("Failed to send transaction: %s", err)
continue
}
}
}
}

View File

@ -73,10 +73,10 @@ type Config struct {
// chain tip within the best chain.
MedianTimePast func() time.Time
// CalcSequenceLock defines the function to use in order to generate
// CalcSequenceLockNoLock defines the function to use in order to generate
// the current sequence lock for the given transaction using the passed
// utxo set.
CalcSequenceLock func(*util.Tx, blockdag.UTXOSet) (*blockdag.SequenceLock, error)
CalcSequenceLockNoLock func(*util.Tx, blockdag.UTXOSet) (*blockdag.SequenceLock, error)
// IsDeploymentActive returns true if the target deploymentID is
// active, and false otherwise. The mempool uses this function to gauge
@ -852,7 +852,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *util.Tx, isNew, rateLimit, rejectDu
// Don't allow the transaction into the mempool unless its sequence
// lock is active, meaning that it'll be allowed into the next block
// with respect to its defined relative lock times.
sequenceLock, err := mp.cfg.CalcSequenceLock(tx, mp.mpUTXOSet)
sequenceLock, err := mp.cfg.CalcSequenceLockNoLock(tx, mp.mpUTXOSet)
if err != nil {
if cerr, ok := err.(blockdag.RuleError); ok {
return nil, nil, dagRuleError(cerr)

View File

@ -319,12 +319,12 @@ func newPoolHarness(dagParams *dagconfig.Params, numOutputs uint32, dbName strin
MinRelayTxFee: 1000, // 1 Satoshi per byte
MaxTxVersion: 1,
},
DAGParams: dagParams,
BestHeight: chain.BestHeight,
MedianTimePast: chain.MedianTimePast,
CalcSequenceLock: calcSequenceLock,
SigCache: nil,
AddrIndex: nil,
DAGParams: dagParams,
BestHeight: chain.BestHeight,
MedianTimePast: chain.MedianTimePast,
CalcSequenceLockNoLock: calcSequenceLock,
SigCache: nil,
AddrIndex: nil,
}),
}
@ -681,7 +681,7 @@ func TestProcessTransaction(t *testing.T) {
}
//Checks that transactions get rejected from mempool if sequence lock is not active
harness.txPool.cfg.CalcSequenceLock = func(tx *util.Tx,
harness.txPool.cfg.CalcSequenceLockNoLock = func(tx *util.Tx,
view blockdag.UTXOSet) (*blockdag.SequenceLock, error) {
return &blockdag.SequenceLock{
@ -704,7 +704,7 @@ func TestProcessTransaction(t *testing.T) {
if err.Error() != expectedErrStr {
t.Errorf("Unexpected error message. Expected \"%s\" but got \"%s\"", expectedErrStr, err.Error())
}
harness.txPool.cfg.CalcSequenceLock = calcSequenceLock
harness.txPool.cfg.CalcSequenceLockNoLock = calcSequenceLock
// This is done in order to increase the input age, so the tx priority will be higher
harness.chain.SetHeight(curHeight + 100)

View File

@ -406,8 +406,7 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe
if err != nil {
return nil, err
}
coinbaseTx, err := CreateCoinbaseTx(g.dagParams, coinbaseScript,
nextBlockHeight, payToAddress)
coinbaseTx, err := CreateCoinbaseTx(g.dagParams, coinbaseScript, nextBlockHeight, payToAddress)
if err != nil {
return nil, err
}
@ -630,7 +629,6 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe
// the total fees accordingly.
blockSize -= wire.MaxVarIntPayload -
uint32(wire.VarIntSerializeSize(uint64(len(blockTxns))))
coinbaseTx.MsgTx().TxOut[0].Value += totalFees
// Calculate the required difficulty for the block. The timestamp
// is potentially adjusted to ensure it comes after the median time of

View File

@ -2476,8 +2476,8 @@ func NewServer(listenAddrs []string, db database.DB, dagParams *dagconfig.Params
DAGParams: dagParams,
BestHeight: func() int32 { return s.DAG.Height() }, //TODO: (Ori) This is probably wrong. Done only for compilation
MedianTimePast: func() time.Time { return s.DAG.CalcPastMedianTime() },
CalcSequenceLock: func(tx *util.Tx, utxoSet blockdag.UTXOSet) (*blockdag.SequenceLock, error) {
return s.DAG.CalcSequenceLock(tx, utxoSet, true)
CalcSequenceLockNoLock: func(tx *util.Tx, utxoSet blockdag.UTXOSet) (*blockdag.SequenceLock, error) {
return s.DAG.CalcSequenceLockNoLock(tx, utxoSet, true)
},
IsDeploymentActive: s.DAG.IsDeploymentActive,
SigCache: s.SigCache,