From 84970a8378adf632feecfc3dce6fbfe14f009eaf Mon Sep 17 00:00:00 2001 From: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com> Date: Mon, 3 Jun 2019 15:44:43 +0300 Subject: [PATCH] [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. --- blockdag/dag.go | 4 ++ blockdag/indexers/addrindex.go | 6 +- cmd/addsubnetwork/addsubnetwork.go | 87 ++++++++++++++++++++++ cmd/addsubnetwork/config.go | 91 +++++++++++++++++++++++ cmd/addsubnetwork/connect.go | 37 ++++++++++ cmd/addsubnetwork/keys.go | 19 +++++ cmd/addsubnetwork/log.go | 17 +++++ cmd/addsubnetwork/registrytx.go | 34 +++++++++ cmd/addsubnetwork/utxo.go | 112 +++++++++++++++++++++++++++++ mempool/mempool.go | 2 +- 10 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 cmd/addsubnetwork/addsubnetwork.go create mode 100644 cmd/addsubnetwork/config.go create mode 100644 cmd/addsubnetwork/connect.go create mode 100644 cmd/addsubnetwork/keys.go create mode 100644 cmd/addsubnetwork/log.go create mode 100644 cmd/addsubnetwork/registrytx.go create mode 100644 cmd/addsubnetwork/utxo.go diff --git a/blockdag/dag.go b/blockdag/dag.go index acefc29f6..cc999432e 100644 --- a/blockdag/dag.go +++ b/blockdag/dag.go @@ -1195,6 +1195,10 @@ func (dag *BlockDAG) GetUTXOEntry(outPoint wire.OutPoint) (*UTXOEntry, bool) { // BlockConfirmationsByHash returns the confirmations number for a block with the // given hash. See blockConfirmations for further details. func (dag *BlockDAG) BlockConfirmationsByHash(hash *daghash.Hash) (uint64, error) { + if hash.IsEqual(&daghash.ZeroHash) { + return 0, nil + } + node := dag.index.LookupNode(hash) if node == nil { return 0, fmt.Errorf("block %s is unknown", hash) diff --git a/blockdag/indexers/addrindex.go b/blockdag/indexers/addrindex.go index 0ed54118b..e52a96320 100644 --- a/blockdag/indexers/addrindex.go +++ b/blockdag/indexers/addrindex.go @@ -134,8 +134,8 @@ func serializeAddrIndexEntry(blockID uint64, txLoc wire.TxLoc) []byte { // Serialize the entry. serialized := make([]byte, 16) byteOrder.PutUint64(serialized, blockID) - byteOrder.PutUint32(serialized[4:], uint32(txLoc.TxStart)) - byteOrder.PutUint32(serialized[8:], uint32(txLoc.TxLen)) + byteOrder.PutUint32(serialized[8:], uint32(txLoc.TxStart)) + byteOrder.PutUint32(serialized[12:], uint32(txLoc.TxLen)) return serialized } @@ -155,7 +155,7 @@ func deserializeAddrIndexEntry(serialized []byte, region *database.BlockRegion, } region.Hash = hash region.Offset = byteOrder.Uint32(serialized[8:12]) - region.Len = byteOrder.Uint32(serialized[12:14]) + region.Len = byteOrder.Uint32(serialized[12:16]) return nil } diff --git a/cmd/addsubnetwork/addsubnetwork.go b/cmd/addsubnetwork/addsubnetwork.go new file mode 100644 index 000000000..576aa383f --- /dev/null +++ b/cmd/addsubnetwork/addsubnetwork.go @@ -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 + } +} diff --git a/cmd/addsubnetwork/config.go b/cmd/addsubnetwork/config.go new file mode 100644 index 000000000..14567f431 --- /dev/null +++ b/cmd/addsubnetwork/config.go @@ -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 +} diff --git a/cmd/addsubnetwork/connect.go b/cmd/addsubnetwork/connect.go new file mode 100644 index 000000000..4bc7a47df --- /dev/null +++ b/cmd/addsubnetwork/connect.go @@ -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 +} diff --git a/cmd/addsubnetwork/keys.go b/cmd/addsubnetwork/keys.go new file mode 100644 index 000000000..14aa42bbe --- /dev/null +++ b/cmd/addsubnetwork/keys.go @@ -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 +} diff --git a/cmd/addsubnetwork/log.go b/cmd/addsubnetwork/log.go new file mode 100644 index 000000000..febafee4e --- /dev/null +++ b/cmd/addsubnetwork/log.go @@ -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") +) diff --git a/cmd/addsubnetwork/registrytx.go b/cmd/addsubnetwork/registrytx.go new file mode 100644 index 000000000..c9cd6cbde --- /dev/null +++ b/cmd/addsubnetwork/registrytx.go @@ -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 +} diff --git a/cmd/addsubnetwork/utxo.go b/cmd/addsubnetwork/utxo.go new file mode 100644 index 000000000..948632f14 --- /dev/null +++ b/cmd/addsubnetwork/utxo.go @@ -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 +} diff --git a/mempool/mempool.go b/mempool/mempool.go index 270d6e94a..79ce1a92b 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -857,7 +857,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *util.Tx, isNew, rateLimit, rejectDu // Check that transaction does not overuse GAS 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) if err != nil { return nil, nil, err