diff --git a/chain/peercoin/address.go b/chain/peercoin/address.go new file mode 100644 index 00000000..8c491743 --- /dev/null +++ b/chain/peercoin/address.go @@ -0,0 +1,54 @@ +package peercoin + +import ( + "github.com/ppcsuite/btcutil" + "github.com/ppcsuite/btcutil/base58" + "github.com/ppcsuite/ppcd/chaincfg" + "github.com/renproject/multichain/api/address" + "github.com/renproject/pack" +) + +type AddressEncodeDecoder struct { + AddressEncoder + AddressDecoder +} + +func NewAddressEncodeDecoder(params *chaincfg.Params) AddressEncodeDecoder { + return AddressEncodeDecoder{ + AddressEncoder: NewAddressEncoder(params), + AddressDecoder: NewAddressDecoder(params), + } +} + +type AddressEncoder struct { + params *chaincfg.Params +} + +func NewAddressEncoder(params *chaincfg.Params) AddressEncoder { + return AddressEncoder{params: params} +} + +func (encoder AddressEncoder) EncodeAddress(rawAddr address.RawAddress) (address.Address, error) { + encodedAddr := base58.Encode([]byte(rawAddr)) + if _, err := btcutil.DecodeAddress(encodedAddr, encoder.params); err != nil { + // Check that the address is valid. + return address.Address(""), err + } + return address.Address(encodedAddr), nil +} + +type AddressDecoder struct { + params *chaincfg.Params +} + +func NewAddressDecoder(params *chaincfg.Params) AddressDecoder { + return AddressDecoder{params: params} +} + +func (decoder AddressDecoder) DecodeAddress(addr address.Address) (pack.Bytes, error) { + if _, err := btcutil.DecodeAddress(string(addr), decoder.params); err != nil { + // Check that the address is valid. + return nil, err + } + return pack.NewBytes(base58.Decode(string(addr))), nil +} diff --git a/chain/peercoin/address_test.go b/chain/peercoin/address_test.go new file mode 100644 index 00000000..82bc2209 --- /dev/null +++ b/chain/peercoin/address_test.go @@ -0,0 +1 @@ +package peercoin_test diff --git a/chain/peercoin/gas.go b/chain/peercoin/gas.go new file mode 100644 index 00000000..dd9bb2ce --- /dev/null +++ b/chain/peercoin/gas.go @@ -0,0 +1,32 @@ +package peercoin + +import ( + "context" + + "github.com/renproject/pack" +) + +// A GasEstimator returns the SATs-per-byte that is needed in order to confirm +// transactions with an estimated maximum delay of one block. In distributed +// networks that collectively build, sign, and submit transactions, it is +// important that all nodes in the network have reached consensus on the +// SATs-per-byte. +type GasEstimator struct { + satsPerByte pack.U256 +} + +// NewGasEstimator returns a simple gas estimator that always returns the given +// number of SATs-per-byte. +func NewGasEstimator(satsPerByte pack.U256) GasEstimator { + return GasEstimator{ + satsPerByte: satsPerByte, + } +} + +// GasPrice returns the number of SATs-per-byte that is needed in order to +// confirm transactions with an estimated maximum delay of one block. It is the +// responsibility of the caller to know the number of bytes in their +// transaction. +func (gasEstimator GasEstimator) GasPrice(_ context.Context) (pack.U256, error) { + return gasEstimator.satsPerByte, nil +} diff --git a/chain/peercoin/gas_test.go b/chain/peercoin/gas_test.go new file mode 100644 index 00000000..82bc2209 --- /dev/null +++ b/chain/peercoin/gas_test.go @@ -0,0 +1 @@ +package peercoin_test diff --git a/chain/peercoin/peercoin.go b/chain/peercoin/peercoin.go new file mode 100644 index 00000000..009f4f93 --- /dev/null +++ b/chain/peercoin/peercoin.go @@ -0,0 +1,297 @@ +package peercoin + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "time" + + "github.com/ppcsuite/btcutil" + "github.com/ppcsuite/ppcd/btcjson" + "github.com/ppcsuite/ppcd/chaincfg/chainhash" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/pack" +) + +const ( + // DefaultClientTimeout used by the Client. + DefaultClientTimeout = time.Minute + // DefaultClientTimeoutRetry used by the Client. + DefaultClientTimeoutRetry = time.Second + // DefaultClientHost used by the Client. This should only be used for local + // deployments of the multichain. + DefaultClientHost = "http://0.0.0.0:18443" + // DefaultClientUser used by the Client. This is insecure, and should only + // be used for local — or publicly accessible — deployments of the + // multichain. + DefaultClientUser = "user" + // DefaultClientPassword used by the Client. This is insecure, and should + // only be used for local — or publicly accessible — deployments of the + // multichain. + DefaultClientPassword = "password" +) + +// ClientOptions are used to parameterise the behaviour of the Client. +type ClientOptions struct { + Timeout time.Duration + TimeoutRetry time.Duration + Host string + User string + Password string +} + +// DefaultClientOptions returns ClientOptions with the default settings. These +// settings are valid for use with the default local deployment of the +// multichain. In production, the host, user, and password should be changed. +func DefaultClientOptions() ClientOptions { + return ClientOptions{ + Timeout: DefaultClientTimeout, + TimeoutRetry: DefaultClientTimeoutRetry, + Host: DefaultClientHost, + User: DefaultClientUser, + Password: DefaultClientPassword, + } +} + +// WithHost sets the URL of the Peercoin node. +func (opts ClientOptions) WithHost(host string) ClientOptions { + opts.Host = host + return opts +} + +// WithUser sets the username that will be used to authenticate with the Peercoin +// node. +func (opts ClientOptions) WithUser(user string) ClientOptions { + opts.User = user + return opts +} + +// WithPassword sets the password that will be used to authenticate with the +// Peercoin node. +func (opts ClientOptions) WithPassword(password string) ClientOptions { + opts.Password = password + return opts +} + +// A Client interacts with an instance of the Peercoin network using the RPC +// interface exposed by a Peercoin node. +type Client interface { + utxo.Client + // UnspentOutputs spendable by the given address. + UnspentOutputs(ctx context.Context, minConf, maxConf int64, address address.Address) ([]utxo.Output, error) + // Confirmations of a transaction in the Peercoin network. + Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error) +} + +type client struct { + opts ClientOptions + httpClient http.Client +} + +// NewClient returns a new Client. +func NewClient(opts ClientOptions) Client { + httpClient := http.Client{} + httpClient.Timeout = opts.Timeout + return &client{ + opts: opts, + httpClient: httpClient, + } +} + +// Output associated with an outpoint, and its number of confirmations. +func (client *client) Output(ctx context.Context, outpoint utxo.Outpoint) (utxo.Output, pack.U64, error) { + resp := btcjson.TxRawResult{} + hash := chainhash.Hash{} + copy(hash[:], outpoint.Hash) + if err := client.send(ctx, &resp, "getrawtransaction", hash.String(), 1); err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad \"gettxout\": %v", err) + } + if outpoint.Index.Uint32() >= uint32(len(resp.Vout)) { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad index: %v is out of range", outpoint.Index) + } + vout := resp.Vout[outpoint.Index.Uint32()] + amount, err := btcutil.NewAmount(vout.Value) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad amount: %v", err) + } + if amount < 0 { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad amount: %v", amount) + } + pubKeyScript, err := hex.DecodeString(vout.ScriptPubKey.Hex) + if err != nil { + return utxo.Output{}, pack.NewU64(0), fmt.Errorf("bad pubkey script: %v", err) + } + output := utxo.Output{ + Outpoint: outpoint, + Value: pack.NewU256FromU64(pack.NewU64(uint64(amount))), + PubKeyScript: pack.NewBytes(pubKeyScript), + } + return output, pack.NewU64(resp.Confirmations), nil +} + +// SubmitTx to the Peercoin network. +func (client *client) SubmitTx(ctx context.Context, tx utxo.Tx) error { + serial, err := tx.Serialize() + if err != nil { + return fmt.Errorf("bad tx: %v", err) + } + resp := "" + if err := client.send(ctx, &resp, "sendrawtransaction", hex.EncodeToString(serial)); err != nil { + return fmt.Errorf("bad \"sendrawtransaction\": %v", err) + } + return nil +} + +// UnspentOutputs spendable by the given address. +func (client *client) UnspentOutputs(ctx context.Context, minConf, maxConf int64, addr address.Address) ([]utxo.Output, error) { + resp := []btcjson.ListUnspentResult{} + if err := client.send(ctx, &resp, "listunspent", minConf, maxConf, []string{string(addr)}); err != nil && err != io.EOF { + return []utxo.Output{}, fmt.Errorf("bad \"listunspent\": %v", err) + } + outputs := make([]utxo.Output, len(resp)) + for i := range outputs { + amount, err := btcutil.NewAmount(resp[i].Amount) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad amount: %v", err) + } + if amount < 0 { + return []utxo.Output{}, fmt.Errorf("bad amount: %v", amount) + } + pubKeyScript, err := hex.DecodeString(resp[i].ScriptPubKey) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad pubkey script: %v", err) + } + txid, err := chainhash.NewHashFromStr(resp[i].TxID) + if err != nil { + return []utxo.Output{}, fmt.Errorf("bad txid: %v", err) + } + outputs[i] = utxo.Output{ + Outpoint: utxo.Outpoint{ + Hash: pack.NewBytes(txid[:]), + Index: pack.NewU32(resp[i].Vout), + }, + Value: pack.NewU256FromU64(pack.NewU64(uint64(amount))), + PubKeyScript: pack.NewBytes(pubKeyScript), + } + } + return outputs, nil +} + +// Confirmations of a transaction in the Peercoin network. +func (client *client) Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error) { + resp := btcjson.GetTransactionResult{} + + size := len(txHash) + txHashReversed := make([]byte, size) + copy(txHashReversed[:], txHash[:]) + for i := 0; i < size/2; i++ { + txHashReversed[i], txHashReversed[size-1-i] = txHashReversed[size-1-i], txHashReversed[i] + } + + if err := client.send(ctx, &resp, "gettransaction", hex.EncodeToString(txHashReversed)); err != nil { + return 0, fmt.Errorf("bad \"gettransaction\": %v", err) + } + confirmations := resp.Confirmations + if confirmations < 0 { + confirmations = 0 + } + return confirmations, nil +} + +func (client *client) send(ctx context.Context, resp interface{}, method string, params ...interface{}) error { + // Encode the request. + data, err := encodeRequest(method, params) + if err != nil { + return err + } + + return retry(ctx, client.opts.TimeoutRetry, func() error { + // Create request and add basic authentication headers. The context is + // not attached to the request, and instead we all each attempt to run + // for the timeout duration, and we keep attempting until success, or + // the context is done. + req, err := http.NewRequest("POST", client.opts.Host, bytes.NewBuffer(data)) + if err != nil { + return fmt.Errorf("building http request: %v", err) + } + req.SetBasicAuth(client.opts.User, client.opts.Password) + + // Send the request and decode the response. + res, err := client.httpClient.Do(req) + if err != nil { + return fmt.Errorf("sending http request: %v", err) + } + defer res.Body.Close() + if err := decodeResponse(resp, res.Body); err != nil { + return fmt.Errorf("decoding http response: %v", err) + } + return nil + }) +} + +func encodeRequest(method string, params []interface{}) ([]byte, error) { + rawParams, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("encoding params: %v", err) + } + req := struct { + Version string `json:"version"` + ID int `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + }{ + Version: "2.0", + ID: rand.Int(), + Method: method, + Params: rawParams, + } + rawReq, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("encoding request: %v", err) + } + return rawReq, nil +} + +func decodeResponse(resp interface{}, r io.Reader) error { + res := struct { + Version string `json:"version"` + ID int `json:"id"` + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` + }{} + if err := json.NewDecoder(r).Decode(&res); err != nil { + return fmt.Errorf("decoding response: %v", err) + } + if res.Error != nil { + return fmt.Errorf("decoding response: %v", string(*res.Error)) + } + if res.Result == nil { + return fmt.Errorf("decoding result: result is nil") + } + if err := json.Unmarshal(*res.Result, resp); err != nil { + return fmt.Errorf("decoding result: %v", err) + } + return nil +} + +func retry(ctx context.Context, dur time.Duration, f func() error) error { + ticker := time.NewTicker(dur) + err := f() + for err != nil { + log.Printf("retrying: %v", err) + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + err = f() + } + } + return nil +} diff --git a/chain/peercoin/peercoin_suite_test.go b/chain/peercoin/peercoin_suite_test.go new file mode 100644 index 00000000..907dd11b --- /dev/null +++ b/chain/peercoin/peercoin_suite_test.go @@ -0,0 +1,13 @@ +package peercoin_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestPeercoin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Peercoin Suite") +} diff --git a/chain/peercoin/peercoin_test.go b/chain/peercoin/peercoin_test.go new file mode 100644 index 00000000..0a631d76 --- /dev/null +++ b/chain/peercoin/peercoin_test.go @@ -0,0 +1,140 @@ +package peercoin_test + +import ( + "context" + "log" + "os" + "reflect" + "time" + + "github.com/ppcsuite/btcutil" + "github.com/ppcsuite/ppcd/chaincfg" + "github.com/renproject/id" + "github.com/renproject/multichain/api/address" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/multichain/chain/peercoin" + "github.com/renproject/pack" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Peercoin", func() { + Context("when submitting transactions", func() { + Context("when sending PPC 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 PEERCOIN_PK to the + // value specified in the `./multichaindeploy/.env` file. + pkEnv := os.Getenv("PEERCOIN_PK") + if pkEnv == "" { + panic("PEERCOIN_PK is undefined") + } + wif, err := btcutil.DecodeWIF(pkEnv) + Expect(err).ToNot(HaveOccurred()) + + // PKH + pkhAddr, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeCompressed()), &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + pkhAddrUncompressed, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(wif.PrivKey.PubKey().SerializeUncompressed()), &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("PKH %v", pkhAddr.EncodeAddress()) + log.Printf("PKH (uncompressed) %v", pkhAddrUncompressed.EncodeAddress()) + + // WPKH + wpkAddr, err := btcutil.NewAddressWitnessPubKeyHash([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19}, &chaincfg.RegressionNetParams) + Expect(err).ToNot(HaveOccurred()) + log.Printf("WPKH %v", wpkAddr.EncodeAddress()) + + // Setup the client and load the unspent transaction outputs. + client := peercoin.NewClient(peercoin.DefaultClientOptions()) + 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: utxo.Output{ + Outpoint: utxo.Outpoint{ + Hash: output.Outpoint.Hash[:], + Index: output.Outpoint.Index, + }, + PubKeyScript: output.PubKeyScript, + Value: output.Value, + }}, + } + recipients := []utxo.Recipient{ + { + To: address.Address(pkhAddr.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 3)), + }, + { + To: address.Address(pkhAddrUncompressed.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 3)), + }, + { + To: address.Address(wpkAddr.EncodeAddress()), + Value: pack.NewU256FromU64(pack.NewU64((output.Value.Int().Uint64() - 1000) / 3)), + }, + } + tx, err := peercoin.NewTxBuilder(&chaincfg.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 Peercoin 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 Peercoin 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/peercoin/utxo.go b/chain/peercoin/utxo.go new file mode 100644 index 00000000..2b304cf8 --- /dev/null +++ b/chain/peercoin/utxo.go @@ -0,0 +1,210 @@ +package peercoin + +import ( + "bytes" + "fmt" + "math/big" + + "github.com/ppcsuite/btcutil" + "github.com/ppcsuite/ppcd/btcec" + "github.com/ppcsuite/ppcd/chaincfg" + "github.com/ppcsuite/ppcd/chaincfg/chainhash" + "github.com/ppcsuite/ppcd/txscript" + "github.com/ppcsuite/ppcd/wire" + "github.com/renproject/multichain/api/utxo" + "github.com/renproject/pack" +) + +// Version of Peercoin transactions supported by the multichain. +const Version int32 = 2 + +// The TxBuilder is an implementation of a UTXO-compatible transaction builder +// for Peercoin. +type TxBuilder struct { + params *chaincfg.Params +} + +// NewTxBuilder returns a transaction builder that builds UTXO-compatible +// Peercoin transactions for the given chain configuration (this means that it +// can be used for regnet, testnet, and mainnet, but also for networks that are +// minimally modified forks of the Peercoin network). +func NewTxBuilder(params *chaincfg.Params) TxBuilder { + return TxBuilder{params: params} +} + +// BuildTx returns a Peercoin transaction that consumes funds from the given +// inputs, and sends them 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 Peercoin network. This fee must be calculated independently of this +// function. Outputs produced for recipients will use P2PKH, P2SH, P2WPKH, or +// P2WSH scripts as the pubkey script, based on the format of the recipient +// address. +func (txBuilder TxBuilder) BuildTx(inputs []utxo.Input, recipients []utxo.Recipient) (utxo.Tx, error) { + msgTx := wire.NewMsgTx(Version) + + // Inputs + for _, input := range inputs { + hash := chainhash.Hash{} + copy(hash[:], input.Hash) + index := input.Index.Uint32() + msgTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&hash, index), nil, nil)) + } + + // Outputs + for _, recipient := range recipients { + addr, err := btcutil.DecodeAddress(string(recipient.To), txBuilder.params) + if err != nil { + return nil, err + } + script, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, err + } + value := recipient.Value.Int().Int64() + 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 Peercoin transaction that implements the Peercoin Compat +// API. +type Tx struct { + inputs []utxo.Input + recipients []utxo.Recipient + + msgTx *wire.MsgTx + + signed bool +} + +func (tx *Tx) Hash() (pack.Bytes, error) { + txhash := tx.msgTx.TxHash() + return pack.NewBytes(txhash[:]), nil +} + +func (tx *Tx) Inputs() ([]utxo.Input, error) { + return tx.inputs, nil +} + +func (tx *Tx) Outputs() ([]utxo.Output, error) { + hash, err := tx.Hash() + if err != nil { + return nil, fmt.Errorf("bad hash: %v", err) + } + outputs := make([]utxo.Output, len(tx.msgTx.TxOut)) + for i := range outputs { + outputs[i].Outpoint = utxo.Outpoint{ + Hash: hash, + Index: pack.NewU32(uint32(i)), + } + outputs[i].PubKeyScript = pack.Bytes(tx.msgTx.TxOut[i].PkScript) + if tx.msgTx.TxOut[i].Value < 0 { + return nil, fmt.Errorf("bad output %v: value is less than zero", i) + } + outputs[i].Value = pack.NewU256FromU64(pack.NewU64(uint64(tx.msgTx.TxOut[i].Value))) + } + return outputs, nil +} + +// Sighashes returns the digests that must be signed before the transaction +// can be submitted by the client. All transactions assume that the f +func (tx *Tx) Sighashes() ([]pack.Bytes32, error) { + sighashes := make([]pack.Bytes32, len(tx.inputs)) + + for i, txin := range tx.inputs { + pubKeyScript := txin.PubKeyScript + sigScript := txin.SigScript + value := txin.Value.Int().Int64() + if value < 0 { + return []pack.Bytes32{}, fmt.Errorf("expected value >= 0, got value %v", value) + } + + var hash []byte + var err error + if sigScript == nil { + if txscript.IsPayToWitnessPubKeyHash(pubKeyScript) { + hash, err = txscript.CalcWitnessSigHash(pubKeyScript, txscript.NewTxSigHashes(tx.msgTx), txscript.SigHashAll, tx.msgTx, i, value) + } else { + hash, err = txscript.CalcSignatureHash(pubKeyScript, txscript.SigHashAll, tx.msgTx, i) + } + } else { + if txscript.IsPayToWitnessScriptHash(pubKeyScript) { + hash, err = txscript.CalcWitnessSigHash(sigScript, txscript.NewTxSigHashes(tx.msgTx), txscript.SigHashAll, tx.msgTx, i, value) + } else { + hash, err = txscript.CalcSignatureHash(sigScript, 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 { + var err error + + // Decode the signature and the pubkey script. + r := new(big.Int).SetBytes(rsv[:32]) + s := new(big.Int).SetBytes(rsv[32:64]) + signature := btcec.Signature{ + R: r, + S: s, + } + pubKeyScript := tx.inputs[i].Output.PubKeyScript + sigScript := tx.inputs[i].SigScript + + // Support segwit. + if sigScript == nil { + if txscript.IsPayToWitnessPubKeyHash(pubKeyScript) || txscript.IsPayToWitnessScriptHash(pubKeyScript) { + tx.msgTx.TxIn[i].Witness = wire.TxWitness([][]byte{append(signature.Serialize(), byte(txscript.SigHashAll)), pubKey}) + continue + } + } else { + if txscript.IsPayToWitnessScriptHash(sigScript) || txscript.IsPayToWitnessScriptHash(sigScript) { + tx.msgTx.TxIn[i].Witness = wire.TxWitness([][]byte{append(signature.Serialize(), byte(txscript.SigHashAll)), pubKey, sigScript}) + continue + } + } + + // Support non-segwit + builder := txscript.NewScriptBuilder() + builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll))) + builder.AddData(pubKey) + if sigScript != nil { + builder.AddData(sigScript) + } + tx.msgTx.TxIn[i].SignatureScript, err = builder.Script() + if err != nil { + return err + } + } + + 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 +} diff --git a/chain/peercoin/utxo_test.go b/chain/peercoin/utxo_test.go new file mode 100644 index 00000000..82bc2209 --- /dev/null +++ b/chain/peercoin/utxo_test.go @@ -0,0 +1 @@ +package peercoin_test diff --git a/go.mod b/go.mod index 9d1029f0..2d2a1895 100644 --- a/go.mod +++ b/go.mod @@ -6,31 +6,33 @@ require ( github.com/btcsuite/btcd v0.20.1-beta github.com/btcsuite/btcutil v1.0.2 github.com/codahale/blake2 v0.0.0-20150924215134-8d10d0420cbf - github.com/cosmos/cosmos-sdk v0.39.1 + github.com/cosmos/cosmos-sdk v0.39.1 // indirect github.com/drand/drand v1.0.3-0.20200714175734-29705eaf09d4 // indirect github.com/ethereum/go-ethereum v1.9.19 - github.com/filecoin-project/go-address v0.0.3 + github.com/filecoin-project/go-address v0.0.3 // indirect github.com/filecoin-project/go-amt-ipld v0.0.0-20191205011053-79efc22d6cdc // indirect github.com/filecoin-project/go-amt-ipld/v2 v2.1.0 // indirect github.com/filecoin-project/go-bitfield v0.1.0 // indirect github.com/filecoin-project/go-data-transfer v0.5.0 // indirect github.com/filecoin-project/go-fil-markets v0.3.2 // indirect - github.com/filecoin-project/lotus v0.4.1 + github.com/filecoin-project/lotus v0.4.1 // indirect github.com/filecoin-project/sector-storage v0.0.0-20200723200950-ed2e57dde6df // indirect - github.com/filecoin-project/specs-actors v0.6.2-0.20200724193152-534b25bdca30 + github.com/filecoin-project/specs-actors v0.6.2-0.20200724193152-534b25bdca30 // indirect github.com/hannahhoward/cbor-gen-for v0.0.0-20200723175505-5892b522820a // indirect github.com/ipfs/go-ds-badger2 v0.1.1-0.20200708190120-187fc06f714e // indirect github.com/ipfs/go-hamt-ipld v0.1.1 // indirect github.com/lib/pq v1.7.0 // indirect - github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/onsi/ginkgo v1.14.0 github.com/onsi/gomega v1.10.1 + github.com/ppcsuite/btcutil v0.0.0-20190610081709-8b47fe3bbbff + github.com/ppcsuite/ppcd v0.0.0-20190610081647-e4b42de6f07b github.com/raulk/clock v1.1.0 // indirect github.com/renproject/id v0.4.2 github.com/renproject/pack v0.2.3 github.com/renproject/surge v1.2.5 - github.com/tendermint/tendermint v0.33.8 - github.com/terra-project/core v0.3.7 + github.com/tendermint/tendermint v0.33.8 // indirect + github.com/terra-project/core v0.3.7 // indirect github.com/xorcare/golden v0.6.1-0.20191112154924-b87f686d7542 // indirect go.uber.org/zap v1.15.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 diff --git a/go.sum b/go.sum index 690ddb94..22283960 100644 --- a/go.sum +++ b/go.sum @@ -1072,6 +1072,10 @@ github.com/polydawn/refmt v0.0.0-20190408063855-01bf1e26dd14/go.mod h1:uIp+gprXx github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.0.0-20190809202753-05966cbd336a/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/ppcsuite/btcutil v0.0.0-20190610081709-8b47fe3bbbff h1:T4NTgqtQTBV2EarPGWWamqzRPZJHN2BEkjQbfX6Ovi4= +github.com/ppcsuite/btcutil v0.0.0-20190610081709-8b47fe3bbbff/go.mod h1:JGdyKoOvrtQNw3D9HTVr6tbgBqHtSGred/tI/6K/B5k= +github.com/ppcsuite/ppcd v0.0.0-20190610081647-e4b42de6f07b h1:GG93OuO35ulaSJ4n7TJEMcv/vWFJ14Orne9M8nThBak= +github.com/ppcsuite/ppcd v0.0.0-20190610081647-e4b42de6f07b/go.mod h1:HMuaHlBY91GvRvDCyAuZyohCsojm3M/h0Ksz1hluSCI= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= diff --git a/infra/.env b/infra/.env index bf2515ac..54020076 100644 --- a/infra/.env +++ b/infra/.env @@ -18,16 +18,6 @@ export BITCOIN_ADDRESS=mwjUmhAW68zCtgZpW5b1xD5g7MZew6xPV4 export BITCOINCASH_PK=cSEohZFQLKuemNeBVrzwxniouUJJxdcx7Tm6HpspYuxraVjytieW export BITCOINCASH_ADDRESS=bchreg:qp6tejc0ghtjeejcxa97amzvxvzacjt4qczpy2n3gf -# -# DigiByte -# - -# DigiByte 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 DIGIBYTE_PK=efbJxdzwR1tZD7KWYmKBhmxR6TNb25P9z29ajaoALhkn1LdNe7Ci -export DIGIBYTE_ADDRESS=shb4rf33ozMX8rinHsC6GghrvvA8RKTyGb - # # Dogecoin # @@ -70,3 +60,20 @@ export TERRA_ADDRESS=terra10s4mg25tu6termrk8egltfyme4q7sg3hl8s38u # access to plenty of testing funds. export ZCASH_PK=cNSVbbsAcBQ6BAmMr6yH6DLWr7QTDptHwdzpy4GYxGDkNZeKnczK export ZCASH_ADDRESS=tmCTReBSJEDMWfFCkXXPMSB3EfuPg6SE9dw + +# +# DigiByte +# + +# DigiByte 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 DIGIBYTE_PK=eaeBaSNZmieYKXBwbwjUwzRazT45kVwi9Rubb13RJ1NgfDEUXXH8 +export DIGIBYTE_ADDRESS=snhzjRDRwL5LD4ognxvjaP2St8G3MEExQp + +# +# Peercoin +# + +export PEERCOIN_PK=PJ27Wd2un9TrjDtcmpmx3tPn74xmCcxVy6 +export PEERCOIN_ADDRESS=U7BQX4wWxhoZaNxv91w27Uz7PeJY94Szwjr1U6tGbFENroeVby6N diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml index 9bd5fa9c..258a9850 100644 --- a/infra/docker-compose.yaml +++ b/infra/docker-compose.yaml @@ -1,3 +1,5 @@ +# As in other parts of multichain, chains should be in alphabetical order. + version: "2" services: # @@ -73,29 +75,29 @@ services: - "/root/run.sh" # - # Solana + # Bitcoin # - solana: + peercoin: build: - context: ./solana + context: ./peercoin ports: - - "0.0.0.0:8899:8899" - - "0.0.0.0:8900:8900" - - "0.0.0.0:9900:9900" + - "0.0.0.0:9902:9902" entrypoint: - "./root/run.sh" + - "${PEERCOIN_ADDRESS}" # - # Zcash + # Solana # - zcash: + solana: build: - context: ./zcash + context: ./solana ports: - - "0.0.0.0:18232:18232" + - "0.0.0.0:8899:8899" + - "0.0.0.0:8900:8900" + - "0.0.0.0:9900:9900" entrypoint: - "./root/run.sh" - - "${ZCASH_ADDRESS}" ## ## Terra @@ -108,3 +110,15 @@ services: entrypoint: - "./root/run.sh" - "${TERRA_ADDRESS}" + + # + # Zcash + # + zcash: + build: + context: ./zcash + ports: + - "0.0.0.0:18232:18232" + entrypoint: + - "./root/run.sh" + - "${ZCASH_ADDRESS}" \ No newline at end of file diff --git a/infra/peercoin/Dockerfile b/infra/peercoin/Dockerfile new file mode 100644 index 00000000..50830e2a --- /dev/null +++ b/infra/peercoin/Dockerfile @@ -0,0 +1,18 @@ +FROM debian:10 + +RUN apt-get update +RUN apt-get install -y apt-transport-https wget gnupg + +RUN sh -c "echo 'deb https://peercoin.github.io/deb-repo/ buster main' >> /etc/apt/sources.list.d/peercoin.list" && \ + wget -O - https://peercoin.github.io/deb-repo/peercoin.apt.key | apt-key add - + +RUN apt-get update && \ + apt-get install -y peercoind peercoin-tx + +COPY peercoin.conf /root/.peercoin/peercoin.conf +COPY run.sh /root/run.sh +RUN chmod +x /root/run.sh + +EXPOSE 9901 9902 9903 9904 + +ENTRYPOINT ["./root/run.sh"] diff --git a/infra/peercoin/peercoin.conf b/infra/peercoin/peercoin.conf new file mode 100644 index 00000000..db039118 --- /dev/null +++ b/infra/peercoin/peercoin.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/peercoin/run.sh b/infra/peercoin/run.sh new file mode 100644 index 00000000..dc27826f --- /dev/null +++ b/infra/peercoin/run.sh @@ -0,0 +1,22 @@ +#!/bin/bash +ADDRESS=$1 + +# Start +/app/bin/peercoind +sleep 10 + +# Print setup +echo "BITCOIN_ADDRESS=$ADDRESS" + +# Import the address +/app/bin/peercoin-cli importaddress $ADDRESS + +# Generate enough block to pass the maturation time +/app/bin/peercoin-cli generatetoaddress 101 $ADDRESS + +# Simulate mining +while : +do + /app/bin/peercoin-cli generatetoaddress 1 $ADDRESS + sleep 10 +done \ No newline at end of file diff --git a/multichain.go b/multichain.go index 8441fba1..73d93e20 100644 --- a/multichain.go +++ b/multichain.go @@ -1,5 +1,3 @@ -// Package multichain defines all supported assets and chains. It also -// re-exports the individual multichain APIs. package multichain import ( @@ -62,6 +60,7 @@ const ( SOL = Asset("SOL") // Solana LUNA = Asset("LUNA") // Luna ZEC = Asset("ZEC") // Zcash + PPC = Asset("PPC") // Peercoin ) // OriginChain returns the chain upon which the asset originates. For example, @@ -86,6 +85,8 @@ func (asset Asset) OriginChain() Chain { return Filecoin case LUNA: return Terra + case PPC: + return Peercoin case SOL: return Solana case ZEC: @@ -127,6 +128,7 @@ const ( Dogecoin = Chain("Dogecoin") Ethereum = Chain("Ethereum") Filecoin = Chain("Filecoin") + Peercoin = Chain("Peercoin") Solana = Chain("Solana") Terra = Chain("Terra") Zcash = Chain("Zcash") diff --git a/test.sh b/test.sh index dce14a35..e0b03f1e 100755 --- a/test.sh +++ b/test.sh @@ -1,7 +1,7 @@ -source ./docker/docker-compose.env -docker-compose -f ./docker/docker-compose.yaml up --build -d +source ./infra/.env +docker-compose -f ./infra/docker-compose.yaml up --build -d echo "Waiting for multichain to boot..." sleep 30 go test -v ./... -docker-compose -f ./docker/docker-compose.yaml down +docker-compose -f ./infra/docker-compose.yaml down echo "Done!" \ No newline at end of file