mirror of
https://github.com/kaspanet/kaspad.git
synced 2026-03-22 16:13:45 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6479820fb4 | ||
|
|
4a0b7ad268 | ||
|
|
b4a38f8f60 | ||
|
|
b794254df6 | ||
|
|
b855f57371 | ||
|
|
9b832997f8 | ||
|
|
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
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
ISC License
|
|
||||||
|
|
||||||
Copyright (c) 2018 The Decred developers
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
|
||||||
copyright notice and this permission notice appear in all copies.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
dnsseeder
|
|
||||||
=========
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
Latest version of [Go](http://golang.org) (currently 1.13)
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
- Install Go according to the installation instructions here:
|
|
||||||
http://golang.org/doc/install
|
|
||||||
|
|
||||||
- Ensure Go was installed properly and is a supported version:
|
|
||||||
|
|
||||||
- Launch a kaspad node for the dnsseeder to connect to
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go version
|
|
||||||
$ go env GOROOT GOPATH
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: The `GOROOT` and `GOPATH` above must not be the same path. It is
|
|
||||||
recommended that `GOPATH` is set to a directory in your home directory such as
|
|
||||||
`~/dev/go` to avoid write permission issues. It is also recommended to add
|
|
||||||
`$GOPATH/bin` to your `PATH` at this point.
|
|
||||||
|
|
||||||
- Run the following commands to obtain dnsseeder, all dependencies, and install it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ git clone https://github.com/kaspanet/dnsseeder $GOPATH/src/github.com/kaspanet/dnsseeder
|
|
||||||
$ cd $GOPATH/src/github.com/kaspanet/dnsseeder
|
|
||||||
$ go install .
|
|
||||||
```
|
|
||||||
|
|
||||||
- dnsseeder will now be installed in either ```$GOROOT/bin``` or
|
|
||||||
```$GOPATH/bin``` depending on your configuration. If you did not already
|
|
||||||
add the bin directory to your system path during Go installation, we
|
|
||||||
recommend you do so now.
|
|
||||||
|
|
||||||
To start dnsseeder listening on udp 127.0.0.1:5354 with an initial connection to working testnet node running on 127.0.0.1:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./dnsseeder -n nameserver.example.com -H network-seed.example.com -s 127.0.0.1 --testnet
|
|
||||||
```
|
|
||||||
|
|
||||||
You will then need to redirect DNS traffic on your public IP port 53 to 127.0.0.1:5354
|
|
||||||
Note: to listen directly on port 53 on most Unix systems, one has to run dnsseeder as root, which is discouraged
|
|
||||||
|
|
||||||
## Setting up DNS Records
|
|
||||||
|
|
||||||
To create a working set-up where dnsseeder can provide IPs to kaspad instances, set the following DNS records:
|
|
||||||
```
|
|
||||||
NAME TYPE VALUE
|
|
||||||
---- ---- -----
|
|
||||||
[your.domain.name] A [your ip address]
|
|
||||||
[ns-your.domain.name] NS [your.domain.name]
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// Copyright (c) 2018 The Decred developers
|
|
||||||
// Use of this source code is governed by an ISC
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/config"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/jessevdk/go-flags"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultConfigFilename = "dnsseeder.conf"
|
|
||||||
defaultLogFilename = "dnsseeder.log"
|
|
||||||
defaultErrLogFilename = "dnsseeder_err.log"
|
|
||||||
defaultListenPort = "5354"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default configuration options
|
|
||||||
defaultHomeDir = util.AppDataDir("dnsseeder", false)
|
|
||||||
defaultConfigFile = filepath.Join(defaultHomeDir, defaultConfigFilename)
|
|
||||||
defaultLogFile = filepath.Join(defaultHomeDir, defaultLogFilename)
|
|
||||||
defaultErrLogFile = filepath.Join(defaultHomeDir, defaultErrLogFilename)
|
|
||||||
)
|
|
||||||
|
|
||||||
var activeConfig *ConfigFlags
|
|
||||||
|
|
||||||
// ActiveConfig returns the active configuration struct
|
|
||||||
func ActiveConfig() *ConfigFlags {
|
|
||||||
return activeConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigFlags holds the configurations set by the command line argument
|
|
||||||
type ConfigFlags struct {
|
|
||||||
Host string `short:"H" long:"host" description:"Seed DNS address"`
|
|
||||||
Listen string `long:"listen" short:"l" description:"Listen on address:port"`
|
|
||||||
Nameserver string `short:"n" long:"nameserver" description:"hostname of nameserver"`
|
|
||||||
Seeder string `short:"s" long:"default seeder" description:"IP address of a working node"`
|
|
||||||
config.NetworkFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig() (*ConfigFlags, error) {
|
|
||||||
err := os.MkdirAll(defaultHomeDir, 0700)
|
|
||||||
if err != nil {
|
|
||||||
// Show a nicer error message if it's because a symlink is
|
|
||||||
// linked to a directory that does not exist (probably because
|
|
||||||
// it's not mounted).
|
|
||||||
if e, ok := err.(*os.PathError); ok && os.IsExist(err) {
|
|
||||||
if link, lerr := os.Readlink(e.Path); lerr == nil {
|
|
||||||
str := "is symlink %s -> %s mounted?"
|
|
||||||
err = errors.Errorf(str, e.Path, link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
str := "failed to create home directory: %v"
|
|
||||||
err := errors.Errorf(str, err)
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default config.
|
|
||||||
activeConfig = &ConfigFlags{
|
|
||||||
Listen: normalizeAddress("localhost", defaultListenPort),
|
|
||||||
}
|
|
||||||
|
|
||||||
preCfg := activeConfig
|
|
||||||
preParser := flags.NewParser(preCfg, flags.Default)
|
|
||||||
_, err = preParser.Parse()
|
|
||||||
if err != nil {
|
|
||||||
e, ok := err.(*flags.Error)
|
|
||||||
if ok && e.Type == flags.ErrHelp {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
preParser.WriteHelp(os.Stderr)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
appName := filepath.Base(os.Args[0])
|
|
||||||
appName = strings.TrimSuffix(appName, filepath.Ext(appName))
|
|
||||||
usageMessage := fmt.Sprintf("Use %s -h to show usage", appName)
|
|
||||||
|
|
||||||
// Load additional config from file.
|
|
||||||
parser := flags.NewParser(activeConfig, flags.Default)
|
|
||||||
err = flags.NewIniParser(parser).ParseFile(defaultConfigFile)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*os.PathError); !ok {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error parsing ConfigFlags "+
|
|
||||||
"file: %v\n", err)
|
|
||||||
fmt.Fprintln(os.Stderr, usageMessage)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse command line options again to ensure they take precedence.
|
|
||||||
_, err = parser.Parse()
|
|
||||||
if err != nil {
|
|
||||||
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
|
|
||||||
parser.WriteHelp(os.Stderr)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(activeConfig.Host) == 0 {
|
|
||||||
str := "Please specify a hostname"
|
|
||||||
err := errors.Errorf(str)
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(activeConfig.Nameserver) == 0 {
|
|
||||||
str := "Please specify a nameserver"
|
|
||||||
err := errors.Errorf(str)
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
activeConfig.Listen = normalizeAddress(activeConfig.Listen, defaultListenPort)
|
|
||||||
|
|
||||||
err = activeConfig.ResolveNetwork(parser)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
initLog(defaultLogFile, defaultErrLogFile)
|
|
||||||
|
|
||||||
return activeConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeAddress returns addr with the passed default port appended if
|
|
||||||
// there is not already a port specified.
|
|
||||||
func normalizeAddress(addr, defaultPort string) string {
|
|
||||||
_, _, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return net.JoinHostPort(addr, defaultPort)
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
238
dnsseeder/dns.go
238
dnsseeder/dns.go
@@ -1,238 +0,0 @@
|
|||||||
// Copyright (c) 2018 The Decred developers
|
|
||||||
// Use of this source code is governed by an ISC
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/connmgr"
|
|
||||||
"github.com/kaspanet/kaspad/util/subnetworkid"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNSServer struct
|
|
||||||
type DNSServer struct {
|
|
||||||
hostname string
|
|
||||||
listen string
|
|
||||||
nameserver string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start - starts server
|
|
||||||
func (d *DNSServer) Start() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
rr := fmt.Sprintf("%s 86400 IN NS %s", d.hostname, d.nameserver)
|
|
||||||
authority, err := dns.NewRR(rr)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("NewRR: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp4", d.listen)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("ResolveUDPAddr: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
udpListen, err := net.ListenUDP("udp", udpAddr)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("ListenUDP: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer udpListen.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
b := make([]byte, 512)
|
|
||||||
mainLoop:
|
|
||||||
err := udpListen.SetReadDeadline(time.Now().Add(time.Second))
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("SetReadDeadline: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
_, addr, err := udpListen.ReadFromUDP(b)
|
|
||||||
if err != nil {
|
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
|
||||||
if atomic.LoadInt32(&systemShutdown) == 0 {
|
|
||||||
// use goto in order to do not re-allocate 'b' buffer
|
|
||||||
goto mainLoop
|
|
||||||
}
|
|
||||||
log.Infof("DNS server shutdown")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("Read: %T", err.(*net.OpError).Err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
spawn(func() { d.handleDNSRequest(addr, authority, udpListen, b) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDNSServer - create DNS server
|
|
||||||
func NewDNSServer(hostname, nameserver, listen string) *DNSServer {
|
|
||||||
if hostname[len(hostname)-1] != '.' {
|
|
||||||
hostname = hostname + "."
|
|
||||||
}
|
|
||||||
if nameserver[len(nameserver)-1] != '.' {
|
|
||||||
nameserver = nameserver + "."
|
|
||||||
}
|
|
||||||
|
|
||||||
return &DNSServer{
|
|
||||||
hostname: hostname,
|
|
||||||
listen: listen,
|
|
||||||
nameserver: nameserver,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DNSServer) extractServicesSubnetworkID(addr *net.UDPAddr, domainName string) (wire.ServiceFlag, *subnetworkid.SubnetworkID, bool, error) {
|
|
||||||
// Domain name may be in following format:
|
|
||||||
// [n[subnetwork].][xservice.]hostname
|
|
||||||
// where connmgr.SubnetworkIDPrefixChar and connmgr.ServiceFlagPrefixChar are prefexes
|
|
||||||
wantedSF := wire.SFNodeNetwork
|
|
||||||
var subnetworkID *subnetworkid.SubnetworkID
|
|
||||||
includeAllSubnetworks := true
|
|
||||||
if d.hostname != domainName {
|
|
||||||
idx := 0
|
|
||||||
labels := dns.SplitDomainName(domainName)
|
|
||||||
if labels[0][0] == connmgr.SubnetworkIDPrefixChar {
|
|
||||||
includeAllSubnetworks = false
|
|
||||||
if len(labels[0]) > 1 {
|
|
||||||
idx = 1
|
|
||||||
subnetworkID, err := subnetworkid.NewFromStr(labels[0][1:])
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%s: subnetworkid.NewFromStr: %v", addr, err)
|
|
||||||
return wantedSF, subnetworkID, includeAllSubnetworks, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if labels[idx][0] == connmgr.ServiceFlagPrefixChar && len(labels[idx]) > 1 {
|
|
||||||
wantedSFStr := labels[idx][1:]
|
|
||||||
u, err := strconv.ParseUint(wantedSFStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%s: ParseUint: %v", addr, err)
|
|
||||||
return wantedSF, subnetworkID, includeAllSubnetworks, err
|
|
||||||
}
|
|
||||||
wantedSF = wire.ServiceFlag(u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return wantedSF, subnetworkID, includeAllSubnetworks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DNSServer) validateDNSRequest(addr *net.UDPAddr, b []byte) (dnsMsg *dns.Msg, domainName string, atype string, err error) {
|
|
||||||
dnsMsg = new(dns.Msg)
|
|
||||||
err = dnsMsg.Unpack(b[:])
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%s: invalid dns message: %v", addr, err)
|
|
||||||
return nil, "", "", err
|
|
||||||
}
|
|
||||||
if len(dnsMsg.Question) != 1 {
|
|
||||||
str := fmt.Sprintf("%s sent more than 1 question: %d", addr, len(dnsMsg.Question))
|
|
||||||
log.Infof("%s", str)
|
|
||||||
return nil, "", "", errors.Errorf("%s", str)
|
|
||||||
}
|
|
||||||
domainName = strings.ToLower(dnsMsg.Question[0].Name)
|
|
||||||
ff := strings.LastIndex(domainName, d.hostname)
|
|
||||||
if ff < 0 {
|
|
||||||
str := fmt.Sprintf("invalid name: %s", dnsMsg.Question[0].Name)
|
|
||||||
log.Infof("%s", str)
|
|
||||||
return nil, "", "", errors.Errorf("%s", str)
|
|
||||||
}
|
|
||||||
atype, err = translateDNSQuestion(addr, dnsMsg)
|
|
||||||
return dnsMsg, domainName, atype, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func translateDNSQuestion(addr *net.UDPAddr, dnsMsg *dns.Msg) (string, error) {
|
|
||||||
var atype string
|
|
||||||
qtype := dnsMsg.Question[0].Qtype
|
|
||||||
switch qtype {
|
|
||||||
case dns.TypeA:
|
|
||||||
atype = "A"
|
|
||||||
case dns.TypeAAAA:
|
|
||||||
atype = "AAAA"
|
|
||||||
case dns.TypeNS:
|
|
||||||
atype = "NS"
|
|
||||||
default:
|
|
||||||
str := fmt.Sprintf("%s: invalid qtype: %d", addr, dnsMsg.Question[0].Qtype)
|
|
||||||
log.Infof("%s", str)
|
|
||||||
return "", errors.Errorf("%s", str)
|
|
||||||
}
|
|
||||||
return atype, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DNSServer) buildDNSResponse(addr *net.UDPAddr, authority dns.RR, dnsMsg *dns.Msg,
|
|
||||||
wantedSF wire.ServiceFlag, includeAllSubnetworks bool, subnetworkID *subnetworkid.SubnetworkID, atype string) ([]byte, error) {
|
|
||||||
respMsg := dnsMsg.Copy()
|
|
||||||
respMsg.Authoritative = true
|
|
||||||
respMsg.Response = true
|
|
||||||
|
|
||||||
qtype := dnsMsg.Question[0].Qtype
|
|
||||||
if qtype != dns.TypeNS {
|
|
||||||
respMsg.Ns = append(respMsg.Ns, authority)
|
|
||||||
addrs := amgr.GoodAddresses(qtype, wantedSF, includeAllSubnetworks, subnetworkID)
|
|
||||||
for _, a := range addrs {
|
|
||||||
rr := fmt.Sprintf("%s 30 IN %s %s", dnsMsg.Question[0].Name, atype, a.IP.String())
|
|
||||||
newRR, err := dns.NewRR(rr)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%s: NewRR: %v", addr, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
respMsg.Answer = append(respMsg.Answer, newRR)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rr := fmt.Sprintf("%s 86400 IN NS %s", dnsMsg.Question[0].Name, d.nameserver)
|
|
||||||
newRR, err := dns.NewRR(rr)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%s: NewRR: %v", addr, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
respMsg.Answer = append(respMsg.Answer, newRR)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendBytes, err := respMsg.Pack()
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%s: failed to pack response: %v", addr, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sendBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DNSServer) handleDNSRequest(addr *net.UDPAddr, authority dns.RR, udpListen *net.UDPConn, b []byte) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
dnsMsg, domainName, atype, err := d.validateDNSRequest(addr, b)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wantedSF, subnetworkID, includeAllSubnetworks, err := d.extractServicesSubnetworkID(addr, domainName)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("%s: query %d for services %v, subnetwork ID %v",
|
|
||||||
addr, dnsMsg.Question[0].Qtype, wantedSF, subnetworkID)
|
|
||||||
|
|
||||||
sendBytes, err := d.buildDNSResponse(addr, authority, dnsMsg, wantedSF, includeAllSubnetworks, subnetworkID, atype)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = udpListen.WriteToUDP(sendBytes, addr)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%s: failed to write response: %v", addr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
// Copyright (c) 2018 The Decred developers
|
|
||||||
// Use of this source code is governed by an ISC
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/connmgr"
|
|
||||||
"github.com/kaspanet/kaspad/peer"
|
|
||||||
"github.com/kaspanet/kaspad/signal"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// nodeTimeout defines the timeout time waiting for
|
|
||||||
// a response from a node.
|
|
||||||
nodeTimeout = time.Second * 3
|
|
||||||
|
|
||||||
// requiredServices describes the default services that are
|
|
||||||
// required to be supported by outbound peers.
|
|
||||||
requiredServices = wire.SFNodeNetwork
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
amgr *Manager
|
|
||||||
wg sync.WaitGroup
|
|
||||||
peersDefaultPort int
|
|
||||||
systemShutdown int32
|
|
||||||
)
|
|
||||||
|
|
||||||
// hostLookup returns the correct DNS lookup function to use depending on the
|
|
||||||
// passed host and configuration options. For example, .onion addresses will be
|
|
||||||
// resolved using the onion specific proxy if one was specified, but will
|
|
||||||
// otherwise treat the normal proxy as tor unless --noonion was specified in
|
|
||||||
// which case the lookup will fail. Meanwhile, normal IP addresses will be
|
|
||||||
// resolved using tor if a proxy was specified unless --noonion was also
|
|
||||||
// specified in which case the normal system DNS resolver will be used.
|
|
||||||
func hostLookup(host string) ([]net.IP, error) {
|
|
||||||
return net.LookupIP(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func creep() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
onAddr := make(chan struct{})
|
|
||||||
onVersion := make(chan struct{})
|
|
||||||
cfg := peer.Config{
|
|
||||||
UserAgentName: "daglabs-sniffer",
|
|
||||||
UserAgentVersion: "0.0.1",
|
|
||||||
DAGParams: ActiveConfig().NetParams(),
|
|
||||||
DisableRelayTx: true,
|
|
||||||
SelectedTip: func() *daghash.Hash { return ActiveConfig().NetParams().GenesisBlock.BlockHash() },
|
|
||||||
|
|
||||||
Listeners: peer.MessageListeners{
|
|
||||||
OnAddr: func(p *peer.Peer, msg *wire.MsgAddr) {
|
|
||||||
added := amgr.AddAddresses(msg.AddrList)
|
|
||||||
log.Infof("Peer %v sent %v addresses, %d new",
|
|
||||||
p.Addr(), len(msg.AddrList), added)
|
|
||||||
onAddr <- struct{}{}
|
|
||||||
},
|
|
||||||
OnVersion: func(p *peer.Peer, msg *wire.MsgVersion) {
|
|
||||||
log.Infof("Adding peer %v with services %v and subnetword ID %v",
|
|
||||||
p.NA().IP.String(), msg.Services, msg.SubnetworkID)
|
|
||||||
// Mark this peer as a good node.
|
|
||||||
amgr.Good(p.NA().IP, msg.Services, msg.SubnetworkID)
|
|
||||||
// Ask peer for some addresses.
|
|
||||||
p.QueueMessage(wire.NewMsgGetAddr(true, nil), nil)
|
|
||||||
// notify that version is received and Peer's subnetwork ID is updated
|
|
||||||
onVersion <- struct{}{}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wgCreep sync.WaitGroup
|
|
||||||
for {
|
|
||||||
peers := amgr.Addresses()
|
|
||||||
if len(peers) == 0 && amgr.AddressCount() == 0 {
|
|
||||||
// Add peers discovered through DNS to the address manager.
|
|
||||||
connmgr.SeedFromDNS(ActiveConfig().NetParams(), requiredServices, true, nil, hostLookup, func(addrs []*wire.NetAddress) {
|
|
||||||
amgr.AddAddresses(addrs)
|
|
||||||
})
|
|
||||||
peers = amgr.Addresses()
|
|
||||||
}
|
|
||||||
if len(peers) == 0 {
|
|
||||||
log.Infof("No stale addresses -- sleeping for 10 minutes")
|
|
||||||
for i := 0; i < 600; i++ {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
if atomic.LoadInt32(&systemShutdown) != 0 {
|
|
||||||
log.Infof("Creep thread shutdown")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range peers {
|
|
||||||
if atomic.LoadInt32(&systemShutdown) != 0 {
|
|
||||||
log.Infof("Waiting creep threads to terminate")
|
|
||||||
wgCreep.Wait()
|
|
||||||
log.Infof("Creep thread shutdown")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
wgCreep.Add(1)
|
|
||||||
go func(addr *wire.NetAddress) {
|
|
||||||
defer wgCreep.Done()
|
|
||||||
|
|
||||||
host := net.JoinHostPort(addr.IP.String(), strconv.Itoa(int(addr.Port)))
|
|
||||||
p, err := peer.NewOutboundPeer(&cfg, host)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("NewOutboundPeer on %v: %v",
|
|
||||||
host, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
amgr.Attempt(addr.IP)
|
|
||||||
conn, err := net.DialTimeout("tcp", p.Addr(), nodeTimeout)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p.AssociateConnection(conn)
|
|
||||||
|
|
||||||
// Wait version messsage or timeout in case of failure.
|
|
||||||
select {
|
|
||||||
case <-onVersion:
|
|
||||||
case <-time.After(nodeTimeout):
|
|
||||||
log.Warnf("version timeout on peer %v",
|
|
||||||
p.Addr())
|
|
||||||
p.Disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-onAddr:
|
|
||||||
case <-time.After(nodeTimeout):
|
|
||||||
log.Warnf("getaddr timeout on peer %v",
|
|
||||||
p.Addr())
|
|
||||||
p.Disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p.Disconnect()
|
|
||||||
}(addr)
|
|
||||||
}
|
|
||||||
wgCreep.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defer panics.HandlePanic(log, nil, nil)
|
|
||||||
cfg, err := loadConfig()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "loadConfig: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
amgr, err = NewManager(defaultHomeDir)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "NewManager: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
peersDefaultPort, err = strconv.Atoi(ActiveConfig().NetParams().DefaultPort)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Invalid peers default port %s: %v\n", ActiveConfig().NetParams().DefaultPort, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Seeder) != 0 {
|
|
||||||
ip := net.ParseIP(cfg.Seeder)
|
|
||||||
if ip == nil {
|
|
||||||
hostAddrs, err := net.LookupHost(cfg.Seeder)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to resolve seed host: %v, %v, ignoring", cfg.Seeder, err)
|
|
||||||
} else {
|
|
||||||
ip = net.ParseIP(hostAddrs[0])
|
|
||||||
if ip == nil {
|
|
||||||
log.Warnf("Failed to resolve seed host: %v, ignoring", cfg.Seeder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ip != nil {
|
|
||||||
amgr.AddAddresses([]*wire.NetAddress{
|
|
||||||
wire.NewNetAddressIPPort(ip, uint16(peersDefaultPort),
|
|
||||||
requiredServices)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
spawn(creep)
|
|
||||||
|
|
||||||
dnsServer := NewDNSServer(cfg.Host, cfg.Nameserver, cfg.Listen)
|
|
||||||
wg.Add(1)
|
|
||||||
spawn(dnsServer.Start)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
log.Infof("Gracefully shutting down the seeder...")
|
|
||||||
atomic.StoreInt32(&systemShutdown, 1)
|
|
||||||
close(amgr.quit)
|
|
||||||
wg.Wait()
|
|
||||||
amgr.wg.Wait()
|
|
||||||
log.Infof("Seeder shutdown complete")
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait until the interrupt signal is received from an OS signal or
|
|
||||||
// shutdown is requested through one of the subsystems such as the RPC
|
|
||||||
// server.
|
|
||||||
interrupt := signal.InterruptListener()
|
|
||||||
<-interrupt
|
|
||||||
}
|
|
||||||
@@ -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 dnsseeder && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o dnsseeder .
|
|
||||||
|
|
||||||
# --- 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/dnsseeder/ /app/
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
|
||||||
CMD ["/app/dnsseeder"]
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
1. Since btcd is not public repository still, copy/checkout
|
|
||||||
https://github.com/kaspanet/kaspad into vendor/github.com/kaspanet/kaspad before
|
|
||||||
running "docker build".
|
|
||||||
|
|
||||||
2. To build docker image invoke following command from dnsseeder directory:
|
|
||||||
docker build -t dnsseeder -f ./docker/Dockerfile .
|
|
||||||
|
|
||||||
3. To run
|
|
||||||
sudo docker run -u root -p 53:53/udp dnsseeder
|
|
||||||
@@ -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("SEED")
|
|
||||||
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,376 +0,0 @@
|
|||||||
// Copyright (c) 2018 The Decred developers
|
|
||||||
// Use of this source code is governed by an ISC
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/util/subnetworkid"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Node repesents a node in the Kaspa network
|
|
||||||
type Node struct {
|
|
||||||
Addr *wire.NetAddress
|
|
||||||
Services wire.ServiceFlag
|
|
||||||
LastAttempt time.Time
|
|
||||||
LastSuccess time.Time
|
|
||||||
LastSeen time.Time
|
|
||||||
SubnetworkID *subnetworkid.SubnetworkID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manager is dnsseeder's main worker-type, storing all information required
|
|
||||||
// for operation
|
|
||||||
type Manager struct {
|
|
||||||
mtx sync.RWMutex
|
|
||||||
|
|
||||||
nodes map[string]*Node
|
|
||||||
wg sync.WaitGroup
|
|
||||||
quit chan struct{}
|
|
||||||
peersFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// defaultMaxAddresses is the maximum number of addresses to return.
|
|
||||||
defaultMaxAddresses = 16
|
|
||||||
|
|
||||||
// defaultStaleTimeout is the time in which a host is considered
|
|
||||||
// stale.
|
|
||||||
defaultStaleTimeout = time.Hour
|
|
||||||
|
|
||||||
// dumpAddressInterval is the interval used to dump the address
|
|
||||||
// cache to disk for future use.
|
|
||||||
dumpAddressInterval = time.Second * 30
|
|
||||||
|
|
||||||
// peersFilename is the name of the file.
|
|
||||||
peersFilename = "nodes.json"
|
|
||||||
|
|
||||||
// pruneAddressInterval is the interval used to run the address
|
|
||||||
// pruner.
|
|
||||||
pruneAddressInterval = time.Minute * 1
|
|
||||||
|
|
||||||
// pruneExpireTimeout is the expire time in which a node is
|
|
||||||
// considered dead.
|
|
||||||
pruneExpireTimeout = time.Hour * 8
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// rfc1918Nets specifies the IPv4 private address blocks as defined by
|
|
||||||
// by RFC1918 (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16).
|
|
||||||
rfc1918Nets = []net.IPNet{
|
|
||||||
ipNet("10.0.0.0", 8, 32),
|
|
||||||
ipNet("172.16.0.0", 12, 32),
|
|
||||||
ipNet("192.168.0.0", 16, 32),
|
|
||||||
}
|
|
||||||
|
|
||||||
// rfc3964Net specifies the IPv6 to IPv4 encapsulation address block as
|
|
||||||
// defined by RFC3964 (2002::/16).
|
|
||||||
rfc3964Net = ipNet("2002::", 16, 128)
|
|
||||||
|
|
||||||
// rfc4380Net specifies the IPv6 teredo tunneling over UDP address block
|
|
||||||
// as defined by RFC4380 (2001::/32).
|
|
||||||
rfc4380Net = ipNet("2001::", 32, 128)
|
|
||||||
|
|
||||||
// rfc4843Net specifies the IPv6 ORCHID address block as defined by
|
|
||||||
// RFC4843 (2001:10::/28).
|
|
||||||
rfc4843Net = ipNet("2001:10::", 28, 128)
|
|
||||||
|
|
||||||
// rfc4862Net specifies the IPv6 stateless address autoconfiguration
|
|
||||||
// address block as defined by RFC4862 (FE80::/64).
|
|
||||||
rfc4862Net = ipNet("FE80::", 64, 128)
|
|
||||||
|
|
||||||
// rfc4193Net specifies the IPv6 unique local address block as defined
|
|
||||||
// by RFC4193 (FC00::/7).
|
|
||||||
rfc4193Net = ipNet("FC00::", 7, 128)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ipNet returns a net.IPNet struct given the passed IP address string, number
|
|
||||||
// of one bits to include at the start of the mask, and the total number of bits
|
|
||||||
// for the mask.
|
|
||||||
func ipNet(ip string, ones, bits int) net.IPNet {
|
|
||||||
return net.IPNet{IP: net.ParseIP(ip), Mask: net.CIDRMask(ones, bits)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRoutable(addr net.IP) bool {
|
|
||||||
if ActiveConfig().NetParams().AcceptUnroutable {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range rfc1918Nets {
|
|
||||||
if n.Contains(addr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rfc3964Net.Contains(addr) ||
|
|
||||||
rfc4380Net.Contains(addr) ||
|
|
||||||
rfc4843Net.Contains(addr) ||
|
|
||||||
rfc4862Net.Contains(addr) ||
|
|
||||||
rfc4193Net.Contains(addr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewManager constructs and returns a new dnsseeder manager, with the provided dataDir
|
|
||||||
func NewManager(dataDir string) (*Manager, error) {
|
|
||||||
amgr := Manager{
|
|
||||||
nodes: make(map[string]*Node),
|
|
||||||
peersFile: filepath.Join(dataDir, peersFilename),
|
|
||||||
quit: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := amgr.deserializePeers()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to parse file %s: %v", amgr.peersFile, err)
|
|
||||||
// if it is invalid we nuke the old one unconditionally.
|
|
||||||
err = os.Remove(amgr.peersFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to remove corrupt peers file %s: %v",
|
|
||||||
amgr.peersFile, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
amgr.wg.Add(1)
|
|
||||||
spawn(amgr.addressHandler)
|
|
||||||
|
|
||||||
return &amgr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAddresses adds an address to this dnsseeder manager, and returns the number of
|
|
||||||
// address currently held
|
|
||||||
func (m *Manager) AddAddresses(addrs []*wire.NetAddress) int {
|
|
||||||
var count int
|
|
||||||
|
|
||||||
m.mtx.Lock()
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if !isRoutable(addr.IP) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addrStr := addr.IP.String()
|
|
||||||
|
|
||||||
_, exists := m.nodes[addrStr]
|
|
||||||
if exists {
|
|
||||||
m.nodes[addrStr].LastSeen = time.Now()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
node := Node{
|
|
||||||
Addr: addr,
|
|
||||||
LastSeen: time.Now(),
|
|
||||||
}
|
|
||||||
m.nodes[addrStr] = &node
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
m.mtx.Unlock()
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addresses returns IPs that need to be tested again.
|
|
||||||
func (m *Manager) Addresses() []*wire.NetAddress {
|
|
||||||
addrs := make([]*wire.NetAddress, 0, defaultMaxAddresses*8)
|
|
||||||
now := time.Now()
|
|
||||||
i := defaultMaxAddresses
|
|
||||||
|
|
||||||
m.mtx.RLock()
|
|
||||||
for _, node := range m.nodes {
|
|
||||||
if i == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if now.Sub(node.LastSuccess) < defaultStaleTimeout ||
|
|
||||||
now.Sub(node.LastAttempt) < defaultStaleTimeout {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addrs = append(addrs, node.Addr)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
m.mtx.RUnlock()
|
|
||||||
|
|
||||||
return addrs
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddressCount returns number of known nodes.
|
|
||||||
func (m *Manager) AddressCount() int {
|
|
||||||
return len(m.nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoodAddresses returns good working IPs that match both the
|
|
||||||
// passed DNS query type and have the requested services.
|
|
||||||
func (m *Manager) GoodAddresses(qtype uint16, services wire.ServiceFlag, includeAllSubnetworks bool, subnetworkID *subnetworkid.SubnetworkID) []*wire.NetAddress {
|
|
||||||
addrs := make([]*wire.NetAddress, 0, defaultMaxAddresses)
|
|
||||||
i := defaultMaxAddresses
|
|
||||||
|
|
||||||
if qtype != dns.TypeA && qtype != dns.TypeAAAA {
|
|
||||||
return addrs
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
m.mtx.RLock()
|
|
||||||
for _, node := range m.nodes {
|
|
||||||
if i == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Addr.Port != uint16(peersDefaultPort) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !includeAllSubnetworks && !node.SubnetworkID.IsEqual(subnetworkID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if qtype == dns.TypeA && node.Addr.IP.To4() == nil {
|
|
||||||
continue
|
|
||||||
} else if qtype == dns.TypeAAAA && node.Addr.IP.To4() != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.LastSuccess.IsZero() ||
|
|
||||||
now.Sub(node.LastSuccess) > defaultStaleTimeout {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Does the node have the requested services?
|
|
||||||
if node.Services&services != services {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addrs = append(addrs, node.Addr)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
m.mtx.RUnlock()
|
|
||||||
|
|
||||||
return addrs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt updates the last connection attempt for the specified ip address to now
|
|
||||||
func (m *Manager) Attempt(ip net.IP) {
|
|
||||||
m.mtx.Lock()
|
|
||||||
node, exists := m.nodes[ip.String()]
|
|
||||||
if exists {
|
|
||||||
node.LastAttempt = time.Now()
|
|
||||||
}
|
|
||||||
m.mtx.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Good updates the last successful connection attempt for the specified ip address to now
|
|
||||||
func (m *Manager) Good(ip net.IP, services wire.ServiceFlag, subnetworkid *subnetworkid.SubnetworkID) {
|
|
||||||
m.mtx.Lock()
|
|
||||||
node, exists := m.nodes[ip.String()]
|
|
||||||
if exists {
|
|
||||||
node.Services = services
|
|
||||||
node.LastSuccess = time.Now()
|
|
||||||
node.SubnetworkID = subnetworkid
|
|
||||||
}
|
|
||||||
m.mtx.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// addressHandler is the main handler for the address manager. It must be run
|
|
||||||
// as a goroutine.
|
|
||||||
func (m *Manager) addressHandler() {
|
|
||||||
defer m.wg.Done()
|
|
||||||
pruneAddressTicker := time.NewTicker(pruneAddressInterval)
|
|
||||||
defer pruneAddressTicker.Stop()
|
|
||||||
dumpAddressTicker := time.NewTicker(dumpAddressInterval)
|
|
||||||
defer dumpAddressTicker.Stop()
|
|
||||||
out:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-dumpAddressTicker.C:
|
|
||||||
m.savePeers()
|
|
||||||
case <-pruneAddressTicker.C:
|
|
||||||
m.prunePeers()
|
|
||||||
case <-m.quit:
|
|
||||||
break out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Infof("Address manager: saving peers")
|
|
||||||
m.savePeers()
|
|
||||||
log.Infof("Address manager shoutdown")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) prunePeers() {
|
|
||||||
var count int
|
|
||||||
now := time.Now()
|
|
||||||
m.mtx.Lock()
|
|
||||||
for k, node := range m.nodes {
|
|
||||||
if now.Sub(node.LastSeen) > pruneExpireTimeout {
|
|
||||||
delete(m.nodes, k)
|
|
||||||
count++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !node.LastSuccess.IsZero() &&
|
|
||||||
now.Sub(node.LastSuccess) > pruneExpireTimeout {
|
|
||||||
delete(m.nodes, k)
|
|
||||||
count++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
l := len(m.nodes)
|
|
||||||
m.mtx.Unlock()
|
|
||||||
|
|
||||||
log.Infof("Pruned %d addresses: %d remaining", count, l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) deserializePeers() error {
|
|
||||||
filePath := m.peersFile
|
|
||||||
_, err := os.Stat(filePath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
r, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("%s error opening file: %v", filePath, err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
var nodes map[string]*Node
|
|
||||||
dec := json.NewDecoder(r)
|
|
||||||
err = dec.Decode(&nodes)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("error reading %s: %v", filePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l := len(nodes)
|
|
||||||
|
|
||||||
m.mtx.Lock()
|
|
||||||
m.nodes = nodes
|
|
||||||
m.mtx.Unlock()
|
|
||||||
|
|
||||||
log.Infof("%d nodes loaded", l)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) savePeers() {
|
|
||||||
m.mtx.RLock()
|
|
||||||
defer m.mtx.RUnlock()
|
|
||||||
|
|
||||||
// Write temporary peers file and then move it into place.
|
|
||||||
tmpfile := m.peersFile + ".new"
|
|
||||||
w, err := os.Create(tmpfile)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error opening file %s: %v", tmpfile, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
if err := enc.Encode(&m.nodes); err != nil {
|
|
||||||
log.Errorf("Failed to encode file %s: %v", tmpfile, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
log.Errorf("Error closing file %s: %v", tmpfile, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := os.Rename(tmpfile, m.peersFile); err != nil {
|
|
||||||
log.Errorf("Error writing file %s: %v", m.peersFile, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jessevdk/go-flags"
|
|
||||||
"github.com/kaspanet/kaspad/dagconfig"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultLogFilename = "faucet.log"
|
|
||||||
defaultErrLogFilename = "faucet_err.log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default configuration options
|
|
||||||
defaultLogDir = util.AppDataDir("faucet", false)
|
|
||||||
defaultDBAddress = "localhost:3306"
|
|
||||||
defaultHTTPListen = "0.0.0.0:8081"
|
|
||||||
|
|
||||||
// activeNetParams are the currently active net params
|
|
||||||
activeNetParams *dagconfig.Params
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config defines the configuration options for the API server.
|
|
||||||
type Config struct {
|
|
||||||
LogDir string `long:"logdir" description:"Directory to log output."`
|
|
||||||
HTTPListen string `long:"listen" description:"HTTP address to listen on (default: 0.0.0.0:8081)"`
|
|
||||||
KasparovdURL string `long:"kasparovd-url" description:"The API server url to connect to"`
|
|
||||||
PrivateKey string `long:"private-key" description:"Faucet Private key"`
|
|
||||||
DBAddress string `long:"dbaddress" description:"Database address"`
|
|
||||||
DBUser string `long:"dbuser" description:"Database user" required:"true"`
|
|
||||||
DBPassword string `long:"dbpass" description:"Database password" required:"true"`
|
|
||||||
DBName string `long:"dbname" description:"Database name" required:"true"`
|
|
||||||
Migrate bool `long:"migrate" description:"Migrate the database to the latest version. The server will not start when using this flag."`
|
|
||||||
FeeRate float64 `long:"fee-rate" description:"Coins per gram fee rate"`
|
|
||||||
TestNet bool `long:"testnet" description:"Connect to testnet"`
|
|
||||||
SimNet bool `long:"simnet" description:"Connect to the simulation test network"`
|
|
||||||
DevNet bool `long:"devnet" description:"Connect to the development test network"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg *Config
|
|
||||||
|
|
||||||
// Parse parses the CLI arguments and returns a config struct.
|
|
||||||
func Parse() error {
|
|
||||||
cfg = &Config{
|
|
||||||
LogDir: defaultLogDir,
|
|
||||||
DBAddress: defaultDBAddress,
|
|
||||||
HTTPListen: defaultHTTPListen,
|
|
||||||
}
|
|
||||||
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
|
|
||||||
_, err := parser.Parse()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.Migrate {
|
|
||||||
if cfg.KasparovdURL == "" {
|
|
||||||
return errors.New("api-server-url argument is required when --migrate flag is not raised")
|
|
||||||
}
|
|
||||||
if cfg.PrivateKey == "" {
|
|
||||||
return errors.New("private-key argument is required when --migrate flag is not raised")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = resolveNetwork(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logFile := filepath.Join(cfg.LogDir, defaultLogFilename)
|
|
||||||
errLogFile := filepath.Join(cfg.LogDir, defaultErrLogFilename)
|
|
||||||
logger.InitLog(logFile, errLogFile)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveNetwork(cfg *Config) error {
|
|
||||||
// Multiple networks can't be selected simultaneously.
|
|
||||||
numNets := 0
|
|
||||||
if cfg.TestNet {
|
|
||||||
numNets++
|
|
||||||
}
|
|
||||||
if cfg.SimNet {
|
|
||||||
numNets++
|
|
||||||
}
|
|
||||||
if cfg.DevNet {
|
|
||||||
numNets++
|
|
||||||
}
|
|
||||||
if numNets > 1 {
|
|
||||||
return errors.New("multiple net params (testnet, simnet, devnet, etc.) can't be used " +
|
|
||||||
"together -- choose one of them")
|
|
||||||
}
|
|
||||||
|
|
||||||
activeNetParams = &dagconfig.MainNetParams
|
|
||||||
switch {
|
|
||||||
case cfg.TestNet:
|
|
||||||
activeNetParams = &dagconfig.TestNetParams
|
|
||||||
case cfg.SimNet:
|
|
||||||
activeNetParams = &dagconfig.SimNetParams
|
|
||||||
case cfg.DevNet:
|
|
||||||
activeNetParams = &dagconfig.DevNetParams
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MainConfig is a getter to the main config
|
|
||||||
func MainConfig() (*Config, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, errors.New("No configuration was set for the faucet")
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveNetParams returns the currently active net params
|
|
||||||
func ActiveNetParams() *dagconfig.Params {
|
|
||||||
return activeNetParams
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
nativeerrors "errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4/source"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// db is the API server database.
|
|
||||||
var db *gorm.DB
|
|
||||||
|
|
||||||
// DB returns a reference to the database connection
|
|
||||||
func DB() (*gorm.DB, error) {
|
|
||||||
if db == nil {
|
|
||||||
return nil, errors.New("Database is not connected")
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type gormLogger struct{}
|
|
||||||
|
|
||||||
func (l gormLogger) Print(v ...interface{}) {
|
|
||||||
str := fmt.Sprint(v...)
|
|
||||||
log.Errorf(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect connects to the database mentioned in
|
|
||||||
// config variable.
|
|
||||||
func Connect() error {
|
|
||||||
connectionString, err := buildConnectionString()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
migrator, driver, err := openMigrator(connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
isCurrent, version, err := isCurrent(migrator, driver)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error checking whether the database is current: %s", err)
|
|
||||||
}
|
|
||||||
if !isCurrent {
|
|
||||||
return errors.Errorf("Database is not current (version %d). Please migrate"+
|
|
||||||
" the database by running the faucet with --migrate flag and then run it again.", version)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err = gorm.Open("mysql", connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
db.SetLogger(gormLogger{})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the connection to the database
|
|
||||||
func Close() error {
|
|
||||||
if db == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := db.Close()
|
|
||||||
db = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildConnectionString() (string, error) {
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True",
|
|
||||||
cfg.DBUser, cfg.DBPassword, cfg.DBAddress, cfg.DBName), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isCurrent resolves whether the database is on the latest
|
|
||||||
// version of the schema.
|
|
||||||
func isCurrent(migrator *migrate.Migrate, driver source.Driver) (bool, uint, error) {
|
|
||||||
// Get the current version
|
|
||||||
version, isDirty, err := migrator.Version()
|
|
||||||
if nativeerrors.Is(err, migrate.ErrNilVersion) {
|
|
||||||
return false, 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, 0, err
|
|
||||||
}
|
|
||||||
if isDirty {
|
|
||||||
return false, 0, errors.Errorf("Database is dirty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The database is current if Next returns ErrNotExist
|
|
||||||
_, err = driver.Next(version)
|
|
||||||
if pathErr, ok := err.(*os.PathError); ok {
|
|
||||||
if pathErr.Err == os.ErrNotExist {
|
|
||||||
return true, version, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, version, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func openMigrator(connectionString string) (*migrate.Migrate, source.Driver, error) {
|
|
||||||
driver, err := source.Open("file://migrations")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
migrator, err := migrate.NewWithSourceInstance(
|
|
||||||
"migrations", driver, "mysql://"+connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return migrator, driver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate database to the latest version.
|
|
||||||
func Migrate() error {
|
|
||||||
connectionString, err := buildConnectionString()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
migrator, driver, err := openMigrator(connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
isCurrent, version, err := isCurrent(migrator, driver)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error checking whether the database is current: %s", err)
|
|
||||||
}
|
|
||||||
if isCurrent {
|
|
||||||
log.Infof("Database is already up-to-date (version %d)", version)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = migrator.Up()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version, isDirty, err := migrator.Version()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isDirty {
|
|
||||||
return errors.Errorf("error migrating database: database is dirty")
|
|
||||||
}
|
|
||||||
log.Infof("Migrated database to the latest version (version %d)", version)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/util/panics"
|
|
||||||
import "github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.BackendLog.Logger("DTBS")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
@@ -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 faucet && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o faucet .
|
|
||||||
|
|
||||||
# --- 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/faucet /app/
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
|
||||||
CMD ["/app/faucet"]
|
|
||||||
332
faucet/faucet.go
332
faucet/faucet.go
@@ -1,332 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/blockdag"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/apimodels"
|
|
||||||
"github.com/kaspanet/kaspad/txscript"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
sendAmount = 10000
|
|
||||||
// Value 8 bytes + serialized varint size for the length of ScriptPubKey +
|
|
||||||
// ScriptPubKey bytes.
|
|
||||||
outputSize uint64 = 8 + 1 + 25
|
|
||||||
minTxFee uint64 = 3000
|
|
||||||
|
|
||||||
requiredConfirmations = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
type utxoSet map[wire.Outpoint]*blockdag.UTXOEntry
|
|
||||||
|
|
||||||
// apiURL returns a full concatenated URL from the base
|
|
||||||
// API server URL and the given path.
|
|
||||||
func apiURL(requestPath string) (string, error) {
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
u, err := url.Parse(cfg.KasparovdURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithStack(err)
|
|
||||||
}
|
|
||||||
u.Path = path.Join(u.Path, requestPath)
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFromAPIServer makes an HTTP GET request to the API server
|
|
||||||
// to the given request path, and returns the response body.
|
|
||||||
func getFromAPIServer(requestPath string) ([]byte, error) {
|
|
||||||
getAPIURL, err := apiURL(requestPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := http.Get(getAPIURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
clientError := &httpserverutils.ClientError{}
|
|
||||||
err := json.Unmarshal(body, &clientError)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
return nil, errors.WithStack(clientError)
|
|
||||||
}
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFromAPIServer makes an HTTP POST request to the API server
|
|
||||||
// to the given request path. It converts the given data to JSON,
|
|
||||||
// and post it as the POST data.
|
|
||||||
func postToAPIServer(requestPath string, data interface{}) error {
|
|
||||||
dataBytes, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
r := bytes.NewReader(dataBytes)
|
|
||||||
postAPIURL, err := apiURL(requestPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp, err := http.Post(postAPIURL, "application/json", r)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
clientError := &httpserverutils.ClientError{}
|
|
||||||
err = json.Unmarshal(body, &clientError)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
return errors.WithStack(clientError)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isUTXOMatured(entry *blockdag.UTXOEntry, confirmations uint64) bool {
|
|
||||||
if entry.IsCoinbase() {
|
|
||||||
return confirmations >= config.ActiveNetParams().BlockCoinbaseMaturity
|
|
||||||
}
|
|
||||||
return confirmations >= requiredConfirmations
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWalletUTXOSet() (utxoSet, error) {
|
|
||||||
body, err := getFromAPIServer(fmt.Sprintf("utxos/address/%s", faucetAddress.EncodeAddress()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
utxoResponses := []*apimodels.TransactionOutputResponse{}
|
|
||||||
err = json.Unmarshal(body, &utxoResponses)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
walletUTXOSet := make(utxoSet)
|
|
||||||
for _, utxoResponse := range utxoResponses {
|
|
||||||
scriptPubKey, err := hex.DecodeString(utxoResponse.ScriptPubKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
txOut := &wire.TxOut{
|
|
||||||
Value: utxoResponse.Value,
|
|
||||||
ScriptPubKey: scriptPubKey,
|
|
||||||
}
|
|
||||||
txID, err := daghash.NewTxIDFromStr(utxoResponse.TransactionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
outpoint := wire.NewOutpoint(txID, utxoResponse.Index)
|
|
||||||
utxoEntry := blockdag.NewUTXOEntry(txOut, *utxoResponse.IsCoinbase, utxoResponse.AcceptingBlockBlueScore)
|
|
||||||
if !isUTXOMatured(utxoEntry, *utxoResponse.Confirmations) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
walletUTXOSet[*outpoint] = utxoEntry
|
|
||||||
}
|
|
||||||
return walletUTXOSet, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToAddress(address util.Address) (*wire.MsgTx, error) {
|
|
||||||
tx, err := createTx(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
|
|
||||||
if err := tx.Serialize(buf); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawTx := &apimodels.RawTransaction{RawTransaction: hex.EncodeToString(buf.Bytes())}
|
|
||||||
return tx, postToAPIServer("transaction", rawTx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTx(address util.Address) (*wire.MsgTx, error) {
|
|
||||||
walletUTXOSet, err := getWalletUTXOSet()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tx, err := createUnsignedTx(walletUTXOSet, address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = signTx(walletUTXOSet, tx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createUnsignedTx(walletUTXOSet utxoSet, address util.Address) (*wire.MsgTx, error) {
|
|
||||||
tx := wire.NewNativeMsgTx(wire.TxVersion, nil, nil)
|
|
||||||
netAmount, isChangeOutputRequired, err := fundTx(walletUTXOSet, tx, sendAmount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isChangeOutputRequired {
|
|
||||||
tx.AddTxOut(&wire.TxOut{
|
|
||||||
Value: sendAmount,
|
|
||||||
ScriptPubKey: address.ScriptAddress(),
|
|
||||||
})
|
|
||||||
tx.AddTxOut(&wire.TxOut{
|
|
||||||
Value: netAmount - sendAmount,
|
|
||||||
ScriptPubKey: faucetScriptPubKey,
|
|
||||||
})
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
tx.AddTxOut(&wire.TxOut{
|
|
||||||
Value: netAmount,
|
|
||||||
ScriptPubKey: address.ScriptAddress(),
|
|
||||||
})
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// signTx signs a transaction
|
|
||||||
func signTx(walletUTXOSet utxoSet, tx *wire.MsgTx) error {
|
|
||||||
for i, txIn := range tx.TxIn {
|
|
||||||
outpoint := txIn.PreviousOutpoint
|
|
||||||
|
|
||||||
sigScript, err := txscript.SignatureScript(tx, i, walletUTXOSet[outpoint].ScriptPubKey(),
|
|
||||||
txscript.SigHashAll, faucetPrivateKey, 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) (netAmount uint64, isChangeOutputRequired bool, err error) {
|
|
||||||
amountSelected := uint64(0)
|
|
||||||
isTxFunded := false
|
|
||||||
for outpoint, entry := range walletUTXOSet {
|
|
||||||
amountSelected += entry.Amount()
|
|
||||||
|
|
||||||
// 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 the current amount selected to pay the fee continue
|
|
||||||
// to grab more coins.
|
|
||||||
isTxFunded, isChangeOutputRequired, netAmount, err = isFundedAndIsChangeOutputRequired(tx, amountSelected, amount, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false, err
|
|
||||||
}
|
|
||||||
if isTxFunded {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isTxFunded {
|
|
||||||
return 0, false, errors.Errorf("not enough funds for coin selection")
|
|
||||||
}
|
|
||||||
|
|
||||||
return netAmount, isChangeOutputRequired, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFundedAndIsChangeOutputRequired returns three values and an error:
|
|
||||||
// * isTxFunded is whether the transaction inputs cover the target amount + the required fee.
|
|
||||||
// * isChangeOutputRequired is whether it is profitable to add an additional change
|
|
||||||
// output to the transaction.
|
|
||||||
// * netAmount is the amount of coins that will be eventually sent to the recipient. If no
|
|
||||||
// change output is needed, the netAmount will be usually a little bit higher than the
|
|
||||||
// targetAmount. Otherwise, it'll be the same as the targetAmount.
|
|
||||||
func isFundedAndIsChangeOutputRequired(tx *wire.MsgTx, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded, isChangeOutputRequired bool, netAmount uint64, err error) {
|
|
||||||
// First check if it can be funded with one output and the required fee for it.
|
|
||||||
isFundedWithOneOutput, oneOutputFee, err := isFundedWithNumberOfOutputs(tx, 1, amountSelected, targetAmount, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, 0, err
|
|
||||||
}
|
|
||||||
if !isFundedWithOneOutput {
|
|
||||||
return false, false, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now check if it can be funded with two outputs and the required fee for it.
|
|
||||||
isFundedWithTwoOutputs, twoOutputsFee, err := isFundedWithNumberOfOutputs(tx, 2, amountSelected, targetAmount, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it can be funded with two outputs, check if adding a change output worth it: i.e. check if
|
|
||||||
// the amount you save by not sending the recipient the whole inputs amount (minus fees) is greater
|
|
||||||
// than the additional fee that is required by adding a change output. If this is the case, return
|
|
||||||
// isChangeOutputRequired as true.
|
|
||||||
if isFundedWithTwoOutputs && twoOutputsFee-oneOutputFee < targetAmount-amountSelected {
|
|
||||||
return true, true, amountSelected - twoOutputsFee, nil
|
|
||||||
}
|
|
||||||
return true, false, amountSelected - oneOutputFee, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFundedWithNumberOfOutputs returns whether the transaction inputs cover
|
|
||||||
// the target amount + the required fee with the assumed number of outputs.
|
|
||||||
func isFundedWithNumberOfOutputs(tx *wire.MsgTx, numberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded bool, fee uint64, err error) {
|
|
||||||
reqFee, err := calcFee(tx, numberOfOutputs, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return false, 0, err
|
|
||||||
}
|
|
||||||
return amountSelected > reqFee && amountSelected-reqFee >= targetAmount, reqFee, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcFee(msgTx *wire.MsgTx, numberOfOutputs uint64, walletUTXOSet utxoSet) (uint64, error) {
|
|
||||||
txMass := calcTxMass(msgTx, walletUTXOSet)
|
|
||||||
txMassWithOutputs := txMass + outputsTotalSize(numberOfOutputs)*blockdag.MassPerTxByte
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
reqFee := uint64(float64(txMassWithOutputs) * cfg.FeeRate)
|
|
||||||
if reqFee < minTxFee {
|
|
||||||
return minTxFee, nil
|
|
||||||
}
|
|
||||||
return reqFee, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
previousScriptPubKeys[i] = walletUTXOSet[outpoint].ScriptPubKey()
|
|
||||||
}
|
|
||||||
return previousScriptPubKeys
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/faucet/database"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const minRequestInterval = time.Hour * 24
|
|
||||||
|
|
||||||
type ipUse struct {
|
|
||||||
IP string
|
|
||||||
LastUse time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func ipFromRequest(r *http.Request) (string, error) {
|
|
||||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateIPUsage(r *http.Request) error {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
timeBeforeMinRequestInterval := now.Add(-minRequestInterval)
|
|
||||||
var count int
|
|
||||||
ip, err := ipFromRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dbResult := db.Model(&ipUse{}).Where(&ipUse{IP: ip}).Where("last_use BETWEEN ? AND ?", timeBeforeMinRequestInterval, now).Count(&count)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return httpserverutils.NewErrorFromDBErrors("Some errors were encountered when checking the last use of an IP:", dbResult.GetErrors())
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
return httpserverutils.NewHandlerError(http.StatusForbidden, errors.New("A user is allowed to to have one request from the faucet every 24 hours"))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateIPUsage(r *http.Request) error {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err := ipFromRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dbResult := db.Where(&ipUse{IP: ip}).Assign(&ipUse{LastUse: time.Now()}).FirstOrCreate(&ipUse{})
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return httpserverutils.NewErrorFromDBErrors("Some errors were encountered when upserting the IP to the new date:", dbResult.GetErrors())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/logger"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.BackendLog.Logger("FAUC")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/kaspanet/kaspad/dagconfig"
|
|
||||||
"github.com/kaspanet/kaspad/ecc"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/database"
|
|
||||||
"github.com/kaspanet/kaspad/txscript"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/kaspanet/kaspad/util/base58"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/database/mysql"
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
||||||
"github.com/kaspanet/kaspad/signal"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
faucetAddress util.Address
|
|
||||||
faucetPrivateKey *ecc.PrivateKey
|
|
||||||
faucetScriptPubKey []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defer panics.HandlePanic(log, nil, nil)
|
|
||||||
|
|
||||||
err := config.Parse()
|
|
||||||
if err != nil {
|
|
||||||
err := errors.Wrap(err, "Error parsing command-line arguments")
|
|
||||||
_, err = fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Migrate {
|
|
||||||
err := database.Migrate()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error migrating database: %s", err))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = database.Connect()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error connecting to database: %s", err))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := database.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error closing the database: %s", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
privateKeyBytes := base58.Decode(cfg.PrivateKey)
|
|
||||||
faucetPrivateKey, _ = ecc.PrivKeyFromBytes(ecc.S256(), privateKeyBytes)
|
|
||||||
|
|
||||||
faucetAddress, err = privateKeyToP2PKHAddress(faucetPrivateKey, config.ActiveNetParams())
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Failed to get P2PKH address from private key: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
faucetScriptPubKey, err = txscript.PayToAddrScript(faucetAddress)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("failed to generate faucetScriptPubKey to address: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdownServer := startHTTPServer(cfg.HTTPListen)
|
|
||||||
defer shutdownServer()
|
|
||||||
|
|
||||||
interrupt := signal.InterruptListener()
|
|
||||||
<-interrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `ip_uses`;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE `ip_uses`
|
|
||||||
(
|
|
||||||
`ip` VARCHAR(39) NOT NULL,
|
|
||||||
`last_use` DATETIME NOT NULL,
|
|
||||||
PRIMARY KEY (`ip`)
|
|
||||||
);
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
const gracefulShutdownTimeout = 30 * time.Second
|
|
||||||
|
|
||||||
// startHTTPServer starts the HTTP REST server and returns a
|
|
||||||
// function to gracefully shutdown it.
|
|
||||||
func startHTTPServer(listenAddr string) func() {
|
|
||||||
router := mux.NewRouter()
|
|
||||||
router.Use(httpserverutils.AddRequestMetadataMiddleware)
|
|
||||||
router.Use(httpserverutils.RecoveryMiddleware)
|
|
||||||
router.Use(httpserverutils.LoggingMiddleware)
|
|
||||||
router.Use(httpserverutils.SetJSONMiddleware)
|
|
||||||
router.HandleFunc(
|
|
||||||
"/request_money",
|
|
||||||
httpserverutils.MakeHandler(requestMoneyHandler)).
|
|
||||||
Methods("POST")
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: listenAddr,
|
|
||||||
Handler: handlers.CORS()(router),
|
|
||||||
}
|
|
||||||
spawn(func() {
|
|
||||||
log.Errorf("%s", httpServer.ListenAndServe())
|
|
||||||
})
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
|
|
||||||
defer cancel()
|
|
||||||
err := httpServer.Shutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error shutting down HTTP server: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestMoneyData struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestMoneyHandler(_ *httpserverutils.ServerContext, r *http.Request, _ map[string]string, _ map[string]string,
|
|
||||||
requestBody []byte) (interface{}, error) {
|
|
||||||
hErr := validateIPUsage(r)
|
|
||||||
if hErr != nil {
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
requestData := &requestMoneyData{}
|
|
||||||
err := json.Unmarshal(requestBody, requestData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "Error unmarshalling request body"),
|
|
||||||
"The request body is not json-formatted")
|
|
||||||
}
|
|
||||||
address, err := util.DecodeAddress(requestData.Address, config.ActiveNetParams().Prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "Error decoding address"),
|
|
||||||
"Error decoding address")
|
|
||||||
}
|
|
||||||
tx, err := sendToAddress(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hErr = updateIPUsage(r)
|
|
||||||
if hErr != nil {
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
return tx.TxID().String(), nil
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package httpserverutils
|
package httpserverutils
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/util/panics"
|
import (
|
||||||
import "github.com/kaspanet/kaspad/kasparov/logger"
|
"github.com/kaspanet/kaspad/logger"
|
||||||
|
"github.com/kaspanet/kaspad/util/panics"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log = logger.BackendLog.Logger("UTIL")
|
log = logger.BackendLog.Logger("UTIL")
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jessevdk/go-flags"
|
|
||||||
"github.com/kaspanet/kaspad/config"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default configuration options
|
|
||||||
defaultDBAddress = "localhost:3306"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KasparovFlags holds configuration common to both the Kasparov server and the Kasparov daemon.
|
|
||||||
type KasparovFlags struct {
|
|
||||||
LogDir string `long:"logdir" description:"Directory to log output."`
|
|
||||||
DebugLevel string `short:"d" long:"debuglevel" description:"Set log level {trace, debug, info, warn, error, critical}"`
|
|
||||||
DBAddress string `long:"dbaddress" description:"Database address"`
|
|
||||||
DBUser string `long:"dbuser" description:"Database user" required:"true"`
|
|
||||||
DBPassword string `long:"dbpass" description:"Database password" required:"true"`
|
|
||||||
DBName string `long:"dbname" description:"Database name" required:"true"`
|
|
||||||
RPCUser string `short:"u" long:"rpcuser" description:"RPC username"`
|
|
||||||
RPCPassword string `short:"P" long:"rpcpass" default-mask:"-" description:"RPC password"`
|
|
||||||
RPCServer string `short:"s" long:"rpcserver" description:"RPC server to connect to"`
|
|
||||||
RPCCert string `short:"c" long:"rpccert" description:"RPC server certificate chain for validation"`
|
|
||||||
DisableTLS bool `long:"notls" description:"Disable TLS"`
|
|
||||||
config.NetworkFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveKasparovFlags parses command line arguments and sets KasparovFlags accordingly.
|
|
||||||
func (kasparovFlags *KasparovFlags) ResolveKasparovFlags(parser *flags.Parser,
|
|
||||||
defaultLogDir, logFilename, errLogFilename string) error {
|
|
||||||
if kasparovFlags.LogDir == "" {
|
|
||||||
kasparovFlags.LogDir = defaultLogDir
|
|
||||||
}
|
|
||||||
logFile := filepath.Join(kasparovFlags.LogDir, logFilename)
|
|
||||||
errLogFile := filepath.Join(kasparovFlags.LogDir, errLogFilename)
|
|
||||||
logger.InitLog(logFile, errLogFile)
|
|
||||||
|
|
||||||
if kasparovFlags.DebugLevel != "" {
|
|
||||||
err := logger.SetLogLevels(kasparovFlags.DebugLevel)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if kasparovFlags.DBAddress == "" {
|
|
||||||
kasparovFlags.DBAddress = defaultDBAddress
|
|
||||||
}
|
|
||||||
if kasparovFlags.RPCUser == "" {
|
|
||||||
return errors.New("--rpcuser is required")
|
|
||||||
}
|
|
||||||
if kasparovFlags.RPCPassword == "" {
|
|
||||||
return errors.New("--rpcpass is required")
|
|
||||||
}
|
|
||||||
if kasparovFlags.RPCServer == "" {
|
|
||||||
return errors.New("--rpcserver is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if kasparovFlags.RPCCert == "" && !kasparovFlags.DisableTLS {
|
|
||||||
return errors.New("--notls has to be disabled if --cert is used")
|
|
||||||
}
|
|
||||||
if kasparovFlags.RPCCert != "" && kasparovFlags.DisableTLS {
|
|
||||||
return errors.New("--cert should be omitted if --notls is used")
|
|
||||||
}
|
|
||||||
return kasparovFlags.ResolveNetwork(parser)
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
nativeerrors "errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/config"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4/source"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// db is the Kasparov database.
|
|
||||||
var db *gorm.DB
|
|
||||||
|
|
||||||
// DB returns a reference to the database connection
|
|
||||||
func DB() (*gorm.DB, error) {
|
|
||||||
if db == nil {
|
|
||||||
return nil, errors.New("Database is not connected")
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type gormLogger struct{}
|
|
||||||
|
|
||||||
func (l gormLogger) Print(v ...interface{}) {
|
|
||||||
str := fmt.Sprint(v...)
|
|
||||||
log.Errorf(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect connects to the database mentioned in
|
|
||||||
// config variable.
|
|
||||||
func Connect(cfg *config.KasparovFlags) error {
|
|
||||||
connectionString := buildConnectionString(cfg)
|
|
||||||
migrator, driver, err := openMigrator(connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
isCurrent, version, err := isCurrent(migrator, driver)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error checking whether the database is current: %s", err)
|
|
||||||
}
|
|
||||||
if !isCurrent {
|
|
||||||
return errors.Errorf("Database is not current (version %d). Please migrate"+
|
|
||||||
" the database by running the server with --migrate flag and then run it again.", version)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err = gorm.Open("mysql", connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
db.SetLogger(gormLogger{})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the connection to the database
|
|
||||||
func Close() error {
|
|
||||||
if db == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := db.Close()
|
|
||||||
db = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildConnectionString(cfg *config.KasparovFlags) string {
|
|
||||||
return fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True",
|
|
||||||
cfg.DBUser, cfg.DBPassword, cfg.DBAddress, cfg.DBName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isCurrent resolves whether the database is on the latest
|
|
||||||
// version of the schema.
|
|
||||||
func isCurrent(migrator *migrate.Migrate, driver source.Driver) (bool, uint, error) {
|
|
||||||
// Get the current version
|
|
||||||
version, isDirty, err := migrator.Version()
|
|
||||||
if nativeerrors.Is(err, migrate.ErrNilVersion) {
|
|
||||||
return false, 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
if isDirty {
|
|
||||||
return false, 0, errors.Errorf("Database is dirty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The database is current if Next returns ErrNotExist
|
|
||||||
_, err = driver.Next(version)
|
|
||||||
if pathErr, ok := err.(*os.PathError); ok {
|
|
||||||
if pathErr.Err == os.ErrNotExist {
|
|
||||||
return true, version, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, version, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func openMigrator(connectionString string) (*migrate.Migrate, source.Driver, error) {
|
|
||||||
driver, err := source.Open("file://../database/migrations")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
migrator, err := migrate.NewWithSourceInstance(
|
|
||||||
"migrations", driver, "mysql://"+connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return migrator, driver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate database to the latest version.
|
|
||||||
func Migrate(cfg *config.KasparovFlags) error {
|
|
||||||
connectionString := buildConnectionString(cfg)
|
|
||||||
migrator, driver, err := openMigrator(connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
isCurrent, version, err := isCurrent(migrator, driver)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error checking whether the database is current: %s", err)
|
|
||||||
}
|
|
||||||
if isCurrent {
|
|
||||||
log.Infof("Database is already up-to-date (version %d)", version)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = migrator.Up()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version, isDirty, err := migrator.Version()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isDirty {
|
|
||||||
return errors.Errorf("error migrating database: database is dirty")
|
|
||||||
}
|
|
||||||
log.Infof("Migrated database to the latest version (version %d)", version)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/util/panics"
|
|
||||||
import "github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.Logger("DTBS")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `blocks`;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
CREATE TABLE `blocks`
|
|
||||||
(
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`block_hash` CHAR(64) NOT NULL,
|
|
||||||
`accepting_block_id` BIGINT UNSIGNED NULL,
|
|
||||||
`version` INT NOT NULL,
|
|
||||||
`hash_merkle_root` CHAR(64) NOT NULL,
|
|
||||||
`accepted_id_merkle_root` CHAR(64) NOT NULL,
|
|
||||||
`utxo_commitment` CHAR(64) NOT NULL,
|
|
||||||
`timestamp` DATETIME NOT NULL,
|
|
||||||
`bits` INT UNSIGNED NOT NULL,
|
|
||||||
`nonce` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`blue_score` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`is_chain_block` TINYINT NOT NULL,
|
|
||||||
`mass` BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE INDEX `idx_blocks_block_hash` (`block_hash`),
|
|
||||||
INDEX `idx_blocks_timestamp` (`timestamp`),
|
|
||||||
INDEX `idx_blocks_is_chain_block` (`is_chain_block`),
|
|
||||||
CONSTRAINT `fk_blocks_accepting_block_id`
|
|
||||||
FOREIGN KEY (`accepting_block_id`)
|
|
||||||
REFERENCES `blocks` (`id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `parent_blocks`;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
CREATE TABLE `parent_blocks`
|
|
||||||
(
|
|
||||||
`block_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`parent_block_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
PRIMARY KEY (`block_id`, `parent_block_id`),
|
|
||||||
CONSTRAINT `fk_parent_blocks_block_id`
|
|
||||||
FOREIGN KEY (`block_id`)
|
|
||||||
REFERENCES `blocks` (`id`),
|
|
||||||
CONSTRAINT `fk_parent_blocks_parent_block_id`
|
|
||||||
FOREIGN KEY (`parent_block_id`)
|
|
||||||
REFERENCES `blocks` (`id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `raw_blocks`;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE `raw_blocks`
|
|
||||||
(
|
|
||||||
`block_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`block_data` MEDIUMBLOB NOT NULL,
|
|
||||||
PRIMARY KEY (`block_id`),
|
|
||||||
CONSTRAINT `fk_raw_blocks_block_id`
|
|
||||||
FOREIGN KEY (`block_id`)
|
|
||||||
REFERENCES `blocks` (`id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `subnetworks`;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
CREATE TABLE `subnetworks`
|
|
||||||
(
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`subnetwork_id` CHAR(64) NOT NULL,
|
|
||||||
`gas_limit` BIGINT UNSIGNED NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE INDEX `idx_subnetworks_subnetwork_id` (`subnetwork_id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `transactions`;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
CREATE TABLE `transactions`
|
|
||||||
(
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`accepting_block_id` BIGINT UNSIGNED NULL,
|
|
||||||
`transaction_hash` CHAR(64) NOT NULL,
|
|
||||||
`transaction_id` CHAR(64) NOT NULL,
|
|
||||||
`lock_time` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`subnetwork_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`gas` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`payload_hash` CHAR(64) NOT NULL,
|
|
||||||
`payload` BLOB NOT NULL,
|
|
||||||
`mass` BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE INDEX `idx_transactions_transaction_hash` (`transaction_hash`),
|
|
||||||
INDEX `idx_transactions_transaction_id` (`transaction_id`),
|
|
||||||
CONSTRAINT `fk_transactions_accepting_block_id`
|
|
||||||
FOREIGN KEY (`accepting_block_id`)
|
|
||||||
REFERENCES `blocks` (`id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `transactions_to_blocks`;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE `transactions_to_blocks`
|
|
||||||
(
|
|
||||||
`transaction_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`block_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`index` INT UNSIGNED NOT NULL,
|
|
||||||
PRIMARY KEY (`transaction_id`, `block_id`),
|
|
||||||
INDEX `idx_transactions_to_blocks_index` (`index`),
|
|
||||||
CONSTRAINT `fk_transactions_to_blocks_block_id`
|
|
||||||
FOREIGN KEY (`block_id`)
|
|
||||||
REFERENCES `blocks` (`id`),
|
|
||||||
CONSTRAINT `fk_transactions_to_blocks_transaction_id`
|
|
||||||
FOREIGN KEY (`transaction_id`)
|
|
||||||
REFERENCES `transactions` (`id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `addresses`;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
CREATE TABLE `addresses`
|
|
||||||
(
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`address` CHAR(50) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE INDEX `idx_addresses_address` (`address`)
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `transaction_outputs`;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
CREATE TABLE `transaction_outputs`
|
|
||||||
(
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`transaction_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`index` INT UNSIGNED NOT NULL,
|
|
||||||
`value` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`script_pub_key` BLOB NOT NULL,
|
|
||||||
`is_spent` TINYINT NOT NULL,
|
|
||||||
`address_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_transaction_outputs_transaction_id` (`transaction_id`),
|
|
||||||
CONSTRAINT `fk_transaction_outputs_transaction_id`
|
|
||||||
FOREIGN KEY (`transaction_id`)
|
|
||||||
REFERENCES `transactions` (`id`),
|
|
||||||
CONSTRAINT `fk_transaction_outputs_address_id`
|
|
||||||
FOREIGN KEY (`address_id`)
|
|
||||||
REFERENCES `addresses` (`id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `transaction_inputs`;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
CREATE TABLE `transaction_inputs`
|
|
||||||
(
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`transaction_id` BIGINT UNSIGNED NULL,
|
|
||||||
`previous_transaction_output_id` BIGINT UNSIGNED NOT NULL,
|
|
||||||
`index` INT UNSIGNED NOT NULL,
|
|
||||||
`signature_script` BLOB NOT NULL,
|
|
||||||
`sequence` BIGINT UNSIGNED NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_transaction_inputs_transaction_id` (`transaction_id`),
|
|
||||||
INDEX `idx_transaction_inputs_previous_transaction_output_id` (`previous_transaction_output_id`),
|
|
||||||
CONSTRAINT `fk_transaction_inputs_transaction_id`
|
|
||||||
FOREIGN KEY (`transaction_id`)
|
|
||||||
REFERENCES `transactions` (`id`),
|
|
||||||
CONSTRAINT `fk_transaction_inputs_previous_transaction_output_id`
|
|
||||||
FOREIGN KEY (`previous_transaction_output_id`)
|
|
||||||
REFERENCES `transaction_outputs` (`id`)
|
|
||||||
);
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package dbmodels
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Block is the gorm model for the 'blocks' table
|
|
||||||
type Block struct {
|
|
||||||
ID uint64 `gorm:"primary_key"`
|
|
||||||
BlockHash string
|
|
||||||
AcceptingBlockID *uint64
|
|
||||||
AcceptingBlock *Block
|
|
||||||
Version int32
|
|
||||||
HashMerkleRoot string
|
|
||||||
AcceptedIDMerkleRoot string
|
|
||||||
UTXOCommitment string
|
|
||||||
Timestamp time.Time
|
|
||||||
Bits uint32
|
|
||||||
Nonce uint64
|
|
||||||
BlueScore uint64
|
|
||||||
IsChainBlock bool
|
|
||||||
Mass uint64
|
|
||||||
ParentBlocks []Block `gorm:"many2many:parent_blocks;"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParentBlock is the gorm model for the 'parent_blocks' table
|
|
||||||
type ParentBlock struct {
|
|
||||||
BlockID uint64
|
|
||||||
Block Block
|
|
||||||
ParentBlockID uint64
|
|
||||||
ParentBlock Block
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawBlock is the gorm model for the 'raw_blocks' table
|
|
||||||
type RawBlock struct {
|
|
||||||
BlockID uint64
|
|
||||||
Block Block
|
|
||||||
BlockData []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subnetwork is the gorm model for the 'subnetworks' table
|
|
||||||
type Subnetwork struct {
|
|
||||||
ID uint64 `gorm:"primary_key"`
|
|
||||||
SubnetworkID string
|
|
||||||
GasLimit *uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction is the gorm model for the 'transactions' table
|
|
||||||
type Transaction struct {
|
|
||||||
ID uint64 `gorm:"primary_key"`
|
|
||||||
AcceptingBlockID *uint64
|
|
||||||
AcceptingBlock *Block
|
|
||||||
TransactionHash string
|
|
||||||
TransactionID string
|
|
||||||
LockTime uint64
|
|
||||||
SubnetworkID uint64
|
|
||||||
Subnetwork Subnetwork
|
|
||||||
Gas uint64
|
|
||||||
PayloadHash string
|
|
||||||
Payload []byte
|
|
||||||
Mass uint64
|
|
||||||
Blocks []Block `gorm:"many2many:transactions_to_blocks;"`
|
|
||||||
TransactionOutputs []TransactionOutput
|
|
||||||
TransactionInputs []TransactionInput
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionBlock is the gorm model for the 'transactions_to_blocks' table
|
|
||||||
type TransactionBlock struct {
|
|
||||||
TransactionID uint64
|
|
||||||
Transaction Transaction
|
|
||||||
BlockID uint64
|
|
||||||
Block Block
|
|
||||||
Index uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName returns the table name associated to the
|
|
||||||
// TransactionBlock gorm model
|
|
||||||
func (TransactionBlock) TableName() string {
|
|
||||||
return "transactions_to_blocks"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionOutput is the gorm model for the 'transaction_outputs' table
|
|
||||||
type TransactionOutput struct {
|
|
||||||
ID uint64 `gorm:"primary_key"`
|
|
||||||
TransactionID uint64
|
|
||||||
Transaction Transaction
|
|
||||||
Index uint32
|
|
||||||
Value uint64
|
|
||||||
ScriptPubKey []byte
|
|
||||||
IsSpent bool
|
|
||||||
AddressID uint64
|
|
||||||
Address Address
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionInput is the gorm model for the 'transaction_inputs' table
|
|
||||||
type TransactionInput struct {
|
|
||||||
ID uint64 `gorm:"primary_key"`
|
|
||||||
TransactionID uint64
|
|
||||||
Transaction Transaction
|
|
||||||
PreviousTransactionOutputID uint64
|
|
||||||
PreviousTransactionOutput TransactionOutput
|
|
||||||
Index uint32
|
|
||||||
SignatureScript []byte
|
|
||||||
Sequence uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address is the gorm model for the 'addresses' table
|
|
||||||
type Address struct {
|
|
||||||
ID uint64 `gorm:"primary_key"`
|
|
||||||
Address string
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package jsonrpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/config"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/rpcclient"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client represents a connection to the JSON-RPC API of a full node
|
|
||||||
type Client struct {
|
|
||||||
*rpcclient.Client
|
|
||||||
OnBlockAdded chan *BlockAddedMsg
|
|
||||||
OnChainChanged chan *ChainChangedMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
var client *Client
|
|
||||||
|
|
||||||
// GetClient returns an instance of the JSON-RPC client, in case we have an active connection
|
|
||||||
func GetClient() (*Client, error) {
|
|
||||||
if client == nil {
|
|
||||||
return nil, errors.New("JSON-RPC is not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockAddedMsg defines the message received in onBlockAdded
|
|
||||||
type BlockAddedMsg struct {
|
|
||||||
ChainHeight uint64
|
|
||||||
Header *wire.BlockHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainChangedMsg defines the message received in onChainChanged
|
|
||||||
type ChainChangedMsg struct {
|
|
||||||
RemovedChainBlockHashes []*daghash.Hash
|
|
||||||
AddedChainBlocks []*rpcclient.ChainBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the connection to the JSON-RPC API server
|
|
||||||
func Close() {
|
|
||||||
if client == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Disconnect()
|
|
||||||
client = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect initiates a connection to the JSON-RPC API Server
|
|
||||||
func Connect(cfg *config.KasparovFlags) error {
|
|
||||||
var cert []byte
|
|
||||||
if !cfg.DisableTLS {
|
|
||||||
var err error
|
|
||||||
cert, err = ioutil.ReadFile(cfg.RPCCert)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error reading certificates file: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connCfg := &rpcclient.ConnConfig{
|
|
||||||
Host: cfg.RPCServer,
|
|
||||||
Endpoint: "ws",
|
|
||||||
User: cfg.RPCUser,
|
|
||||||
Pass: cfg.RPCPassword,
|
|
||||||
DisableTLS: cfg.DisableTLS,
|
|
||||||
RequestTimeout: time.Second * 60,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.DisableTLS {
|
|
||||||
connCfg.Certificates = cert
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
client, err = newClient(connCfg)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error connecting to address %s: %s", cfg.RPCServer, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClient(connCfg *rpcclient.ConnConfig) (*Client, error) {
|
|
||||||
client = &Client{
|
|
||||||
OnBlockAdded: make(chan *BlockAddedMsg),
|
|
||||||
OnChainChanged: make(chan *ChainChangedMsg),
|
|
||||||
}
|
|
||||||
notificationHandlers := &rpcclient.NotificationHandlers{
|
|
||||||
OnFilteredBlockAdded: func(height uint64, header *wire.BlockHeader,
|
|
||||||
txs []*util.Tx) {
|
|
||||||
client.OnBlockAdded <- &BlockAddedMsg{
|
|
||||||
ChainHeight: height,
|
|
||||||
Header: header,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OnChainChanged: func(removedChainBlockHashes []*daghash.Hash,
|
|
||||||
addedChainBlocks []*rpcclient.ChainBlock) {
|
|
||||||
client.OnChainChanged <- &ChainChangedMsg{
|
|
||||||
RemovedChainBlockHashes: removedChainBlockHashes,
|
|
||||||
AddedChainBlocks: addedChainBlocks,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if err = client.NotifyChainChanges(); err != nil {
|
|
||||||
return nil, errors.Errorf("Error while registering client %s for chain changes notifications: %s", client.Host(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package jsonrpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
"github.com/kaspanet/kaspad/rpcclient"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.BackendLog.Logger("RPCC")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rpcclient.UseLogger(log)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package apimodels
|
|
||||||
|
|
||||||
// RawTransaction is a json representation of a raw transaction
|
|
||||||
type RawTransaction struct {
|
|
||||||
RawTransaction string `json:"rawTransaction"`
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package apimodels
|
|
||||||
|
|
||||||
// TransactionResponse is a json representation of a transaction
|
|
||||||
type TransactionResponse struct {
|
|
||||||
TransactionHash string `json:"transactionHash"`
|
|
||||||
TransactionID string `json:"transactionId"`
|
|
||||||
AcceptingBlockHash *string `json:"acceptingBlockHash,omitempty"`
|
|
||||||
AcceptingBlockBlueScore *uint64 `json:"acceptingBlockBlueScore,omitempty"`
|
|
||||||
SubnetworkID string `json:"subnetworkId"`
|
|
||||||
LockTime uint64 `json:"lockTime"`
|
|
||||||
Gas uint64 `json:"gas,omitempty"`
|
|
||||||
PayloadHash string `json:"payloadHash,omitempty"`
|
|
||||||
Payload string `json:"payload,omitempty"`
|
|
||||||
Inputs []*TransactionInputResponse `json:"inputs"`
|
|
||||||
Outputs []*TransactionOutputResponse `json:"outputs"`
|
|
||||||
Mass uint64 `json:"mass"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionOutputResponse is a json representation of a transaction output
|
|
||||||
type TransactionOutputResponse struct {
|
|
||||||
TransactionID string `json:"transactionId,omitempty"`
|
|
||||||
Value uint64 `json:"value"`
|
|
||||||
ScriptPubKey string `json:"scriptPubKey"`
|
|
||||||
Address string `json:"address,omitempty"`
|
|
||||||
AcceptingBlockHash *string `json:"acceptingBlockHash,omitempty"`
|
|
||||||
AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"`
|
|
||||||
Index uint32 `json:"index"`
|
|
||||||
IsCoinbase *bool `json:"isCoinbase,omitempty"`
|
|
||||||
IsSpendable *bool `json:"isSpendable,omitempty"`
|
|
||||||
Confirmations *uint64 `json:"confirmations,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionInputResponse is a json representation of a transaction input
|
|
||||||
type TransactionInputResponse struct {
|
|
||||||
TransactionID string `json:"transactionId,omitempty"`
|
|
||||||
PreviousTransactionID string `json:"previousTransactionId"`
|
|
||||||
PreviousTransactionOutputIndex uint32 `json:"previousTransactionOutputIndex"`
|
|
||||||
SignatureScript string `json:"signatureScript"`
|
|
||||||
Sequence uint64 `json:"sequence"`
|
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockResponse is a json representation of a block
|
|
||||||
type BlockResponse struct {
|
|
||||||
BlockHash string `json:"blockHash"`
|
|
||||||
Version int32 `json:"version"`
|
|
||||||
HashMerkleRoot string `json:"hashMerkleRoot"`
|
|
||||||
AcceptedIDMerkleRoot string `json:"acceptedIDMerkleRoot"`
|
|
||||||
UTXOCommitment string `json:"utxoCommitment"`
|
|
||||||
Timestamp uint64 `json:"timestamp"`
|
|
||||||
Bits uint32 `json:"bits"`
|
|
||||||
Nonce uint64 `json:"nonce"`
|
|
||||||
AcceptingBlockHash *string `json:"acceptingBlockHash"`
|
|
||||||
BlueScore uint64 `json:"blueScore"`
|
|
||||||
IsChainBlock bool `json:"isChainBlock"`
|
|
||||||
Mass uint64 `json:"mass"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// FeeEstimateResponse is a json representation of a fee estimate
|
|
||||||
type FeeEstimateResponse struct {
|
|
||||||
HighPriority float64 `json:"highPriority"`
|
|
||||||
NormalPriority float64 `json:"normalPriority"`
|
|
||||||
LowPriority float64 `json:"lowPriority"`
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jessevdk/go-flags"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/config"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
logFilename = "kasparovd.log"
|
|
||||||
errLogFilename = "kasparovd_err.log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default configuration options
|
|
||||||
defaultLogDir = util.AppDataDir("kasparovd", false)
|
|
||||||
defaultHTTPListen = "0.0.0.0:8080"
|
|
||||||
activeConfig *Config
|
|
||||||
)
|
|
||||||
|
|
||||||
// ActiveConfig returns the active configuration struct
|
|
||||||
func ActiveConfig() *Config {
|
|
||||||
return activeConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config defines the configuration options for the API server.
|
|
||||||
type Config struct {
|
|
||||||
HTTPListen string `long:"listen" description:"HTTP address to listen on (default: 0.0.0.0:8080)"`
|
|
||||||
config.KasparovFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses the CLI arguments and returns a config struct.
|
|
||||||
func Parse() error {
|
|
||||||
activeConfig = &Config{
|
|
||||||
HTTPListen: defaultHTTPListen,
|
|
||||||
}
|
|
||||||
parser := flags.NewParser(activeConfig, flags.PrintErrors|flags.HelpFlag)
|
|
||||||
_, err := parser.Parse()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = activeConfig.ResolveKasparovFlags(parser, defaultLogDir, logFilename, errLogFilename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/database"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/dbmodels"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/apimodels"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// OrderAscending is parameter that can be used
|
|
||||||
// in a get list handler to get a list ordered
|
|
||||||
// in an ascending order.
|
|
||||||
OrderAscending = "asc"
|
|
||||||
|
|
||||||
// OrderDescending is parameter that can be used
|
|
||||||
// in a get list handler to get a list ordered
|
|
||||||
// in an ascending order.
|
|
||||||
OrderDescending = "desc"
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxGetBlocksLimit = 100
|
|
||||||
|
|
||||||
// GetBlockByHashHandler returns a block by a given hash.
|
|
||||||
func GetBlockByHashHandler(blockHash string) (interface{}, error) {
|
|
||||||
if bytes, err := hex.DecodeString(blockHash); err != nil || len(bytes) != daghash.HashSize {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
|
|
||||||
errors.Errorf("The given block hash is not a hex-encoded %d-byte hash.", daghash.HashSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
block := &dbmodels.Block{}
|
|
||||||
dbResult := db.Where(&dbmodels.Block{BlockHash: blockHash}).Preload("AcceptingBlock").First(block)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusNotFound, errors.New("No block with the given block hash was found"))
|
|
||||||
}
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading transactions from the database:",
|
|
||||||
dbResult.GetErrors())
|
|
||||||
}
|
|
||||||
return convertBlockModelToBlockResponse(block), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlocksHandler searches for all blocks
|
|
||||||
func GetBlocksHandler(order string, skip uint64, limit uint64) (interface{}, error) {
|
|
||||||
if limit < 1 || limit > maxGetBlocksLimit {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusBadRequest,
|
|
||||||
errors.Errorf("Limit higher than %d or lower than 1 was requested.", maxGetTransactionsLimit))
|
|
||||||
}
|
|
||||||
blocks := []*dbmodels.Block{}
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
query := db.
|
|
||||||
Limit(limit).
|
|
||||||
Offset(skip).
|
|
||||||
Preload("AcceptingBlock")
|
|
||||||
if order == OrderAscending {
|
|
||||||
query = query.Order("`id` ASC")
|
|
||||||
} else if order == OrderDescending {
|
|
||||||
query = query.Order("`id` DESC")
|
|
||||||
} else {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, errors.Errorf("'%s' is not a valid order", order))
|
|
||||||
}
|
|
||||||
query.Find(&blocks)
|
|
||||||
blockResponses := make([]*apimodels.BlockResponse, len(blocks))
|
|
||||||
for i, block := range blocks {
|
|
||||||
blockResponses[i] = convertBlockModelToBlockResponse(block)
|
|
||||||
}
|
|
||||||
return blockResponses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAcceptedTransactionIDsByBlockHashHandler returns an array of transaction IDs for a given block hash
|
|
||||||
func GetAcceptedTransactionIDsByBlockHashHandler(blockHash string) ([]string, error) {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var transactions []dbmodels.Transaction
|
|
||||||
dbResult := db.
|
|
||||||
Joins("LEFT JOIN `blocks` ON `blocks`.`id` = `transactions`.`accepting_block_id`").
|
|
||||||
Where("`blocks`.`block_hash` = ?", blockHash).
|
|
||||||
Find(&transactions)
|
|
||||||
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Failed to find transactions: ", dbErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]string, len(transactions))
|
|
||||||
for _, transaction := range transactions {
|
|
||||||
result = append(result, transaction.TransactionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/dbmodels"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/apimodels"
|
|
||||||
"github.com/kaspanet/kaspad/rpcmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertTxDBModelToTxResponse(tx *dbmodels.Transaction) *apimodels.TransactionResponse {
|
|
||||||
txRes := &apimodels.TransactionResponse{
|
|
||||||
TransactionHash: tx.TransactionHash,
|
|
||||||
TransactionID: tx.TransactionID,
|
|
||||||
SubnetworkID: tx.Subnetwork.SubnetworkID,
|
|
||||||
LockTime: tx.LockTime,
|
|
||||||
Gas: tx.Gas,
|
|
||||||
PayloadHash: tx.PayloadHash,
|
|
||||||
Payload: hex.EncodeToString(tx.Payload),
|
|
||||||
Inputs: make([]*apimodels.TransactionInputResponse, len(tx.TransactionInputs)),
|
|
||||||
Outputs: make([]*apimodels.TransactionOutputResponse, len(tx.TransactionOutputs)),
|
|
||||||
Mass: tx.Mass,
|
|
||||||
}
|
|
||||||
if tx.AcceptingBlock != nil {
|
|
||||||
txRes.AcceptingBlockHash = &tx.AcceptingBlock.BlockHash
|
|
||||||
txRes.AcceptingBlockBlueScore = &tx.AcceptingBlock.BlueScore
|
|
||||||
}
|
|
||||||
for i, txOut := range tx.TransactionOutputs {
|
|
||||||
txRes.Outputs[i] = &apimodels.TransactionOutputResponse{
|
|
||||||
Value: txOut.Value,
|
|
||||||
ScriptPubKey: hex.EncodeToString(txOut.ScriptPubKey),
|
|
||||||
Address: txOut.Address.Address,
|
|
||||||
Index: txOut.Index,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, txIn := range tx.TransactionInputs {
|
|
||||||
txRes.Inputs[i] = &apimodels.TransactionInputResponse{
|
|
||||||
PreviousTransactionID: txIn.PreviousTransactionOutput.Transaction.TransactionID,
|
|
||||||
PreviousTransactionOutputIndex: txIn.PreviousTransactionOutput.Index,
|
|
||||||
SignatureScript: hex.EncodeToString(txIn.SignatureScript),
|
|
||||||
Sequence: txIn.Sequence,
|
|
||||||
Address: txIn.PreviousTransactionOutput.Address.Address,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return txRes
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertBlockModelToBlockResponse(block *dbmodels.Block) *apimodels.BlockResponse {
|
|
||||||
blockRes := &apimodels.BlockResponse{
|
|
||||||
BlockHash: block.BlockHash,
|
|
||||||
Version: block.Version,
|
|
||||||
HashMerkleRoot: block.HashMerkleRoot,
|
|
||||||
AcceptedIDMerkleRoot: block.AcceptedIDMerkleRoot,
|
|
||||||
UTXOCommitment: block.UTXOCommitment,
|
|
||||||
Timestamp: uint64(block.Timestamp.Unix()),
|
|
||||||
Bits: block.Bits,
|
|
||||||
Nonce: block.Nonce,
|
|
||||||
BlueScore: block.BlueScore,
|
|
||||||
IsChainBlock: block.IsChainBlock,
|
|
||||||
Mass: block.Mass,
|
|
||||||
}
|
|
||||||
if block.AcceptingBlock != nil {
|
|
||||||
blockRes.AcceptingBlockHash = rpcmodel.String(block.AcceptingBlock.BlockHash)
|
|
||||||
}
|
|
||||||
return blockRes
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/kasparov/kasparovd/apimodels"
|
|
||||||
|
|
||||||
// GetFeeEstimatesHandler returns the fee estimates for different priorities
|
|
||||||
// for accepting a transaction in the DAG.
|
|
||||||
func GetFeeEstimatesHandler() (interface{}, error) {
|
|
||||||
return &apimodels.FeeEstimateResponse{
|
|
||||||
HighPriority: 3,
|
|
||||||
NormalPriority: 2,
|
|
||||||
LowPriority: 1,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/database"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/dbmodels"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/jsonrpc"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/apimodels"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/config"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/blockdag"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/kaspanet/kaspad/util/subnetworkid"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/kaspanet/kaspad/rpcmodel"
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxGetTransactionsLimit = 1000
|
|
||||||
|
|
||||||
// GetTransactionByIDHandler returns a transaction by a given transaction ID.
|
|
||||||
func GetTransactionByIDHandler(txID string) (interface{}, error) {
|
|
||||||
if bytes, err := hex.DecodeString(txID); err != nil || len(bytes) != daghash.TxIDSize {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
|
|
||||||
errors.Errorf("The given txid is not a hex-encoded %d-byte hash.", daghash.TxIDSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := &dbmodels.Transaction{}
|
|
||||||
query := db.Where(&dbmodels.Transaction{TransactionID: txID})
|
|
||||||
dbResult := addTxPreloadedFields(query).First(&tx)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusNotFound, errors.New("No transaction with the given txid was found"))
|
|
||||||
}
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading transaction from the database:", dbErrors)
|
|
||||||
}
|
|
||||||
return convertTxDBModelToTxResponse(tx), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactionByHashHandler returns a transaction by a given transaction hash.
|
|
||||||
func GetTransactionByHashHandler(txHash string) (interface{}, error) {
|
|
||||||
if bytes, err := hex.DecodeString(txHash); err != nil || len(bytes) != daghash.HashSize {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
|
|
||||||
errors.Errorf("The given txhash is not a hex-encoded %d-byte hash.", daghash.HashSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := &dbmodels.Transaction{}
|
|
||||||
query := db.Where(&dbmodels.Transaction{TransactionHash: txHash})
|
|
||||||
dbResult := addTxPreloadedFields(query).First(&tx)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusNotFound, errors.Errorf("No transaction with the given txhash was found."))
|
|
||||||
}
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading transaction from the database:", dbErrors)
|
|
||||||
}
|
|
||||||
return convertTxDBModelToTxResponse(tx), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactionsByAddressHandler searches for all transactions
|
|
||||||
// where the given address is either an input or an output.
|
|
||||||
func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64) (interface{}, error) {
|
|
||||||
if limit < 1 || limit > maxGetTransactionsLimit {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusBadRequest,
|
|
||||||
errors.Errorf("Limit higher than %d or lower than 1 was requested.", maxGetTransactionsLimit))
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
txs := []*dbmodels.Transaction{}
|
|
||||||
query := joinTxInputsTxOutputsAndAddresses(db).
|
|
||||||
Where("`out_addresses`.`address` = ?", address).
|
|
||||||
Or("`in_addresses`.`address` = ?", address).
|
|
||||||
Limit(limit).
|
|
||||||
Offset(skip).
|
|
||||||
Order("`transactions`.`id` ASC")
|
|
||||||
dbResult := addTxPreloadedFields(query).Find(&txs)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbErrors)
|
|
||||||
}
|
|
||||||
txResponses := make([]*apimodels.TransactionResponse, len(txs))
|
|
||||||
for i, tx := range txs {
|
|
||||||
txResponses[i] = convertTxDBModelToTxResponse(tx)
|
|
||||||
}
|
|
||||||
return txResponses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchSelectedTip() (*dbmodels.Block, error) {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
block := &dbmodels.Block{}
|
|
||||||
dbResult := db.Order("blue_score DESC").
|
|
||||||
Where(&dbmodels.Block{IsChainBlock: true}).
|
|
||||||
First(block)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbErrors)
|
|
||||||
}
|
|
||||||
return block, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func areTxsInBlock(blockID uint64, txIDs []uint64) (map[uint64]bool, error) {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
transactionBlocks := []*dbmodels.TransactionBlock{}
|
|
||||||
dbErrors := db.
|
|
||||||
Where(&dbmodels.TransactionBlock{BlockID: blockID}).
|
|
||||||
Where("transaction_id in (?)", txIDs).
|
|
||||||
Find(&transactionBlocks).GetErrors()
|
|
||||||
|
|
||||||
if len(dbErrors) > 0 {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading UTXOs from the database:", dbErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
isInBlock := make(map[uint64]bool)
|
|
||||||
for _, transactionBlock := range transactionBlocks {
|
|
||||||
isInBlock[transactionBlock.TransactionID] = true
|
|
||||||
}
|
|
||||||
return isInBlock, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUTXOsByAddressHandler searches for all UTXOs that belong to a certain address.
|
|
||||||
func GetUTXOsByAddressHandler(address string) (interface{}, error) {
|
|
||||||
_, err := util.DecodeAddress(address, config.ActiveConfig().ActiveNetParams.Prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "error decoding address"),
|
|
||||||
"The given address is not a well-formatted P2PKH or P2SH address.")
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var transactionOutputs []*dbmodels.TransactionOutput
|
|
||||||
dbErrors := db.
|
|
||||||
Joins("LEFT JOIN `addresses` ON `addresses`.`id` = `transaction_outputs`.`address_id`").
|
|
||||||
Where("`addresses`.`address` = ? AND `transaction_outputs`.`is_spent` = 0", address).
|
|
||||||
Preload("Transaction.AcceptingBlock").
|
|
||||||
Preload("Transaction.Subnetwork").
|
|
||||||
Find(&transactionOutputs).GetErrors()
|
|
||||||
if len(dbErrors) > 0 {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading UTXOs from the database:", dbErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonAcceptedTxIds := make([]uint64, len(transactionOutputs))
|
|
||||||
for i, txOut := range transactionOutputs {
|
|
||||||
if txOut.Transaction.AcceptingBlock == nil {
|
|
||||||
nonAcceptedTxIds[i] = txOut.TransactionID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedTip *dbmodels.Block
|
|
||||||
var isTxInSelectedTip map[uint64]bool
|
|
||||||
if len(nonAcceptedTxIds) != 0 {
|
|
||||||
selectedTip, err = fetchSelectedTip()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
isTxInSelectedTip, err = areTxsInBlock(selectedTip.ID, nonAcceptedTxIds)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeNetParams := config.ActiveConfig().NetParams()
|
|
||||||
|
|
||||||
UTXOsResponses := make([]*apimodels.TransactionOutputResponse, len(transactionOutputs))
|
|
||||||
for i, transactionOutput := range transactionOutputs {
|
|
||||||
subnetworkID := &subnetworkid.SubnetworkID{}
|
|
||||||
err := subnetworkid.Decode(subnetworkID, transactionOutput.Transaction.Subnetwork.SubnetworkID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, fmt.Sprintf("Couldn't decode subnetwork id %s", transactionOutput.Transaction.Subnetwork.SubnetworkID))
|
|
||||||
}
|
|
||||||
var acceptingBlockHash *string
|
|
||||||
var confirmations uint64
|
|
||||||
acceptingBlockBlueScore := blockdag.UnacceptedBlueScore
|
|
||||||
if isTxInSelectedTip[transactionOutput.ID] {
|
|
||||||
confirmations = 1
|
|
||||||
} else if transactionOutput.Transaction.AcceptingBlock != nil {
|
|
||||||
acceptingBlockHash = rpcmodel.String(transactionOutput.Transaction.AcceptingBlock.BlockHash)
|
|
||||||
acceptingBlockBlueScore = transactionOutput.Transaction.AcceptingBlock.BlueScore
|
|
||||||
confirmations = selectedTip.BlueScore - acceptingBlockBlueScore + 2
|
|
||||||
}
|
|
||||||
isCoinbase := subnetworkID.IsEqual(subnetworkid.SubnetworkIDCoinbase)
|
|
||||||
UTXOsResponses[i] = &apimodels.TransactionOutputResponse{
|
|
||||||
TransactionID: transactionOutput.Transaction.TransactionID,
|
|
||||||
Value: transactionOutput.Value,
|
|
||||||
ScriptPubKey: hex.EncodeToString(transactionOutput.ScriptPubKey),
|
|
||||||
AcceptingBlockHash: acceptingBlockHash,
|
|
||||||
AcceptingBlockBlueScore: acceptingBlockBlueScore,
|
|
||||||
Index: transactionOutput.Index,
|
|
||||||
IsCoinbase: rpcmodel.Bool(isCoinbase),
|
|
||||||
Confirmations: rpcmodel.Uint64(confirmations),
|
|
||||||
IsSpendable: rpcmodel.Bool(!isCoinbase || confirmations >= activeNetParams.BlockCoinbaseMaturity),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return UTXOsResponses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinTxInputsTxOutputsAndAddresses(query *gorm.DB) *gorm.DB {
|
|
||||||
return query.
|
|
||||||
Joins("LEFT JOIN `transaction_outputs` ON `transaction_outputs`.`transaction_id` = `transactions`.`id`").
|
|
||||||
Joins("LEFT JOIN `addresses` AS `out_addresses` ON `out_addresses`.`id` = `transaction_outputs`.`address_id`").
|
|
||||||
Joins("LEFT JOIN `transaction_inputs` ON `transaction_inputs`.`transaction_id` = `transactions`.`id`").
|
|
||||||
Joins("LEFT JOIN `transaction_outputs` AS `inputs_outs` ON `inputs_outs`.`id` = `transaction_inputs`.`previous_transaction_output_id`").
|
|
||||||
Joins("LEFT JOIN `addresses` AS `in_addresses` ON `in_addresses`.`id` = `inputs_outs`.`address_id`")
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTxPreloadedFields(query *gorm.DB) *gorm.DB {
|
|
||||||
return query.Preload("AcceptingBlock").
|
|
||||||
Preload("Subnetwork").
|
|
||||||
Preload("TransactionOutputs").
|
|
||||||
Preload("TransactionOutputs.Address").
|
|
||||||
Preload("TransactionInputs.PreviousTransactionOutput.Transaction").
|
|
||||||
Preload("TransactionInputs.PreviousTransactionOutput.Address")
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostTransaction forwards a raw transaction to the JSON-RPC API server
|
|
||||||
func PostTransaction(requestBody []byte) error {
|
|
||||||
client, err := jsonrpc.GetClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rawTx := &apimodels.RawTransaction{}
|
|
||||||
err = json.Unmarshal(requestBody, rawTx)
|
|
||||||
if err != nil {
|
|
||||||
return httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "Error unmarshalling request body"),
|
|
||||||
"The request body is not json-formatted")
|
|
||||||
}
|
|
||||||
|
|
||||||
txBytes, err := hex.DecodeString(rawTx.RawTransaction)
|
|
||||||
if err != nil {
|
|
||||||
return httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "Error decoding hex raw transaction"),
|
|
||||||
"The raw transaction is not a hex-encoded transaction")
|
|
||||||
}
|
|
||||||
|
|
||||||
txReader := bytes.NewReader(txBytes)
|
|
||||||
tx := &wire.MsgTx{}
|
|
||||||
err = tx.KaspaDecode(txReader, 0)
|
|
||||||
if err != nil {
|
|
||||||
return httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "Error decoding raw transaction"),
|
|
||||||
"Error decoding raw transaction")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = client.SendRawTransaction(tx, true)
|
|
||||||
if err != nil {
|
|
||||||
switch err := errors.Cause(err).(type) {
|
|
||||||
case *rpcmodel.RPCError:
|
|
||||||
return httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, err)
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactionsByIDsHandler finds transactions by the given transactionIds.
|
|
||||||
func GetTransactionsByIDsHandler(transactionIds []string) ([]*apimodels.TransactionResponse, error) {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var txs []*dbmodels.Transaction
|
|
||||||
query := joinTxInputsTxOutputsAndAddresses(db).
|
|
||||||
Where("`transactions`.`transaction_id` IN (?)", transactionIds)
|
|
||||||
|
|
||||||
dbResult := addTxPreloadedFields(query).Find(&txs)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return nil, httpserverutils.NewErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
txResponses := make([]*apimodels.TransactionResponse, len(txs))
|
|
||||||
for i, tx := range txs {
|
|
||||||
txResponses[i] = convertTxDBModelToTxResponse(tx)
|
|
||||||
}
|
|
||||||
return txResponses, nil
|
|
||||||
}
|
|
||||||
@@ -1,29 +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 kasparov/kasparovd && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kasparovd .
|
|
||||||
|
|
||||||
# --- 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/kasparov/kasparovd/ /app/
|
|
||||||
COPY --from=build /go/src/github.com/kaspanet/kaspad/kasparov/database/migrations/ /database/migrations/
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
|
||||||
CMD ["/app/kasparovd"]
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.Logger("KVSV")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/database/mysql"
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/database"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/jsonrpc"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/config"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/server"
|
|
||||||
"github.com/kaspanet/kaspad/signal"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defer panics.HandlePanic(log, nil, nil)
|
|
||||||
|
|
||||||
err := config.Parse()
|
|
||||||
if err != nil {
|
|
||||||
errString := fmt.Sprintf("Error parsing command-line arguments: %s", err)
|
|
||||||
_, fErr := fmt.Fprintf(os.Stderr, errString)
|
|
||||||
if fErr != nil {
|
|
||||||
panic(errString)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = database.Connect(&config.ActiveConfig().KasparovFlags)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error connecting to database: %s", err))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := database.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error closing the database: %s", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = jsonrpc.Connect(&config.ActiveConfig().KasparovFlags)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error connecting to servers: %s", err))
|
|
||||||
}
|
|
||||||
defer jsonrpc.Close()
|
|
||||||
|
|
||||||
shutdownServer := server.Start(config.ActiveConfig().HTTPListen)
|
|
||||||
defer shutdownServer()
|
|
||||||
|
|
||||||
interrupt := signal.InterruptListener()
|
|
||||||
<-interrupt
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/util/panics"
|
|
||||||
import "github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.Logger("REST")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/controllers"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
routeParamTxID = "txID"
|
|
||||||
routeParamTxHash = "txHash"
|
|
||||||
routeParamAddress = "address"
|
|
||||||
routeParamBlockHash = "blockHash"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
queryParamSkip = "skip"
|
|
||||||
queryParamLimit = "limit"
|
|
||||||
queryParamOrder = "order"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultGetTransactionsLimit = 100
|
|
||||||
defaultGetBlocksLimit = 25
|
|
||||||
defaultGetBlocksOrder = controllers.OrderDescending
|
|
||||||
)
|
|
||||||
|
|
||||||
func mainHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string, _ []byte) (interface{}, error) {
|
|
||||||
return struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{
|
|
||||||
Message: "Kasparov server is running",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addRoutes(router *mux.Router) {
|
|
||||||
router.HandleFunc("/", httpserverutils.MakeHandler(mainHandler))
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
fmt.Sprintf("/transaction/id/{%s}", routeParamTxID),
|
|
||||||
httpserverutils.MakeHandler(getTransactionByIDHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
fmt.Sprintf("/transaction/hash/{%s}", routeParamTxHash),
|
|
||||||
httpserverutils.MakeHandler(getTransactionByHashHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
fmt.Sprintf("/transactions/address/{%s}", routeParamAddress),
|
|
||||||
httpserverutils.MakeHandler(getTransactionsByAddressHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
fmt.Sprintf("/utxos/address/{%s}", routeParamAddress),
|
|
||||||
httpserverutils.MakeHandler(getUTXOsByAddressHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
fmt.Sprintf("/block/{%s}", routeParamBlockHash),
|
|
||||||
httpserverutils.MakeHandler(getBlockByHashHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
"/blocks",
|
|
||||||
httpserverutils.MakeHandler(getBlocksHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
"/fee-estimates",
|
|
||||||
httpserverutils.MakeHandler(getFeeEstimatesHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
router.HandleFunc(
|
|
||||||
"/transaction",
|
|
||||||
httpserverutils.MakeHandler(postTransactionHandler)).
|
|
||||||
Methods("POST")
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertQueryParamToInt(queryParams map[string]string, param string, defaultValue int) (int, error) {
|
|
||||||
if _, ok := queryParams[param]; ok {
|
|
||||||
intValue, err := strconv.Atoi(queryParams[param])
|
|
||||||
if err != nil {
|
|
||||||
errorMessage := fmt.Sprintf("Couldn't parse the '%s' query parameter", param)
|
|
||||||
return 0, httpserverutils.NewHandlerErrorWithCustomClientMessage(
|
|
||||||
http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, errorMessage),
|
|
||||||
errorMessage)
|
|
||||||
}
|
|
||||||
return intValue, nil
|
|
||||||
}
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTransactionByIDHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
|
|
||||||
_ []byte) (interface{}, error) {
|
|
||||||
|
|
||||||
return controllers.GetTransactionByIDHandler(routeParams[routeParamTxID])
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTransactionByHashHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
|
|
||||||
_ []byte) (interface{}, error) {
|
|
||||||
|
|
||||||
return controllers.GetTransactionByHashHandler(routeParams[routeParamTxHash])
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTransactionsByAddressHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, queryParams map[string]string,
|
|
||||||
_ []byte) (interface{}, error) {
|
|
||||||
|
|
||||||
skip, err := convertQueryParamToInt(queryParams, queryParamSkip, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
limit, err := convertQueryParamToInt(queryParams, queryParamLimit, defaultGetTransactionsLimit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, ok := queryParams[queryParamLimit]; ok {
|
|
||||||
var err error
|
|
||||||
limit, err = strconv.Atoi(queryParams[queryParamLimit])
|
|
||||||
if err != nil {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, fmt.Sprintf("Couldn't parse the '%s' query parameter", queryParamLimit)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return controllers.GetTransactionsByAddressHandler(routeParams[routeParamAddress], uint64(skip), uint64(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUTXOsByAddressHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
|
|
||||||
_ []byte) (interface{}, error) {
|
|
||||||
|
|
||||||
return controllers.GetUTXOsByAddressHandler(routeParams[routeParamAddress])
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBlockByHashHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
|
|
||||||
_ []byte) (interface{}, error) {
|
|
||||||
|
|
||||||
return controllers.GetBlockByHashHandler(routeParams[routeParamBlockHash])
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFeeEstimatesHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string,
|
|
||||||
_ []byte) (interface{}, error) {
|
|
||||||
|
|
||||||
return controllers.GetFeeEstimatesHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBlocksHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, queryParams map[string]string,
|
|
||||||
_ []byte) (interface{}, error) {
|
|
||||||
|
|
||||||
skip, err := convertQueryParamToInt(queryParams, queryParamSkip, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
limit, err := convertQueryParamToInt(queryParams, queryParamLimit, defaultGetBlocksLimit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
order := defaultGetBlocksOrder
|
|
||||||
if orderParamValue, ok := queryParams[queryParamOrder]; ok {
|
|
||||||
if orderParamValue != controllers.OrderAscending && orderParamValue != controllers.OrderDescending {
|
|
||||||
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, errors.Errorf(
|
|
||||||
"Couldn't parse the '%s' query parameter", queryParamOrder))
|
|
||||||
}
|
|
||||||
order = orderParamValue
|
|
||||||
}
|
|
||||||
return controllers.GetBlocksHandler(order, uint64(skip), uint64(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func postTransactionHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string,
|
|
||||||
requestBody []byte) (interface{}, error) {
|
|
||||||
return nil, controllers.PostTransaction(requestBody)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
const gracefulShutdownTimeout = 30 * time.Second
|
|
||||||
|
|
||||||
// Start starts the HTTP REST server and returns a
|
|
||||||
// function to gracefully shutdown it.
|
|
||||||
func Start(listenAddr string) func() {
|
|
||||||
router := mux.NewRouter()
|
|
||||||
router.Use(httpserverutils.AddRequestMetadataMiddleware)
|
|
||||||
router.Use(httpserverutils.RecoveryMiddleware)
|
|
||||||
router.Use(httpserverutils.LoggingMiddleware)
|
|
||||||
router.Use(httpserverutils.SetJSONMiddleware)
|
|
||||||
addRoutes(router)
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: listenAddr,
|
|
||||||
Handler: handlers.CORS()(router),
|
|
||||||
}
|
|
||||||
spawn(func() {
|
|
||||||
log.Errorf("%s", httpServer.ListenAndServe())
|
|
||||||
})
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
|
|
||||||
defer cancel()
|
|
||||||
err := httpServer.Shutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error shutting down HTTP server: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jessevdk/go-flags"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/config"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
logFilename = "kasparov_syncd.log"
|
|
||||||
errLogFilename = "kasparov_syncd_err.log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default configuration options
|
|
||||||
defaultLogDir = util.AppDataDir("kasparov_syncd", false)
|
|
||||||
activeConfig *Config
|
|
||||||
)
|
|
||||||
|
|
||||||
// ActiveConfig returns the active configuration struct
|
|
||||||
func ActiveConfig() *Config {
|
|
||||||
return activeConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config defines the configuration options for the sync daemon.
|
|
||||||
type Config struct {
|
|
||||||
Migrate bool `long:"migrate" description:"Migrate the database to the latest version. The daemon will not start when using this flag."`
|
|
||||||
MQTTBrokerAddress string `long:"mqttaddress" description:"MQTT broker address" required:"false"`
|
|
||||||
MQTTUser string `long:"mqttuser" description:"MQTT server user" required:"false"`
|
|
||||||
MQTTPassword string `long:"mqttpass" description:"MQTT server password" required:"false"`
|
|
||||||
config.KasparovFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses the CLI arguments and returns a config struct.
|
|
||||||
func Parse() error {
|
|
||||||
activeConfig = &Config{}
|
|
||||||
parser := flags.NewParser(activeConfig, flags.PrintErrors|flags.HelpFlag)
|
|
||||||
_, err := parser.Parse()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = activeConfig.ResolveKasparovFlags(parser, defaultLogDir, logFilename, errLogFilename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeConfig.MQTTBrokerAddress != "" || activeConfig.MQTTUser != "" || activeConfig.MQTTPassword != "") &&
|
|
||||||
(activeConfig.MQTTBrokerAddress == "" || activeConfig.MQTTUser == "" || activeConfig.MQTTPassword == "") {
|
|
||||||
return errors.New("--mqttaddress, --mqttuser, and --mqttpass must be passed all together")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,29 +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 kasparov/kasparovsyncd && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kasparov-syncd .
|
|
||||||
|
|
||||||
# --- 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/kasparov/kasparovsyncd/ /app/
|
|
||||||
COPY --from=build /go/src/github.com/kaspanet/kaspad/kasparov/database/migrations/ /database/migrations/
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
|
||||||
CMD ["/app/kasparov-syncd"]
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.Logger("KVSD")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/database/mysql"
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/database"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/jsonrpc"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovsyncd/config"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovsyncd/mqtt"
|
|
||||||
"github.com/kaspanet/kaspad/signal"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defer panics.HandlePanic(log, nil, nil)
|
|
||||||
|
|
||||||
err := config.Parse()
|
|
||||||
if err != nil {
|
|
||||||
errString := fmt.Sprintf("Error parsing command-line arguments: %s", err)
|
|
||||||
_, fErr := fmt.Fprintf(os.Stderr, errString)
|
|
||||||
if fErr != nil {
|
|
||||||
panic(errString)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ActiveConfig().Migrate {
|
|
||||||
err := database.Migrate(&config.ActiveConfig().KasparovFlags)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error migrating database: %s", err))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = database.Connect(&config.ActiveConfig().KasparovFlags)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error connecting to database: %s", err))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := database.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error closing the database: %s", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = mqtt.Connect()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error connecting to MQTT: %s", err))
|
|
||||||
}
|
|
||||||
defer mqtt.Close()
|
|
||||||
|
|
||||||
err = jsonrpc.Connect(&config.ActiveConfig().KasparovFlags)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error connecting to servers: %s", err))
|
|
||||||
}
|
|
||||||
defer jsonrpc.Close()
|
|
||||||
|
|
||||||
doneChan := make(chan struct{}, 1)
|
|
||||||
spawn(func() {
|
|
||||||
err := startSync(doneChan)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
interrupt := signal.InterruptListener()
|
|
||||||
<-interrupt
|
|
||||||
|
|
||||||
// Gracefully stop syncing
|
|
||||||
doneChan <- struct{}{}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package mqtt
|
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/util/panics"
|
|
||||||
import "github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.Logger("MQTT")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package mqtt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovsyncd/config"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// client is an instance of the MQTT client, in case we have an active connection
|
|
||||||
var client mqtt.Client
|
|
||||||
|
|
||||||
const (
|
|
||||||
qualityOfService = 2
|
|
||||||
quiesceMilliseconds = 250
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetClient returns an instance of the MQTT client, in case we have an active connection
|
|
||||||
func GetClient() (mqtt.Client, error) {
|
|
||||||
if client == nil {
|
|
||||||
return nil, errors.New("MQTT is not connected")
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isConnected() bool {
|
|
||||||
return client != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect initiates a connection to the MQTT server, if defined
|
|
||||||
func Connect() error {
|
|
||||||
cfg := config.ActiveConfig()
|
|
||||||
if cfg.MQTTBrokerAddress == "" {
|
|
||||||
// MQTT broker not defined -- nothing to do
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
options := mqtt.NewClientOptions()
|
|
||||||
options.AddBroker(cfg.MQTTBrokerAddress)
|
|
||||||
options.SetUsername(cfg.MQTTUser)
|
|
||||||
options.SetPassword(cfg.MQTTPassword)
|
|
||||||
options.SetAutoReconnect(true)
|
|
||||||
|
|
||||||
newClient := mqtt.NewClient(options)
|
|
||||||
if token := newClient.Connect(); token.Wait() && token.Error() != nil {
|
|
||||||
return token.Error()
|
|
||||||
}
|
|
||||||
client = newClient
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the connection to the MQTT server, if previously connected
|
|
||||||
func Close() {
|
|
||||||
if client == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client.Disconnect(quiesceMilliseconds)
|
|
||||||
client = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func publish(topic string, data interface{}) error {
|
|
||||||
payload, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
token := client.Publish(topic, qualityOfService, false, payload)
|
|
||||||
token.Wait()
|
|
||||||
if token.Error() != nil {
|
|
||||||
return errors.WithStack(token.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package mqtt
|
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/kasparov/kasparovd/controllers"
|
|
||||||
|
|
||||||
const selectedTipTopic = "dag/selected-tip"
|
|
||||||
|
|
||||||
// PublishSelectedTipNotification publishes notification for a new selected tip
|
|
||||||
func PublishSelectedTipNotification(selectedTipHash string) error {
|
|
||||||
if !isConnected() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
block, err := controllers.GetBlockByHashHandler(selectedTipHash)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return publish(selectedTipTopic, block)
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package mqtt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/apimodels"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/controllers"
|
|
||||||
"github.com/kaspanet/kaspad/rpcclient"
|
|
||||||
"github.com/kaspanet/kaspad/rpcmodel"
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PublishTransactionsNotifications publishes notification for each transaction of the given block
|
|
||||||
func PublishTransactionsNotifications(rawTransactions []rpcmodel.TxRawResult) error {
|
|
||||||
if !isConnected() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
transactionIDs := make([]string, len(rawTransactions))
|
|
||||||
for i, tx := range rawTransactions {
|
|
||||||
transactionIDs[i] = tx.TxID
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions, err := controllers.GetTransactionsByIDsHandler(transactionIDs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, transaction := range transactions {
|
|
||||||
err = publishTransactionNotifications(transaction, "transactions")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func publishTransactionNotifications(transaction *apimodels.TransactionResponse, topic string) error {
|
|
||||||
addresses := uniqueAddressesForTransaction(transaction)
|
|
||||||
for _, address := range addresses {
|
|
||||||
err := publishTransactionNotificationForAddress(transaction, address, topic)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniqueAddressesForTransaction(transaction *apimodels.TransactionResponse) []string {
|
|
||||||
addressesMap := make(map[string]struct{})
|
|
||||||
addresses := []string{}
|
|
||||||
for _, output := range transaction.Outputs {
|
|
||||||
if _, exists := addressesMap[output.Address]; !exists {
|
|
||||||
addresses = append(addresses, output.Address)
|
|
||||||
addressesMap[output.Address] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, input := range transaction.Inputs {
|
|
||||||
if _, exists := addressesMap[input.Address]; !exists {
|
|
||||||
addresses = append(addresses, input.Address)
|
|
||||||
addressesMap[input.Address] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
func publishTransactionNotificationForAddress(transaction *apimodels.TransactionResponse, address string, topic string) error {
|
|
||||||
return publish(path.Join(topic, address), transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishAcceptedTransactionsNotifications publishes notification for each accepted transaction of the given chain-block
|
|
||||||
func PublishAcceptedTransactionsNotifications(addedChainBlocks []*rpcclient.ChainBlock) error {
|
|
||||||
for _, addedChainBlock := range addedChainBlocks {
|
|
||||||
for _, acceptedBlock := range addedChainBlock.AcceptedBlocks {
|
|
||||||
transactionIDs := make([]string, len(acceptedBlock.AcceptedTxIDs))
|
|
||||||
for i, acceptedTxID := range acceptedBlock.AcceptedTxIDs {
|
|
||||||
transactionIDs[i] = acceptedTxID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions, err := controllers.GetTransactionsByIDsHandler(transactionIDs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, transaction := range transactions {
|
|
||||||
err = publishTransactionNotifications(transaction, "transactions/accepted")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishUnacceptedTransactionsNotifications publishes notification for each unaccepted transaction of the given chain-block
|
|
||||||
func PublishUnacceptedTransactionsNotifications(removedChainHashes []*daghash.Hash) error {
|
|
||||||
for _, removedHash := range removedChainHashes {
|
|
||||||
transactionIDs, err := controllers.GetAcceptedTransactionIDsByBlockHashHandler(removedHash.String())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions, err := controllers.GetTransactionsByIDsHandler(transactionIDs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, transaction := range transactions {
|
|
||||||
err = publishTransactionNotifications(transaction, "transactions/unaccepted")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/kaspanet/kaspad/logs"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BackendLog is the logging backend used to create all subsystem loggers.
|
|
||||||
var BackendLog = logs.NewBackend()
|
|
||||||
var loggers []logs.Logger
|
|
||||||
|
|
||||||
// InitLog attaches log file and error log file to the backend 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger returns a new logger for a particular subsystem that writes to
|
|
||||||
// BackendLog, and add it to a slice so it will be possible to access it
|
|
||||||
// later and change its log level
|
|
||||||
func Logger(subsystemTag string) logs.Logger {
|
|
||||||
logger := BackendLog.Logger(subsystemTag)
|
|
||||||
loggers = append(loggers, logger)
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLogLevels sets the logging level for all of the subsystems in Kasparov.
|
|
||||||
func SetLogLevels(level string) error {
|
|
||||||
lvl, ok := logs.LevelFromString(level)
|
|
||||||
if !ok {
|
|
||||||
return errors.Errorf("Invalid log level %s", level)
|
|
||||||
}
|
|
||||||
for _, logger := range loggers {
|
|
||||||
logger.SetLevel(lvl)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
|
||||||
"github.com/kaspanet/kaspad/dagconfig"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const instanceStateCodeActive = "16"
|
|
||||||
|
|
||||||
func getAddressList(cfg *config) ([]string, error) {
|
|
||||||
if cfg.AddressListPath != "" {
|
|
||||||
return getAddressListFromPath(cfg)
|
|
||||||
}
|
|
||||||
return getAddressListFromAWS(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAddressListFromAWS(cfg *config) ([]string, error) {
|
|
||||||
log.Infof("Getting hosts list for autoscaling group %s", cfg.AutoScalingGroup)
|
|
||||||
sess := session.Must(session.NewSession(&aws.Config{Region: aws.String(cfg.Region)}))
|
|
||||||
ec2Client := ec2.New(sess)
|
|
||||||
instances, err := ec2Client.DescribeInstances(&ec2.DescribeInstancesInput{
|
|
||||||
Filters: []*ec2.Filter{
|
|
||||||
&ec2.Filter{Name: aws.String("tag:aws:autoscaling:groupName"), Values: []*string{&cfg.AutoScalingGroup}},
|
|
||||||
&ec2.Filter{Name: aws.String("instance-state-code"), Values: []*string{aws.String(instanceStateCodeActive)}},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Error describing instances")
|
|
||||||
}
|
|
||||||
|
|
||||||
addressList := []string{}
|
|
||||||
for _, reservation := range instances.Reservations {
|
|
||||||
for _, instance := range reservation.Instances {
|
|
||||||
if instance.PrivateDnsName == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addressList = append(addressList, fmt.Sprintf("%s:%s", *instance.PrivateDnsName, dagconfig.DevNetParams.RPCPort))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return addressList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAddressListFromPath(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
|
|
||||||
}
|
|
||||||
@@ -1,38 +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 simulatorClient struct {
|
|
||||||
*rpcclient.Client
|
|
||||||
onBlockAdded chan struct{}
|
|
||||||
notifyForNewBlocks bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSimulatorClient(address string, connCfg *rpcclient.ConnConfig) (*simulatorClient, error) {
|
|
||||||
client := &simulatorClient{
|
|
||||||
onBlockAdded: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
notificationHandlers := &rpcclient.NotificationHandlers{
|
|
||||||
OnFilteredBlockAdded: func(height uint64, header *wire.BlockHeader,
|
|
||||||
txs []*util.Tx) {
|
|
||||||
if client.notifyForNewBlocks {
|
|
||||||
client.onBlockAdded <- struct{}{}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
client.Client, err = rpcclient.New(connCfg, notificationHandlers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("Error connecting to address %s: %s", address, 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,66 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/jessevdk/go-flags"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultLogFilename = "miningsimulator.log"
|
|
||||||
defaultErrLogFilename = "miningsimulator_err.log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default configuration options
|
|
||||||
defaultHomeDir = util.AppDataDir("miningsimulator", false)
|
|
||||||
defaultLogFile = filepath.Join(defaultHomeDir, defaultLogFilename)
|
|
||||||
defaultErrLogFile = filepath.Join(defaultHomeDir, defaultErrLogFilename)
|
|
||||||
)
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
AutoScalingGroup string `long:"autoscaling" description:"AWS AutoScalingGroup to use for address list"`
|
|
||||||
Region string `long:"region" description:"AWS region to use for address list"`
|
|
||||||
AddressListPath string `long:"addresslist" description:"Path to a list of nodes' JSON-RPC endpoints"`
|
|
||||||
CertificatePath string `long:"cert" description:"Path to certificate accepted by JSON-RPC endpoint"`
|
|
||||||
DisableTLS bool `long:"notls" description:"Disable TLS"`
|
|
||||||
Verbose bool `long:"verbose" short:"v" description:"Enable logging of RPC requests"`
|
|
||||||
BlockDelay uint64 `long:"block-delay" description:"Delay for block submission (in milliseconds)"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cfg.AutoScalingGroup == "" || cfg.Region == "") && cfg.AddressListPath == "" {
|
|
||||||
return nil, errors.New("Either (--autoscaling and --region) or --addresslist must be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cfg.AutoScalingGroup != "" || cfg.Region != "") && cfg.AddressListPath != "" {
|
|
||||||
return nil, errors.New("Both (--autoscaling and --region) and --addresslist can't be specified at the same time")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.AutoScalingGroup != "" && cfg.Region == "" {
|
|
||||||
return nil, errors.New("If --autoscaling is specified --region must be specified as well")
|
|
||||||
}
|
|
||||||
|
|
||||||
initLog(defaultLogFile, defaultErrLogFile)
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/rpcclient"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type connectionManager struct {
|
|
||||||
addressList []string
|
|
||||||
cert []byte
|
|
||||||
clients []*simulatorClient
|
|
||||||
cfg *config
|
|
||||||
disconnectChan chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConnectionManager(cfg *config) (*connectionManager, error) {
|
|
||||||
connManager := &connectionManager{
|
|
||||||
cfg: cfg,
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
connManager.addressList, err = getAddressList(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
connManager.cert, err = readCert(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
connManager.clients, err = connectToServers(connManager.addressList, connManager.cert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.AutoScalingGroup != "" {
|
|
||||||
connManager.disconnectChan = make(chan struct{})
|
|
||||||
spawn(func() { connManager.refreshAddressesLoop() })
|
|
||||||
}
|
|
||||||
|
|
||||||
return connManager, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectToServer(address string, cert []byte) (*simulatorClient, error) {
|
|
||||||
connCfg := &rpcclient.ConnConfig{
|
|
||||||
Host: address,
|
|
||||||
Endpoint: "ws",
|
|
||||||
User: "user",
|
|
||||||
Pass: "pass",
|
|
||||||
DisableTLS: cert == nil,
|
|
||||||
RequestTimeout: time.Second * 10,
|
|
||||||
Certificates: cert,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := newSimulatorClient(address, connCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Connected to server %s", address)
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectToServers(addressList []string, cert []byte) ([]*simulatorClient, error) {
|
|
||||||
clients := make([]*simulatorClient, 0, len(addressList))
|
|
||||||
|
|
||||||
for _, address := range addressList {
|
|
||||||
client, err := connectToServer(address, cert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clients = append(clients, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
return clients, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readCert(cfg *config) ([]byte, error) {
|
|
||||||
if cfg.DisableTLS {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := ioutil.ReadFile(cfg.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("Error reading certificates file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *connectionManager) close() {
|
|
||||||
if cm.disconnectChan != nil {
|
|
||||||
cm.disconnectChan <- struct{}{}
|
|
||||||
}
|
|
||||||
for _, client := range cm.clients {
|
|
||||||
client.Disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshAddressInterval = time.Minute * 10
|
|
||||||
|
|
||||||
func (cm *connectionManager) refreshAddressesLoop() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(refreshAddressInterval):
|
|
||||||
err := cm.refreshAddresses()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
case <-cm.disconnectChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *connectionManager) refreshAddresses() error {
|
|
||||||
newAddressList, err := getAddressList(cm.cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(newAddressList) == len(cm.addressList) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
outerLoop:
|
|
||||||
for _, newAddress := range newAddressList {
|
|
||||||
for _, oldAddress := range cm.addressList {
|
|
||||||
if newAddress == oldAddress {
|
|
||||||
continue outerLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := connectToServer(newAddress, cm.cert)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cm.clients = append(cm.clients, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
cm.addressList = newAddressList
|
|
||||||
|
|
||||||
return 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 mining/simulator && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mining_simulator .
|
|
||||||
|
|
||||||
# --- 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/mining/simulator/mining_simulator /app/
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
|
||||||
CMD ["/app/mining_simulator"]
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
1. To build docker image invoke following command from btcd root directory:
|
|
||||||
docker build -t mining_simulator -f ./mining/simulator/docker/Dockerfile .
|
|
||||||
|
|
||||||
2. To run:
|
|
||||||
a. create folder ~/.btcd/mining_simulator 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 mining_simulator
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/kaspanet/kaspad/logs"
|
|
||||||
"github.com/kaspanet/kaspad/rpcclient"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
backendLog = logs.NewBackend()
|
|
||||||
log = backendLog.Logger("MNSM")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func enableRPCLogging() {
|
|
||||||
rpclog := backendLog.Logger("RPCC")
|
|
||||||
rpclog.SetLevel(logs.LevelTrace)
|
|
||||||
rpcclient.UseLogger(rpclog)
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/signal"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defer panics.HandlePanic(log, nil, nil)
|
|
||||||
cfg, err := parseConfig()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error parsing command-line arguments: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Verbose {
|
|
||||||
enableRPCLogging()
|
|
||||||
}
|
|
||||||
|
|
||||||
connManager, err := newConnectionManager(cfg)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error initializing connection manager: %s", err))
|
|
||||||
}
|
|
||||||
defer connManager.close()
|
|
||||||
|
|
||||||
spawn(func() {
|
|
||||||
err = mineLoop(connManager, cfg.BlockDelay)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error in main loop: %s", err))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
interrupt := signal.InterruptListener()
|
|
||||||
<-interrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
func disconnect(clients []*simulatorClient) {
|
|
||||||
for _, client := range clients {
|
|
||||||
client.Disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
nativeerrors "errors"
|
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/rpcclient"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/blockdag"
|
|
||||||
"github.com/kaspanet/kaspad/rpcmodel"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
)
|
|
||||||
|
|
||||||
var random = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
|
|
||||||
func parseBlock(template *rpcmodel.GetBlockTemplateResult) (*util.Block, error) {
|
|
||||||
// parse parent hashes
|
|
||||||
parentHashes := make([]*daghash.Hash, len(template.ParentHashes))
|
|
||||||
for i, parentHash := range template.ParentHashes {
|
|
||||||
hash, err := daghash.NewHashFromStr(parentHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("Error decoding hash %s: %s", parentHash, err)
|
|
||||||
}
|
|
||||||
parentHashes[i] = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse Bits
|
|
||||||
bitsInt64, err := strconv.ParseInt(template.Bits, 16, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("Error decoding bits %s: %s", template.Bits, err)
|
|
||||||
}
|
|
||||||
bits := uint32(bitsInt64)
|
|
||||||
|
|
||||||
// parseAcceptedIDMerkleRoot
|
|
||||||
acceptedIDMerkleRoot, err := daghash.NewHashFromStr(template.AcceptedIDMerkleRoot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("Error parsing acceptedIDMerkleRoot: %s", err)
|
|
||||||
}
|
|
||||||
utxoCommitment, err := daghash.NewHashFromStr(template.UTXOCommitment)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("Error parsing utxoCommitment: %s", err)
|
|
||||||
}
|
|
||||||
// parse rest of block
|
|
||||||
msgBlock := wire.NewMsgBlock(
|
|
||||||
wire.NewBlockHeader(template.Version, parentHashes, &daghash.Hash{},
|
|
||||||
acceptedIDMerkleRoot, utxoCommitment, bits, 0))
|
|
||||||
|
|
||||||
for i, txResult := range append([]rpcmodel.GetBlockTemplateResultTx{*template.CoinbaseTxn}, template.Transactions...) {
|
|
||||||
reader := hex.NewDecoder(strings.NewReader(txResult.Data))
|
|
||||||
tx := &wire.MsgTx{}
|
|
||||||
if err := tx.KaspaDecode(reader, 0); err != nil {
|
|
||||||
return nil, errors.Errorf("Error decoding tx #%d: %s", i, err)
|
|
||||||
}
|
|
||||||
msgBlock.AddTransaction(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
block := util.NewBlock(msgBlock)
|
|
||||||
msgBlock.Header.HashMerkleRoot = blockdag.BuildHashMerkleTreeStore(block.Transactions()).Root()
|
|
||||||
return block, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func solveBlock(block *util.Block, stopChan chan struct{}, foundBlock chan *util.Block) {
|
|
||||||
msgBlock := block.MsgBlock()
|
|
||||||
targetDifficulty := util.CompactToBig(msgBlock.Header.Bits)
|
|
||||||
initialNonce := random.Uint64()
|
|
||||||
for i := random.Uint64(); i != initialNonce-1; i++ {
|
|
||||||
select {
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
msgBlock.Header.Nonce = i
|
|
||||||
hash := msgBlock.BlockHash()
|
|
||||||
if daghash.HashToBig(hash).Cmp(targetDifficulty) <= 0 {
|
|
||||||
foundBlock <- block
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBlockTemplate(client *simulatorClient, longPollID string) (*rpcmodel.GetBlockTemplateResult, error) {
|
|
||||||
return client.GetBlockTemplate([]string{"coinbasetxn"}, longPollID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func templatesLoop(client *simulatorClient, newTemplateChan chan *rpcmodel.GetBlockTemplateResult, errChan chan error, stopChan chan struct{}) {
|
|
||||||
longPollID := ""
|
|
||||||
getBlockTemplateLongPoll := func() {
|
|
||||||
if longPollID != "" {
|
|
||||||
log.Infof("Requesting template with longPollID '%s' from %s", longPollID, client.Host())
|
|
||||||
} else {
|
|
||||||
log.Infof("Requesting template without longPollID from %s", client.Host())
|
|
||||||
}
|
|
||||||
template, err := getBlockTemplate(client, longPollID)
|
|
||||||
if nativeerrors.Is(err, rpcclient.ErrResponseTimedOut) {
|
|
||||||
log.Infof("Got timeout while requesting template '%s' from %s", longPollID, client.Host())
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
errChan <- errors.Errorf("Error getting block template: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if template.LongPollID != longPollID {
|
|
||||||
log.Infof("Got new long poll template: %s", template.LongPollID)
|
|
||||||
longPollID = template.LongPollID
|
|
||||||
newTemplateChan <- template
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getBlockTemplateLongPoll()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopChan:
|
|
||||||
close(newTemplateChan)
|
|
||||||
return
|
|
||||||
case <-client.onBlockAdded:
|
|
||||||
getBlockTemplateLongPoll()
|
|
||||||
case <-time.Tick(500 * time.Millisecond):
|
|
||||||
getBlockTemplateLongPoll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func solveLoop(newTemplateChan chan *rpcmodel.GetBlockTemplateResult, foundBlock chan *util.Block, errChan chan error) {
|
|
||||||
var stopOldTemplateSolving chan struct{}
|
|
||||||
for template := range newTemplateChan {
|
|
||||||
if stopOldTemplateSolving != nil {
|
|
||||||
close(stopOldTemplateSolving)
|
|
||||||
}
|
|
||||||
stopOldTemplateSolving = make(chan struct{})
|
|
||||||
block, err := parseBlock(template)
|
|
||||||
if err != nil {
|
|
||||||
errChan <- errors.Errorf("Error parsing block: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go solveBlock(block, stopOldTemplateSolving, foundBlock)
|
|
||||||
}
|
|
||||||
if stopOldTemplateSolving != nil {
|
|
||||||
close(stopOldTemplateSolving)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mineNextBlock(client *simulatorClient, foundBlock chan *util.Block, templateStopChan chan struct{}, errChan chan error) {
|
|
||||||
newTemplateChan := make(chan *rpcmodel.GetBlockTemplateResult)
|
|
||||||
go templatesLoop(client, newTemplateChan, errChan, templateStopChan)
|
|
||||||
go solveLoop(newTemplateChan, foundBlock, errChan)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFoundBlock(client *simulatorClient, block *util.Block) error {
|
|
||||||
log.Infof("Found block %s with parents %s! Submitting to %s", block.Hash(), block.MsgBlock().Header.ParentHashes, client.Host())
|
|
||||||
|
|
||||||
err := client.SubmitBlock(block, &rpcmodel.SubmitBlockOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error submitting block %s to %s: %s", block.Hash(), client.Host(), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRandomClient(clients []*simulatorClient) *simulatorClient {
|
|
||||||
clientsCount := int64(len(clients))
|
|
||||||
if clientsCount == 1 {
|
|
||||||
return clients[0]
|
|
||||||
}
|
|
||||||
return clients[random.Int63n(clientsCount)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func mineLoop(connManager *connectionManager, blockDelay uint64) error {
|
|
||||||
errChan := make(chan error)
|
|
||||||
|
|
||||||
templateStopChan := make(chan struct{})
|
|
||||||
|
|
||||||
spawn(func() {
|
|
||||||
for {
|
|
||||||
foundBlock := make(chan *util.Block)
|
|
||||||
currentClient := getRandomClient(connManager.clients)
|
|
||||||
currentClient.notifyForNewBlocks = true
|
|
||||||
log.Infof("Next block will be mined by: %s", currentClient.Host())
|
|
||||||
mineNextBlock(currentClient, foundBlock, templateStopChan, errChan)
|
|
||||||
block, ok := <-foundBlock
|
|
||||||
if !ok {
|
|
||||||
errChan <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentClient.notifyForNewBlocks = false
|
|
||||||
templateStopChan <- struct{}{}
|
|
||||||
spawn(func() {
|
|
||||||
if blockDelay != 0 {
|
|
||||||
time.Sleep(time.Duration(blockDelay) * time.Millisecond)
|
|
||||||
}
|
|
||||||
err := handleFoundBlock(currentClient, block)
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
err := <-errChan
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user