From c517643769ed26a9a0eddd9241bc093ba3257319 Mon Sep 17 00:00:00 2001 From: Tadhg Riordan Date: Thu, 13 Aug 2020 09:00:57 +0000 Subject: [PATCH] Add support for Zcoin --- chain/zcoin/zcoin.go | 243 ++++++++++++++++++++++++++++++++ chain/zcoin/zcoin_suite_test.go | 13 ++ chain/zcoin/zcoin_test.go | 119 ++++++++++++++++ docker/docker-compose.env | 12 +- docker/docker-compose.yaml | 88 +++++++----- docker/zcoin/Dockerfile | 48 +++++++ docker/zcoin/run.sh | 22 +++ docker/zcoin/zcoin.conf | 9 ++ multichain.go | 2 + test.sh | 2 +- 10 files changed, 520 insertions(+), 38 deletions(-) create mode 100644 chain/zcoin/zcoin.go create mode 100644 chain/zcoin/zcoin_suite_test.go create mode 100644 chain/zcoin/zcoin_test.go create mode 100644 docker/zcoin/Dockerfile create mode 100644 docker/zcoin/run.sh create mode 100644 docker/zcoin/zcoin.conf diff --git a/chain/zcoin/zcoin.go b/chain/zcoin/zcoin.go new file mode 100644 index 00000000..f9bd7bbb --- /dev/null +++ b/chain/zcoin/zcoin.go @@ -0,0 +1,243 @@ +package zcoin + +import ( + "bytes" + "crypto/sha256" + "fmt" + "math/big" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/base58" + "github.com/renproject/multichain/compat/bitcoincompat" + "github.com/renproject/pack" +) + +// Version of Zcoin transactions supported by the multichain. +const Version int32 = 1 + +type txBuilder struct { + params *chaincfg.Params +} + +// NewTxBuilder returns an implementation the transaction builder interface from +// the Bitcoin Compat API, and exposes the functionality to build simple Zcoin +// transactions. +func NewTxBuilder(params *chaincfg.Params) bitcoincompat.TxBuilder { + return txBuilder{params: params} +} + +// BuildTx returns a simple Zcoin transaction that consumes the funds from the +// given outputs, and sends the 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 Zcoin network. +// +// It is assumed that the required signature scripts require the SIGHASH_ALL +// signatures and the serialized public key: +// +// builder := txscript.NewScriptBuilder() +// builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll|SighashForkID))) +// builder.AddData(serializedPubKey) +// +// Outputs produced for recipients will use P2PKH, or P2SH scripts as the pubkey +// script, based on the format of the recipient address. +func (txBuilder txBuilder) BuildTx(inputs []bitcoincompat.Output, recipients []bitcoincompat.Recipient) (bitcoincompat.Tx, error) { + msgTx := wire.NewMsgTx(Version) + + // Inputs + for _, input := range inputs { + hash := chainhash.Hash(input.Outpoint.Hash) + index := input.Outpoint.Index.Uint32() + msgTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&hash, index), nil, nil)) + } + + // Outputs + for _, recipient := range recipients { + var script []byte + var err error + switch addr := recipient.Address.(type) { + case AddressPubKeyHash: + script, err = txscript.PayToAddrScript(addr.AddressPubKeyHash) + default: + script, err = txscript.PayToAddrScript(recipient.Address) + } + if err != nil { + return nil, err + } + value := int64(recipient.Value.Uint64()) + 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 Zcoin transaction that implements the Bitcoin Compat +// API. +type Tx struct { + inputs []bitcoincompat.Output + recipients []bitcoincompat.Recipient + + msgTx *wire.MsgTx + + signed bool +} + +func (tx *Tx) Hash() pack.Bytes32 { + serial, err := tx.Serialize() + if err != nil { + return pack.Bytes32{} + } + return pack.NewBytes32(chainhash.DoubleHashH(serial)) +} + +func (tx *Tx) Sighashes() ([]pack.Bytes32, error) { + sighashes := make([]pack.Bytes32, len(tx.inputs)) + for i, txin := range tx.inputs { + pubKeyScript := txin.PubKeyScript + + var hash []byte + var err error + hash, err = txscript.CalcSignatureHash(pubKeyScript, 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 +} + +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 { + r := new(big.Int).SetBytes(rsv[:32]) + s := new(big.Int).SetBytes(rsv[32:64]) + signature := btcec.Signature{ + R: r, + S: s, + } + + builder := txscript.NewScriptBuilder() + builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll))) + builder.AddData(pubKey) + signatureScript, err := builder.Script() + if err != nil { + return err + } + tx.msgTx.TxIn[i].SignatureScript = signatureScript + } + tx.signed = true + return nil +} + +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 +} + +// AddressPubKeyHash represents an address for P2PKH transactions for Zcoin that +// is compatible with the Bitcoin Compat API. +type AddressPubKeyHash struct { + *btcutil.AddressPubKeyHash + params *chaincfg.Params +} + +// NewAddressPubKeyHash returns a new AddressPubKeyHash that is compatible with +// the Bitcoin Compat API. +func NewAddressPubKeyHash(pkh []byte, params *chaincfg.Params) (AddressPubKeyHash, error) { + addr, err := btcutil.NewAddressPubKeyHash(pkh, params) + return AddressPubKeyHash{AddressPubKeyHash: addr, params: params}, err +} + +// String returns the string encoding of the transaction output destination. +// +// Please note that String differs subtly from EncodeAddress: String will return +// the value as a string without any conversion, while EncodeAddress may convert +// destination types (for example, converting pubkeys to P2PKH addresses) before +// encoding as a payment address string. + +// EncodeAddress returns the string encoding of the payment address associated +// with the Address value. See the comment on String for how this method differs +// from String. +func (addr AddressPubKeyHash) EncodeAddress() string { + hash := *addr.AddressPubKeyHash.Hash160() + var prefix []byte + switch addr.params { + case &chaincfg.RegressionNetParams: + prefix = regnet.p2pkhPrefix + case &chaincfg.TestNet3Params: + prefix = testnet.p2pkhPrefix + case &chaincfg.MainNetParams: + prefix = mainnet.p2pkhPrefix + } + return encodeAddress(hash[:], prefix) +} + +func encodeAddress(hash, prefix []byte) string { + var ( + body = append(prefix, hash...) + chk = checksum(body) + cksum [4]byte + ) + copy(cksum[:], chk[:4]) + return base58.Encode(append(body, cksum[:]...)) +} + +func checksum(input []byte) (cksum [4]byte) { + var ( + h = sha256.Sum256(input) + h2 = sha256.Sum256(h[:]) + ) + copy(cksum[:], h2[:4]) + return +} + +type netParams struct { + name string + params *chaincfg.Params + + p2shPrefix []byte + p2pkhPrefix []byte +} + +var ( + mainnet = netParams{ + name: "mainnet", + params: &chaincfg.MainNetParams, + + p2pkhPrefix: []byte{0x52}, + p2shPrefix: []byte{0x07}, + } + testnet = netParams{ + name: "testnet", + params: &chaincfg.TestNet3Params, + + p2pkhPrefix: []byte{0x41}, + p2shPrefix: []byte{0xB2}, + } + regnet = netParams{ + name: "regtest", + params: &chaincfg.RegressionNetParams, + + p2pkhPrefix: []byte{0x41}, + p2shPrefix: []byte{0xB2}, + } +) diff --git a/chain/zcoin/zcoin_suite_test.go b/chain/zcoin/zcoin_suite_test.go new file mode 100644 index 00000000..c0d039b3 --- /dev/null +++ b/chain/zcoin/zcoin_suite_test.go @@ -0,0 +1,13 @@ +package zcoin_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestZcoin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Zcoin Suite") +} diff --git a/chain/zcoin/zcoin_test.go b/chain/zcoin/zcoin_test.go new file mode 100644 index 00000000..7f16f237 --- /dev/null +++ b/chain/zcoin/zcoin_test.go @@ -0,0 +1,119 @@ +package zcoin_test + +import ( + "context" + "log" + "os" + "reflect" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/renproject/id" + "github.com/renproject/multichain/chain/zcoin" + "github.com/renproject/multichain/compat/bitcoincompat" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Zcoin", func() { + Context("when submitting transactions", func() { + Context("when sending XZC 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 ZCOIN_PK to the + // value specified in the `./multichaindeploy/.env` file. + pkEnv := os.Getenv("ZCOIN_PK") + if pkEnv == "" { + panic("ZCOIN_PK is undefined") + } + wif, err := btcutil.DecodeWIF(pkEnv) + Expect(err).ToNot(HaveOccurred()) + + // PKH + pkhAddr, err := zcoin.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeCompressed()), &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + pkhAddrUncompressed, err := zcoin.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeUncompressed()), &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("PKH not enc %v", pkhAddr) + log.Printf("PKH %v", pkhAddr.EncodeAddress()) + log.Printf("PKH (uncompressed) %v", pkhAddrUncompressed.EncodeAddress()) + + // Setup the client and load the unspent transaction outputs. + client := bitcoincompat.NewClient(bitcoincompat.DefaultClientOptions().WithHost("http://127.0.0.1:19232")) + outputs, err := client.UnspentOutputs(context.Background(), 0, 999999999, pkhAddr) + 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. + recipients := []bitcoincompat.Recipient{ + { + Address: pkhAddr, + Value: pack.NewU64((output.Value.Uint64() - 1000) / 2), + }, + { + Address: pkhAddrUncompressed, + Value: pack.NewU64((output.Value.Uint64() - 1000) / 2), + }, + } + tx, err := zcoin.NewTxBuilder(&chaincfg.RegressionNetParams).BuildTx([]bitcoincompat.Output{output}, 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 Zcoin node. Again, this + // should be running a la `./multichaindeploy`. + txHash, 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 Zcoin 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/docker/docker-compose.env b/docker/docker-compose.env index 4b400792..2dde87ac 100644 --- a/docker/docker-compose.env +++ b/docker/docker-compose.env @@ -36,4 +36,14 @@ export DOGECOIN_ADDRESS=mwjUmhAW68zCtgZpW5b1xD5g7MZew6xPV4 # for which the private key is known by a test suite. This allows the test suite # access to plenty of testing funds. export ZCASH_PK=cNSVbbsAcBQ6BAmMr6yH6DLWr7QTDptHwdzpy4GYxGDkNZeKnczK -export ZCASH_ADDRESS=tmCTReBSJEDMWfFCkXXPMSB3EfuPg6SE9dw \ No newline at end of file +export ZCASH_ADDRESS=tmCTReBSJEDMWfFCkXXPMSB3EfuPg6SE9dw + +# +# Zcoin +# + +# 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 ZCOIN_PK=cRgCPDGWj9mCKaZ9cd6VkUuVZZakomga4nVkLCH5r3xUXbxKNSci +export ZCOIN_ADDRESS=TTnPecgXLVLQedi2Y7ZrPSh54hnDGHmf1M diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 739ee837..e597769b 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,35 +1,35 @@ version: "2" services: - # ## - # ## Bitcoin - # ## - - # bitcoin: - # build: - # context: ./bitcoin - # ports: - # - "0.0.0.0:18443:18443" - # entrypoint: - # - "./root/run.sh" - # - "${BITCOIN_ADDRESS}" - - # ## - # ## Bitcoin Cash - # ## + ## + ## Bitcoin + ## - # bitcoincash: - # build: - # context: ./bitcoincash - # ports: - # - "0.0.0.0:19443:19443" - # entrypoint: - # - "./root/run.sh" - # - "${BITCOINCASH_ADDRESS}" + bitcoin: + build: + context: ./bitcoin + ports: + - "0.0.0.0:18443:18443" + entrypoint: + - "./root/run.sh" + - "${BITCOIN_ADDRESS}" ## - ## Dogecoin + ## Bitcoin Cash ## + bitcoincash: + build: + context: ./bitcoincash + ports: + - "0.0.0.0:19443:19443" + entrypoint: + - "./root/run.sh" + - "${BITCOINCASH_ADDRESS}" + + # + # Dogecoin + # + dogecoin: build: context: ./dogecoin @@ -38,14 +38,30 @@ services: entrypoint: - "./root/run.sh" - "${DOGECOIN_ADDRESS}" - # ## - # ## Zcash - # ## - # zcash: - # build: - # context: ./zcash - # ports: - # - "0.0.0.0:18232:18232" - # entrypoint: - # - "./root/run.sh" - # - "${ZCASH_ADDRESS}" + + ## + ## Zcash + ## + + zcash: + build: + context: ./zcash + ports: + - "0.0.0.0:18232:18232" + entrypoint: + - "./root/run.sh" + - "${ZCASH_ADDRESS}" + + ## + ## Zcoin + ## + + zcoin: + build: + context: ./zcoin + ports: + - "0.0.0.0:19232:19232" + entrypoint: + - "./root/run.sh" + - "${ZCOIN_ADDRESS}" + diff --git a/docker/zcoin/Dockerfile b/docker/zcoin/Dockerfile new file mode 100644 index 00000000..16c77e68 --- /dev/null +++ b/docker/zcoin/Dockerfile @@ -0,0 +1,48 @@ +FROM ubuntu:xenial + +RUN apt-get update --fix-missing && apt-get install --yes software-properties-common + +# Install all of the dependencies that are needed to build zcoind and +# zcoin-cli from source. +RUN apt-get update && apt-get install -y \ + automake \ + bsdmainutils \ + curl \ + g++ \ + git \ + libboost-all-dev \ + libevent-dev \ + libssl-dev \ + libtool \ + libzmq3-dev \ + make \ + openjdk-8-jdk \ + pkg-config \ + zlib1g-dev \ + libminizip-dev \ + cmake \ + libgmp-dev + +# Install Berkeley DB 4.8 +RUN curl -L http://download.oracle.com/berkeley-db/db-4.8.30.tar.gz | tar -xz -C /tmp && \ + cd /tmp/db-4.8.30/build_unix && \ + ../dist/configure --enable-cxx --includedir=/usr/include/bdb4.8 --libdir=/usr/lib && \ + make && make install && \ + cd / && rm -rf /tmp/db-4.8.30 + +# Clone the repository and build. +RUN git clone https://github.com/zcoinofficial/zcoin && \ + cd zcoin && \ + ./autogen.sh && \ + ./configure --without-gui --without-miniupnpc && \ + make && \ + make install + +COPY zcoin.conf /root/.zcoin/zcoin.conf +COPY run.sh /root/run.sh +RUN chmod +x /root/run.sh + +# Regtest network port +EXPOSE 19232 + +ENTRYPOINT ["./root/run.sh"] diff --git a/docker/zcoin/run.sh b/docker/zcoin/run.sh new file mode 100644 index 00000000..d647ba72 --- /dev/null +++ b/docker/zcoin/run.sh @@ -0,0 +1,22 @@ +#!/bin/bash +ADDRESS=$1 + +# Start +zcoind +sleep 10 + +# Print setup +echo "ZCOIN_ADDRESS=$ADDRESS" + +# Import the address +zcoin-cli importaddress $ADDRESS + +# Generate enough blocks to pass the maturation time +zcoin-cli generatetoaddress 101 $ADDRESS + +# Simulate mining +while : +do + zcoin-cli generatetoaddress 1 $ADDRESS + sleep 10 +done diff --git a/docker/zcoin/zcoin.conf b/docker/zcoin/zcoin.conf new file mode 100644 index 00000000..2cc200f0 --- /dev/null +++ b/docker/zcoin/zcoin.conf @@ -0,0 +1,9 @@ +daemon=1 +regtest=1 +rpcuser=user +rpcpassword=password +rpcallowip=0.0.0.0/0 +rpcport=19232 +server=1 +txindex=1 + diff --git a/multichain.go b/multichain.go index f338e4ae..8f941669 100644 --- a/multichain.go +++ b/multichain.go @@ -13,6 +13,7 @@ const ( BTC = Asset("BTC") // Bitcoin DOGE = Asset("DOGE") // Dogecoin ETH = Asset("ETH") // Ether + XZC = Asset("XZC") // Zcoin ZEC = Asset("ZEC") // Zcash ) @@ -44,6 +45,7 @@ const ( BitcoinCash = Chain("BitcoinCash") Ethereum = Chain("Ethereum") Zcash = Chain("Zcash") + Zcoin = Chain("Zcoin") ) // SizeHint returns the number of bytes required to represent the chain in diff --git a/test.sh b/test.sh index ab8a04e3..c6b4afd9 100755 --- a/test.sh +++ b/test.sh @@ -4,4 +4,4 @@ echo "Waiting for multichain to boot..." sleep 10 go test -v ./... docker-compose -f ./docker/docker-compose.yaml down -echo "Done!" \ No newline at end of file +echo "Done!"