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

Acala Network #54

Open
wants to merge 18 commits 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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
dogecoin \
terra \
zcash
docker run -d -p 1234:1234 -h 0.0.0.0 \
docker run -d -p 1234:1234 -h 0.0.0.0 \
--name infra_filecoin_1 rohitnarurkar/multichain_filecoin:latest

- name: Sleep until the nodes are up
Expand All @@ -86,7 +86,7 @@ jobs:
export PATH=$PATH:$(go env GOPATH)/bin
source ./infra/.env
cd $GITHUB_WORKSPACE
CI=true go test -timeout 1500s
go test -timeout 1500s

build:
runs-on: ubuntu-latest
Expand Down
169 changes: 169 additions & 0 deletions chain/acala/acala.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package acala

import (
"context"
"fmt"

gsrpc "github.com/centrifuge/go-substrate-rpc-client"
"github.com/centrifuge/go-substrate-rpc-client/types"
"github.com/renproject/multichain/api/address"
"github.com/renproject/multichain/api/contract"
"github.com/renproject/pack"
"github.com/renproject/surge"
"go.uber.org/zap"
)

const (
// DefaultClientRPCURL is the default RPC URL used while connecting to an
// Acala node.
DefaultClientRPCURL = "ws://127.0.0.1:9944"
)

// ClientOptions define the options for Acala client.
type ClientOptions struct {
Logger *zap.Logger
rpcURL pack.String
}

// DefaultClientOptions are the Acala client options used by default.
func DefaultClientOptions() ClientOptions {
logger, err := zap.NewDevelopment()
if err != nil {
panic(err)
}
return ClientOptions{
Logger: logger,
rpcURL: DefaultClientRPCURL,
}
}

// WithRPCURL sets the RPC URL in the client options to return a new instance
// of client options.
func (opts ClientOptions) WithRPCURL(rpcURL pack.String) ClientOptions {
opts.rpcURL = rpcURL
return opts
}

// Client represents an Acala client. It interacts with a Substrate node (Acala)
// via the Go-substrate RPC API.
type Client struct {
opts ClientOptions
api gsrpc.SubstrateAPI
}

// NewClient constructs a new Acala client using the client options.
func NewClient(opts ClientOptions) (*Client, error) {
substrateAPI, err := gsrpc.NewSubstrateAPI(string(opts.rpcURL))
if err != nil {
return nil, err
}

return &Client{
opts: opts,
api: *substrateAPI,
}, nil
}

// BurnLogInput defines a structure used to serialize relevant data to fetch
// Acala burn logs from RenVmBridge's system events.
type BurnLogInput struct {
Blockhash pack.Bytes32
ExtSign pack.Bytes
}

// BurnLogOutput defines a structure used to represent burn data from Acala.
type BurnLogOutput struct {
Amount pack.U256
Recipient address.RawAddress
Confs pack.U64
}

// CallContractSystemEvents fetches burn logs from the substrate system events.
func (client *Client) CallContractSystemEvents(_ context.Context, _ address.Address, calldata contract.CallData) (pack.Bytes, error) {
// Deserialise the calldata bytes.
input := BurnLogInput{}
if err := surge.FromBinary(&input, calldata); err != nil {
return pack.Bytes{}, fmt.Errorf("deserialise calldata: %v", err)
}

// Get chain metadata.
meta, err := client.api.RPC.State.GetMetadataLatest()
if err != nil {
return pack.Bytes{}, fmt.Errorf("get metadata: %v", err)
}

// This key is used to read the state storage at the block of interest.
key, err := types.CreateStorageKey(meta, "System", "Events", nil, nil)
if err != nil {
return pack.Bytes{}, fmt.Errorf("create storage key: %v", err)
}

// Get the block in which the burn event was logged.
block, err := client.api.RPC.Chain.GetBlock(types.Hash(input.Blockhash))
if err != nil {
return pack.Bytes{}, fmt.Errorf("get block: %v", err)
}

// Get the latest block header. This will be used to calculate number of block
// confirmations of the burn log of interest.
header, err := client.api.RPC.Chain.GetHeaderLatest()
if err != nil {
return pack.Bytes{}, fmt.Errorf("get header: %v", err)
}

// Retrieve raw bytes from storage at the block and storage key of interest.
data, err := client.api.RPC.State.GetStorageRaw(key, types.Hash(input.Blockhash))
if err != nil {
return pack.Bytes{}, fmt.Errorf("get storage: %v", err)
}

// Fetch the extrinsic's index in the block.
extID := -1
for i, ext := range block.Block.Extrinsics {
if input.ExtSign.Equal(pack.Bytes(ext.Signature.Signature.AsSr25519[:])) {
extID = i
break
}
}
if extID == -1 {
return pack.Bytes{}, fmt.Errorf("extrinsic not found in block")
}

// Decode the event data to get the burn log.
burnEvent, err := decodeEventData(meta, data, uint32(extID))
if err != nil {
return pack.Bytes{}, err
}

// Calculate block confirmations for the event.
confs := header.Number - block.Block.Header.Number + 1

burnLogOutput := BurnLogOutput{
Amount: pack.NewU256FromInt(burnEvent.Amount.Int),
Recipient: address.RawAddress(burnEvent.Dest[:]),
Confs: pack.NewU64(uint64(confs)),
}

out, err := surge.ToBinary(burnLogOutput)
if err != nil {
return pack.Bytes{}, fmt.Errorf("serialise output: %v", err)
}

return pack.Bytes(out), nil
}

