[NOD-201] Create AddSubnetwork cli tool (#319)

* [NOD-201] Implemented the AddSubnetwork CLI tool.

* [NOD-201] Fixed various bugs in AddSubnetwork.

* [NOD-201] Fixed mempool maybeAcceptTransaction verifying gasLimit for a subnetwork registry transaction.

* [NOD-201] Fixed serialization/deserialization bugs in addrIndex.

* [NOD-201] Fixed BlockConfirmationsByHash not handling the zeroHash.

* [NOD-201] Used btclog instead of go log.

* [NOD-201] Made gasLimit a command-line flag. Made waitForSubnetworkToBecomeAccepted only return an error.

* [NOD-201] Filtered out mempool transactions.

* [NOD-201] Fixed embarrassing typos.

* [NOD-201] Added subnetwork registry tx fee + appropriate cli flag.

* [NOD-201] Skipped TXOs that can't pay for registration.
This commit is contained in:
stasatdaglabs 2019-06-03 15:44:43 +03:00 committed by Svarog
parent 901bde1fd4
commit 84970a8378
10 changed files with 405 additions and 4 deletions

View File

@ -1195,6 +1195,10 @@ func (dag *BlockDAG) GetUTXOEntry(outPoint wire.OutPoint) (*UTXOEntry, bool) {
// BlockConfirmationsByHash returns the confirmations number for a block with the // BlockConfirmationsByHash returns the confirmations number for a block with the
// given hash. See blockConfirmations for further details. // given hash. See blockConfirmations for further details.
func (dag *BlockDAG) BlockConfirmationsByHash(hash *daghash.Hash) (uint64, error) { func (dag *BlockDAG) BlockConfirmationsByHash(hash *daghash.Hash) (uint64, error) {
if hash.IsEqual(&daghash.ZeroHash) {
return 0, nil
}
node := dag.index.LookupNode(hash) node := dag.index.LookupNode(hash)
if node == nil { if node == nil {
return 0, fmt.Errorf("block %s is unknown", hash) return 0, fmt.Errorf("block %s is unknown", hash)

View File

@ -134,8 +134,8 @@ func serializeAddrIndexEntry(blockID uint64, txLoc wire.TxLoc) []byte {
// Serialize the entry. // Serialize the entry.
serialized := make([]byte, 16) serialized := make([]byte, 16)
byteOrder.PutUint64(serialized, blockID) byteOrder.PutUint64(serialized, blockID)
byteOrder.PutUint32(serialized[4:], uint32(txLoc.TxStart)) byteOrder.PutUint32(serialized[8:], uint32(txLoc.TxStart))
byteOrder.PutUint32(serialized[8:], uint32(txLoc.TxLen)) byteOrder.PutUint32(serialized[12:], uint32(txLoc.TxLen))
return serialized return serialized
} }
@ -155,7 +155,7 @@ func deserializeAddrIndexEntry(serialized []byte, region *database.BlockRegion,
} }
region.Hash = hash region.Hash = hash
region.Offset = byteOrder.Uint32(serialized[8:12]) region.Offset = byteOrder.Uint32(serialized[8:12])
region.Len = byteOrder.Uint32(serialized[12:14]) region.Len = byteOrder.Uint32(serialized[12:16])
return nil return nil
} }

View File

@ -0,0 +1,87 @@
package main
import (
"fmt"
"github.com/daglabs/btcd/blockdag"
"github.com/daglabs/btcd/btcjson"
"github.com/daglabs/btcd/rpcclient"
"github.com/daglabs/btcd/util/subnetworkid"
"time"
)
const (
getSubnetworkRetryDelay = 5 * time.Second
maxGetSubnetworkRetries = 12
)
func main() {
cfg, err := parseConfig()
if err != nil {
panic(fmt.Errorf("error parsing command-line arguments: %s", err))
}
privateKey, addrPubKeyHash, err := decodeKeys(cfg)
if err != nil {
panic(fmt.Errorf("error decoding public key: %s", err))
}
client, err := connect(cfg)
if err != nil {
panic(fmt.Errorf("could not connect to RPC server: %s", err))
}
log.Infof("Connected to server %s", cfg.RPCServer)
fundingOutPoint, fundingTx, err := findUnspentTXO(cfg, client, addrPubKeyHash)
if err != nil {
panic(fmt.Errorf("error finding unspent transactions: %s", err))
}
if fundingOutPoint == nil || fundingTx == nil {
panic(fmt.Errorf("could not find any unspent transactions this for key"))
}
log.Infof("Found transaction to spend: %s:%d", fundingOutPoint.TxID, fundingOutPoint.Index)
registryTx, err := buildSubnetworkRegistryTx(cfg, fundingOutPoint, fundingTx, privateKey)
if err != nil {
panic(fmt.Errorf("error building subnetwork registry tx: %s", err))
}
_, err = client.SendRawTransaction(registryTx, true)
if err != nil {
panic(fmt.Errorf("failed sending subnetwork registry tx: %s", err))
}
log.Infof("Successfully sent subnetwork registry transaction")
subnetworkID, err := blockdag.TxToSubnetworkID(registryTx)
if err != nil {
panic(fmt.Errorf("could not build subnetwork ID: %s", err))
}
err = waitForSubnetworkToBecomeAccepted(client, subnetworkID)
if err != nil {
panic(fmt.Errorf("error waiting for subnetwork to become accepted: %s", err))
}
log.Infof("Subnetwork '%s' was successfully registered.", subnetworkID)
}
func waitForSubnetworkToBecomeAccepted(client *rpcclient.Client, subnetworkID *subnetworkid.SubnetworkID) error {
retries := 0
for {
_, err := client.GetSubnetwork(subnetworkID.String())
if err != nil {
if rpcError, ok := err.(*btcjson.RPCError); ok && rpcError.Code == btcjson.ErrRPCSubnetworkNotFound {
log.Infof("Subnetwork not found")
retries++
if retries == maxGetSubnetworkRetries {
return fmt.Errorf("failed to get subnetwork %d times: %s", maxGetSubnetworkRetries, err)
}
log.Infof("Waiting %d seconds...", int(getSubnetworkRetryDelay.Seconds()))
<-time.After(getSubnetworkRetryDelay)
continue
}
return fmt.Errorf("failed getting subnetwork: %s", err)
}
return nil
}
}

View File

@ -0,0 +1,91 @@
package main
import (
"errors"
"fmt"
"github.com/daglabs/btcd/dagconfig"
"github.com/jessevdk/go-flags"
)
type config struct {
PrivateKey string `short:"k" long:"private-key" description:"Private key" required:"true"`
RPCUser string `short:"u" long:"rpcuser" description:"RPC username" required:"true"`
RPCPassword string `short:"P" long:"rpcpass" default-mask:"-" description:"RPC password" required:"true"`
RPCServer string `short:"s" long:"rpcserver" description:"RPC server to connect to" required:"true"`
RPCCert string `short:"c" long:"rpccert" description:"RPC server certificate chain for validation"`
DisableTLS bool `long:"notls" description:"Disable TLS"`
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"`
GasLimit uint64 `long:"gaslimit" description:"The gas limit of the new subnetwork"`
RegistryTxFee uint64 `long:"regtxfee" description:"The fee for the subnetwork registry transaction"`
}
const (
defaultSubnetworkGasLimit = 1000
defaultRegistryTxFee = 3000
)
var (
activeNetParams dagconfig.Params
)
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.RPCCert == "" && !cfg.DisableTLS {
return nil, errors.New("--notls has to be disabled if --cert is used")
}
if cfg.RPCCert != "" && cfg.DisableTLS {
return nil, errors.New("--cert should be omitted if --notls is used")
}
// 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 nil, 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.TestNet3Params
case cfg.SimNet:
activeNetParams = dagconfig.SimNetParams
case cfg.DevNet:
activeNetParams = dagconfig.DevNetParams
}
if cfg.GasLimit < 0 {
return nil, fmt.Errorf("gaslimit may not be smaller than 0")
}
if cfg.GasLimit == 0 {
cfg.GasLimit = defaultSubnetworkGasLimit
}
if cfg.RegistryTxFee < 0 {
return nil, fmt.Errorf("regtxfee may not be smaller than 0")
}
if cfg.RegistryTxFee == 0 {
cfg.RegistryTxFee = defaultRegistryTxFee
}
return cfg, nil
}

