diff --git a/README.md b/README.md index c17fcc8..bfe1f1f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,25 @@ # dcrvsp -- `main.go` initialises the program with hard-coded config. -- `responses.go` contains the API response types copied from #625. -- `router.go` contains the webserver init and config. -- `methods.go` contains an implementation of payfee, copied from #625. -- `database.go` contains stubbed database methods. -- `wallet.go` contains stubbed dcrwallet calls. +## Design decisions + +- [gin-gonic](https://github.com/gin-gonic/gin) webserver +- [bbolt](https://github.com/etcd-io/bbolt) database + +## MVP features + +- VSP API "v3" as described in [dcrstakepool #574](https://github.com/decred/dcrstakepool/issues/574) +and implemented in [dcrstakepool #625](https://github.com/decred/dcrstakepool/pull/625) + - Request fee amount + - Request fee address + - Pay fee + - Set voting preferences +- A minimal, static, web front-end providing pool stats and basic connection instructions. + +## Future features + +- Write database backups to disk periodically. +- Backup over http. +- Status check API call as described in [dcrstakepool #628](https://github.com/decred/dcrstakepool/issues/628). +- Accountability for both client and server changes to voting preferences. +- Consistency checking across connected wallets. +- Validate votebits provided in PayFee request are valid per current agendas. diff --git a/database.go b/database.go deleted file mode 100644 index 7e6e4bf..0000000 --- a/database.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -type Database struct { -} - -type Fees struct { - TicketHash string - CommitmentSignature string - FeeAddress string - Address string - SDiff int64 - BlockHeight int64 - VoteBits uint16 - VotingKey string -} - -func (db *Database) GetInactiveFeeAddresses() ([]string, error) { - return []string{""}, nil -} - -func (db *Database) GetFeesByFeeAddress(feeAddr string) (Fees, error) { - return Fees{}, nil -} - -func (db *Database) InsertFeeAddressVotingKey(address, votingKey string, voteBits uint16) error { - return nil -} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..85af16b --- /dev/null +++ b/database/database.go @@ -0,0 +1,64 @@ +package database + +import ( + "encoding/binary" + "fmt" + "time" + + bolt "go.etcd.io/bbolt" +) + +// VspDatabase wraps an instance of bbolt DB and provides VSP specific +// convenience functions. +type VspDatabase struct { + db *bolt.DB +} + +var ( + // vspBkt is the main parent bucket of the VSP. All values and other buckets + // are nested within it. + vspBkt = []byte("vspbkt") + feesBkt = []byte("feesbkt") + versionK = []byte("version") + backupFile = "backup.kv" + version = 1 +) + +// New initialises and returns a database connection. If no database file is +// found at the provided path, a new one will be created. Returns an open +// database connection which should be closed after use. +func New(dbFile string) (*VspDatabase, error) { + db, err := bolt.Open(dbFile, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return nil, fmt.Errorf("unable to open db file: %v", err) + } + + err = createBuckets(db) + if err != nil { + return nil, err + } + + return &VspDatabase{db: db}, nil +} + +// createBuckets creates all storage buckets of the VSP. +func createBuckets(db *bolt.DB) error { + return db.Update(func(tx *bolt.Tx) error { + pbkt := tx.Bucket(feesBkt) + if pbkt == nil { + pbkt, err := tx.CreateBucket(feesBkt) + if err != nil { + return fmt.Errorf("failed to create %s bucket: %v", string(feesBkt), err) + } + + vbytes := make([]byte, 4) + binary.LittleEndian.PutUint32(vbytes, uint32(version)) + err = pbkt.Put(versionK, vbytes) + if err != nil { + return err + } + } + + return nil + }) +} diff --git a/database/fees.go b/database/fees.go new file mode 100644 index 0000000..627a6e4 --- /dev/null +++ b/database/fees.go @@ -0,0 +1,32 @@ +package database + +type Fees struct { + TicketHash string + CommitmentSignature string + FeeAddress string + Address string + SDiff int64 + BlockHeight int64 + VoteBits uint16 + VotingKey string +} + +func (db *VspDatabase) InsertFeeAddressVotingKey(address, votingKey string, voteBits uint16) error { + return nil +} + +func (db *VspDatabase) InsertFeeAddress() error { + return nil +} + +func (db *VspDatabase) GetInactiveFeeAddresses() ([]string, error) { + return []string{""}, nil +} + +func (db *VspDatabase) GetFeesByFeeAddress(feeAddr string) (Fees, error) { + return Fees{}, nil +} + +func (db *VspDatabase) GetFeeAddressByTicketHash() (Fees, error) { + return Fees{}, nil +} diff --git a/go.mod b/go.mod index 330d87c..05e6a19 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,16 @@ -module dcrvsp +module github.com/jholdstock/dcrvsp go 1.14 require ( decred.org/dcrwallet v1.2.3-0.20200507155221-397dd551e317 github.com/decred/dcrd/chaincfg/chainhash v1.0.2 - github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8 + github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b github.com/decred/dcrd/dcrec v1.0.0 - github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8 + github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200511175520-d08cb3f72b3b github.com/decred/dcrd/rpcclient v1.1.0 - github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b + github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b github.com/decred/dcrd/wire v1.3.0 github.com/gin-gonic/gin v1.6.3 + go.etcd.io/bbolt v1.3.4 ) diff --git a/go.sum b/go.sum index 2d83b0e..4c05cc9 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1: github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8= github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8 h1:UweAZ771bCDNxumIVk7b0y2EJ8JUqeWhXbPpdXKsClc= github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8= +github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:L6qM+5ISaaYSnNMAFMouu/FGcIN0tP42rJUdtAJqKgY= +github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4= github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= @@ -82,6 +84,8 @@ github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:4 github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw= github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8 h1:l7N4vMUp1k8Ugs+ol3WLRKB3Xij0MOf5hC2bY/W6bS4= github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg= +github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:94fhkXS9ObD1P0SxOxk1TAFVhdFBpVeQeUWl0nYb6X8= +github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A= github.com/decred/dcrd/gcs v1.0.1 h1:MpJXLskT41+JDaD3RLdlSlF2vlu1sxPpZgiRI7FVTWw= github.com/decred/dcrd/gcs v1.0.1/go.mod h1:YwutGzusSdJM79CJtxCo9t7WRCvnkLtWSD19TPo1i9g= github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o= @@ -97,6 +101,8 @@ github.com/decred/dcrd/txscript/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1: github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc= github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b h1:HqIl8oViATdNtoYDFbHzGhHe4q9irezWXF16fhKy8/4= github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0= +github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:PbEqUN+q0hg/TJdi2IQ0Y/5Qc8GNFrnioXeBXm060n8= +github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0= github.com/decred/dcrd/wire v1.1.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM= github.com/decred/dcrd/wire v1.2.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM= github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE= @@ -153,6 +159,7 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20180718160520-a2144134853f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/main.go b/main.go index 75c7634..cb1c3ff 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,8 @@ package main import ( "crypto/ed25519" "crypto/rand" + "errors" + "fmt" "io/ioutil" "log" "os" @@ -10,39 +12,46 @@ import ( "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/rpcclient" + "github.com/jholdstock/dcrvsp/database" ) const listen = ":3000" -// Config vars -var ( +type Config struct { signKey ed25519.PrivateKey pubKey ed25519.PublicKey poolFees float64 netParams *chaincfg.Params -) + dbFile string +} + +var cfg Config // Database with stubbed methods -var db Database +var db *database.VspDatabase +// RPC clients var nodeConnection *rpcclient.Client var walletConnection *WalletClient -func initConfig() { - seedPath := filepath.Join("dcrvsp", "sign.seed") +func initConfig() (*Config, error) { + homePath := "~/.dcrvsp" + + seedPath := filepath.Join(homePath, "sign.seed") seed, err := ioutil.ReadFile(seedPath) + var signKey ed25519.PrivateKey if err != nil { if !os.IsNotExist(err) { - log.Fatal("seedPath does not exist") + return nil, errors.New("seedPath does not exist") } _, signKey, err = ed25519.GenerateKey(rand.Reader) if err != nil { - log.Fatalf("failed to generate signing key: %v", err) + return nil, fmt.Errorf("failed to generate signing key: %v", err) } err = ioutil.WriteFile(seedPath, signKey.Seed(), 0400) if err != nil { - log.Fatalf("failed to save signing key: %v", err) + return nil, fmt.Errorf("failed to save signing key: %v", err) } } else { signKey = ed25519.NewKeyFromSeed(seed) @@ -50,15 +59,29 @@ func initConfig() { pubKey, ok := signKey.Public().(ed25519.PublicKey) if !ok { - log.Fatalf("failed to cast signing key: %T", pubKey) + return nil, fmt.Errorf("failed to cast signing key: %T", pubKey) } - netParams = chaincfg.TestNet3Params() + return &Config{ + netParams: chaincfg.TestNet3Params(), + dbFile: filepath.Join(homePath, "database.db"), + pubKey: pubKey, + poolFees: 0.1, + signKey: signKey, + }, nil } func main() { - initConfig() + cfg, err := initConfig() + if err != nil { + log.Fatalf("config error: %v", err) + } + + db, err = database.New(cfg.dbFile) + if err != nil { + log.Fatalf("database error: %v", err) + } // Start HTTP server log.Printf("Listening on %s", listen) diff --git a/methods.go b/methods.go index a00180f..5ae56e1 100644 --- a/methods.go +++ b/methods.go @@ -28,7 +28,7 @@ func sendJSONResponse(resp interface{}, code int, c *gin.Context) { return } - sig := ed25519.Sign(signKey, dec) + sig := ed25519.Sign(cfg.signKey, dec) c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig)) c.Writer.Write(dec) @@ -41,7 +41,7 @@ func payFee(c *gin.Context) { // voteBits - voting preferences in little endian votingKey := c.Param("votingKey") - votingWIF, err := dcrutil.DecodeWIF(votingKey, netParams.PrivateKeyID) + votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.netParams.PrivateKeyID) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -84,7 +84,7 @@ func payFee(c *gin.Context) { findAddress: for _, txOut := range feeTx.TxOut { _, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion, - txOut.PkScript, netParams) + txOut.PkScript, cfg.netParams) if err != nil { fmt.Printf("Extract: %v", err) c.AbortWithError(http.StatusInternalServerError, err) @@ -113,13 +113,13 @@ findAddress: c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) return } - voteAddr, err := dcrutil.DecodeAddress(feeEntry.Address, netParams) + voteAddr, err := dcrutil.DecodeAddress(feeEntry.Address, cfg.netParams) if err != nil { fmt.Errorf("PayFee: DecodeAddress: %v", err) c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) return } - _, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), netParams, + _, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), cfg.netParams, dcrec.STEcdsaSecp256k1) if err != nil { fmt.Errorf("PayFee: NewAddressPubKeyHash: %v", err) @@ -137,7 +137,7 @@ findAddress: return } - minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(feeEntry.BlockHeight), poolFees, netParams) + minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(feeEntry.BlockHeight), cfg.poolFees, cfg.netParams) if feeAmount < minFee { fmt.Printf("too cheap: %v %v", feeAmount, minFee) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("dont get cheap on me, dodgson (sent:%v required:%v)", feeAmount, minFee))