func decodeEventData(meta *types.Metadata, data *types.StorageDataRaw, id uint32) (eventBurnt, error) {
events := RenVmBridgeEvents{}
if err := types.EventRecordsRaw(*data).DecodeEventRecords(meta, &events); err != nil {
return eventBurnt{}, fmt.Errorf("decode event data: %v", err)
}

// Match the event to the appropriate extrinsic index.
for _, event := range events.RenVmBridge_Burnt {
if event.Phase.AsApplyExtrinsic == id {
return event, nil
}
}

return eventBurnt{}, fmt.Errorf("burn event not found")
}
13 changes: 13 additions & 0 deletions chain/acala/acala_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package acala_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestSubstratecompat(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Acala Suite")
}
1 change: 1 addition & 0 deletions chain/acala/acala_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package acala_test
91 changes: 91 additions & 0 deletions chain/acala/address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package acala

import (
"fmt"

"github.com/btcsuite/btcutil/base58"
"github.com/renproject/multichain"

"golang.org/x/crypto/blake2b"
)

const (
// AddressTypeDefault is the default address type byte for a substrate chain.
AddressTypeDefault = byte(42)
// AddressTypeTestnet is the address type used for testnet.
AddressTypeTestnet = byte(42)
// AddressTypeCanaryNetwork is the address type used for canary network.
AddressTypeCanaryNetwork = byte(8)
// AddressTypeMainnet is the address type used for mainnet.
AddressTypeMainnet = byte(10)
)

var (
// Prefix used before hashing the address bytes for calculating checksum
Prefix = []byte("SS58PRE")
)

// GetAddressType returns the appropriate prefix address type for a network
// type.
func GetAddressType(network multichain.Network) byte {
switch network {
case multichain.NetworkLocalnet, multichain.NetworkDevnet:
return AddressTypeDefault
case multichain.NetworkTestnet:
return AddressTypeTestnet
case multichain.NetworkMainnet:
return AddressTypeMainnet
default:
return AddressTypeDefault
}
}

// AddressDecoder implements the address.Decoder interface.
type AddressDecoder struct {
addressType byte
}

// AddressEncoder implements the address.Encoder interface.
type AddressEncoder struct {
addressType byte
}

// AddressEncodeDecoder implements the address.EncodeDecoder interface.
type AddressEncodeDecoder struct {
AddressEncoder
AddressDecoder
}

// NewAddressEncodeDecoder constructs a new AddressEncodeDecoder.
func NewAddressEncodeDecoder(addressType byte) AddressEncodeDecoder {
return AddressEncodeDecoder{
AddressEncoder: AddressEncoder{addressType},
AddressDecoder: AddressDecoder{addressType},
}
}

// DecodeAddress the string using the Bitcoin base58 alphabet. The substrate
// address is decoded and only the 32-byte public key is returned as the raw
// address.
func (decoder AddressDecoder) DecodeAddress(addr multichain.Address) (multichain.RawAddress, error) {
data := base58.Decode(string(addr))
if len(data) != 35 {
return multichain.RawAddress([]byte{}), fmt.Errorf("expected 35 bytes, got %v bytes", len(data))
}
return multichain.RawAddress(data[1:33]), nil
}

// EncodeAddress the raw bytes using the Bitcoin base58 alphabet. We expect a
// 32-byte substrate public key as the address in its raw bytes representation.
// A checksum encoded key is then encoded in the base58 format.
func (encoder AddressEncoder) EncodeAddress(rawAddr multichain.RawAddress) (multichain.Address, error) {
if len(rawAddr) != 32 {
return multichain.Address(""), fmt.Errorf("expected 32 bytes, got %v bytes", len(rawAddr))
}
checksummedAddr := append([]byte{encoder.addressType}, rawAddr...)
checksum := blake2b.Sum512(append(Prefix, checksummedAddr...))

checksummedAddr = append(checksummedAddr, checksum[0:2]...)

return multichain.Address(base58.Encode(checksummedAddr)), nil
}
36 changes: 36 additions & 0 deletions chain/acala/address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package acala_test

import (
"github.com/centrifuge/go-substrate-rpc-client/signature"
"github.com/renproject/multichain/api/address"
"github.com/renproject/multichain/chain/acala"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Address", func() {
addrEncodeDecoder := acala.NewAddressEncodeDecoder(acala.AddressTypeDefault)

Context("when encoding raw address", func() {
It("should match the human-readable address", func() {
addr, err := addrEncodeDecoder.EncodeAddress(address.RawAddress(signature.TestKeyringPairAlice.PublicKey))
Expect(err).NotTo(HaveOccurred())
Expect(addr).To(Equal(address.Address(signature.TestKeyringPairAlice.Address)))
rawAddr, err := addrEncodeDecoder.DecodeAddress(addr)
Expect(err).NotTo(HaveOccurred())
Expect(rawAddr).To(Equal(address.RawAddress(signature.TestKeyringPairAlice.PublicKey)))
})
})

Context("when decoding human-readable address", func() {
It("should match the raw address", func() {
rawAddr, err := addrEncodeDecoder.DecodeAddress(address.Address(signature.TestKeyringPairAlice.Address))
Expect(err).NotTo(HaveOccurred())
Expect(rawAddr).To(Equal(address.RawAddress(signature.TestKeyringPairAlice.PublicKey)))
addr, err := addrEncodeDecoder.EncodeAddress(rawAddr)
Expect(err).NotTo(HaveOccurred())
Expect(addr).To(Equal(address.Address(signature.TestKeyringPairAlice.Address)))
})
})
})
Loading