Skip to content

Commit c517643

Browse files
committed
Add support for Zcoin
1 parent 56a1158 commit c517643

File tree

10 files changed

+520
-38
lines changed

10 files changed

+520
-38
lines changed

chain/zcoin/zcoin.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package zcoin
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"fmt"
7+
"math/big"
8+
9+
"github.com/btcsuite/btcd/btcec"
10+
"github.com/btcsuite/btcd/chaincfg"
11+
"github.com/btcsuite/btcd/chaincfg/chainhash"
12+
"github.com/btcsuite/btcd/txscript"
13+
"github.com/btcsuite/btcd/wire"
14+
"github.com/btcsuite/btcutil"
15+
"github.com/btcsuite/btcutil/base58"
16+
"github.com/renproject/multichain/compat/bitcoincompat"
17+
"github.com/renproject/pack"
18+
)
19+
20+
// Version of Zcoin transactions supported by the multichain.
21+
const Version int32 = 1
22+
23+
type txBuilder struct {
24+
params *chaincfg.Params
25+
}
26+
27+
// NewTxBuilder returns an implementation the transaction builder interface from
28+
// the Bitcoin Compat API, and exposes the functionality to build simple Zcoin
29+
// transactions.
30+
func NewTxBuilder(params *chaincfg.Params) bitcoincompat.TxBuilder {
31+
return txBuilder{params: params}
32+
}
33+
34+
// BuildTx returns a simple Zcoin transaction that consumes the funds from the
35+
// given outputs, and sends the to the given recipients. The difference in the
36+
// sum value of the inputs and the sum value of the recipients is paid as a fee
37+
// to the Zcoin network.
38+
//
39+
// It is assumed that the required signature scripts require the SIGHASH_ALL
40+
// signatures and the serialized public key:
41+
//
42+
// builder := txscript.NewScriptBuilder()
43+
// builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll|SighashForkID)))
44+
// builder.AddData(serializedPubKey)
45+
//
46+
// Outputs produced for recipients will use P2PKH, or P2SH scripts as the pubkey
47+
// script, based on the format of the recipient address.
48+
func (txBuilder txBuilder) BuildTx(inputs []bitcoincompat.Output, recipients []bitcoincompat.Recipient) (bitcoincompat.Tx, error) {
49+
msgTx := wire.NewMsgTx(Version)
50+
51+
// Inputs
52+
for _, input := range inputs {
53+
hash := chainhash.Hash(input.Outpoint.Hash)
54+
index := input.Outpoint.Index.Uint32()
55+
msgTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&hash, index), nil, nil))
56+
}
57+
58+
// Outputs
59+
for _, recipient := range recipients {
60+
var script []byte
61+
var err error
62+
switch addr := recipient.Address.(type) {
63+
case AddressPubKeyHash:
64+
script, err = txscript.PayToAddrScript(addr.AddressPubKeyHash)
65+
default:
66+
script, err = txscript.PayToAddrScript(recipient.Address)
67+
}
68+
if err != nil {
69+
return nil, err
70+
}
71+
value := int64(recipient.Value.Uint64())
72+
if value < 0 {
73+
return nil, fmt.Errorf("expected value >= 0, got value = %v", value)
74+
}
75+
msgTx.AddTxOut(wire.NewTxOut(value, script))
76+
}
77+
78+
return &Tx{inputs: inputs, recipients: recipients, msgTx: msgTx, signed: false}, nil
79+
}
80+
81+
// Tx represents a simple Zcoin transaction that implements the Bitcoin Compat
82+
// API.
83+
type Tx struct {
84+
inputs []bitcoincompat.Output
85+
recipients []bitcoincompat.Recipient
86+
87+
msgTx *wire.MsgTx
88+
89+
signed bool
90+
}
91+
92+
func (tx *Tx) Hash() pack.Bytes32 {
93+
serial, err := tx.Serialize()
94+
if err != nil {
95+
return pack.Bytes32{}
96+
}
97+
return pack.NewBytes32(chainhash.DoubleHashH(serial))
98+
}
99+
100+
func (tx *Tx) Sighashes() ([]pack.Bytes32, error) {
101+
sighashes := make([]pack.Bytes32, len(tx.inputs))
102+
for i, txin := range tx.inputs {
103+
pubKeyScript := txin.PubKeyScript
104+
105+
var hash []byte
106+
var err error
107+
hash, err = txscript.CalcSignatureHash(pubKeyScript, txscript.SigHashAll, tx.msgTx, i)
108+
if err != nil {
109+
return []pack.Bytes32{}, err
110+
}
111+
112+
sighash := [32]byte{}
113+
copy(sighash[:], hash)
114+
sighashes[i] = pack.NewBytes32(sighash)
115+
}
116+
return sighashes, nil
117+
}
118+
119+
func (tx *Tx) Sign(signatures []pack.Bytes65, pubKey pack.Bytes) error {
120+
if tx.signed {
121+
return fmt.Errorf("already signed")
122+
}
123+
if len(signatures) != len(tx.msgTx.TxIn) {
124+
return fmt.Errorf("expected %v signatures, got %v signatures", len(tx.msgTx.TxIn), len(signatures))
125+
}
126+
127+
for i, rsv := range signatures {
128+
r := new(big.Int).SetBytes(rsv[:32])
129+
s := new(big.Int).SetBytes(rsv[32:64])
130+
signature := btcec.Signature{
131+
R: r,
132+
S: s,
133+
}
134+
135+
builder := txscript.NewScriptBuilder()
136+
builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll)))
137+
builder.AddData(pubKey)
138+
signatureScript, err := builder.Script()
139+
if err != nil {
140+
return err
141+
}
142+
tx.msgTx.TxIn[i].SignatureScript = signatureScript
143+
}
144+
tx.signed = true
145+
return nil
146+
}
147+
148+
func (tx *Tx) Serialize() (pack.Bytes, error) {
149+
buf := new(bytes.Buffer)
150+
if err := tx.msgTx.Serialize(buf); err != nil {
151+
return pack.Bytes{}, err
152+
}
153+
return pack.NewBytes(buf.Bytes()), nil
154+
}
155+
156+
// AddressPubKeyHash represents an address for P2PKH transactions for Zcoin that
157+
// is compatible with the Bitcoin Compat API.
158+
type AddressPubKeyHash struct {
159+
*btcutil.AddressPubKeyHash
160+
params *chaincfg.Params
161+
}
162+
163+
// NewAddressPubKeyHash returns a new AddressPubKeyHash that is compatible with
164+
// the Bitcoin Compat API.
165+
func NewAddressPubKeyHash(pkh []byte, params *chaincfg.Params) (AddressPubKeyHash, error) {
166+
addr, err := btcutil.NewAddressPubKeyHash(pkh, params)
167+
return AddressPubKeyHash{AddressPubKeyHash: addr, params: params}, err
168+
}
169+
170+
// String returns the string encoding of the transaction output destination.
171+
//
172+
// Please note that String differs subtly from EncodeAddress: String will return
173+
// the value as a string without any conversion, while EncodeAddress may convert
174+
// destination types (for example, converting pubkeys to P2PKH addresses) before
175+
// encoding as a payment address string.
176+
177+
// EncodeAddress returns the string encoding of the payment address associated
178+
// with the Address value. See the comment on String for how this method differs
179+
// from String.
180+
func (addr AddressPubKeyHash) EncodeAddress() string {
181+
hash := *addr.AddressPubKeyHash.Hash160()
182+
var prefix []byte
183+
switch addr.params {
184+
case &chaincfg.RegressionNetParams:
185+
prefix = regnet.p2pkhPrefix
186+
case &chaincfg.TestNet3Params:
187+
prefix = testnet.p2pkhPrefix
188+
case &chaincfg.MainNetParams:
189+
prefix = mainnet.p2pkhPrefix
190+
}
191+
return encodeAddress(hash[:], prefix)
192+
}
193+
194+
func encodeAddress(hash, prefix []byte) string {
195+
var (
196+
body = append(prefix, hash...)
197+
chk = checksum(body)
198+
cksum [4]byte
199+
)
200+
copy(cksum[:], chk[:4])
201+
return base58.Encode(append(body, cksum[:]...))
202+
}
203+
204+
func checksum(input []byte) (cksum [4]byte) {
205+
var (
206+
h = sha256.Sum256(input)
207+
h2 = sha256.Sum256(h[:])
208+
)
209+
copy(cksum[:], h2[:4])
210+
return
211+
}
212+
213+
type netParams struct {
214+
name string
215+
params *chaincfg.Params
216+
217+
p2shPrefix []byte
218+
p2pkhPrefix []byte
219+
}
220+
221+
var (
222+
mainnet = netParams{
223+
name: "mainnet",
224+
params: &chaincfg.MainNetParams,
225+
226+
p2pkhPrefix: []byte{0x52},
227+
p2shPrefix: []byte{0x07},
228+
}
229+
testnet = netParams{
230+
name: "testnet",
231+
params: &chaincfg.TestNet3Params,
232+
233+
p2pkhPrefix: []byte{0x41},
234+
p2shPrefix: []byte{0xB2},
235+
}
236+
regnet = netParams{
237+
name: "regtest",
238+
params: &chaincfg.RegressionNetParams,
239+
240+
p2pkhPrefix: []byte{0x41},
241+
p2shPrefix: []byte{0xB2},
242+
}
243+
)

