Skip to content

Commit 591c210

Browse files
committed
initial commit
0 parents  commit 591c210

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3940
-0
lines changed

README.md

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# `🔗 multichain`
2+
3+
4+
## Example
5+
6+
The `🔗 multichain` is designed to be flexible enough to support any kind of chain. Anyone is free to contribute to the `🔗 multichain` by adding support for a new chain, or improving support for an existing chain. To show how this is done, we will walk-through an example: adding support for Dogecoin.
7+
8+
### Chains and Assets
9+
10+
Before doing anything else, let's add an enumeration for the `Asset` and `Chain` types, which can be found in `package multichain`. To avoid favouritism, all assets and chains are listed in alphabetical order. Unless otherwise advised by an offiical team member, the names and tickers found on https://coinmarketcap.com must be used.
11+
12+
Adding an `Asset`:
13+
14+
```go
15+
// Enumeration of supported assets. When introducing a new chain, or new asset
16+
// from an existing chain, you must add a human-readable string to this set of
17+
// enumerated values. Assets must be listed in alphabetical order.
18+
const (
19+
BCH = Asset("BCH") // Bitcoin Cash
20+
BTC = Asset("BTC") // Bitcoin
21+
DOGE = Asset("DOGE") // Dogecoin (This is our new asset!)
22+
ETH = Asset("ETH") // Ether
23+
ZEC = Asset("ZEC") // Zcash
24+
)
25+
```
26+
27+
Adding a `Chain`:
28+
29+
```go
30+
// Enumeration of supported chains. When introducing a new chain, you must add a
31+
// human-readable string to this set of enumerated values. Chains must be listed
32+
// in alphabetical order.
33+
const (
34+
Acala = Chain("Acala")
35+
Bitcoin = Chain("Bitcoin")
36+
BitcoinCash = Chain("BitcoinCash")
37+
Dogecoin = Chain("Dogecoin") // (This is our new chain!)
38+
Ethereum = Chain("Ethereum")
39+
Zcash = Chain("Zcash")
40+
)
41+
```
42+
43+
### Docker
44+
45+
Next, we need to setup a Docker container in the `docker/` folder. This is needed for local test suites, allowing for end-to-end integrated testing directly against a node. Doing this requires a couple of steps.
46+
47+
First, we create a new `dogecoin/` folder in the `docker/` folder:
48+
49+
```
50+
docker/
51+
|-- bitcoin/
52+
|-- bitcoincash/
53+
|-- dogecoin/ # This is our new folder!
54+
| |-- Dockerfile # This is our new Dockerfile!
55+
| |-- dogecoin.conf
56+
| |-- run.sh # This is our new run file!
57+
|-- zcash/
58+
|-- docker-compose.env
59+
|-- docker-compose.yaml
60+
```
61+
62+
The new folder _must_ at least contain a `Dockerfile` that installs the node, and a `run.sh` file that runs the nodes. The node _should_ be run in test mode. The new folder can also contain other files that are specific to the needs of the chain being added. In our case, the `dogecoin.conf` file is also needed to configure the node. (We will omit showing all the code here, since there is quite a bit of it, but you can check it out in the `docker/dogecoin/` folder.)
63+
64+
Second, we add an entry to the `docker-compose.env` file. Our entry _must_ include a private key that will have access to funds, and the public address associated with that private key. We will add:
65+
66+
```sh
67+
#
68+
# Dogecoin
69+
#
70+
71+
# Address that will receive mining rewards. Generally, this is set to an address
72+
# for which the private key is known by a test suite. This allows the test suite
73+
# access to plenty of testing funds.
74+
export DOGECOIN_PK=cUJCHRMSUwkcofsHjFWBELT3yEAejokdKhyTNv3DScodYWzztBae
75+
export DOGECOIN_ADDRESS=mwjUmhAW68zCtgZpW5b1xD5g7MZew6xPV4
76+
```
77+
78+
Last, we add a service to the `docker-compose.yaml` file. This allows the node to boot alongside the other nodes in the multichain. This entry must expose the node for use in tests, and must not overlap with other nodes that already exist (ports are reserved on a first-come-first-serve basis). We will define the service as:
79+
80+
```yaml
81+
##
82+
## Dogecoin
83+
##
84+
85+
dogecoin:
86+
build:
87+
context: ./dogecoin
88+
ports:
89+
- "0.0.0.0:18332:18332"
90+
entrypoint:
91+
- "./root/run.sh"
92+
- "${DOGECOIN_ADDRESS}"
93+
```
94+
95+
### Runtime
96+
97+
The final thing that is required before the `🔗 multichain` supports our new chain is an integration into the runtime, defined in `package runtime`. The exact requirements for integration into the runtime vary from chain-to-chain. To make life easier, there is a set of common interfaces, known as the _Compat API_, that can be implemented by new chains. The Compat API is a set of interfaces, designed with the intention for multiple implementations to exist. For example, the Bitcoin Compat API is used by Bitcoin, Bitcoin Cash, and Zcash.
98+
99+
The Compat API is defined by `package compat` (and is used by the `Runtime` type in `package runtime`). All of the interfaces in `package bitcoincompat` belong to the Bitcoin Compat API, all of the interfaces in `package ethereumcompat` belong to the Ethereum Compat API, all of the interfaces in `package substratecompat` belong to the Substrate Compat API, and so on. Similarly, the `BitcoinXX`, `EthereumXXX`, and `SubstrateXXX` methods (defined by the `Runtime` type in `package runtime`) are all abstractions over the respective Compat APIs, but do not need to be modified!
100+
101+
Dogecoin is a fork of Bitcoin, so it is natural that we will support it by implementing the Bitcoin Compat API. Dogecoin is, in fact, so similar to Bitcoin that the implementation is trivial. All implementations belond in `package chain`, so we will create a new `package dogecoin` in that directory. Here, we create the `dogecoin.go` file and fill it with:
102+
103+
```go
104+
package dogecoin
105+
106+
import (
107+
"github.com/renproject/multichain/chain/bitcoin"
108+
"github.com/renproject/multichain/compat/bitcoincompat"
109+
)
110+
111+
// NewTxBuilder returns an implementation of the transaction builder interface
112+
// from the Bitcoin Compat API, and exposes the functionality to build simple
113+
// Dogecoin transactions.
114+
func NewTxBuilder() bitcoincompat.TxBuilder {
115+
return bitcoin.NewTxBuilder()
116+
}
117+
118+
// The Tx type is copied from Bitcoin.
119+
type Tx = bitcoin.Tx
120+
```
121+
122+
For a coin as simple as Dogecoin, nothing else is required! For more complex examples, you can checkout `package bitcoincash` and `package zcash` which need to define their own address and transaction formats.
123+
124+
### Custom Runtimes
125+
126+
Not all chains are as simple as the Dogecoin chain, and an existing Compat API may not be sufficient for your needs. In these scenarios, a little more work is required.
127+
128+
1. Define your own compat package (e.g. `package myawesomechaincompat`) in the `compat/` folder.
129+
2. Define your own compat interfaces in your new compat package.
130+
3. Define your own compat methods on the `Runtime` type in `package runtime`. You will always need a `MyAwesomeChainDecodeAddress(pack.String) (myawesoemcompat.Address, error)` method. If your blockchain is programmable, then defining methods for querying relevant events is usually sufficient. Otherwise, building/submitting transactions is probably going to be required.
131+
132+
If in doubt, get in touch with the Ren team at https://t.me/renproject and we will help you out!
133+
134+
## Test Suite
135+
136+
1. Install Docker
137+
2. Install Docker Compose
138+
3. Run Docker
139+
4. Run `./test.sh`
140+
141+
Example output:
142+
143+
```sh
144+
Creating network "docker_default" with the default driver
145+
Building bitcoin
146+
147+
...
148+
149+
Successfully built 1ebb03faa04f
150+
Successfully tagged docker_bitcoin:latest
151+
Building bitcoincash
152+
153+
...
154+
155+
Successfully built e12e98011869
156+
Successfully tagged docker_bitcoincash:latest
157+
Building zcash
158+
159+
...
160+
161+
Successfully built 56231a29ca2e
162+
Successfully tagged docker_zcash:latest
163+
docker_bitcoin_1 is up-to-date
164+
docker_bitcoincash_1 is up-to-date
165+
docker_zcash_1 is up-to-date
166+
Waiting for multichain to boot...
167+
=== RUN TestMultichain
168+
Running Suite: Multichain Suite
169+
===============================
170+
171+
...
172+
173+
Stopping docker_bitcoincash_1 ... done
174+
Stopping docker_zcash_1 ... done
175+
Stopping docker_bitcoin_1 ... done
176+
Removing docker_bitcoincash_1 ... done
177+
Removing docker_zcash_1 ... done
178+
Removing docker_bitcoin_1 ... done
179+
Removing network docker_default
180+
Done!
181+
```

