From 1be92a6c8f1f3ebb19be36360af6073d4fcd63c0 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 21 Apr 2021 11:43:33 +0100 Subject: [PATCH 01/35] Add DCR to supported assets and Decred to supported Chains --- multichain.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/multichain.go b/multichain.go index ef2eee7e..22c0a04b 100644 --- a/multichain.go +++ b/multichain.go @@ -103,6 +103,7 @@ const ( BCH = Asset("BCH") // Bitcoin Cash BNB = Asset("BNB") // Binance Coin BTC = Asset("BTC") // Bitcoin + DCR = Asset("DCR") // Decred DGB = Asset("DGB") // DigiByte DOGE = Asset("DOGE") // Dogecoin ETH = Asset("ETH") // Ether @@ -134,6 +135,8 @@ func (asset Asset) OriginChain() Chain { return BinanceSmartChain case BTC: return Bitcoin + case DCR: + return Decred case DGB: return DigiByte case DOGE: @@ -173,7 +176,7 @@ func (asset Asset) OriginChain() Chain { // ChainType returns the chain-type (Account or UTXO) for the given asset func (asset Asset) ChainType() ChainType { switch asset { - case BCH, BTC, DGB, DOGE, ZEC: + case BCH, BTC, DCR, DGB, DOGE, ZEC: return ChainTypeUTXOBased case AVAX, BNB, ETH, FIL, GLMR, LUNA, MATIC: return ChainTypeAccountBased @@ -218,6 +221,7 @@ const ( BinanceSmartChain = Chain("BinanceSmartChain") Bitcoin = Chain("Bitcoin") BitcoinCash = Chain("BitcoinCash") + Decred = Chain("Decred") DigiByte = Chain("DigiByte") Dogecoin = Chain("Dogecoin") Ethereum = Chain("Ethereum") @@ -259,7 +263,7 @@ func (chain *Chain) Unmarshal(buf []byte, rem int) ([]byte, int, error) { // for the chain. func (chain Chain) ChainType() ChainType { switch chain { - case Bitcoin, BitcoinCash, DigiByte, Dogecoin, Zcash: + case Bitcoin, BitcoinCash, Decred, DigiByte, Dogecoin, Zcash: return ChainTypeUTXOBased case Avalanche, BinanceSmartChain, Ethereum, Fantom, Filecoin, Moonbeam, Polygon, Solana, Terra: return ChainTypeAccountBased @@ -301,6 +305,8 @@ func (chain Chain) NativeAsset() Asset { return BCH case Bitcoin: return BTC + case Decred: + return DCR case DigiByte: return DGB case Dogecoin: From 412f827236cf2c40b47bbd064c8abcc4e98ba889 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Fri, 7 May 2021 11:10:40 +0100 Subject: [PATCH 02/35] Add Decred docker conatiner --- infra/decred/Dockerfile | 31 +++++++++++++++++++++++++++++++ infra/decred/dcrd.conf | 5 +++++ infra/decred/run.sh | 8 ++++++++ 3 files changed, 44 insertions(+) create mode 100644 infra/decred/Dockerfile create mode 100644 infra/decred/dcrd.conf create mode 100644 infra/decred/run.sh diff --git a/infra/decred/Dockerfile b/infra/decred/Dockerfile new file mode 100644 index 00000000..82b2a262 --- /dev/null +++ b/infra/decred/Dockerfile @@ -0,0 +1,31 @@ +FROM ubuntu:xenial + +ENV VERSION v1.6.2 +ENV RELEASE_NAME decred-linux-amd64-${VERSION} +ENV RELEASE_FILE ${RELEASE_NAME}.tar.gz +ENV DCR_URL https://github.com/decred/decred-binaries/releases/download/${VERSION}/${RELEASE_FILE} + + +RUN apt-get update && apt-get install --yes software-properties-common +RUN apt-get install --yes curl + +RUN cd /tmp +RUN curl -SLO ${DCR_URL} +Run tar xvzf ${RELEASE_FILE} +Run mkdir /decred +RUN mv ${RELEASE_NAME}/dcrd /decred +RUN mv ${RELEASE_NAME}/dcrwallet /decred +RUN chmod +x /decred/dcrd +RUN chmod +x /decred/dcrwallet + +COPY dcrd.conf /root/.dcrd/ +COPY run.sh /root/ +RUN chmod +x /root/run.sh + +# Cleanup +RUN cd /root/ && rm -rf /tmp/* + +# PEER & RPC PORTS +EXPOSE 19556 18555 + +ENTRYPOINT ["./root/run.sh"] diff --git a/infra/decred/dcrd.conf b/infra/decred/dcrd.conf new file mode 100644 index 00000000..5660f42a --- /dev/null +++ b/infra/decred/dcrd.conf @@ -0,0 +1,5 @@ +simnet=1 +rpcuser=user +rpcpass=password +rpclisten=0.0.0.0 +txindex=1 diff --git a/infra/decred/run.sh b/infra/decred/run.sh new file mode 100644 index 00000000..3c00acac --- /dev/null +++ b/infra/decred/run.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Start +/decred/dcrd --simnet +sleep 2 + +# Print setup +echo "DCRD STARTED" From f41361d835fe7145164907b480cbf81cb6bc581b Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Fri, 7 May 2021 12:07:27 +0100 Subject: [PATCH 03/35] Add Decred entry to docker-compose.yaml --- infra/docker-compose.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index 402fe7ec..c404f1ac 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -50,7 +50,17 @@ services: entrypoint: - "./root/run.sh" - "${BITCOINCASH_ADDRESS}" - + # + # Decred + # + decred: + build: + context: ./decred + ports: + - "0.0.0.0:19555:19555" + entrypoint: + - "./root/run.sh" + # # DigiByte # From ac2038550c7ec4d51ab7c511c1a0f86b392f9d44 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Tue, 18 May 2021 13:46:00 +0100 Subject: [PATCH 04/35] Add dcrd simnet rpc cert/key --- infra/decred/rpc.cert | 12 ++++++++++++ infra/decred/rpc.key | 5 +++++ 2 files changed, 17 insertions(+) create mode 100644 infra/decred/rpc.cert create mode 100644 infra/decred/rpc.key diff --git a/infra/decred/rpc.cert b/infra/decred/rpc.cert new file mode 100644 index 00000000..1241debd --- /dev/null +++ b/infra/decred/rpc.cert @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBvDCCAWKgAwIBAgIQFapkfcx1xsuJayzCsAb5ejAKBggqhkjOPQQDAjAnMREw +DwYDVQQKEwhnZW5jZXJ0czESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTIxMDUxNTE0 +MjQ1MFoXDTMxMDUxNDE0MjQ1MFowJzERMA8GA1UEChMIZ2VuY2VydHMxEjAQBgNV +BAMTCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABF7wYx4foGY8 +9ucRNuYtciqzsq53L8SNZUqoB3rovTLJv/r/bujzMZaLhWD5QTQ88+y7CJ7RsUfr +gNDLyKahFpyjcDBuMA4GA1UdDwEB/wQEAwIChDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBTFJqSTEBFsGmGSza2kB1XwnARDBzAsBgNVHREEJTAjgglsb2NhbGhv +c3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0EAwIDSAAwRQIhAKsW +aMDXTIYIYEH06AHiPneWPVdufNbn3INGwA5cPEoDAiAFGYejnqUGsB6tih6lKaZZ +4CX3QAo92ZnRy+qg90wX6w== +-----END CERTIFICATE----- diff --git a/infra/decred/rpc.key b/infra/decred/rpc.key new file mode 100644 index 00000000..1e1d0bc5 --- /dev/null +++ b/infra/decred/rpc.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiiXTS03qdz2Zf9zg +xqi0vSra5R9HVFGMuS13t0IQS6ihRANCAARe8GMeH6BmPPbnETbmLXIqs7Kudy/E +jWVKqAd66L0yyb/6/27o8zGWi4Vg+UE0PPPsuwie0bFH64DQy8imoRac +-----END PRIVATE KEY----- From 14deb0a9474d13a44a097da57cf46e50ea5f98e9 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Tue, 18 May 2021 13:48:37 +0100 Subject: [PATCH 05/35] Add dcrctl to decred docker toolchain --- infra/decred/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infra/decred/Dockerfile b/infra/decred/Dockerfile index 82b2a262..216e74be 100644 --- a/infra/decred/Dockerfile +++ b/infra/decred/Dockerfile @@ -15,10 +15,14 @@ Run tar xvzf ${RELEASE_FILE} Run mkdir /decred RUN mv ${RELEASE_NAME}/dcrd /decred RUN mv ${RELEASE_NAME}/dcrwallet /decred +RUN mv ${RELEASE_NAME}/dcrctl /decred RUN chmod +x /decred/dcrd RUN chmod +x /decred/dcrwallet +RUN chmod +x /decred/dcrctl COPY dcrd.conf /root/.dcrd/ +COPY rpc.key /root/.dcrd/ +COPY rpc.cert /root/.dcrd/ COPY run.sh /root/ RUN chmod +x /root/run.sh From b8b9ac1933d47445b6f1df86619c4f023e4e190b Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Tue, 18 May 2021 14:06:35 +0100 Subject: [PATCH 06/35] Implement decred client LatestBlock API --- chain/decred/decred.go | 304 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 chain/decred/decred.go diff --git a/chain/decred/decred.go b/chain/decred/decred.go new file mode 100644 index 00000000..a14604c7 --- /dev/null +++ b/chain/decred/decred.go @@ -0,0 +1,304 @@ +package decred + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "time" + + "github.com/decred/dcrd/dcrjson/v3" + "github.com/renproject/pack" +) + +const ( + // DefaultClientTimeout used by the Client. + DefaultClientTimeout = time.Minute + // DefaultClientTimeoutRetry used by the Client. + DefaultClientTimeoutRetry = time.Second + // DefaultClientHost used by the Client. This should only be used for local + // deployments of the multichain. + DefaultClientHost = "https://127.0.0.1:19556" + // DefaultClientUser used by the Client. This is insecure, and should only + // be used for local — or publicly accessible — deployments of the + // multichain. + DefaultClientUser = "user" + // DefaultClientPassword used by the Client. This is insecure, and should + // only be used for local — or publicly accessible — deployments of the + // multichain. + DefaultClientPassword = "password" + DefaultClientNoTLS = false + // Authorization types. + DefaultClientAuthTypeBasic = "basic" + DefaultClientAuthTypeClientCert = "clientcert" + DefaultClientTLSSkipVerify = false + DefaultClientCert = "rpc.cert" +) + +// ClientOptions are used to parameterise the behaviour of the Client. +type ClientOptions struct { + Timeout time.Duration + TimeoutRetry time.Duration + NoTLS bool + Host string + User string + Password string + TLSSkipVerify bool + AuthType string + ClientCert string + ClientKey string + RPCCert string +} + +// DefaultClientOptions returns ClientOptions with the default settings. These +// settings are valid for use with the default local deployment of the +// multichain. In production, the host, user, and password should be changed. +func DefaultClientOptions() ClientOptions { + + dir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + defaultCertFile := dir + "/decred/" + DefaultClientCert + + return ClientOptions{ + Timeout: DefaultClientTimeout, + TimeoutRetry: DefaultClientTimeoutRetry, + NoTLS: DefaultClientNoTLS, + Host: DefaultClientHost, + User: DefaultClientUser, + Password: DefaultClientPassword, + TLSSkipVerify: DefaultClientTLSSkipVerify, + AuthType: DefaultClientAuthTypeBasic, + RPCCert: defaultCertFile, + } +} + +// WithHost sets the URL of the Bitcoin node. +func (opts ClientOptions) WithHost(host string) ClientOptions { + opts.Host = host + return opts +} + +// WithUser sets the username that will be used to authenticate with the Bitcoin +// node. +func (opts ClientOptions) WithUser(user string) ClientOptions { + opts.User = user + return opts +} + +// WithPassword sets the password that will be used to authenticate with the +// Bitcoin node. +func (opts ClientOptions) WithPassword(password string) ClientOptions { + opts.Password = password + return opts +} + +type client struct { + opts ClientOptions + httpClient http.Client +} + +// NewClient returns a new Client. +func NewClient(opts ClientOptions) *client { + httpClient := http.Client{} + httpClient.Timeout = opts.Timeout + + // Configure TLS if needed. + var tlsConfig *tls.Config + if !opts.NoTLS { + tlsConfig = &tls.Config{ + InsecureSkipVerify: opts.TLSSkipVerify, + } + if !opts.TLSSkipVerify && opts.AuthType == DefaultClientAuthTypeClientCert { + serverCAs := x509.NewCertPool() + serverCert, err := ioutil.ReadFile(opts.RPCCert) + if err != nil { + return nil + } + if !serverCAs.AppendCertsFromPEM(serverCert) { + return nil + } + keypair, err := tls.LoadX509KeyPair(opts.ClientCert, opts.ClientKey) + if err != nil { + return nil + } + + tlsConfig.Certificates = []tls.Certificate{keypair} + tlsConfig.RootCAs = serverCAs + + } + if !opts.TLSSkipVerify && opts.RPCCert != "" { + pem, err := ioutil.ReadFile(opts.RPCCert) + if err != nil { + return nil + } + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(pem); !ok { + return nil + } + tlsConfig.RootCAs = pool + } + } + + httpClient.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + + return &client{ + opts: opts, + httpClient: httpClient, + } +} + +// LatestBlock returns the height of the longest blockchain. +func (client *client) LatestBlock(ctx context.Context) (pack.U64, error) { + //var resp int64 + var resp dcrjson.Response + if err := client.send(ctx, &resp, "getbestblock"); err != nil { + return pack.NewU64(0), fmt.Errorf("get block count: %v", err) + } + + result := struct { + Hash string `json:"hash"` + Height uint64 `json:"height"` + }{} + + err := json.Unmarshal(resp.Result, &result) + if err != nil { + return pack.NewU64(0), err + } + + if result.Height < 0 { + return pack.NewU64(0), fmt.Errorf("unexpected block count, expected > 0, got: %v", result.Height) + } + + return pack.NewU64(uint64(result.Height)), nil +} + +func (client *client) send(ctx context.Context, resp *dcrjson.Response, method string, params ...interface{}) error { + // Encode the request. + data, err := encodeRequest(method, params) + if err != nil { + return err + } + + return retry(ctx, client.opts.TimeoutRetry, func() error { + // Create request and add basic authentication headers. The context is + // not attached to the request, and instead we all each attempt to run + // for the timeout duration, and we keep attempting until success, or + // the context is done. + req, err := http.NewRequest("POST", client.opts.Host, bytes.NewBuffer(data)) + if err != nil { + return fmt.Errorf("building http request: %v", err) + } + req.SetBasicAuth(client.opts.User, client.opts.Password) + + // Send the request and decode the response. + res, err := client.httpClient.Do(req) + if err != nil { + return fmt.Errorf("sending http request: %v", err) + } + // Read the raw bytes and close the response. + respBytes, err := ioutil.ReadAll(res.Body) + //fmt.Printf("Response: %+v \n", res) + defer res.Body.Close() + //if err := decodeResponse(resp, res.Body); err != nil { + // return fmt.Errorf("decoding http response: %v", err) + //} + + if err != nil { + err = fmt.Errorf("error reading json reply: %w", err) + return err + } + + // Handle unsuccessful HTTP responses + if res.StatusCode < 200 || res.StatusCode >= 300 { + // Generate a standard error to return if the server body is + // empty. This should not happen very often, but it's better + // than showing nothing in case the target server has a poor + // implementation. + if len(respBytes) == 0 { + return fmt.Errorf("%d %s", res.StatusCode, + http.StatusText(res.StatusCode)) + } + return fmt.Errorf("%s", respBytes) + } + + // Unmarshal the response. + // var resp dcrjson.Response + if err := json.Unmarshal(respBytes, resp); err != nil { + return err + } + + return nil + }) +} + +func encodeRequest(method string, params []interface{}) ([]byte, error) { + rawParams, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("encoding params: %v", err) + } + req := struct { + Version string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + }{ + Version: "1.0", + ID: 1, + Method: method, + Params: rawParams, + } + rawReq, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("encoding request: %v", err) + } + return rawReq, nil +} + +func decodeResponse(resp interface{}, r io.Reader) error { + res := struct { + Version string `json:"version"` + ID int `json:"id"` + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` + }{} + if err := json.NewDecoder(r).Decode(&res); err != nil { + return fmt.Errorf("decoding response: %v", err) + } + if res.Error != nil { + return fmt.Errorf("decoding response: %v", string(*res.Error)) + } + if res.Result == nil { + return fmt.Errorf("decoding result: result is nil") + } + if err := json.Unmarshal(*res.Result, resp); err != nil { + return fmt.Errorf("decoding result: %v", err) + } + return nil +} + +func retry(ctx context.Context, dur time.Duration, f func() error) error { + ticker := time.NewTicker(dur) + err := f() + for err != nil { + log.Printf("retrying: %v", err) + select { + case <-ctx.Done(): + return fmt.Errorf("%v: %v", ctx.Err(), err) + case <-ticker.C: + err = f() + } + } + return nil +} From a170767a83a42c1ff78249cf70abf06882e7e3b5 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sun, 30 May 2021 20:33:04 +0100 Subject: [PATCH 07/35] Add dcrwallet config --- infra/decred/Dockerfile | 3 +++ infra/decred/dcrwallet.conf | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 infra/decred/dcrwallet.conf diff --git a/infra/decred/Dockerfile b/infra/decred/Dockerfile index 216e74be..cf8f3831 100644 --- a/infra/decred/Dockerfile +++ b/infra/decred/Dockerfile @@ -21,8 +21,11 @@ RUN chmod +x /decred/dcrwallet RUN chmod +x /decred/dcrctl COPY dcrd.conf /root/.dcrd/ +COPY dcrwallet.conf /root/.dcrwallet/ COPY rpc.key /root/.dcrd/ COPY rpc.cert /root/.dcrd/ +COPY rpc.key /root/.dcrwallet/ +COPY rpc.cert /root/.dcrwallet/ COPY run.sh /root/ RUN chmod +x /root/run.sh diff --git a/infra/decred/dcrwallet.conf b/infra/decred/dcrwallet.conf new file mode 100644 index 00000000..0de3dcae --- /dev/null +++ b/infra/decred/dcrwallet.conf @@ -0,0 +1,9 @@ +simnet=1 +pass=password +cafile=~/.dcrd/rpc.cert +rpccert=~/.dcrwallet/rpc.cert +rpckey=~/.dcrwallet/rpc.key +rpclisten=0.0.0.0 +username=user +password=password +appdata=~/.dcrwallet \ No newline at end of file From 3090f5a4437e23afdfc7b6b273add7824bba13b8 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sun, 30 May 2021 20:34:17 +0100 Subject: [PATCH 08/35] Docker Image: install screen --- infra/decred/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/decred/Dockerfile b/infra/decred/Dockerfile index cf8f3831..4ab4ca4b 100644 --- a/infra/decred/Dockerfile +++ b/infra/decred/Dockerfile @@ -7,6 +7,7 @@ ENV DCR_URL https://github.com/decred/decred-binaries/releases/download/${VERSIO RUN apt-get update && apt-get install --yes software-properties-common +Run apt-get install --yes screen RUN apt-get install --yes curl RUN cd /tmp From dd766adcef162d6bd196450a9561a6ab2a285d3b Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sun, 30 May 2021 20:43:46 +0100 Subject: [PATCH 09/35] Add decred PK and Address to env --- infra/.env | 10 ++++++++++ infra/docker-compose.yaml | 2 ++ 2 files changed, 12 insertions(+) diff --git a/infra/.env b/infra/.env index 7d32d6e5..d92c70c6 100644 --- a/infra/.env +++ b/infra/.env @@ -25,6 +25,16 @@ export BITCOIN_ADDRESS=mwjUmhAW68zCtgZpW5b1xD5g7MZew6xPV4 export BITCOINCASH_PK=cSEohZFQLKuemNeBVrzwxniouUJJxdcx7Tm6HpspYuxraVjytieW export BITCOINCASH_ADDRESS=bchreg:qp6tejc0ghtjeejcxa97amzvxvzacjt4qczpy2n3gf +# +# Decred +# + +# Address that will receive mining rewards. Generally, this is set to an address +# for which the private key is known by a test suite. This allows the test suite +# access to plenty of testing funds. +export DECRED_PK=PsUQEpYDXVwphd9xNXUMj63LyxSWPTor3RDgfw9DMdH9tDkJaosyp +export DECRED_ADDRESS=SsaGEEZu2L8x93qvKzzahtzQ7yzkec3i8wL + # # DigiByte # diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index c404f1ac..787ee638 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -60,6 +60,8 @@ services: - "0.0.0.0:19555:19555" entrypoint: - "./root/run.sh" + - "${DECRED_ADDRESS}" + - "${DECRED_PK}" # # DigiByte From bfef60f54a2a54e8f8ee850edb129a2d93a631ac Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Mon, 31 May 2021 04:08:54 +0100 Subject: [PATCH 10/35] Impliment decred keygen --- infra/decred/keygen.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 infra/decred/keygen.go diff --git a/infra/decred/keygen.go b/infra/decred/keygen.go new file mode 100644 index 00000000..f74f57ec --- /dev/null +++ b/infra/decred/keygen.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + + "github.com/decred/dcrd/dcrec" + "github.com/decred/dcrd/dcrec/secp256k1/v3" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/decred/dcrd/chaincfg/v3" +) + +func main() { + simNetPrivKeyID := [2]byte{0x23, 0x07} + privKey, err := secp256k1.GeneratePrivateKey() + if err != nil { + panic(err) + } + wif, err := dcrutil.NewWIF(privKey.Serialize(), simNetPrivKeyID, dcrec.STEcdsaSecp256k1) + if err != nil { + panic(err) + } + addrPubKeyHash, err := dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(wif.PubKey()), chaincfg.SimNetParams(), dcrec.STEcdsaSecp256k1) + if err != nil { + panic(err) + } + fmt.Printf("DECRED_PK=%v\n", wif) + fmt.Printf("DECRED_ADDRESS=%v\n", addrPubKeyHash) +} From b77caf5f80cca49c85df5df2e5a726a9137fa0f1 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sat, 5 Jun 2021 08:06:14 +0100 Subject: [PATCH 11/35] Add dcrwallet client --- chain/decred/decred.go | 125 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 12 deletions(-) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index a14604c7..472b0203 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/hex" "encoding/json" "fmt" "io" @@ -14,7 +15,11 @@ import ( "os" "time" + "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrjson/v3" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" "github.com/renproject/pack" ) @@ -26,6 +31,9 @@ const ( // DefaultClientHost used by the Client. This should only be used for local // deployments of the multichain. DefaultClientHost = "https://127.0.0.1:19556" + // DefaultWalletHost used by dcrwallet. This should only be used for local + // deployments of the multichain. + DefaultWalletHost = "https://127.0.0.1:9110" // DefaultClientUser used by the Client. This is insecure, and should only // be used for local — or publicly accessible — deployments of the // multichain. @@ -48,6 +56,7 @@ type ClientOptions struct { TimeoutRetry time.Duration NoTLS bool Host string + WalletHost string User string Password string TLSSkipVerify bool @@ -55,6 +64,14 @@ type ClientOptions struct { ClientCert string ClientKey string RPCCert string + WalletRPCCert string +} + +type ClientSetting struct { + user string + password string + host string + httpClient http.Client } // DefaultClientOptions returns ClientOptions with the default settings. These @@ -73,6 +90,7 @@ func DefaultClientOptions() ClientOptions { TimeoutRetry: DefaultClientTimeoutRetry, NoTLS: DefaultClientNoTLS, Host: DefaultClientHost, + WalletHost: DefaultWalletHost, User: DefaultClientUser, Password: DefaultClientPassword, TLSSkipVerify: DefaultClientTLSSkipVerify, @@ -81,13 +99,31 @@ func DefaultClientOptions() ClientOptions { } } -// WithHost sets the URL of the Bitcoin node. +// WithHost sets the URL of the dcrd node. func (opts ClientOptions) WithHost(host string) ClientOptions { opts.Host = host return opts } -// WithUser sets the username that will be used to authenticate with the Bitcoin +// WithRPCCert sets the path of the dcrd RPC cert. +func (opts ClientOptions) WithRPCCert(certPath string) ClientOptions { + opts.RPCCert = certPath + return opts +} + +// WithWalletHost sets the URL of the dcrwallet node. +func (opts ClientOptions) WithWalletHost(host string) ClientOptions { + opts.WalletHost = host + return opts +} + +// WithHost sets the path of the dcrwallet RPC cert. +func (opts ClientOptions) WithWalletRPCCert(certPath string) ClientOptions { + opts.WalletRPCCert = certPath + return opts +} + +// WithUser sets the username that will be used to authenticate with the dcrd // node. func (opts ClientOptions) WithUser(user string) ClientOptions { opts.User = user @@ -95,15 +131,16 @@ func (opts ClientOptions) WithUser(user string) ClientOptions { } // WithPassword sets the password that will be used to authenticate with the -// Bitcoin node. +// dcrd node. func (opts ClientOptions) WithPassword(password string) ClientOptions { opts.Password = password return opts } type client struct { - opts ClientOptions - httpClient http.Client + opts ClientOptions + httpClient http.Client + walletClient http.Client } // NewClient returns a new Client. @@ -153,9 +190,56 @@ func NewClient(opts ClientOptions) *client { TLSClientConfig: tlsConfig, } + // Wallet Client. + walletClient := http.Client{} + walletClient.Timeout = opts.Timeout + + // Configure TLS if needed. + var tlsConf *tls.Config + if !opts.NoTLS { + tlsConf = &tls.Config{ + InsecureSkipVerify: opts.TLSSkipVerify, + } + if !opts.TLSSkipVerify && opts.AuthType == DefaultClientAuthTypeClientCert { + serverCAs := x509.NewCertPool() + serverCert, err := ioutil.ReadFile(opts.RPCCert) + if err != nil { + return nil + } + if !serverCAs.AppendCertsFromPEM(serverCert) { + return nil + } + keypair, err := tls.LoadX509KeyPair(opts.ClientCert, opts.ClientKey) + if err != nil { + return nil + } + + tlsConf.Certificates = []tls.Certificate{keypair} + tlsConf.RootCAs = serverCAs + + } + if !opts.TLSSkipVerify && opts.WalletRPCCert != "" { + pem, err := ioutil.ReadFile(opts.WalletRPCCert) + if err != nil { + return nil + } + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(pem); !ok { + return nil + } + tlsConf.RootCAs = pool + } + } + + walletClient.Transport = &http.Transport{ + TLSClientConfig: tlsConf, + } + return &client{ - opts: opts, - httpClient: httpClient, + opts: opts, + httpClient: httpClient, + walletClient: walletClient, } } @@ -191,32 +275,49 @@ func (client *client) send(ctx context.Context, resp *dcrjson.Response, method s return err } + var clSetting *ClientSetting + switch method { + case "getbestblock": + clSetting = &ClientSettings{ + user: client.opts.User, + password: client.opts.Password, + host: client.opts.Host, + httpClient: client.httpClient, + } + case "listunspent": + clSetting = &ClientSettings{ + user: client.opts.User, + password: client.opts.Password, + host: client.opts.WalletHost, + httpClient: client.walletClient, + } + } + return retry(ctx, client.opts.TimeoutRetry, func() error { // Create request and add basic authentication headers. The context is // not attached to the request, and instead we all each attempt to run // for the timeout duration, and we keep attempting until success, or // the context is done. - req, err := http.NewRequest("POST", client.opts.Host, bytes.NewBuffer(data)) + req, err := http.NewRequest("POST", clSetting.host, bytes.NewBuffer(data)) if err != nil { return fmt.Errorf("building http request: %v", err) } - req.SetBasicAuth(client.opts.User, client.opts.Password) + req.SetBasicAuth(clSetting.user, clSetting.password) // Send the request and decode the response. - res, err := client.httpClient.Do(req) + res, err := clSetting.httpClient.Do(req) if err != nil { return fmt.Errorf("sending http request: %v", err) } // Read the raw bytes and close the response. respBytes, err := ioutil.ReadAll(res.Body) - //fmt.Printf("Response: %+v \n", res) defer res.Body.Close() //if err := decodeResponse(resp, res.Body); err != nil { // return fmt.Errorf("decoding http response: %v", err) //} if err != nil { - err = fmt.Errorf("error reading json reply: %w", err) + err = fmt.Errorf("error reading json reply: %s", err) return err } From c6b0d3a9e66c58b39b97f6cf6d384b2790d9619d Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sat, 5 Jun 2021 08:09:35 +0100 Subject: [PATCH 12/35] Impliment decred client UnspentOutputs --- chain/decred/decred.go | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index 472b0203..cc38bcf2 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -268,6 +268,68 @@ func (client *client) LatestBlock(ctx context.Context) (pack.U64, error) { return pack.NewU64(uint64(result.Height)), nil } +// UnspentOutputs spendable by the given address. +func (client *client) UnspentOutputs(ctx context.Context, minConf, maxConf int64, addr address.Address) ([]utxo.Output, error) { + var outputs []utxo.Output + + //var resp int64 + var resp dcrjson.Response + if err := client.send(ctx, &resp, "listunspent", minConf, maxConf, []string{string(addr)}); err != nil { + return []utxo.Output{}, fmt.Errorf("bad \"listunspent\": %v", err) + } + + //outputs := make([]utxo.Output, len(resp.Result)) + type Result struct { + TxId string `json:"txid"` + VOut uint32 `json:"vout"` + Tree int `json:"tree"` + TxType int `json:"txtype"` + Address string `json:"address"` + Account string `json:"account"` + ScriptPubKey string `json:"scriptPubKey"` + Amount float64 `json:"amount"` + Confirmations int64 `json:"confirmations"` + Spendable bool `json"spendable"` + } + + var result []Result + + err := json.Unmarshal(resp.Result, &result) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad \"listunspent\": %v", err) + } + + for _, v := range result { + amount, err := dcrutil.NewAmount(v.Amount) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad amount: %v", err) + } + if amount < 0 { + return []utxo.Output{}, fmt.Errorf("bad amount: %v", amount) + } + pubKeyScript, err := hex.DecodeString(v.ScriptPubKey) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad pubkey script: %v", err) + } + txid, err := chainhash.NewHashFromStr(v.TxId) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad txid: %v", err) + } + o := utxo.Output{ + Outpoint: utxo.Outpoint{ + Hash: pack.NewBytes(txid[:]), + Index: pack.NewU32(v.VOut), + }, + Value: pack.NewU256FromU64(pack.NewU64(uint64(amount))), + PubKeyScript: pack.NewBytes(pubKeyScript), + } + + outputs = append(outputs, o) + } + + return outputs, nil +} + func (client *client) send(ctx context.Context, resp *dcrjson.Response, method string, params ...interface{}) error { // Encode the request. data, err := encodeRequest(method, params) From c7e62337e1587c23bfaa597812b8f06276af4b4b Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sat, 5 Jun 2021 09:16:16 +0100 Subject: [PATCH 13/35] Fix typo --- chain/decred/decred.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index cc38bcf2..09d44c92 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -340,14 +340,14 @@ func (client *client) send(ctx context.Context, resp *dcrjson.Response, method s var clSetting *ClientSetting switch method { case "getbestblock": - clSetting = &ClientSettings{ + clSetting = &ClientSetting{ user: client.opts.User, password: client.opts.Password, host: client.opts.Host, httpClient: client.httpClient, } case "listunspent": - clSetting = &ClientSettings{ + clSetting = &ClientSetting{ user: client.opts.User, password: client.opts.Password, host: client.opts.WalletHost, From cd426090b454ba4dbf8a49e31fdb82b8d2f13ead Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sat, 5 Jun 2021 09:53:16 +0100 Subject: [PATCH 14/35] Impliment decred client Output --- chain/decred/decred.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index 09d44c92..ea0def66 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -330,6 +330,46 @@ func (client *client) UnspentOutputs(ctx context.Context, minConf, maxConf int64 return outputs, nil } +// Output associated with an outpoint, and its number of confirmations. +func (client *client) Output(ctx context.Context, outpoint utxo.Outpoint) (utxo.Output, pack.U64, error) { + + //var resp int64 + var resp dcrjson.Response + hash := chainhash.Hash{} + copy(hash[:], outpoint.Hash) + if err := client.send(ctx, &resp, "getrawtransaction", hash.String(), 1); err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad \"getrawtransaction\": %v", err) + } + + result := types.TxRawResult{} + err := json.Unmarshal(resp.Result, &result) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad \"getrawtransaction\": %v", err) + } + + if outpoint.Index.Uint32() >= uint32(len(result.Vout)) { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad index: %v is out of range", outpoint.Index) + } + vout := result.Vout[outpoint.Index.Uint32()] + amount, err := dcrutil.NewAmount(vout.Value) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad amount: %v", err) + } + if amount < 0 { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad amount: %v", amount) + } + pubKeyScript, err := hex.DecodeString(vout.ScriptPubKey.Hex) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad pubkey script: %v", err) + } + output := utxo.Output{ + Outpoint: outpoint, + Value: pack.NewU256FromU64(pack.NewU64(uint64(amount))), + PubKeyScript: pack.NewBytes(pubKeyScript), + } + return output, pack.NewU64(uint64(result.Confirmations)), nil +} + func (client *client) send(ctx context.Context, resp *dcrjson.Response, method string, params ...interface{}) error { // Encode the request. data, err := encodeRequest(method, params) From f70fc0604ac757e9c8d147f88c4e95194853dfaf Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Sat, 5 Jun 2021 10:14:47 +0100 Subject: [PATCH 15/35] Impliment decred client UnspentOutput --- chain/decred/decred.go | 43 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index ea0def66..30d3d2f2 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -370,6 +370,47 @@ func (client *client) Output(ctx context.Context, outpoint utxo.Outpoint) (utxo. return output, pack.NewU64(uint64(result.Confirmations)), nil } +// UnspentOutput returns the unspent transaction output identified by the +// given outpoint. It also returns the number of confirmations for the +// output. If the output cannot be found before the context is done, the +// output is invalid, or the output has been spent, then an error should be +// returned. +func (client *client) UnspentOutput(ctx context.Context, outpoint utxo.Outpoint) (utxo.Output, pack.U64, error) { + var resp dcrjson.Response + hash := chainhash.Hash{} + copy(hash[:], outpoint.Hash) + if err := client.send(ctx, &resp, "gettxout", hash.String(), outpoint.Index.Uint32()); err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad \"gettxout\": %v", err) + } + + result := types.GetTxOutResult{} + err := json.Unmarshal(resp.Result, &result) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad \"gettxout\": %v", err) + } + + amount, err := dcrutil.NewAmount(result.Value) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad amount: %v", err) + } + if amount < 0 { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad amount: %v", amount) + } + if result.Confirmations < 0 { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad confirmations: %v", result.Confirmations) + } + pubKeyScript, err := hex.DecodeString(result.ScriptPubKey.Hex) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad pubkey script: %v", err) + } + output := utxo.Output{ + Outpoint: outpoint, + Value: pack.NewU256FromU64(pack.NewU64(uint64(amount))), + PubKeyScript: pack.NewBytes(pubKeyScript), + } + return output, pack.NewU64(uint64(result.Confirmations)), nil +} + func (client *client) send(ctx context.Context, resp *dcrjson.Response, method string, params ...interface{}) error { // Encode the request. data, err := encodeRequest(method, params) @@ -379,7 +420,7 @@ func (client *client) send(ctx context.Context, resp *dcrjson.Response, method s var clSetting *ClientSetting switch method { - case "getbestblock": + case "getbestblock", "getrawtransaction", "gettxout": clSetting = &ClientSetting{ user: client.opts.User, password: client.opts.Password, From c111d6e613a80e2b75e6ead44c3aa31e5fd41edc Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 10:58:17 +0100 Subject: [PATCH 16/35] Impliment Decred BuildTx --- chain/decred/utxo.go | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 chain/decred/utxo.go diff --git a/chain/decred/utxo.go b/chain/decred/utxo.go new file mode 100644 index 00000000..00f03afe --- /dev/null +++ b/chain/decred/utxo.go @@ -0,0 +1,85 @@ +package decred + +import ( + "bytes" + "errors" + "fmt" + "math/big" + + "github.com/btcsuite/btcd/btcec" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/decred/dcrd/txscript/v3" + "github.com/decred/dcrd/wire" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/pack" +) + +// The TxBuilder is an implementation of a UTXO-compatible transaction builder +// for Bitcoin. +type TxBuilder struct { + params *chaincfg.Params +} + +// NewTxBuilder returns a transaction builder that builds UTXO-compatible +// Bitcoin transactions for the given chain configuration (this means that it +// can be used for regnet, testnet, and mainnet, but also for networks that are +// minimally modified forks of the Bitcoin network). +func NewTxBuilder(params *chaincfg.Params) TxBuilder { + return TxBuilder{params: params} +} + +// BuildTx returns a Decred transaction that consumes funds from the given +// inputs, and sends them to the given recipients. The difference in the sum +// value of the inputs and the sum value of the recipients is paid as a fee to +// the Bitcoin network. This fee must be calculated independently of this +// function. Outputs produced for recipients will use P2PKH, P2SH, P2WPKH, or +// P2WSH scripts as the pubkey script, based on the format of the recipient +// address. +func (txBuilder TxBuilder) BuildTx(inputs []utxo.Input, recipients []utxo.Recipient) (utxo.Tx, error) { + msgTx := wire.NewMsgTx() + + // Inputs + for _, input := range inputs { + hash := chainhash.Hash{} + copy(hash[:], input.Hash) + index := input.Index.Uint32() + amt, err := dcrutil.NewAmount(1) + if err != nil { + return nil, err + } + prevOutV := int64(amt) + msgTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&hash, index, wire.TxTreeRegular), prevOutV, []byte{})) + } + + // Outputs + for _, recipient := range recipients { + addr, err := dcrutil.DecodeAddress(string(recipient.To), txBuilder.params) + if err != nil { + return nil, err + } + // Ensure the address is one of the supported types. + switch addr.(type) { + case *dcrutil.AddressPubKeyHash: + fmt.Printf("Address Type: %+v \n", "PubKeyHash") + case *dcrutil.AddressScriptHash: + fmt.Printf("Address Type: %+v \n", "ScriptHash") + default: + return nil, errors.New("Invalid address type") + } + + script, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, err + } + + value := recipient.Value.Int().Int64() + if value < 0 { + return nil, fmt.Errorf("expected value >= 0, got value %v", value) + } + msgTx.AddTxOut(wire.NewTxOut(value, script)) + } + + return &Tx{inputs: inputs, recipients: recipients, msgTx: msgTx, signed: false}, nil +} From e000d46363172df0f2f048e8769a220f5d648b87 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 11:06:18 +0100 Subject: [PATCH 17/35] Impliment Decred utxo.Inputs and utxo.Outputs --- chain/decred/utxo.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/chain/decred/utxo.go b/chain/decred/utxo.go index 00f03afe..d6715229 100644 --- a/chain/decred/utxo.go +++ b/chain/decred/utxo.go @@ -62,9 +62,7 @@ func (txBuilder TxBuilder) BuildTx(inputs []utxo.Input, recipients []utxo.Recipi // Ensure the address is one of the supported types. switch addr.(type) { case *dcrutil.AddressPubKeyHash: - fmt.Printf("Address Type: %+v \n", "PubKeyHash") case *dcrutil.AddressScriptHash: - fmt.Printf("Address Type: %+v \n", "ScriptHash") default: return nil, errors.New("Invalid address type") } @@ -83,3 +81,39 @@ func (txBuilder TxBuilder) BuildTx(inputs []utxo.Input, recipients []utxo.Recipi return &Tx{inputs: inputs, recipients: recipients, msgTx: msgTx, signed: false}, nil } + +// Tx represents a simple Decred transaction +type Tx struct { + inputs []utxo.Input + recipients []utxo.Recipient + + msgTx *wire.MsgTx + + signed bool +} + +// Inputs returns the UTXO inputs in the underlying transaction. +func (tx *Tx) Inputs() ([]utxo.Input, error) { + return tx.inputs, nil +} + +// Outputs returns the UTXO outputs in the underlying transaction. +func (tx *Tx) Outputs() ([]utxo.Output, error) { + hash, err := tx.Hash() + if err != nil { + return nil, fmt.Errorf("bad hash: %v", err) + } + outputs := make([]utxo.Output, len(tx.msgTx.TxOut)) + for i := range outputs { + outputs[i].Outpoint = utxo.Outpoint{ + Hash: hash, + Index: pack.NewU32(uint32(i)), + } + outputs[i].PubKeyScript = pack.Bytes(tx.msgTx.TxOut[i].PkScript) + if tx.msgTx.TxOut[i].Value < 0 { + return nil, fmt.Errorf("bad output %v: value is less than zero", i) + } + outputs[i].Value = pack.NewU256FromU64(pack.NewU64(uint64(tx.msgTx.TxOut[i].Value))) + } + return outputs, nil +} From baae53bc616051b54374ec228b05e6b28c558f5f Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 11:11:39 +0100 Subject: [PATCH 18/35] Impliment Decred utxo.sighashes --- chain/decred/utxo.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/chain/decred/utxo.go b/chain/decred/utxo.go index d6715229..e4cdfbd4 100644 --- a/chain/decred/utxo.go +++ b/chain/decred/utxo.go @@ -117,3 +117,35 @@ func (tx *Tx) Outputs() ([]utxo.Output, error) { } return outputs, nil } + +// Sighashes returns the digests that must be signed before the transaction +// can be submitted by the client. +func (tx *Tx) Sighashes() ([]pack.Bytes32, error) { + sighashes := make([]pack.Bytes32, len(tx.inputs)) + + for i, txin := range tx.inputs { + pubKeyScript := txin.PubKeyScript + sigScript := txin.SigScript + value := txin.Value.Int().Int64() + if value < 0 { + return []pack.Bytes32{}, fmt.Errorf("expected value >= 0, got value %v", value) + } + + var hash []byte + var err error + if sigScript == nil { + hash, err = txscript.CalcSignatureHash(pubKeyScript, txscript.SigHashAll, tx.msgTx, i, nil) + } else { + hash, err = txscript.CalcSignatureHash(sigScript, txscript.SigHashAll, tx.msgTx, i, nil) + } + if err != nil { + return []pack.Bytes32{}, err + } + + sighash := [32]byte{} + copy(sighash[:], hash) + sighashes[i] = pack.NewBytes32(sighash) + } + + return sighashes, nil +} From 4a8d6e17fb9363059a438233e640eb06ceb8cd0c Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 11:13:41 +0100 Subject: [PATCH 19/35] Impliment Decred utxo.sign --- chain/decred/utxo.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/chain/decred/utxo.go b/chain/decred/utxo.go index e4cdfbd4..eb0e9bf0 100644 --- a/chain/decred/utxo.go +++ b/chain/decred/utxo.go @@ -149,3 +149,41 @@ func (tx *Tx) Sighashes() ([]pack.Bytes32, error) { return sighashes, nil } + +// Sign consumes a list of signatures, and adds them to the list of UTXOs in +// the underlying transactions. +func (tx *Tx) Sign(signatures []pack.Bytes65, pubKey pack.Bytes) error { + if tx.signed { + return fmt.Errorf("already signed") + } + if len(signatures) != len(tx.msgTx.TxIn) { + return fmt.Errorf("expected %v signatures, got %v signatures", len(tx.msgTx.TxIn), len(signatures)) + } + + for i, rsv := range signatures { + var err error + + // Decode the signature and the pubkey script. + r := new(big.Int).SetBytes(rsv[:32]) + s := new(big.Int).SetBytes(rsv[32:64]) + signature := btcec.Signature{ + R: r, + S: s, + } + sigScript := tx.inputs[i].SigScript + + builder := txscript.NewScriptBuilder() + builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll))) + builder.AddData(pubKey) + if sigScript != nil { + builder.AddData(sigScript) + } + tx.msgTx.TxIn[i].SignatureScript, err = builder.Script() + if err != nil { + return err + } + } + + tx.signed = true + return nil +} From de1a6f9e89d8b0aa8d545e8b3b03c999e92c6c7e Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 11:15:06 +0100 Subject: [PATCH 20/35] Impliment Decred utxo.serialize --- chain/decred/utxo.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/chain/decred/utxo.go b/chain/decred/utxo.go index eb0e9bf0..157889da 100644 --- a/chain/decred/utxo.go +++ b/chain/decred/utxo.go @@ -187,3 +187,12 @@ func (tx *Tx) Sign(signatures []pack.Bytes65, pubKey pack.Bytes) error { tx.signed = true return nil } + +// Serialize serializes the UTXO transaction to bytes +func (tx *Tx) Serialize() (pack.Bytes, error) { + buf := new(bytes.Buffer) + if err := tx.msgTx.Serialize(buf); err != nil { + return pack.Bytes{}, err + } + return pack.NewBytes(buf.Bytes()), nil +} From 67e7694330117072fc807a4c8dbefa66221dd213 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 11:40:43 +0100 Subject: [PATCH 21/35] impliment Decred utxo.hash --- chain/decred/utxo.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/chain/decred/utxo.go b/chain/decred/utxo.go index 157889da..b1196b53 100644 --- a/chain/decred/utxo.go +++ b/chain/decred/utxo.go @@ -92,6 +92,12 @@ type Tx struct { signed bool } +// Hash returns the transaction hash of the given underlying transaction. +func (tx *Tx) Hash() (pack.Bytes, error) { + txhash := tx.msgTx.TxHash() + return pack.NewBytes(txhash[:]), nil +} + // Inputs returns the UTXO inputs in the underlying transaction. func (tx *Tx) Inputs() ([]utxo.Input, error) { return tx.inputs, nil From 343155488bd9713e58e61b7e5f019668fa1f70f9 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 12:43:06 +0100 Subject: [PATCH 22/35] Impliment decred client SubmitTx --- chain/decred/decred.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index 30d3d2f2..e738b080 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -18,6 +18,7 @@ import ( "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrjson/v3" "github.com/decred/dcrd/dcrutil/v3" + "github.com/decred/dcrd/rpc/jsonrpc/types/v2" "github.com/renproject/multichain/api/address" "github.com/renproject/multichain/api/utxo" "github.com/renproject/pack" @@ -420,14 +421,14 @@ func (client *client) send(ctx context.Context, resp *dcrjson.Response, method s var clSetting *ClientSetting switch method { - case "getbestblock", "getrawtransaction", "gettxout": + case "getbestblock", "getrawtransaction", "gettxout", "sendrawtransaction": clSetting = &ClientSetting{ user: client.opts.User, password: client.opts.Password, host: client.opts.Host, httpClient: client.httpClient, } - case "listunspent": + case "listunspent", "gettransaction": clSetting = &ClientSetting{ user: client.opts.User, password: client.opts.Password, @@ -546,3 +547,17 @@ func retry(ctx context.Context, dur time.Duration, f func() error) error { } return nil } + +// SubmitTx to the Decred network. +func (client *client) SubmitTx(ctx context.Context, tx utxo.Tx) error { + serial, err := tx.Serialize() + if err != nil { + return fmt.Errorf("bad tx: %v", err) + } + var resp dcrjson.Response + if err := client.send(ctx, &resp, "sendrawtransaction", hex.EncodeToString(serial), true); err != nil { + return fmt.Errorf("bad \"sendrawtransaction\": %v", err) + } + fmt.Printf("Response: %+v \n", resp) + return nil +} From 226abb5709dfa5e5bb0836e826c32dec23f62b24 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 13:30:16 +0100 Subject: [PATCH 23/35] Implement Decred client estimate fee --- chain/decred/decred.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index e738b080..850cd27e 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -561,3 +561,34 @@ func (client *client) SubmitTx(ctx context.Context, tx utxo.Tx) error { fmt.Printf("Response: %+v \n", resp) return nil } + +func (client *client) EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error) { + resp := btcjson.EstimateSmartFeeResult{} + + if err := client.send(ctx, &resp, "estimatesmartfee", numBlocks); err != nil { + return 0.0, fmt.Errorf("estimating smart fee: %v", err) + } + + if resp.Errors != nil && len(resp.Errors) > 0 { + return 0.0, fmt.Errorf("estimating smart fee: %v", resp.Errors[0]) + } + + return *resp.FeeRate, nil +} + +func (client *client) EstimateFeeLegacy(ctx context.Context, numBlocks int64) (float64, error) { + var resp float64 + + switch numBlocks { + case int64(0): + if err := client.send(ctx, &resp, "estimatefee"); err != nil { + return 0.0, fmt.Errorf("estimating fee: %v", err) + } + default: + if err := client.send(ctx, &resp, "estimatefee", numBlocks); err != nil { + return 0.0, fmt.Errorf("estimating fee: %v", err) + } + } + + return resp, nil +} From a52ebf85d163f2e4693202d0920422125ebd1250 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 13:35:51 +0100 Subject: [PATCH 24/35] Add Decred DEPRIV_KEY/ADDRESS Run Daemons in screen/ sinmet minning --- infra/decred/run.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/infra/decred/run.sh b/infra/decred/run.sh index 3c00acac..11940344 100644 --- a/infra/decred/run.sh +++ b/infra/decred/run.sh @@ -1,8 +1,21 @@ #!/bin/bash +ADDRESS=SsaGEEZu2L8x93qvKzzahtzQ7yzkec3i8wL +PRIV_KEY=PsUQEpYDXVwphd9xNXUMj63LyxSWPTor3RDgfw9DMdH9tDkJaosyp # Start -/decred/dcrd --simnet +screen -dm /decred/dcrd --simnet --miningaddr=$ADDRESS +screen -dm /decred/dcrwallet --simnet --createtemp sleep 2 # Print setup -echo "DCRD STARTED" +echo "DECRED_ADDRESS=$ADDRESS" + +/decred/dcrctl --simnet generate 101 + +# Simulate mining +while : +do + /decred/dcrctl --simnet generate 10 + sleep 5 +done + From af3eec3b99bfa210bc3f0ad4ebaba3aaa964bb8d Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Wed, 9 Jun 2021 13:40:06 +0100 Subject: [PATCH 25/35] Decred tests --- chain/decred/decred_suite_test.go | 13 ++++ chain/decred/decred_test.go | 111 ++++++++++++++++++++++++++++++ chain/decred/utxo_test.go | 1 + 3 files changed, 125 insertions(+) create mode 100644 chain/decred/decred_suite_test.go create mode 100644 chain/decred/decred_test.go create mode 100644 chain/decred/utxo_test.go diff --git a/chain/decred/decred_suite_test.go b/chain/decred/decred_suite_test.go new file mode 100644 index 00000000..0424de42 --- /dev/null +++ b/chain/decred/decred_suite_test.go @@ -0,0 +1,13 @@ +package decred_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestDecred(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Decred Suite") +} diff --git a/chain/decred/decred_test.go b/chain/decred/decred_test.go new file mode 100644 index 00000000..dafe5ba0 --- /dev/null +++ b/chain/decred/decred_test.go @@ -0,0 +1,111 @@ +package decred_test + +import ( + "context" + "log" + "os" + "reflect" + + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrec" + "github.com/decred/dcrd/dcrec/secp256k1/v3" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/renproject/id" + "github.com/renproject/pack" + + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/multichain/chain/decred" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Decred", func() { + Context("when submitting transactions", func() { + Context("when sending DCR to multiple addresses", func() { + It("should work", func() { + // Load private key, and assume that the associated address has + // funds to spend. You can do this by setting DECRED_PK to the + // value specified in the `./multichaindeploy/.env` file. + pkEnv := os.Getenv("DECRED_PK") + if pkEnv == "" { + panic("DECRED_PK is undefined") + } + simNetPrivKeyID := [2]byte{0x23, 0x07} + wif, err := dcrutil.DecodeWIF(pkEnv, simNetPrivKeyID) + Expect(err).ToNot(HaveOccurred()) + + // PKH + addrPubKeyHash, err := dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(wif.PubKey()), chaincfg.SimNetParams(), dcrec.STEcdsaSecp256k1) + Expect(err).ToNot(HaveOccurred()) + + log.Printf("PKH %v", addrPubKeyHash.String()) + + // Setup the client and load the unspent transaction outputs. + client := decred.NewClient(decred.DefaultClientOptions()) + outputs, err := client.UnspentOutputs(context.Background(), 0, 999999999, address.Address(addrPubKeyHash.String())) + Expect(err).ToNot(HaveOccurred()) + Expect(len(outputs)).To(BeNumerically(">", 0)) + output := outputs[0] + + // Check that we can load the output and that it is equal. + // Otherwise, something strange is happening with the RPC + // client. + output2, _, err := client.Output(context.Background(), output.Outpoint) + Expect(err).ToNot(HaveOccurred()) + Expect(reflect.DeepEqual(output, output2)).To(BeTrue()) + output2, _, err = client.UnspentOutput(context.Background(), output.Outpoint) + Expect(err).ToNot(HaveOccurred()) + Expect(reflect.DeepEqual(output, output2)).To(BeTrue()) + + // Build the transaction by consuming the outputs and spending + // them to a set of recipients. + inputs := []utxo.Input{ + {Output: utxo.Output{ + Outpoint: utxo.Outpoint{ + Hash: output.Outpoint.Hash[:], + Index: output.Outpoint.Index, + }, + PubKeyScript: output.PubKeyScript, + Value: output.Value, + }}, + } + recipients := []utxo.Recipient{ + { + To: address.Address(addrPubKeyHash.String()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 3)), + }, + } + tx, err := decred.NewTxBuilder(chaincfg.SimNetParams()).BuildTx(inputs, recipients) + Expect(err).ToNot(HaveOccurred()) + + // Get the digests that need signing from the transaction, and + // sign them. In production, this would be done using the RZL + // MPC algorithm, but for the purposes of this test, using an + // explicit privkey is ok. + sighashes, err := tx.Sighashes() + signatures := make([]pack.Bytes65, len(sighashes)) + Expect(err).ToNot(HaveOccurred()) + for i := range sighashes { + hash := id.Hash(sighashes[i]) + priv := secp256k1.PrivKeyFromBytes(wif.PrivKey()) + privk := priv.ToECDSA() + privKey := (*id.PrivKey)(privk) + signature, err := privKey.Sign(&hash) + Expect(err).ToNot(HaveOccurred()) + signatures[i] = pack.NewBytes65(signature) + } + Expect(tx.Sign(signatures, pack.NewBytes(wif.PubKey()))).To(Succeed()) + + // Submit the transaction to the dcrd node. Again, this + // should be running a la `./multichaindeploy`. + txHash, err := tx.Hash() + Expect(err).ToNot(HaveOccurred()) + err = client.SubmitTx(context.Background(), tx) + Expect(err).ToNot(HaveOccurred()) + log.Printf("TXID %v", txHash) + }) + }) + }) +}) diff --git a/chain/decred/utxo_test.go b/chain/decred/utxo_test.go new file mode 100644 index 00000000..3d06df08 --- /dev/null +++ b/chain/decred/utxo_test.go @@ -0,0 +1 @@ +package decred_test \ No newline at end of file From b798b913478b93272b27e25628ed2451a54e5000 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Thu, 17 Jun 2021 12:55:39 +0100 Subject: [PATCH 26/35] Modify dcrd port/add dcrwallet port --- infra/docker-compose.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index 787ee638..73e2a5a8 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -57,7 +57,8 @@ services: build: context: ./decred ports: - - "0.0.0.0:19555:19555" + - "0.0.0.0:19556:19556" + - "0.0.0.0:19557:19557" entrypoint: - "./root/run.sh" - "${DECRED_ADDRESS}" From 0c97d86863662120c1c91b9f0a6104ef60214363 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Thu, 17 Jun 2021 12:58:24 +0100 Subject: [PATCH 27/35] Docker image: expose dcrwallet port --- infra/decred/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/decred/Dockerfile b/infra/decred/Dockerfile index 4ab4ca4b..31414f0c 100644 --- a/infra/decred/Dockerfile +++ b/infra/decred/Dockerfile @@ -34,6 +34,6 @@ RUN chmod +x /root/run.sh RUN cd /root/ && rm -rf /tmp/* # PEER & RPC PORTS -EXPOSE 19556 18555 +EXPOSE 19556 18555 19557 ENTRYPOINT ["./root/run.sh"] From a9c47b483ae46db587b5ffe3f9c0cbb8cb3915fa Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Thu, 17 Jun 2021 13:00:46 +0100 Subject: [PATCH 28/35] Modify Default DCRWalletHost/Error on failed NewClient/modify default certfile path --- chain/decred/decred.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index 850cd27e..3f1d3b72 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -34,7 +34,7 @@ const ( DefaultClientHost = "https://127.0.0.1:19556" // DefaultWalletHost used by dcrwallet. This should only be used for local // deployments of the multichain. - DefaultWalletHost = "https://127.0.0.1:9110" + DefaultWalletHost = "https://127.0.0.1:19557" // DefaultClientUser used by the Client. This is insecure, and should only // be used for local — or publicly accessible — deployments of the // multichain. @@ -84,7 +84,7 @@ func DefaultClientOptions() ClientOptions { if err != nil { log.Fatal(err) } - defaultCertFile := dir + "/decred/" + DefaultClientCert + defaultCertFile := dir + "/../../infra/decred/" + DefaultClientCert return ClientOptions{ Timeout: DefaultClientTimeout, @@ -97,6 +97,7 @@ func DefaultClientOptions() ClientOptions { TLSSkipVerify: DefaultClientTLSSkipVerify, AuthType: DefaultClientAuthTypeBasic, RPCCert: defaultCertFile, + WalletRPCCert: defaultCertFile, } } @@ -159,13 +160,16 @@ func NewClient(opts ClientOptions) *client { serverCAs := x509.NewCertPool() serverCert, err := ioutil.ReadFile(opts.RPCCert) if err != nil { + fmt.Printf("Error: %s \n", err) return nil } if !serverCAs.AppendCertsFromPEM(serverCert) { + fmt.Println("Cannot Append Sever cert") return nil } keypair, err := tls.LoadX509KeyPair(opts.ClientCert, opts.ClientKey) if err != nil { + fmt.Printf("Error: %s \n", err) return nil } @@ -176,6 +180,7 @@ func NewClient(opts ClientOptions) *client { if !opts.TLSSkipVerify && opts.RPCCert != "" { pem, err := ioutil.ReadFile(opts.RPCCert) if err != nil { + fmt.Printf("Error: %s \n", err) return nil } @@ -203,8 +208,9 @@ func NewClient(opts ClientOptions) *client { } if !opts.TLSSkipVerify && opts.AuthType == DefaultClientAuthTypeClientCert { serverCAs := x509.NewCertPool() - serverCert, err := ioutil.ReadFile(opts.RPCCert) + serverCert, err := ioutil.ReadFile(opts.WalletRPCCert) if err != nil { + fmt.Printf("Error: %s \n", err) return nil } if !serverCAs.AppendCertsFromPEM(serverCert) { @@ -212,6 +218,7 @@ func NewClient(opts ClientOptions) *client { } keypair, err := tls.LoadX509KeyPair(opts.ClientCert, opts.ClientKey) if err != nil { + fmt.Printf("Error: %s \n", err) return nil } @@ -222,6 +229,7 @@ func NewClient(opts ClientOptions) *client { if !opts.TLSSkipVerify && opts.WalletRPCCert != "" { pem, err := ioutil.ReadFile(opts.WalletRPCCert) if err != nil { + fmt.Printf("Error: %s \n", err) return nil } @@ -300,6 +308,7 @@ func (client *client) UnspentOutputs(ctx context.Context, minConf, maxConf int64 return []utxo.Output{}, fmt.Errorf("bad \"listunspent\": %v", err) } + //fmt.Printf("Unspent Output: %+v \n", resp) for _, v := range result { amount, err := dcrutil.NewAmount(v.Amount) if err != nil { @@ -558,11 +567,11 @@ func (client *client) SubmitTx(ctx context.Context, tx utxo.Tx) error { if err := client.send(ctx, &resp, "sendrawtransaction", hex.EncodeToString(serial), true); err != nil { return fmt.Errorf("bad \"sendrawtransaction\": %v", err) } - fmt.Printf("Response: %+v \n", resp) + fmt.Printf("Response: %+v \n", string(resp.Result)) return nil } -func (client *client) EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error) { +/*func (client *client) EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error) { resp := btcjson.EstimateSmartFeeResult{} if err := client.send(ctx, &resp, "estimatesmartfee", numBlocks); err != nil { @@ -592,3 +601,4 @@ func (client *client) EstimateFeeLegacy(ctx context.Context, numBlocks int64) (f return resp, nil } +*/ \ No newline at end of file From 34b0aac41ac4e9806bac87a9f74b7cf9a3e790d5 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Fri, 18 Jun 2021 14:43:08 +0100 Subject: [PATCH 29/35] Fetch tx confirmations/test tx confirmation. --- chain/decred/decred.go | 30 +++++++++++++++++++++++++++++- chain/decred/decred_test.go | 23 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index 3f1d3b72..3745911b 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -601,4 +601,32 @@ func (client *client) EstimateFeeLegacy(ctx context.Context, numBlocks int64) (f return resp, nil } -*/ \ No newline at end of file +*/ + +// Confirmations of a transaction in the decred network. +func (client *client) Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error) { + var resp dcrjson.Response + + size := len(txHash) + txHashReversed := make([]byte, size) + copy(txHashReversed[:], txHash[:]) + for i := 0; i < size/2; i++ { + txHashReversed[i], txHashReversed[size-1-i] = txHashReversed[size-1-i], txHashReversed[i] + } + + if err := client.send(ctx, &resp, "gettransaction", hex.EncodeToString(txHashReversed)); err != nil { + return 0, fmt.Errorf("bad \"gettransaction\": %v", err) + } + + result := types.TxRawResult{} + err := json.Unmarshal(resp.Result, &result) + if err != nil { + return 0, fmt.Errorf("bad \"gettransaction\": %v", err) + } + + confirmations := result.Confirmations + if confirmations < 0 { + confirmations = 0 + } + return confirmations, nil +} diff --git a/chain/decred/decred_test.go b/chain/decred/decred_test.go index dafe5ba0..3fdd0403 100644 --- a/chain/decred/decred_test.go +++ b/chain/decred/decred_test.go @@ -5,6 +5,7 @@ import ( "log" "os" "reflect" + "time" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/dcrec" @@ -105,6 +106,28 @@ var _ = Describe("Decred", func() { err = client.SubmitTx(context.Background(), tx) Expect(err).ToNot(HaveOccurred()) log.Printf("TXID %v", txHash) + + for { + // Loop until the transaction has at least a few + // confirmations. This implies that the transaction is + // definitely valid, and the test has passed. We were + // successfully able to use the multichain to construct and + // submit a Bitcoin transaction! + confs, err := client.Confirmations(context.Background(), txHash) + Expect(err).ToNot(HaveOccurred()) + log.Printf(" %v/3 confirmations", confs) + if confs >= 1 { + break + } + time.Sleep(10 * time.Second) + } + + // Check that we can load the output and that it is equal. + // Otherwise, something strange is happening with the RPC + // client. + output2, _, err = client.Output(context.Background(), output.Outpoint) + Expect(err).ToNot(HaveOccurred()) + Expect(reflect.DeepEqual(output, output2)).To(BeTrue()) }) }) }) From 8d484f4036e54a9de00b05c870eaf2dc44d27f59 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Mon, 21 Jun 2021 08:19:21 +0100 Subject: [PATCH 30/35] Refactor decred estimatefee/estimatesmartfee API --- chain/decred/decred.go | 44 +++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index 3745911b..c6883b31 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -13,6 +13,7 @@ import ( "log" "net/http" "os" + "strconv" "time" "github.com/decred/dcrd/chaincfg/chainhash" @@ -430,7 +431,8 @@ func (client *client) send(ctx context.Context, resp *dcrjson.Response, method s var clSetting *ClientSetting switch method { - case "getbestblock", "getrawtransaction", "gettxout", "sendrawtransaction": + case "getbestblock", "getrawtransaction", "gettxout", + "sendrawtransaction", "estimatesmartfee", "estimatefee": clSetting = &ClientSetting{ user: client.opts.User, password: client.opts.Password, @@ -571,37 +573,43 @@ func (client *client) SubmitTx(ctx context.Context, tx utxo.Tx) error { return nil } -/*func (client *client) EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error) { - resp := btcjson.EstimateSmartFeeResult{} +func (client *client) EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error) { + resp := dcrjson.Response{} if err := client.send(ctx, &resp, "estimatesmartfee", numBlocks); err != nil { return 0.0, fmt.Errorf("estimating smart fee: %v", err) } - if resp.Errors != nil && len(resp.Errors) > 0 { - return 0.0, fmt.Errorf("estimating smart fee: %v", resp.Errors[0]) + if resp.Error != nil { + return 0.0, resp.Error } - return *resp.FeeRate, nil + result, err := strconv.ParseFloat(string(resp.Result), 10) + if err != nil { + return 0.0, err + } + + return result, nil } func (client *client) EstimateFeeLegacy(ctx context.Context, numBlocks int64) (float64, error) { - var resp float64 + resp := dcrjson.Response{} - switch numBlocks { - case int64(0): - if err := client.send(ctx, &resp, "estimatefee"); err != nil { - return 0.0, fmt.Errorf("estimating fee: %v", err) - } - default: - if err := client.send(ctx, &resp, "estimatefee", numBlocks); err != nil { - return 0.0, fmt.Errorf("estimating fee: %v", err) - } + if err := client.send(ctx, &resp, "estimatefee", numBlocks); err != nil { + return 0.0, fmt.Errorf("estimating fee: %v", err) + } + + if resp.Error != nil { + return 0.0, resp.Error + } + + result, err := strconv.ParseFloat(string(resp.Result), 10) + if err != nil { + return 0.0, err } - return resp, nil + return result, nil } -*/ // Confirmations of a transaction in the decred network. func (client *client) Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error) { From 5d5a8a3f83f6fab38bc5dec631ff802c73e9f7d7 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Mon, 21 Jun 2021 10:02:42 +0100 Subject: [PATCH 31/35] Implement decred gas API/gas test --- chain/decred/gas.go | 57 ++++++++++++++++++++++++++++++++++++++++ chain/decred/gas_test.go | 52 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 chain/decred/gas.go create mode 100644 chain/decred/gas_test.go diff --git a/chain/decred/gas.go b/chain/decred/gas.go new file mode 100644 index 00000000..3b652755 --- /dev/null +++ b/chain/decred/gas.go @@ -0,0 +1,57 @@ +package decred + +import ( + "context" + "fmt" + "math" + + "github.com/renproject/pack" +) + +const ( + dcrToAtom = 1e8 + kilobyteToByte = 1024 +) + +// A GasEstimator returns the SATs-per-byte that is needed in order to confirm +// transactions with an estimated maximum delay of one block. In distributed +// networks that collectively build, sign, and submit transactions, it is +// important that all nodes in the network have reached consensus on the +// SATs-per-byte. +type GasEstimator struct { + client Client + numBlocks int64 + fallbackGas pack.U256 +} + +// NewGasEstimator returns a simple gas estimator that always returns the given +// number of SATs-per-byte. +func NewGasEstimator(client Client, numBlocks int64, fallbackGas pack.U256) GasEstimator { + return GasEstimator{ + client: client, + numBlocks: numBlocks, + fallbackGas: fallbackGas, + } +} + +// EstimateGas returns the number of SATs-per-byte (for both price and cap) that +// is needed in order to confirm transactions with an estimated maximum delay of +// `numBlocks` block. It is the responsibility of the caller to know the number +// of bytes in their transaction. This method calls the `estimatesmartfee` RPC +// call to the node, which based on a conservative (considering longer history) +// strategy returns the estimated DCR per kilobyte of data in the transaction. +// An error will be returned if the node hasn't observed enough blocks +// to make an estimate for the provided target `numBlocks`. +func (gasEstimator GasEstimator) EstimateGas(ctx context.Context) (pack.U256, pack.U256, error) { + feeRate, err := gasEstimator.client.EstimateFeeLegacy(ctx, gasEstimator.numBlocks) + if err != nil { + return gasEstimator.fallbackGas, gasEstimator.fallbackGas, err + } + + if feeRate <= 0.0 { + return gasEstimator.fallbackGas, gasEstimator.fallbackGas, fmt.Errorf("invalid fee rate: %v", feeRate) + } + + satsPerByte := uint64(math.Ceil(feeRate * dcrToAtom / kilobyteToByte)) + return pack.NewU256FromUint64(satsPerByte), pack.NewU256FromUint64(satsPerByte), nil +} diff --git a/chain/decred/gas_test.go b/chain/decred/gas_test.go new file mode 100644 index 00000000..8d5983c6 --- /dev/null +++ b/chain/decred/gas_test.go @@ -0,0 +1,52 @@ +package decred_test + +import ( + "context" + + "github.com/renproject/multichain/chain/decred" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Gas", func() { + Context("when estimating decred network fee", func() { + It("should work", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client := decred.NewClient(decred.DefaultClientOptions()) + + // estimate fee to include tx within 1 block. + fallback1 := uint64(123) + gasEstimator1 := decred.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1)) + gasPrice1, _, err := gasEstimator1.EstimateGas(ctx) + if err != nil { + Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1))) + } + + // estimate fee to include tx within 10 blocks. + fallback2 := uint64(234) + gasEstimator2 := decred.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2)) + gasPrice2, _, err := gasEstimator2.EstimateGas(ctx) + if err != nil { + Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2))) + } + + // estimate fee to include tx within 100 blocks. + fallback3 := uint64(345) + gasEstimator3 := decred.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3)) + gasPrice3, _, err := gasEstimator3.EstimateGas(ctx) + if err != nil { + Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3))) + } + + // expect fees in this order at the very least. + if err == nil { + Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue()) + Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue()) + } + }) + }) +}) From 916e8646032c1626bc0356c07417135486f5f857 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Mon, 21 Jun 2021 12:22:15 +0100 Subject: [PATCH 32/35] Implement Decred Address encoder/decoder --- chain/decred/address.go | 79 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 chain/decred/address.go diff --git a/chain/decred/address.go b/chain/decred/address.go new file mode 100644 index 00000000..bbefbbcf --- /dev/null +++ b/chain/decred/address.go @@ -0,0 +1,79 @@ +package decred + +import ( + "fmt" + + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/decred/base58" + "github.com/renproject/multichain/api/address" +) + +// AddressEncodeDecoder implements the address.EncodeDecoder interface +type AddressEncodeDecoder struct { + AddressEncoder + AddressDecoder +} + +// NewAddressEncodeDecoder constructs a new AddressEncodeDecoder with the +// chain specific configurations +func NewAddressEncodeDecoder(params *chaincfg.Params) AddressEncodeDecoder { + return AddressEncodeDecoder{ + AddressEncoder: NewAddressEncoder(params), + AddressDecoder: NewAddressDecoder(params), + } +} + +// AddressEncoder encapsulates the chain specific configurations and implements +// the address.Encoder interface +type AddressEncoder struct { + params *chaincfg.Params +} + +// NewAddressEncoder constructs a new AddressEncoder with the chain specific +// configurations +func NewAddressEncoder(params *chaincfg.Params) AddressEncoder { + return AddressEncoder{params: params} +} + +// EncodeAddress implements the address.Encoder interface +func (encoder AddressEncoder) EncodeAddress(rawAddr address.RawAddress) (address.Address, error) { + + // Validate that the base58 address is in fact in correct format. + encodedAddr := base58.Encode([]byte(rawAddr)) + if _, err := dcrutil.DecodeAddress(encodedAddr, encoder.params); err != nil { + return address.Address(""), err + } + + return address.Address(encodedAddr), nil + //return address.Address(""), nil +} + + + +// AddressDecoder encapsulates the chain specific configurations and implements +// the address.Decoder interface +type AddressDecoder struct { + params *chaincfg.Params +} + +// NewAddressDecoder constructs a new AddressDecoder with the chain specific +// configurations +func NewAddressDecoder(params *chaincfg.Params) AddressDecoder { + return AddressDecoder{params: params} +} + +// DecodeAddress implements the address.Decoder interface +func (decoder AddressDecoder) DecodeAddress(addr address.Address) (address.RawAddress, error) { + decodedAddr, err := dcrutil.DecodeAddress(string(addr), decoder.params) + if err != nil { + return nil, fmt.Errorf("decode address: %v", err) + } + + switch a := decodedAddr.(type) { + case *dcrutil.AddressPubKeyHash, *dcrutil.AddressScriptHash: + return address.RawAddress(base58.Decode(string(addr))), nil + default: + return nil, fmt.Errorf("non-exhaustive pattern: address %T", a) + } +} From 219695d1b09241bcdd484b36d6eb886245cad4f4 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Mon, 21 Jun 2021 12:24:22 +0100 Subject: [PATCH 33/35] Add decred client interface --- chain/decred/decred.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/chain/decred/decred.go b/chain/decred/decred.go index c6883b31..de97d4ea 100644 --- a/chain/decred/decred.go +++ b/chain/decred/decred.go @@ -140,6 +140,20 @@ func (opts ClientOptions) WithPassword(password string) ClientOptions { return opts } +// A Client interacts with an instance of the decred network using the RPC +// interface exposed by a dcrd/dcrwallet node. +type Client interface { + utxo.Client + // UnspentOutputs spendable by the given address. + UnspentOutputs(ctx context.Context, minConf, maxConf int64, address address.Address) ([]utxo.Output, error) + // Confirmations of a transaction in the Bitcoin network. + Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error) + // EstimateSmartFee + EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error) + // EstimateFeeLegacy + EstimateFeeLegacy(ctx context.Context, numBlocks int64) (float64, error) +} + type client struct { opts ClientOptions httpClient http.Client From a81ba90502deb86304a96344823f57fe79f0b62a Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Mon, 21 Jun 2021 12:26:50 +0100 Subject: [PATCH 34/35] Import private key into dcrwallet for testing --- infra/decred/run.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/decred/run.sh b/infra/decred/run.sh index 11940344..58d0a5fc 100644 --- a/infra/decred/run.sh +++ b/infra/decred/run.sh @@ -5,11 +5,13 @@ PRIV_KEY=PsUQEpYDXVwphd9xNXUMj63LyxSWPTor3RDgfw9DMdH9tDkJaosyp # Start screen -dm /decred/dcrd --simnet --miningaddr=$ADDRESS screen -dm /decred/dcrwallet --simnet --createtemp -sleep 2 +sleep 10 # Print setup echo "DECRED_ADDRESS=$ADDRESS" +/decred/dcrctl --simnet --wallet importprivkey $PRIV_KEY + /decred/dcrctl --simnet generate 101 # Simulate mining From b95b00be87f8fd2376d6db759981160d9126cca8 Mon Sep 17 00:00:00 2001 From: "Amos .E" Date: Mon, 21 Jun 2021 13:24:19 +0100 Subject: [PATCH 35/35] Test credentials from .env --- infra/decred/run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/decred/run.sh b/infra/decred/run.sh index 58d0a5fc..d36b9519 100644 --- a/infra/decred/run.sh +++ b/infra/decred/run.sh @@ -1,6 +1,6 @@ #!/bin/bash -ADDRESS=SsaGEEZu2L8x93qvKzzahtzQ7yzkec3i8wL -PRIV_KEY=PsUQEpYDXVwphd9xNXUMj63LyxSWPTor3RDgfw9DMdH9tDkJaosyp +ADDRESS=$1 +PRIV_KEY=$2 # Start screen -dm /decred/dcrd --simnet --miningaddr=$ADDRESS