diff --git a/chain/qtum/address.go b/chain/qtum/address.go new file mode 100644 index 00000000..43f61744 --- /dev/null +++ b/chain/qtum/address.go @@ -0,0 +1,82 @@ +package qtum + +// This is a copy of Bitcoin's version with btcsuite swapped to qtumsuite + +import ( + "fmt" + + "github.com/qtumproject/qtumsuite/chaincfg" + "github.com/qtumproject/qtumsuite" + "github.com/qtumproject/qtumsuite/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 := qtumsuite.DecodeAddress(encodedAddr, encoder.params); err != nil { + return address.Address(""), err + } + + return address.Address(encodedAddr), 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) { + // Decode the checksummed base58 format address. + decoded, ver, err := base58.CheckDecode(string(addr)) + if err != nil { + return nil, fmt.Errorf("checking: %v", err) + } + if len(decoded) != 20 { + return nil, fmt.Errorf("expected len 20, got len %v", len(decoded)) + } + + // Validate the address format. + switch ver { + case decoder.params.PubKeyHashAddrID, decoder.params.ScriptHashAddrID: + return address.RawAddress(base58.Decode(string(addr))), nil + default: + return nil, fmt.Errorf("unexpected address prefix") + } +} diff --git a/chain/qtum/address_test.go b/chain/qtum/address_test.go new file mode 100644 index 00000000..2c162c04 --- /dev/null +++ b/chain/qtum/address_test.go @@ -0,0 +1 @@ +package qtum_test diff --git a/chain/qtum/gas.go b/chain/qtum/gas.go new file mode 100644 index 00000000..231e025d --- /dev/null +++ b/chain/qtum/gas.go @@ -0,0 +1,11 @@ +package qtum + +// I *Think* it is fine to simply use Bitcoin's estimator here + +import "github.com/renproject/multichain/chain/bitcoin" + +// GasEstimator re-exports bitcoin.GasEstimator. +type GasEstimator = bitcoin.GasEstimator + +// NewGasEstimator re-exports bitcoin.NewGasEstimator. +var NewGasEstimator = bitcoin.NewGasEstimator diff --git a/chain/qtum/gas_test.go b/chain/qtum/gas_test.go new file mode 100644 index 00000000..7b750127 --- /dev/null +++ b/chain/qtum/gas_test.go @@ -0,0 +1,54 @@ +package qtum_test + +// Shamelessly stolen from Dogecoin's re-export of bitcoin's implementation. Steal-ception! + +import ( + "context" + + "github.com/renproject/multichain/chain/qtum" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Gas", func() { + Context("when estimating qtum network fee", func() { + It("should work", func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client := qtum.NewClient(qtum.DefaultClientOptions()) + + // estimate fee to include tx within 1 block. + fallback1 := uint64(123) + gasEstimator1 := qtum.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 := qtum.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 := qtum.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()) + } + }) + }) +}) diff --git a/chain/qtum/qtum.go b/chain/qtum/qtum.go new file mode 100644 index 00000000..7d5ec8f4 --- /dev/null +++ b/chain/qtum/qtum.go @@ -0,0 +1,460 @@ +package qtum + +// This is a copy of Bitcoin's implementation with btcsuite swapped for qtumquite, +// along with some minimal additional changes + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/btcjson" + "github.com/qtumproject/qtumsuite/chaincfg/chainhash" + "github.com/qtumproject/qtumsuite" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "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 = "http://0.0.0.0:13889" // This should be testnet address, but works for regtest apparently + // 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" +) + +// Added the below because the other chains do, haven't tested them though. +// Don't really know which parameters are important, we'll see +// If you get type errors using these, try importing +// "github.com/qtumproject/qtumsuite/chaincfg" +// And use those parameters instead + +// MainNetParams returns the chain configuration for mainnet. +var MainNetParams = chaincfg.Params{ + Name: "mainnet", + Net: 0xd3a6cff1, + DefaultPort: "3888", + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "qc", // always bc for main net + + // Address encoding magics + PubKeyHashAddrID: 58, // starts with Q + ScriptHashAddrID: 50, // starts with M + PrivateKeyID: 128, // starts with 5 (uncompressed) or K (compressed) + WitnessPubKeyHashAddrID: 0x06, // starts with p2 + WitnessScriptHashAddrID: 0x0A, // starts with 7Xh + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x88, 0xad, 0xe4}, // starts with xprv + HDPublicKeyID: [4]byte{0x04, 0x88, 0xb2, 0x1e}, // starts with xpub + + // BIP44 coin type used in the hierarchical deterministic path for + // address generation. + HDCoinType: 2301, +} + +// TestNetParams returns the chain configuration for testnet. +var TestNetParams = chaincfg.Params{ + Name: "testnet3", + Net: 0x0615220d, + DefaultPort: "13888", + + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "tq", // always tb for test net + + // Address encoding magics + PubKeyHashAddrID: 120, // starts with m or n + ScriptHashAddrID: 110, // starts with 2 + WitnessPubKeyHashAddrID: 0x03, // starts with QW + WitnessScriptHashAddrID: 0x28, // starts with T7n + PrivateKeyID: 239, // starts with 9 (uncompressed) or c (compressed) + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with tprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with tpub + + // BIP44 coin type used in the hierarchical deterministic path for + // address generation. + HDCoinType: 1, +} + +// RegressionNetParams returns the chain configuration for regression net. +var RegressionNetParams = chaincfg.Params{ + Name: "regtest", + Net: 0xe1c6ddfd, + DefaultPort: "23888", + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "qcrt", // always bcrt for reg test net + + // Address encoding magics + PubKeyHashAddrID: 120, // starts with m or n + ScriptHashAddrID: 110, // starts with 2 + PrivateKeyID: 239, // starts with 9 (uncompressed) or c (compressed) + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with tprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with tpub + + // BIP44 coin type used in the hierarchical deterministic path for + // address generation. + HDCoinType: 1, +} + +// ClientOptions are used to parameterise the behaviour of the Client. +type ClientOptions struct { + Timeout time.Duration + TimeoutRetry time.Duration + Host string + User string + Password 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 { + return ClientOptions{ + Timeout: DefaultClientTimeout, + TimeoutRetry: DefaultClientTimeoutRetry, + Host: DefaultClientHost, + User: DefaultClientUser, + Password: DefaultClientPassword, + } +} + +// 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 +} + +// A Client interacts with an instance of the Bitcoin network using the RPC +// interface exposed by a Bitcoin 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 +} + +// NewClient returns a new Client. +func NewClient(opts ClientOptions) Client { + httpClient := http.Client{} + httpClient.Timeout = opts.Timeout + return &client{ + opts: opts, + httpClient: httpClient, + } +} + +// 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) { + resp := btcjson.TxRawResult{} + 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) + } + if outpoint.Index.Uint32() >= uint32(len(resp.Vout)) { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad index: %v is out of range", outpoint.Index) + } + vout := resp.Vout[outpoint.Index.Uint32()] + amount, err := qtumsuite.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(resp.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) { + resp := btcjson.GetTxOutResult{} + 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) + } + amount, err := qtumsuite.NewAmount(resp.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 resp.Confirmations < 0 { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad confirmations: %v", resp.Confirmations) + } + pubKeyScript, err := hex.DecodeString(resp.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(resp.Confirmations)), nil +} + +// SubmitTx to the Bitcoin 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) + } + resp := "" + if err := client.send(ctx, &resp, "sendrawtransaction", hex.EncodeToString(serial)); err != nil { + return fmt.Errorf("bad \"sendrawtransaction\": %v", err) + } + return nil +} + +// UnspentOutputs spendable by the given address. +func (client *client) UnspentOutputs(ctx context.Context, minConf, maxConf int64, addr address.Address) ([]utxo.Output, error) { + + resp := []btcjson.ListUnspentResult{} + if err := client.send(ctx, &resp, "listunspent", minConf, maxConf, []string{string(addr)}); err != nil && err != io.EOF { + return []utxo.Output{}, fmt.Errorf("bad \"listunspent\": %v", err) + } + outputs := make([]utxo.Output, len(resp)) + for i := range outputs { + amount, err := qtumsuite.NewAmount(resp[i].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(resp[i].ScriptPubKey) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad pubkey script: %v", err) + } + txid, err := chainhash.NewHashFromStr(resp[i].TxID) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad txid: %v", err) + } + outputs[i] = utxo.Output{ + Outpoint: utxo.Outpoint{ + Hash: pack.NewBytes(txid[:]), + Index: pack.NewU32(resp[i].Vout), + }, + Value: pack.NewU256FromU64(pack.NewU64(uint64(amount))), + PubKeyScript: pack.NewBytes(pubKeyScript), + } + } + return outputs, nil +} + +// Confirmations of a transaction in the Bitcoin network. +func (client *client) Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error) { + resp := btcjson.GetTransactionResult{} + + 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) + } + confirmations := resp.Confirmations + if confirmations < 0 { + confirmations = 0 + } + return confirmations, nil +} + +// EstimateSmartFee fetches the estimated bitcoin network fees to be paid (in +// BTC per kilobyte) needed for a transaction to be confirmed within `numBlocks` +// blocks. An error will be returned if the bitcoin node hasn't observed enough +// blocks to make an estimate for the provided target `numBlocks`. +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 +} + +func (client *client) send(ctx context.Context, resp interface{}, 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) + } + defer res.Body.Close() + if err := decodeResponse(resp, res.Body); err != nil { + return fmt.Errorf("decoding http response: %v", 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:"version"` + ID int `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + }{ + Version: "2.0", + ID: rand.Int(), + 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 +} diff --git a/chain/qtum/qtum_suite_test.go b/chain/qtum/qtum_suite_test.go new file mode 100644 index 00000000..243cecde --- /dev/null +++ b/chain/qtum/qtum_suite_test.go @@ -0,0 +1,13 @@ +package qtum_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestQtum(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Qtum Suite") +} diff --git a/chain/qtum/qtum_test.go b/chain/qtum/qtum_test.go new file mode 100644 index 00000000..863ce0dd --- /dev/null +++ b/chain/qtum/qtum_test.go @@ -0,0 +1,152 @@ +package qtum_test + +// This is a copy of Bitcoin's implementation with btcsuite swapped for qtumsuite, +// along with some minimal additional changes + +import ( + "context" + "log" + "os" + "reflect" + "time" + + "github.com/qtumproject/qtumsuite/chaincfg" + "github.com/qtumproject/qtumsuite" + "github.com/renproject/id" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/multichain/chain/qtum" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Qtum", func() { + Context("when submitting transactions", func() { + Context("when sending QTUM 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 BITCOIN_PK to the + // value specified in the `./multichaindeploy/.env` file. + pkEnv := os.Getenv("QTUM_PK") + if pkEnv == "" { + panic("QTUM_PK is undefined") + } + wif, err := qtumsuite.DecodeWIF(pkEnv) + Expect(err).ToNot(HaveOccurred()) + + // PKH + pkhAddr, err := qtumsuite.NewAddressPubKeyHash(qtumsuite.Hash160(wif.PrivKey.PubKey().SerializeCompressed()), &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + pkhAddrUncompressed, err := qtumsuite.NewAddressPubKeyHash(qtumsuite.Hash160(wif.PrivKey.PubKey().SerializeUncompressed()), &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("PKH %v", pkhAddr.EncodeAddress()) + log.Printf("PKH (uncompressed) %v", pkhAddrUncompressed.EncodeAddress()) + + // WPKH + wpkAddr, err := qtumsuite.NewAddressWitnessPubKeyHash([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19}, &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("WPKH %v", wpkAddr.EncodeAddress()) + + // Setup the client and load the unspent transaction outputs. + client := qtum.NewClient(qtum.DefaultClientOptions().WithHost("http://127.0.0.1:13889")) // This is actually supposed to be Qtum's testnet port, but it seems to work + outputs, err := client.UnspentOutputs(context.Background(), 0, 999999999, address.Address(pkhAddr.EncodeAddress())) + 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, + }}, + } + // The constant (relay fee) subtracted below should really be derived by + // an estimatesmartfee call to the node, but if BTC is lazy we can be too + recipients := []utxo.Recipient{ + { + To: address.Address(pkhAddr.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 150000) / 3)), + }, + { + To: address.Address(pkhAddrUncompressed.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 150000) / 3)), + }, + { + To: address.Address(wpkAddr.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 150000) / 3)), + }, + } + tx, err := qtum.NewTxBuilder(&chaincfg.RegressionNetParams).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]) + privKey := (*id.PrivKey)(wif.PrivKey) + signature, err := privKey.Sign(&hash) + Expect(err).ToNot(HaveOccurred()) + signatures[i] = pack.NewBytes65(signature) + } + Expect(tx.Sign(signatures, pack.NewBytes(wif.SerializePubKey()))).To(Succeed()) + + // Submit the transaction to the Bitcoin 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) + + 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/1 confirmations", confs) + if confs >= 1 { // Only wait for 1 conf to cut 20 sec of test time + break + } + time.Sleep(10 * time.Second) + } + ctxWithTimeout, cancelCtxWithTimeout := context.WithTimeout(context.Background(), time.Second) + defer cancelCtxWithTimeout() + _, _, err = client.UnspentOutput(ctxWithTimeout, output.Outpoint) + Expect(err).To(HaveOccurred()) + + // 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()) + }) + }) + }) +}) diff --git a/chain/qtum/utxo.go b/chain/qtum/utxo.go new file mode 100644 index 00000000..dc0a3fcc --- /dev/null +++ b/chain/qtum/utxo.go @@ -0,0 +1,218 @@ +package qtum + +// This is a copy of Bitcoin's version with btcsuite swapped to qtumsuite + +import ( + "bytes" + "fmt" + "math/big" + + "github.com/btcsuite/btcd/btcec" + "github.com/qtumproject/qtumsuite/chaincfg" + "github.com/qtumproject/qtumsuite/chaincfg/chainhash" + "github.com/qtumproject/qtumsuite/txscript" + "github.com/qtumproject/qtumsuite/wire" + "github.com/qtumproject/qtumsuite" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/pack" +) + +// Version of Bitcoin transactions supported by the multichain. +const Version int32 = 2 + +// 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 Bitcoin 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(Version) + + // Inputs + for _, input := range inputs { + hash := chainhash.Hash{} + copy(hash[:], input.Hash) + index := input.Index.Uint32() + msgTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&hash, index), nil, nil)) + } + + // Outputs + for _, recipient := range recipients { + addr, err := qtumsuite.DecodeAddress(string(recipient.To), txBuilder.params) + if err != nil { + return nil, err + } + 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 +} + +// Tx represents a simple Bitcoin transaction that implements the Bitcoin Compat +// API. +type Tx struct { + inputs []utxo.Input + recipients []utxo.Recipient + + msgTx *wire.MsgTx + + 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 +} + +// 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 +} + +// 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 { + if txscript.IsPayToWitnessPubKeyHash(pubKeyScript) { + hash, err = txscript.CalcWitnessSigHash(pubKeyScript, txscript.NewTxSigHashes(tx.msgTx), txscript.SigHashAll, tx.msgTx, i, value) + } else { + hash, err = txscript.CalcSignatureHash(pubKeyScript, txscript.SigHashAll, tx.msgTx, i) + } + } else { + if txscript.IsPayToWitnessScriptHash(pubKeyScript) { + hash, err = txscript.CalcWitnessSigHash(sigScript, txscript.NewTxSigHashes(tx.msgTx), txscript.SigHashAll, tx.msgTx, i, value) + } else { + hash, err = txscript.CalcSignatureHash(sigScript, txscript.SigHashAll, tx.msgTx, i) + } + } + if err != nil { + return []pack.Bytes32{}, err + } + + sighash := [32]byte{} + copy(sighash[:], hash) + sighashes[i] = pack.NewBytes32(sighash) + } + + 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, + } + pubKeyScript := tx.inputs[i].Output.PubKeyScript + sigScript := tx.inputs[i].SigScript + + // Support segwit. + if sigScript == nil { + if txscript.IsPayToWitnessPubKeyHash(pubKeyScript) || txscript.IsPayToWitnessScriptHash(pubKeyScript) { + tx.msgTx.TxIn[i].Witness = wire.TxWitness([][]byte{append(signature.Serialize(), byte(txscript.SigHashAll)), pubKey}) + continue + } + } else { + if txscript.IsPayToWitnessScriptHash(sigScript) || txscript.IsPayToWitnessScriptHash(sigScript) { + tx.msgTx.TxIn[i].Witness = wire.TxWitness([][]byte{append(signature.Serialize(), byte(txscript.SigHashAll)), pubKey, sigScript}) + continue + } + } + + // Support non-segwit + 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 +} + +// 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 +} diff --git a/chain/qtum/utxo_test.go b/chain/qtum/utxo_test.go new file mode 100644 index 00000000..2c162c04 --- /dev/null +++ b/chain/qtum/utxo_test.go @@ -0,0 +1 @@ +package qtum_test diff --git a/infra/.env b/infra/.env index 92ced4f5..97c93156 100644 --- a/infra/.env +++ b/infra/.env @@ -58,6 +58,18 @@ export ETHEREUM_ADDRESS=0xa0df350d2637096571F7A701CBc1C5fdE30dF76A export FILECOIN_PK=7b2254797065223a22736563703235366b31222c22507269766174654b6579223a22756d6a634e436a487a5438455757485849754a4c4b58745035437153323435666238626c656c756e5448493d227d export FILECOIN_ADDRESS=f1ej2tountzqwnu6uswhqdzvw6yy5xvcig6rxl2qa +# +# Qtum +# + +# DEZU: TODO +# Qtum Address that will have plenty funds. 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 QTUM_MNEMONIC="hold mushroom planet ski feed exact monitor vacant move giggle mean tape" +export QTUM_PK=cT6skKBVREsDtjXv26YjgNHomJGPo9oMmTYugbTUc3YDQQLB4T4T +export QTUM_ADDRESS=qb15NCu3w4zyd14L21P99AdqmovHCiCEqC + # # Terra # diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index b30be3fb..47e937bf 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -112,6 +112,20 @@ services: entrypoint: - "/root/run.sh" + # + # Qtum + # + qtum: + build: + context: ./qtum + ports: + - "0.0.0.0:13888:13888" # These addresses are actually for testnet, + - "0.0.0.0:13889:13889" # but they work in regtest too apparently + entrypoint: + - "./root/run.sh" + - "${QTUM_ADDRESS}" + - "${QTUM_PK}" + # # Solana # diff --git a/infra/qtum/Dockerfile b/infra/qtum/Dockerfile new file mode 100644 index 00000000..a7ee6b4c --- /dev/null +++ b/infra/qtum/Dockerfile @@ -0,0 +1,21 @@ +# DEZU: Modified version of file from qtum-docker github page +# I'm not 100% sure if all commands here are necessary, I've done a lot of copypasta +FROM ubuntu + +RUN apt-get update --fix-missing && apt-get install --yes -qq --no-install-recommends ca-certificates curl wget apt-utils jq +RUN apt-get install --yes curl + +# install qtum binaries +RUN wget -c https://github.com/qtumproject/qtum/releases/download/mainnet-ignition-v0.20.1/qtum-0.20.1-x86_64-linux-gnu.tar.gz -O - | tar xz +RUN mv ./qtum-0.20.1 /app +RUN chmod +x /app/bin/qtumd +RUN chmod +x /app/bin/qtum-cli + +COPY qtum.conf /root/.qtum/qtum.conf +COPY run.sh /root/run.sh +RUN chmod +x /root/run.sh + +# These addresses are actually for testnet, but they work in regtest too apparently +EXPOSE 13888 13889 + +ENTRYPOINT ["./root/run.sh"] diff --git a/infra/qtum/keygen.go b/infra/qtum/keygen.go new file mode 100644 index 00000000..f9bb9496 --- /dev/null +++ b/infra/qtum/keygen.go @@ -0,0 +1,47 @@ +package main + +// DEZU: Straight copy of bitcoin's implementation with Qtums chain configs tacked on + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/renproject/id" +) + +// Copied from multichain/chain/qtum/qtum.go +// RegressionNetParams returns the chain configuration for regression net. +var RegressionNetParams = chaincfg.Params{ + Name: "regtest", + Net: 0xe1c6ddfd, + DefaultPort: "23888", + + // Address encoding magics + PubKeyHashAddrID: 120, // starts with m or n + ScriptHashAddrID: 110, // starts with 2 + PrivateKeyID: 239, // starts with 9 (uncompressed) or c (compressed) + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with tprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with tpub + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "qcrt", // always bcrt for reg test net +} + +func main() { + privKey := id.NewPrivKey() + wif, err := btcutil.NewWIF((*btcec.PrivateKey)(privKey), &RegressionNetParams, true) + if err != nil { + panic(err) + } + addrPubKeyHash, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.SerializePubKey()), &RegressionNetParams) + if err != nil { + panic(err) + } + fmt.Printf("QTUM_PK=%v\n", wif) + fmt.Printf("QTUM_ADDRESS=%v\n", addrPubKeyHash) +} diff --git a/infra/qtum/qtum.conf b/infra/qtum/qtum.conf new file mode 100644 index 00000000..b04d1351 --- /dev/null +++ b/infra/qtum/qtum.conf @@ -0,0 +1,10 @@ +daemon=1 +regtest=1 +rpcuser=user +rpcpassword=password +rpcallowip=0.0.0.0/0 +server=1 +txindex=1 + +[regtest] +rpcbind=0.0.0.0 diff --git a/infra/qtum/run.sh b/infra/qtum/run.sh new file mode 100644 index 00000000..9a85ca8c --- /dev/null +++ b/infra/qtum/run.sh @@ -0,0 +1,47 @@ +#!/bin/bash +ADDRESS=$1 +PRIV_KEY=$2 + +# Start +/app/bin/qtumd +sleep 10 + +# Print setup +echo "(QTUM): QTUM_ADDRESS=$ADDRESS" + +# Import the address +/app/bin/qtum-cli importaddress $ADDRESS + +# Import the private key to spend UTXOs +/app/bin/qtum-cli importprivkey $PRIV_KEY + +echo "(QTUM): Trying to generate 501 blocks..." # DEZU: TODO: Remove debug +# Generate enough block to pass the maturation time (500 for Qtum) +/app/bin/qtum-cli generatetoaddress 501 $ADDRESS +echo "(QTUM): Blocks (hopefully) generated!" # DEZU: TODO: Remove debug + +#echo "(QTUM): Running 'getbalance'..." # DEZU: TODO: Remove debug +#/app/bin/qtum-cli getbalance + +#echo "(QTUM): Running 'getwalletinfo'..." # DEZU: TODO: Remove debug +#/app/bin/qtum-cli getwalletinfo + +#echo "(QTUM): Running 'estimatesmartfee 10'..." # DEZU: TODO: Remove debug +#/app/bin/qtum-cli estimatesmartfee 10 + +#echo "(QTUM): Running 'listunspent 0 999999999 qb15NCu3w4zyd14L21P99AdqmovHCiCEqC'..." # DEZU: TODO: Remove debug +#/app/bin/qtum-cli listunspent 0 999999999 qb15NCu3w4zyd14L21P99AdqmovHCiCEqC | cat + +# Simulate mining +while : +do + echo "(QTUM): Running 'generatetoaddress 1'..." # DEZU: TODO: Remove debug + /app/bin/qtum-cli generatetoaddress 1 $ADDRESS + sleep 5 + # send tx to own address while paying fee to the miner + echo "(QTUM): Running 'sendtoaddress $ADDRESS 0.5 "" "" true'..." # DEZU: TODO: Remove debug + /app/bin/qtum-cli sendtoaddress $ADDRESS 0.5 "" "" true + sleep 5 + #echo "(QTUM): Running 'estimatesmartfee 15'..." # DEZU: TODO: Remove debug + #/app/bin/qtum-cli estimatesmartfee 10 +done diff --git a/multichain.go b/multichain.go index 436a7fc0..9026e605 100644 --- a/multichain.go +++ b/multichain.go @@ -108,6 +108,7 @@ const ( ETH = Asset("ETH") // Ether FIL = Asset("FIL") // Filecoin FTM = Asset("FTM") // Fantom + QTUM = Asset("QTUM") // Qtum SOL = Asset("SOL") // Solana LUNA = Asset("LUNA") // Luna ZEC = Asset("ZEC") // Zcash @@ -144,6 +145,8 @@ func (asset Asset) OriginChain() Chain { return Fantom case LUNA: return Terra + case QTUM: + return Qtum case SOL: return Solana case ZEC: @@ -167,7 +170,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, DGB, DOGE, ZEC, QTUM: return ChainTypeUTXOBased case BNB, ETH, FIL, LUNA: return ChainTypeAccountBased @@ -218,6 +221,7 @@ const ( Ethereum = Chain("Ethereum") Fantom = Chain("Fantom") Filecoin = Chain("Filecoin") + Qtum = Chain("Qtum") Solana = Chain("Solana") Terra = Chain("Terra") Zcash = Chain("Zcash") @@ -252,7 +256,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, DigiByte, Dogecoin, Zcash, Qtum: return ChainTypeUTXOBased case BinanceSmartChain, Ethereum, Filecoin, Terra: return ChainTypeAccountBased @@ -302,6 +306,8 @@ func (chain Chain) NativeAsset() Asset { return FIL case Terra: return LUNA + case Qtum: + return QTUM case Zcash: return ZEC diff --git a/test.sh b/test.sh index e0b03f1e..3e7747f7 100755 --- a/test.sh +++ b/test.sh @@ -4,4 +4,4 @@ echo "Waiting for multichain to boot..." sleep 30 go test -v ./... docker-compose -f ./infra/docker-compose.yaml down -echo "Done!" \ No newline at end of file +echo "Done!"