chain/bitcoin/address.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package bitcoin
2+
3+
import (
4+
"github.com/btcsuite/btcd/chaincfg"
5+
"github.com/btcsuite/btcutil"
6+
"github.com/renproject/multichain/compat/bitcoincompat"
7+
"github.com/renproject/pack"
8+
)
9+
10+
type addressDecoder struct {
11+
defaultNet *chaincfg.Params
12+
}
13+
14+
// NewAddressDecoder returns an implementation of the address decoder interface
15+
// from the Bitcoin Compat API, and exposes the functionality to decode strings
16+
// into addresses.
17+
func NewAddressDecoder(defaultNet *chaincfg.Params) bitcoincompat.AddressDecoder {
18+
return addressDecoder{defaultNet: defaultNet}
19+
}
20+
21+
func (addressDecoder addressDecoder) DecodeAddress(encoded pack.String) (bitcoincompat.Address, error) {
22+
return btcutil.DecodeAddress(encoded.String(), addressDecoder.defaultNet)
23+
}

chain/bitcoin/address_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package bitcoin_test

chain/bitcoin/bitcoin.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package bitcoin
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"math/big"
7+
8+
"github.com/btcsuite/btcd/btcec"
9+
"github.com/btcsuite/btcd/chaincfg/chainhash"
10+
"github.com/btcsuite/btcd/txscript"
11+
"github.com/btcsuite/btcd/wire"
12+
"github.com/renproject/multichain/compat/bitcoincompat"
13+
"github.com/renproject/pack"
14+
)
15+
16+
// Version of Bitcoin transactions supported by the multichain.
17+
const Version int32 = 2
18+
19+
type txBuilder struct{}
20+
21+
// NewTxBuilder returns an implementation of the transaction builder interface
22+
// from the Bitcoin Compat API, and exposes the functionality to build simple
23+
// Bitcoin transactions.
24+
func NewTxBuilder() bitcoincompat.TxBuilder {
25+
return txBuilder{}
26+
}
27+
28+
// BuildTx returns a simple Bitcoin transaction that consumes the funds from the
29+
// given outputs, and sends the to the given recipients. The difference in the
30+
// sum value of the inputs and the sum value of the recipients is paid as a fee
31+
// to the Bitcoin network.
32+
//
33+
// It is assumed that the required signature scripts require the SIGHASH_ALL
34+
// signatures and the serialized public key:
35+
//
36+
// builder := txscript.NewScriptBuilder()
37+
// builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll)))
38+
// builder.AddData(serializedPubKey)
39+
//
40+
// Outputs produced for recipients will use P2PKH, P2SH, P2WPKH, or P2WSH
41+
// scripts as the pubkey script, based on the format of the recipient address.
42+
func (txBuilder) BuildTx(inputs []bitcoincompat.Output, recipients []bitcoincompat.Recipient) (bitcoincompat.Tx, error) {
43+
msgTx := wire.NewMsgTx(Version)
44+
45+
// Inputs
46+
for _, input := range inputs {
47+
hash := chainhash.Hash(input.Outpoint.Hash)
48+
index := input.Outpoint.Index.Uint32()
49+
msgTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&hash, index), nil, nil))
50+
}
51+
52+
// Outputs
53+
for _, recipient := range recipients {
54+
script, err := txscript.PayToAddrScript(recipient.Address)
55+
if err != nil {
56+
return &Tx{}, err
57+
}
58+
value := int64(recipient.Value.Uint64())
59+
if value < 0 {
60+
return &Tx{}, fmt.Errorf("expected value >= 0, got value = %v", value)
61+
}
62+
msgTx.AddTxOut(wire.NewTxOut(value, script))
63+
}
64+
65+
return &Tx{inputs: inputs, recipients: recipients, msgTx: msgTx, signed: false}, nil
66+
}
67+
68+
// Tx represents a simple Bitcoin transaction that implements the Bitcoin Compat
69+
// API.
70+
type Tx struct {
71+
inputs []bitcoincompat.Output
72+
recipients []bitcoincompat.Recipient
73+
74+
msgTx *wire.MsgTx
75+
76+
signed bool
77+
}
78+
79+
func (tx *Tx) Hash() pack.Bytes32 {
80+
return pack.NewBytes32(tx.msgTx.TxHash())
81+
}
82+
83+
func (tx *Tx) Sighashes() ([]pack.Bytes32, error) {
84+
sighashes := make([]pack.Bytes32, len(tx.inputs))
85+
86+
for i := range tx.inputs {
87+
pubKeyScript := tx.inputs[i].PubKeyScript
88+
value := int64(tx.inputs[i].Value.Uint64())
89+
if value < 0 {
90+
return []pack.Bytes32{}, fmt.Errorf("expected value >= 0, got value = %v", value)
91+
}
92+
93+
var hash []byte
94+
var err error
95+
if txscript.IsPayToWitnessPubKeyHash(pubKeyScript) {
96+
hash, err = txscript.CalcWitnessSigHash(pubKeyScript, txscript.NewTxSigHashes(tx.msgTx), txscript.SigHashAll, tx.msgTx, i, value)
97+
} else {
98+
hash, err = txscript.CalcSignatureHash(pubKeyScript, txscript.SigHashAll, tx.msgTx, i)
99+
}
100+
if err != nil {
101+
return []pack.Bytes32{}, err
102+
}
103+
104+
sighash := [32]byte{}
105+
copy(sighash[:], hash)
106+
sighashes[i] = pack.NewBytes32(sighash)
107+
}
108+
109+
return sighashes, nil
110+
}
111+
112+
func (tx *Tx) Sign(signatures []pack.Bytes65, pubKey pack.Bytes) error {
113+
if tx.signed {
114+
return fmt.Errorf("signed")
115+
}
116+
if len(signatures) != len(tx.msgTx.TxIn) {
117+
return fmt.Errorf("expected %v signatures, got %v signatures", len(tx.msgTx.TxIn), len(signatures))
118+
}
119+
120+
for i, rsv := range signatures {
121+
// Decode the signature and the pubkey script.
122+
r := new(big.Int).SetBytes(rsv[:32])
123+
s := new(big.Int).SetBytes(rsv[32:64])
124+
signature := btcec.Signature{
125+
R: r,
126+
S: s,
127+
}
128+
pubKeyScript := tx.inputs[i].PubKeyScript
129+
130+
// Support the consumption of SegWit outputs.
131+
if txscript.IsPayToWitnessPubKeyHash(pubKeyScript) || txscript.IsPayToWitnessScriptHash(pubKeyScript) {
132+
tx.msgTx.TxIn[i].Witness = wire.TxWitness([][]byte{append(signature.Serialize(), byte(txscript.SigHashAll)), pubKey})
133+
continue
134+
}
135+
136+
// Support the consumption of non-SegWite outputs.
137+
builder := txscript.NewScriptBuilder()
138+
builder.AddData(append(signature.Serialize(), byte(txscript.SigHashAll)))
139+
builder.AddData(pubKey)
140+
signatureScript, err := builder.Script()
141+
if err != nil {
142+
return err
143+
}
144+
tx.msgTx.TxIn[i].SignatureScript = signatureScript
145+
}
146+
147+
tx.signed = true
148+
return nil
149+
}
150+
151+
func (tx *Tx) Serialize() (pack.Bytes, error) {
152+
buf := new(bytes.Buffer)
153+
if err := tx.msgTx.Serialize(buf); err != nil {
154+
return pack.Bytes{}, err
155+
}
156+
return pack.NewBytes(buf.Bytes()), nil
157+
}

chain/bitcoin/bitcoin_suite_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package bitcoin_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestBitcoin(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Bitcoin Suite")
13+
}

0 commit comments

Comments
 (0)