Add v3tool. (#366)

This commit adds a new executable - v3tool - a developer testing tool which hits endpoints of the vspd API.
This commit is contained in:
Jamie Holdstock 2023-03-24 18:10:44 +00:00 committed by GitHub
parent 0c06145d67
commit b5ac64891e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 397 additions and 0 deletions

21
cmd/v3tool/README.md Normal file
View File

@ -0,0 +1,21 @@
# v3tool
v3tool is a simple client for manual testing of vspd.
It is a developer tool, not suitable for end users or production use.
## Prerequisites
1. An instance of dcrwallet which owns at least one immature or live ticket.
1. An instance of vspd to test.
## What v3tool does
1. Retrieve the pubkey from vspd.
1. Retrieve the list of owned immature/live tickets from dcrwallet.
1. For each ticket:
1. Use dcrwallet to find the tx hex, voting privkey and commitment address of the ticket.
1. Get a fee address and amount from vspd to register this ticket.
1. Create the fee tx and send it to vspd.
1. Get the ticket status.
1. Change vote choices on the ticket.
1. Get the ticket status again.

165
cmd/v3tool/dcrwallet.go Normal file
View File

@ -0,0 +1,165 @@
package main
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
wallettypes "decred.org/dcrwallet/v2/rpc/jsonrpc/types"
"github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v3"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/txscript/v4/stdscript"
"github.com/decred/dcrd/wire"
"github.com/jrick/wsrpc/v2"
)
type dcrwallet struct {
*wsrpc.Client
}
func newWalletRPC(rpcURL, rpcUser, rpcPass string) (*dcrwallet, error) {
tlsOpt := wsrpc.WithTLSConfig(&tls.Config{
InsecureSkipVerify: true,
})
authOpt := wsrpc.WithBasicAuth(rpcUser, rpcPass)
rpc, err := wsrpc.Dial(context.TODO(), rpcURL, tlsOpt, authOpt)
if err != nil {
return nil, err
}
return &dcrwallet{rpc}, nil
}
func (w *dcrwallet) createFeeTx(feeAddress string, fee int64) (string, error) {
amounts := make(map[string]float64)
amounts[feeAddress] = dcrutil.Amount(fee).ToCoin()
var msgtxstr string
err := w.Call(context.TODO(), "createrawtransaction", &msgtxstr, nil, amounts)
if err != nil {
return "", err
}
zero := int32(0)
opt := wallettypes.FundRawTransactionOptions{
ConfTarget: &zero,
}
var fundTx wallettypes.FundRawTransactionResult
err = w.Call(context.TODO(), "fundrawtransaction", &fundTx, msgtxstr, "default", &opt)
if err != nil {
return "", err
}
tx := wire.NewMsgTx()
err = tx.Deserialize(hex.NewDecoder(strings.NewReader(fundTx.Hex)))
if err != nil {
return "", err
}
transactions := make([]dcrdtypes.TransactionInput, 0)
for _, v := range tx.TxIn {
transactions = append(transactions, dcrdtypes.TransactionInput{
Txid: v.PreviousOutPoint.Hash.String(),
Vout: v.PreviousOutPoint.Index,
})
}
var locked bool
unlock := false
err = w.Call(context.TODO(), "lockunspent", &locked, unlock, transactions)
if err != nil {
return "", err
}
if !locked {
return "", errors.New("unspent output not locked")
}
var signedTx wallettypes.SignRawTransactionResult
err = w.Call(context.TODO(), "signrawtransaction", &signedTx, fundTx.Hex)
if err != nil {
return "", err
}
if !signedTx.Complete {
return "", fmt.Errorf("not all signed")
}
return signedTx.Hex, nil
}
func (w *dcrwallet) SignMessage(ctx context.Context, msg string, commitmentAddr stdaddr.Address) ([]byte, error) {
var signature string
err := w.Call(context.TODO(), "signmessage", &signature, commitmentAddr.String(), msg)
if err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(signature)
}
func (w *dcrwallet) dumpPrivKey(addr stdaddr.Address) (string, error) {
var privKeyStr string
err := w.Call(context.TODO(), "dumpprivkey", &privKeyStr, addr.String())
if err != nil {
return "", err
}
return privKeyStr, nil
}
func (w *dcrwallet) getTickets() (*wallettypes.GetTicketsResult, error) {
var tickets wallettypes.GetTicketsResult
includeImmature := true
err := w.Call(context.TODO(), "gettickets", &tickets, includeImmature)
if err != nil {
return nil, err
}
return &tickets, nil
}
// getTicketDetails returns the ticket hex, privkey for voting, and the
// commitment address.
func (w *dcrwallet) getTicketDetails(ticketHash string) (string, string, stdaddr.Address, error) {
var getTransactionResult wallettypes.GetTransactionResult
err := w.Call(context.TODO(), "gettransaction", &getTransactionResult, ticketHash, false)
if err != nil {
fmt.Printf("gettransaction: %v\n", err)
return "", "", nil, err
}
msgTx := wire.NewMsgTx()
if err = msgTx.Deserialize(hex.NewDecoder(strings.NewReader(getTransactionResult.Hex))); err != nil {
return "", "", nil, err
}
if len(msgTx.TxOut) < 2 {
return "", "", nil, errors.New("msgTx.TxOut < 2")
}
const scriptVersion = 0
scriptType, submissionAddr := stdscript.ExtractAddrs(scriptVersion,
msgTx.TxOut[0].PkScript, chaincfg.TestNet3Params())
if scriptType == stdscript.STNonStandard {
return "", "", nil, fmt.Errorf("invalid script version %d", scriptVersion)
}
if len(submissionAddr) != 1 {
return "", "", nil, errors.New("submissionAddr != 1")
}
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript,
chaincfg.TestNet3Params())
if err != nil {
return "", "", nil, err
}
privKeyStr, err := w.dumpPrivKey(submissionAddr[0])
if err != nil {
return "", "", nil, err
}
return getTransactionResult.Hex, privKeyStr, addr, nil
}

