Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for Zcoin #4

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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