View File

@ -0,0 +1,37 @@
package main
import (
"fmt"
"github.com/daglabs/btcd/rpcclient"
"io/ioutil"
)
func connect(cfg *config) (*rpcclient.Client, error) {
var cert []byte
if !cfg.DisableTLS {
var err error
cert, err = ioutil.ReadFile(cfg.RPCCert)
if err != nil {
return nil, fmt.Errorf("error reading certificates file: %s", err)
}
}
connCfg := &rpcclient.ConnConfig{
Host: cfg.RPCServer,
Endpoint: "ws",
User: cfg.RPCUser,
Pass: cfg.RPCPassword,
DisableTLS: cfg.DisableTLS,
}
if !cfg.DisableTLS {
connCfg.Certificates = cert
}
client, err := rpcclient.New(connCfg, nil)
if err != nil {
return nil, fmt.Errorf("error connecting to address %s: %s", cfg.RPCServer, err)
}
return client, nil
}

19
cmd/addsubnetwork/keys.go Normal file
View File

@ -0,0 +1,19 @@
package main
import (
"github.com/daglabs/btcd/btcec"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/util/base58"
)
func decodeKeys(cfg *config) (*btcec.PrivateKey, *util.AddressPubKeyHash, error) {
privateKeyBytes := base58.Decode(cfg.PrivateKey)
privateKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), privateKeyBytes)
serializedPrivateKey := privateKey.PubKey().SerializeCompressed()
pubKeyAddr, err := util.NewAddressPubKey(serializedPrivateKey, activeNetParams.Prefix)
if err != nil {
return nil, nil, err
}
return privateKey, pubKeyAddr.AddressPubKeyHash(), nil
}