208
cmd/v3tool/main.go Normal file
View File

@ -0,0 +1,208 @@
package main
import (
"context"
"encoding/json"
"io"
"net/http"
"os"
"time"
"github.com/decred/slog"
"github.com/decred/vspd/client"
"github.com/decred/vspd/types"
)
const (
vspdURL = "http://localhost:8800"
// dcrwallet RPC.
rpcURL = "wss://localhost:19110/ws"
rpcUser = "user"
rpcPass = "pass"
)
func getVspPubKey(url string) ([]byte, error) {
resp, err := http.Get(url + "/api/v3/vspinfo")
if err != nil {
return nil, err
}
b, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
var j types.VspInfoResponse
err = json.Unmarshal(b, &j)
if err != nil {
return nil, err
}
err = client.ValidateServerSignature(resp, b, j.PubKey)
if err != nil {
return nil, err
}
return j.PubKey, nil
}
func run() int {
log := slog.NewBackend(os.Stdout).Logger("")
log.SetLevel(slog.LevelTrace)
walletRPC, err := newWalletRPC(rpcURL, rpcUser, rpcPass)
if err != nil {
log.Errorf("%v", err)
return 1
}
defer walletRPC.Close()
log.Infof("vpsd url: %s", vspdURL)
pubKey, err := getVspPubKey(vspdURL)
if err != nil {
log.Errorf("%v", err)
return 1
}
log.Infof("vspd pubkey: %x", pubKey)
vClient := client.Client{
URL: vspdURL,
PubKey: pubKey,
Sign: walletRPC.SignMessage,
Log: log,
}
if err != nil {
log.Errorf("%v", err)
return 1
}
// Get list of tickets
tickets, err := walletRPC.getTickets()
if err != nil {
log.Errorf("%v", err)
return 1
}
if len(tickets.Hashes) == 0 {
log.Errorf("wallet owns no tickets")
return 1
}
log.Infof("wallet returned %d ticket(s):", len(tickets.Hashes))
for _, tkt := range tickets.Hashes {
log.Infof(" %s", tkt)
}
for i := 0; i < len(tickets.Hashes); i++ {
ticketHash := tickets.Hashes[i]
hex, privKeyStr, commitmentAddr, err := walletRPC.getTicketDetails(ticketHash)
if err != nil {
log.Errorf("%v", err)
return 1
}
log.Infof("")
log.Infof("Processing ticket %d of %d:", i+1, len(tickets.Hashes))
log.Infof(" Hash: %s", ticketHash)
log.Infof(" privKeyStr: %s", privKeyStr)
log.Infof(" commitmentAddr: %s", commitmentAddr)
log.Infof("")
feeAddrReq := types.FeeAddressRequest{
TicketHex: hex,
// Hack for ParentHex, can't be bothered to get the real one. It doesn't
// make a difference when testing locally anyway.
ParentHex: hex,
TicketHash: ticketHash,
Timestamp: time.Now().Unix(),
}
feeAddrResp, err := vClient.FeeAddress(context.TODO(), feeAddrReq, commitmentAddr)
if err != nil {
log.Errorf("getFeeAddress error: %v", err)
break
}
log.Infof("feeAddress: %v", feeAddrResp.FeeAddress)
log.Infof("privKeyStr: %v", privKeyStr)
feeTx, err := walletRPC.createFeeTx(feeAddrResp.FeeAddress, feeAddrResp.FeeAmount)
if err != nil {
log.Errorf("createFeeTx error: %v", err)
break
}
voteChoices := map[string]string{"autorevocations": "no"}
tspend := map[string]string{
"6c78690fa2fa31803df0376897725704e9dc19ecbdf80061e79b69de93ca1360": "no",
"abb86660dda1f1b66544bab24a823a22e9213ada48649f0d913623f49e17dacb": "yes",
}
treasury := map[string]string{
"03f6e7041f1cf51ee10e0a01cd2b0385ce3cd9debaabb2296f7e9dee9329da946c": "no",
"0319a37405cb4d1691971847d7719cfce70857c0f6e97d7c9174a3998cf0ab86dd": "yes",
}
payFeeReq := types.PayFeeRequest{
FeeTx: feeTx,
VotingKey: privKeyStr,
TicketHash: ticketHash,
Timestamp: time.Now().Unix(),
VoteChoices: voteChoices,
TSpendPolicy: tspend,
TreasuryPolicy: treasury,
}
_, err = vClient.PayFee(context.TODO(), payFeeReq, commitmentAddr)
if err != nil {
log.Errorf("payFee error: %v", err)
continue
}
ticketStatusReq := types.TicketStatusRequest{
TicketHash: ticketHash,
}
_, err = vClient.TicketStatus(context.TODO(), ticketStatusReq, commitmentAddr)
if err != nil {
log.Errorf("getTicketStatus error: %v", err)
break
}
voteChoices["autorevocations"] = "yes"
// Sleep to ensure a new timestamp. vspd will reject old/reused timestamps.
time.Sleep(1001 * time.Millisecond)
voteChoiceReq := types.SetVoteChoicesRequest{
Timestamp: time.Now().Unix(),
TicketHash: ticketHash,
VoteChoices: voteChoices,
TSpendPolicy: tspend,
TreasuryPolicy: treasury,
}
_, err = vClient.SetVoteChoices(context.TODO(), voteChoiceReq, commitmentAddr)
if err != nil {
log.Errorf("setVoteChoices error: %v", err)
break
}
_, err = vClient.TicketStatus(context.TODO(), ticketStatusReq, commitmentAddr)
if err != nil {
log.Errorf("getTicketStatus error: %v", err)
break
}
time.Sleep(1 * time.Second)
}
return 0
}
func main() {
os.Exit(run())
}

1
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/decred/dcrd/txscript/v4 v4.0.0
github.com/decred/dcrd/wire v1.5.0
github.com/decred/slog v1.2.0
github.com/decred/vspd/client v1.1.0
github.com/decred/vspd/types v1.1.0
github.com/dustin/go-humanize v1.0.1
github.com/gin-gonic/gin v1.8.2

2
go.sum
View File

@ -69,6 +69,8 @@ github.com/decred/dcrd/wire v1.5.0/go.mod h1:fzAjVqw32LkbAZIt5mnrvBR751GTa3e0rRQ
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM=
github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
github.com/decred/vspd/client v1.1.0 h1:vyMjbT9jGuH96yNsv2msx5E/DP7xk3ePIU7XWhu1Des=
github.com/decred/vspd/client v1.1.0/go.mod h1:+gn860mKciikYxQrPm+hSVMEG8CQ7EWf/vN9pZa3Baw=
github.com/decred/vspd/types v1.1.0 h1:hTeqQwgRUN2FGIbuCIdyzBejKV+jxKrmEIcLKxpsB1g=
github.com/decred/vspd/types v1.1.0/go.mod h1:THsO8aBSwWBq6ZsIG25cNqbkNb+EEASXzLhFvODVc0s=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=