Skip to content

Commit

Permalink
Add support for Zcoin
Browse files Browse the repository at this point in the history
  • Loading branch information
riordant committed Aug 13, 2020
1 parent 56a1158 commit c517643
Show file tree
Hide file tree
Showing 10 changed files with 520 additions and 38 deletions.
243 changes: 243 additions & 0 deletions chain/zcoin/zcoin.go
Original file line number Diff line number Diff line change
@@ -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},
}
)
13 changes: 13 additions & 0 deletions chain/zcoin/zcoin_suite_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
119 changes: 119 additions & 0 deletions chain/zcoin/zcoin_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
})
})
})
12 changes: 11 additions & 1 deletion docker/docker-compose.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Loading

0 comments on commit c517643

Please sign in to comment.