17
cmd/addsubnetwork/log.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"github.com/btcsuite/btclog"
"os"
)
type logWriter struct{}
func (logWriter) Write(p []byte) (n int, err error) {
return os.Stdout.Write(p)
}
var (
backendLog = btclog.NewBackend(logWriter{})
log = backendLog.Logger("ASUB")
)

View File

@ -0,0 +1,34 @@
package main
import (
"fmt"
"github.com/daglabs/btcd/blockdag"
"github.com/daglabs/btcd/btcec"
"github.com/daglabs/btcd/txscript"
"github.com/daglabs/btcd/wire"
)
func buildSubnetworkRegistryTx(cfg *config, fundingOutPoint *wire.OutPoint, fundingTx *wire.MsgTx, privateKey *btcec.PrivateKey) (*wire.MsgTx, error) {
txIn := &wire.TxIn{
PreviousOutPoint: *fundingOutPoint,
Sequence: wire.MaxTxInSequenceNum,
}
pkScript, err := txscript.PayToScriptHashScript(blockdag.OpTrueScript)
if err != nil {
return nil, err
}
txOut := &wire.TxOut{
PkScript: pkScript,
Value: fundingTx.TxOut[fundingOutPoint.Index].Value - cfg.RegistryTxFee,
}
registryTx := wire.NewRegistryMsgTx(1, []*wire.TxIn{txIn}, []*wire.TxOut{txOut}, cfg.GasLimit)
SignatureScript, err := txscript.SignatureScript(registryTx, 0, fundingTx.TxOut[fundingOutPoint.Index].PkScript,
txscript.SigHashAll, privateKey, true)
if err != nil {
return nil, fmt.Errorf("failed to build signature script: %s", err)
}
txIn.SignatureScript = SignatureScript
return registryTx, nil
}