chain/zcoin/zcoin_suite_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zcoin_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestZcoin(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Zcoin Suite")
13+
}

chain/zcoin/zcoin_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package zcoin_test
2+
3+
import (
4+
"context"
5+
"log"
6+
"os"
7+
"reflect"
8+
"time"
9+
10+
"github.com/btcsuite/btcd/chaincfg"
11+
"github.com/btcsuite/btcutil"
12+
"github.com/renproject/id"
13+
"github.com/renproject/multichain/chain/zcoin"
14+
"github.com/renproject/multichain/compat/bitcoincompat"
15+
"github.com/renproject/pack"
16+
17+
. "github.com/onsi/ginkgo"
18+
. "github.com/onsi/gomega"
19+
)
20+
21+
var _ = Describe("Zcoin", func() {
22+
Context("when submitting transactions", func() {
23+
Context("when sending XZC to multiple addresses", func() {
24+
It("should work", func() {
25+
// Load private key, and assume that the associated address has
26+
// funds to spend. You can do this by setting ZCOIN_PK to the
27+
// value specified in the `./multichaindeploy/.env` file.
28+
pkEnv := os.Getenv("ZCOIN_PK")
29+
if pkEnv == "" {
30+
panic("ZCOIN_PK is undefined")
31+
}
32+
wif, err := btcutil.DecodeWIF(pkEnv)
33+
Expect(err).ToNot(HaveOccurred())
34+
35+
// PKH
36+
pkhAddr, err := zcoin.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeCompressed()), &chaincfg.RegressionNetParams)
37+
Expect(err).ToNot(HaveOccurred())
38+
pkhAddrUncompressed, err := zcoin.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeUncompressed()), &chaincfg.RegressionNetParams)
39+
Expect(err).ToNot(HaveOccurred())
40+
log.Printf("PKH not enc %v", pkhAddr)
41+
log.Printf("PKH %v", pkhAddr.EncodeAddress())
42+
log.Printf("PKH (uncompressed) %v", pkhAddrUncompressed.EncodeAddress())
43+
44+
// Setup the client and load the unspent transaction outputs.
45+
client := bitcoincompat.NewClient(bitcoincompat.DefaultClientOptions().WithHost("http://127.0.0.1:19232"))
46+
outputs, err := client.UnspentOutputs(context.Background(), 0, 999999999, pkhAddr)
47+
Expect(err).ToNot(HaveOccurred())
48+
Expect(len(outputs)).To(BeNumerically(">", 0))
49+
output := outputs[0]
50+
51+
// Check that we can load the output and that it is equal.
52+
// Otherwise, something strange is happening with the RPC
53+
// client.
54+
output2, _, err := client.Output(context.Background(), output.Outpoint)
55+
Expect(err).ToNot(HaveOccurred())
56+
Expect(reflect.DeepEqual(output, output2)).To(BeTrue())
57+
58+
// Build the transaction by consuming the outputs and spending
59+
// them to a set of recipients.
60+
recipients := []bitcoincompat.Recipient{
61+
{
62+
Address: pkhAddr,
63+
Value: pack.NewU64((output.Value.Uint64() - 1000) / 2),
64+
},
65+
{
66+
Address: pkhAddrUncompressed,
67+
Value: pack.NewU64((output.Value.Uint64() - 1000) / 2),
68+
},
69+
}
70+
tx, err := zcoin.NewTxBuilder(&chaincfg.RegressionNetParams).BuildTx([]bitcoincompat.Output{output}, recipients)
71+
Expect(err).ToNot(HaveOccurred())
72+
73+
// Get the digests that need signing from the transaction, and
74+
// sign them. In production, this would be done using the RZL
75+
// MPC algorithm, but for the purposes of this test, using an
76+
// explicit privkey is ok.
77+
sighashes, err := tx.Sighashes()
78+
signatures := make([]pack.Bytes65, len(sighashes))
79+
Expect(err).ToNot(HaveOccurred())
80+
for i := range sighashes {
81+
hash := id.Hash(sighashes[i])
82+
privKey := (*id.PrivKey)(wif.PrivKey)
83+
signature, err := privKey.Sign(&hash)
84+
Expect(err).ToNot(HaveOccurred())
85+
signatures[i] = pack.NewBytes65(signature)
86+
}
87+
Expect(tx.Sign(signatures, pack.NewBytes(wif.SerializePubKey()))).To(Succeed())
88+
89+
// Submit the transaction to the Zcoin node. Again, this
90+
// should be running a la `./multichaindeploy`.
91+
txHash, err := client.SubmitTx(context.Background(), tx)
92+
Expect(err).ToNot(HaveOccurred())
93+
log.Printf("TXID %v", txHash)
94+
95+
for {
96+
// Loop until the transaction has at least a few
97+
// confirmations. This implies that the transaction is
98+
// definitely valid, and the test has passed. We were
99+
// successfully able to use the multichain to construct and
100+
// submit a Zcoin transaction!
101+
confs, err := client.Confirmations(context.Background(), txHash)
102+
Expect(err).ToNot(HaveOccurred())
103+
log.Printf(" %v/3 confirmations", confs)
104+
if confs >= 3 {
105+
break
106+
}
107+
time.Sleep(10 * time.Second)
108+
}
109+
110+
// Check that we can load the output and that it is equal.
111+
// Otherwise, something strange is happening with the RPC
112+
// client.
113+
output2, _, err = client.Output(context.Background(), output.Outpoint)
114+
Expect(err).ToNot(HaveOccurred())
115+
Expect(reflect.DeepEqual(output, output2)).To(BeTrue())
116+
})
117+
})
118+
})
119+
})

docker/docker-compose.env

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,14 @@ export DOGECOIN_ADDRESS=mwjUmhAW68zCtgZpW5b1xD5g7MZew6xPV4
3636
# for which the private key is known by a test suite. This allows the test suite
3737
# access to plenty of testing funds.
3838
export ZCASH_PK=cNSVbbsAcBQ6BAmMr6yH6DLWr7QTDptHwdzpy4GYxGDkNZeKnczK
39-
export ZCASH_ADDRESS=tmCTReBSJEDMWfFCkXXPMSB3EfuPg6SE9dw
39+
export ZCASH_ADDRESS=tmCTReBSJEDMWfFCkXXPMSB3EfuPg6SE9dw
40+
41+
#
42+
# Zcoin
43+
#
44+
45+
# Address that will receive mining rewards. Generally, this is set to an address
46+
# for which the private key is known by a test suite. This allows the test suite
47+
# access to plenty of testing funds.
48+
export ZCOIN_PK=cRgCPDGWj9mCKaZ9cd6VkUuVZZakomga4nVkLCH5r3xUXbxKNSci
49+
export ZCOIN_ADDRESS=TTnPecgXLVLQedi2Y7ZrPSh54hnDGHmf1M

0 commit comments

Comments
 (0)