diff --git a/chain/dash/address.go b/chain/dash/address.go new file mode 100644 index 00000000..3463d66b --- /dev/null +++ b/chain/dash/address.go @@ -0,0 +1,9 @@ +package dash + +import "github.com/renproject/multichain/chain/bitcoin" + +type ( + AddressEncoder = bitcoin.AddressEncoder + AddressDecoder = bitcoin.AddressDecoder + AddressEncodeDecoder = bitcoin.AddressEncodeDecoder +) diff --git a/chain/dash/address_test.go b/chain/dash/address_test.go new file mode 100644 index 00000000..6191a790 --- /dev/null +++ b/chain/dash/address_test.go @@ -0,0 +1 @@ +package dash_test diff --git a/chain/dash/dash.go b/chain/dash/dash.go new file mode 100644 index 00000000..07ad21a0 --- /dev/null +++ b/chain/dash/dash.go @@ -0,0 +1,80 @@ +package dash + +import ( + "github.com/btcsuite/btcd/chaincfg" +) + +func init() { + if err := chaincfg.Register(&MainNetParams); err != nil { + panic(err) + } + if err := chaincfg.Register(&TestNetParams); err != nil { + panic(err) + } + if err := chaincfg.Register(&RegressionNetParams); err != nil { + panic(err) + } +} + +var MainNetParams = chaincfg.Params{ + Name: "mainnet", + Net: 0xd9b4bef9, + + + // Address encoding magics + PubKeyHashAddrID: 0x00, // starts with 1 + ScriptHashAddrID: 0x05, // starts with 3 + PrivateKeyID: 0x80, // starts with 5 (uncompressed) or K (compressed) + + // 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 + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. Dash does not actually support this, but we do not want to + // collide with real addresses, so we specify it. + Bech32HRPSegwit: "dash", +} + +var TestNetParams = chaincfg.Params{ + Name: "testnet", + Net: 0x0709110b, + + + // Address encoding magics + PubKeyHashAddrID: 0x6f, // starts with m or n + ScriptHashAddrID: 0xc4, // starts with 2 + PrivateKeyID: 0xef, // 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. Dash does not actually support this, but we do not want to + // collide with real addresses, so we specify it. + Bech32HRPSegwit: "dasht", +} + +var RegressionNetParams = chaincfg.Params{ + Name: "regtest", + + // Dash has 0xdab5bffa as RegTest (same as Bitcoin's RegTest). + // Setting it to an arbitrary value, so that we can + // register the regtest network. + Net: 0xdab5bffb, + + // Address encoding magics + PubKeyHashAddrID: 0x6f, // starts with m or n + ScriptHashAddrID: 0xc4, // starts with 2 + PrivateKeyID: 0xef, // 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. Dash does not actually support this, but we do not want to + // collide with real addresses, so we specify it. + Bech32HRPSegwit: "dashrt", +} diff --git a/chain/dash/dash_suite_test.go b/chain/dash/dash_suite_test.go new file mode 100644 index 00000000..b50eebf5 --- /dev/null +++ b/chain/dash/dash_suite_test.go @@ -0,0 +1,13 @@ +package dash_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestDash(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Dash Suite") +} diff --git a/chain/dash/dash_test.go b/chain/dash/dash_test.go new file mode 100644 index 00000000..8ce85949 --- /dev/null +++ b/chain/dash/dash_test.go @@ -0,0 +1,123 @@ +package dash_test + +import ( + "context" + "log" + "os" + "reflect" + "time" + + "github.com/btcsuite/btcutil" + "github.com/renproject/id" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/multichain/chain/dash" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Dash", func() { + Context("when submitting transactions", func() { + Context("when sending DASH 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 DASH_PK to the + // value specified in the `./multichaindeploy/.env` file. + pkEnv := os.Getenv("DASH_PK") + if pkEnv == "" { + panic("DASH_PK is undefined") + } + wif, err := btcutil.DecodeWIF(pkEnv) + Expect(err).ToNot(HaveOccurred()) + + // PKH + pkhAddr, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeCompressed()), &dash.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + pkhAddrUncompressed, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeUncompressed()), &dash.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("PKH %v", pkhAddr.EncodeAddress()) + log.Printf("PKH (uncompressed) %v", pkhAddrUncompressed.EncodeAddress()) + + // Setup the client and load the unspent transaction outputs. + client := dash.NewClient(dash.DefaultClientOptions().WithHost("http://127.0.0.1:21443")) + 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()) + + // Build the transaction by consuming the outputs and spending + // them to a set of recipients. + inputs := []utxo.Input{ + {Output: output}, + } + recipients := []utxo.Recipient{ + { + To: address.Address(pkhAddr.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 2)), + }, + { + To: address.Address(pkhAddrUncompressed.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 2)), + }, + } + tx, err := dash.NewTxBuilder(&dash.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 Dash 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 Dash transaction! + confs, err := client.Confirmations(context.Background(), txHash) + Expect(err).ToNot(HaveOccurred()) + log.Printf(" %v/3 confirmations", confs) + if confs >= 3 { + 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()) + }) + }) + }) +}) diff --git a/chain/dash/gas.go b/chain/dash/gas.go new file mode 100644 index 00000000..2364efbb --- /dev/null +++ b/chain/dash/gas.go @@ -0,0 +1,7 @@ +package dash + +import "github.com/renproject/multichain/chain/bitcoin" + +type GasEstimator = bitcoin.GasEstimator + +var NewGasEstimator = bitcoin.NewGasEstimator diff --git a/chain/dash/gas_test.go b/chain/dash/gas_test.go new file mode 100644 index 00000000..6191a790 --- /dev/null +++ b/chain/dash/gas_test.go @@ -0,0 +1 @@ +package dash_test diff --git a/chain/dash/utxo.go b/chain/dash/utxo.go new file mode 100644 index 00000000..f82e0dc2 --- /dev/null +++ b/chain/dash/utxo.go @@ -0,0 +1,16 @@ +package dash + +import "github.com/renproject/multichain/chain/bitcoin" + +type ( + Tx = bitcoin.Tx + TxBuilder = bitcoin.TxBuilder + Client = bitcoin.Client + ClientOptions = bitcoin.ClientOptions +) + +var ( + NewTxBuilder = bitcoin.NewTxBuilder + NewClient = bitcoin.NewClient + DefaultClientOptions = bitcoin.DefaultClientOptions +) diff --git a/chain/dash/utxo_test.go b/chain/dash/utxo_test.go new file mode 100644 index 00000000..6191a790 --- /dev/null +++ b/chain/dash/utxo_test.go @@ -0,0 +1 @@ +package dash_test diff --git a/infra/.env b/infra/.env index 2a3da981..aec446ef 100644 --- a/infra/.env +++ b/infra/.env @@ -25,6 +25,16 @@ export BITCOIN_ADDRESS=mwjUmhAW68zCtgZpW5b1xD5g7MZew6xPV4 export BITCOINCASH_PK=cSEohZFQLKuemNeBVrzwxniouUJJxdcx7Tm6HpspYuxraVjytieW export BITCOINCASH_ADDRESS=bchreg:qp6tejc0ghtjeejcxa97amzvxvzacjt4qczpy2n3gf +# +# Dash +# + +# 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 DASH_PK=cR7WgPTaRZXA24XANeYKZyfiFqbrTwaNXm26UJLVez5Zmc3TNDYu +export DASH_ADDRESS=ycbPd6Uk4BDw5iVYvQFV4xBPfyo6cFmUF5 + # # DigiByte # diff --git a/infra/dash/Dockerfile b/infra/dash/Dockerfile new file mode 100644 index 00000000..418bb734 --- /dev/null +++ b/infra/dash/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:xenial + +RUN apt-get update --fix-missing && apt-get install --yes software-properties-common wget + +RUN wget -c https://github.com/dashpay/dash/releases/download/v0.15.0.0/dashcore-0.15.0.0-x86_64-linux-gnu.tar.gz -O - | tar xz +RUN mv ./dashcore-0.15.0 /app +RUN chmod +x /app/bin/dashd +RUN chmod +x /app/bin/dash-cli + +COPY dash.conf /root/.dashcore/dash.conf +COPY run.sh /root/run.sh +RUN chmod +x /root/run.sh + +EXPOSE 21443 + +ENTRYPOINT ["./root/run.sh"] diff --git a/infra/dash/dash.conf b/infra/dash/dash.conf new file mode 100644 index 00000000..db039118 --- /dev/null +++ b/infra/dash/dash.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 \ No newline at end of file diff --git a/infra/dash/keygen.go b/infra/dash/keygen.go new file mode 100644 index 00000000..0602f9d6 --- /dev/null +++ b/infra/dash/keygen.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + + "github.com/renproject/id" + "github.com/renproject/multichain/chain/dash" +) + +func main() { + privKey := id.NewPrivKey() + wif, err := btcutil.NewWIF((*btcec.PrivateKey)(privKey), &dash.RegressionNetParams, true) + if err != nil { + panic(err) + } + addrPubKeyHash, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.SerializePubKey()), &dash.RegressionNetParams) + if err != nil { + panic(err) + } + fmt.Printf("DASH_PK=%v\n", wif) + fmt.Printf("DASH_ADDRESS=%v\n", addrPubKeyHash) +} diff --git a/infra/dash/run.sh b/infra/dash/run.sh new file mode 100644 index 00000000..13299e0a --- /dev/null +++ b/infra/dash/run.sh @@ -0,0 +1,22 @@ +#!/bin/bash +ADDRESS=$1 + +# Start +/app/bin/dashd -conf=/root/.dashcore/dash.conf # -server -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcuser=user -rpcpassword=password +sleep 10 + +# Print setup +echo "DASH_ADDRESS=$ADDRESS" + +# Import the address +/app/bin/dash-cli importaddress $ADDRESS + +# Generate enough block to pass the maturation time +/app/bin/dash-cli generatetoaddress 101 $ADDRESS + +# Simulate mining +while : +do + /app/bin/dash-cli generatetoaddress 1 $ADDRESS + sleep 10 +done \ No newline at end of file diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index fd515594..971f6b13 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -50,6 +50,18 @@ services: - "./root/run.sh" - "${BITCOINCASH_ADDRESS}" + # + # Dash + # + dash: + build: + context: ./dash + ports: + - "0.0.0.0:21443:21443" + entrypoint: + - "./root/run.sh" + - "${DASH_ADDRESS}" + # # DigiByte # diff --git a/multichain.go b/multichain.go index 9300a11e..e35afab9 100644 --- a/multichain.go +++ b/multichain.go @@ -103,6 +103,7 @@ const ( BNB = Asset("BNB") // Binance Coin BTC = Asset("BTC") // Bitcoin CELO = Asset("CELO") // Celo + DASH = Asset("DASH") // Dash Digital Cash DGB = Asset("DGB") // DigiByte DOGE = Asset("DOGE") // Dogecoin ETH = Asset("ETH") // Ether @@ -212,6 +213,7 @@ const ( Bitcoin = Chain("Bitcoin") BitcoinCash = Chain("BitcoinCash") Celo = Chain("Celo") + Dash = Chain("Dash") DigiByte = Chain("DigiByte") Dogecoin = Chain("Dogecoin") Ethereum = Chain("Ethereum")