112
cmd/addsubnetwork/utxo.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/daglabs/btcd/btcjson"
"github.com/daglabs/btcd/rpcclient"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/wire"
)
const (
resultsCount = 1000
minConfirmations = 10
)
func findUnspentTXO(cfg *config, client *rpcclient.Client, addrPubKeyHash *util.AddressPubKeyHash) (*wire.OutPoint, *wire.MsgTx, error) {
txs, err := collectTransactions(client, addrPubKeyHash)
if err != nil {
return nil, nil, err
}
utxos := buildUTXOs(txs)
for outPoint, tx := range utxos {
// Skip TXOs that can't pay for registration
if tx.TxOut[outPoint.Index].Value < cfg.RegistryTxFee {
continue
}
return &outPoint, tx, nil
}
return nil, nil, nil
}
func collectTransactions(client *rpcclient.Client, addrPubKeyHash *util.AddressPubKeyHash) ([]*wire.MsgTx, error) {
txs := make([]*wire.MsgTx, 0)
skip := 0
for {
results, err := client.SearchRawTransactionsVerbose(addrPubKeyHash, skip, resultsCount, true, false, nil)
if err != nil {
// Break when there are no further txs
if rpcError, ok := err.(*btcjson.RPCError); ok && rpcError.Code == btcjson.ErrRPCNoTxInfo {
break
}
return nil, err
}
for _, result := range results {
// Mempool transactions bring about unnecessary complexity, so
// simply don't bother processing them
if result.IsInMempool {
continue
}
tx, err := parseRawTransactionResult(result)
if err != nil {
return nil, fmt.Errorf("failed to process SearchRawTransactionResult: %s", err)
}
if tx == nil {
continue
}
if !isTxMatured(tx, *result.Confirmations) {
continue
}
txs = append(txs, tx)
}
skip += resultsCount
}
return txs, nil
}
func parseRawTransactionResult(result *btcjson.SearchRawTransactionsResult) (*wire.MsgTx, error) {
txBytes, err := hex.DecodeString(result.Hex)
if err != nil {
return nil, fmt.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, fmt.Errorf("failed to deserialize transaction: %s", err)
}
return &tx, nil
}
func isTxMatured(tx *wire.MsgTx, confirmations uint64) bool {
if !tx.IsBlockReward() {
return confirmations >= minConfirmations
}
return confirmations >= activeNetParams.BlockRewardMaturity
}
func buildUTXOs(txs []*wire.MsgTx) map[wire.OutPoint]*wire.MsgTx {
utxos := make(map[wire.OutPoint]*wire.MsgTx)
for _, tx := range txs {
for i := range tx.TxOut {
outPoint := wire.NewOutPoint(tx.TxID(), uint32(i))
utxos[*outPoint] = tx
}
}
for _, tx := range txs {
for _, input := range tx.TxIn {
delete(utxos, input.PreviousOutPoint)
}
}
return utxos
}

View File

@ -857,7 +857,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *util.Tx, isNew, rateLimit, rejectDu
// Check that transaction does not overuse GAS // Check that transaction does not overuse GAS
msgTx := tx.MsgTx() msgTx := tx.MsgTx()
if !msgTx.SubnetworkID.IsEqual(subnetworkid.SubnetworkIDNative) { if !msgTx.SubnetworkID.IsEqual(subnetworkid.SubnetworkIDNative) && !msgTx.SubnetworkID.IsEqual(subnetworkid.SubnetworkIDRegistry) {
gasLimit, err := mp.cfg.DAG.SubnetworkStore.GasLimit(&msgTx.SubnetworkID) gasLimit, err := mp.cfg.DAG.SubnetworkStore.GasLimit(&msgTx.SubnetworkID)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err