From 903a5889235f419506e85dc674e77a34cecb6c72 Mon Sep 17 00:00:00 2001 From: marun Date: Tue, 3 Oct 2023 22:56:03 +0200 Subject: [PATCH] Migrate xsvm from github.com/ava-labs/xsvm (#2045) Co-authored-by: Stephen Buttolph Co-authored-by: Ceyhun Onur Co-authored-by: Sam Liokumovich --- scripts/build_xsvm.sh | 19 + vms/example/xsvm/README.md | 369 +++++++++++++++++++ vms/example/xsvm/api/client.go | 243 ++++++++++++ vms/example/xsvm/api/server.go | 199 ++++++++++ vms/example/xsvm/block/block.go | 37 ++ vms/example/xsvm/block/codec.go | 11 + vms/example/xsvm/builder/builder.go | 139 +++++++ vms/example/xsvm/chain/block.go | 219 +++++++++++ vms/example/xsvm/chain/chain.go | 117 ++++++ vms/example/xsvm/cmd/account/cmd.go | 47 +++ vms/example/xsvm/cmd/account/flags.go | 83 +++++ vms/example/xsvm/cmd/chain/cmd.go | 23 ++ vms/example/xsvm/cmd/chain/create/cmd.go | 85 +++++ vms/example/xsvm/cmd/chain/create/flags.go | 107 ++++++ vms/example/xsvm/cmd/chain/genesis/cmd.go | 56 +++ vms/example/xsvm/cmd/chain/genesis/flags.go | 83 +++++ vms/example/xsvm/cmd/issue/cmd.go | 25 ++ vms/example/xsvm/cmd/issue/export/cmd.go | 70 ++++ vms/example/xsvm/cmd/issue/export/flags.go | 125 +++++++ vms/example/xsvm/cmd/issue/importtx/cmd.go | 135 +++++++ vms/example/xsvm/cmd/issue/importtx/flags.go | 105 ++++++ vms/example/xsvm/cmd/issue/transfer/cmd.go | 69 ++++ vms/example/xsvm/cmd/issue/transfer/flags.go | 119 ++++++ vms/example/xsvm/cmd/run/cmd.go | 25 ++ vms/example/xsvm/cmd/version/cmd.go | 38 ++ vms/example/xsvm/cmd/xsvm/main.go | 37 ++ vms/example/xsvm/constants.go | 21 ++ vms/example/xsvm/execute/block.go | 71 ++++ vms/example/xsvm/execute/expects_context.go | 38 ++ vms/example/xsvm/execute/genesis.go | 51 +++ vms/example/xsvm/execute/tx.go | 178 +++++++++ vms/example/xsvm/factory.go | 17 + vms/example/xsvm/genesis/codec.go | 11 + vms/example/xsvm/genesis/genesis.go | 37 ++ vms/example/xsvm/genesis/genesis_test.go | 35 ++ vms/example/xsvm/state/keys.go | 25 ++ vms/example/xsvm/state/storage.go | 210 +++++++++++ vms/example/xsvm/tx/codec.go | 33 ++ vms/example/xsvm/tx/export.go | 24 ++ vms/example/xsvm/tx/import.go | 18 + vms/example/xsvm/tx/payload.go | 48 +++ vms/example/xsvm/tx/transfer.go | 23 ++ vms/example/xsvm/tx/tx.go | 64 ++++ vms/example/xsvm/tx/unsigned.go | 8 + vms/example/xsvm/tx/visitor.go | 10 + vms/example/xsvm/vm.go | 190 ++++++++++ 46 files changed, 3697 insertions(+) create mode 100755 scripts/build_xsvm.sh create mode 100644 vms/example/xsvm/README.md create mode 100644 vms/example/xsvm/api/client.go create mode 100644 vms/example/xsvm/api/server.go create mode 100644 vms/example/xsvm/block/block.go create mode 100644 vms/example/xsvm/block/codec.go create mode 100644 vms/example/xsvm/builder/builder.go create mode 100644 vms/example/xsvm/chain/block.go create mode 100644 vms/example/xsvm/chain/chain.go create mode 100644 vms/example/xsvm/cmd/account/cmd.go create mode 100644 vms/example/xsvm/cmd/account/flags.go create mode 100644 vms/example/xsvm/cmd/chain/cmd.go create mode 100644 vms/example/xsvm/cmd/chain/create/cmd.go create mode 100644 vms/example/xsvm/cmd/chain/create/flags.go create mode 100644 vms/example/xsvm/cmd/chain/genesis/cmd.go create mode 100644 vms/example/xsvm/cmd/chain/genesis/flags.go create mode 100644 vms/example/xsvm/cmd/issue/cmd.go create mode 100644 vms/example/xsvm/cmd/issue/export/cmd.go create mode 100644 vms/example/xsvm/cmd/issue/export/flags.go create mode 100644 vms/example/xsvm/cmd/issue/importtx/cmd.go create mode 100644 vms/example/xsvm/cmd/issue/importtx/flags.go create mode 100644 vms/example/xsvm/cmd/issue/transfer/cmd.go create mode 100644 vms/example/xsvm/cmd/issue/transfer/flags.go create mode 100644 vms/example/xsvm/cmd/run/cmd.go create mode 100644 vms/example/xsvm/cmd/version/cmd.go create mode 100644 vms/example/xsvm/cmd/xsvm/main.go create mode 100644 vms/example/xsvm/constants.go create mode 100644 vms/example/xsvm/execute/block.go create mode 100644 vms/example/xsvm/execute/expects_context.go create mode 100644 vms/example/xsvm/execute/genesis.go create mode 100644 vms/example/xsvm/execute/tx.go create mode 100644 vms/example/xsvm/factory.go create mode 100644 vms/example/xsvm/genesis/codec.go create mode 100644 vms/example/xsvm/genesis/genesis.go create mode 100644 vms/example/xsvm/genesis/genesis_test.go create mode 100644 vms/example/xsvm/state/keys.go create mode 100644 vms/example/xsvm/state/storage.go create mode 100644 vms/example/xsvm/tx/codec.go create mode 100644 vms/example/xsvm/tx/export.go create mode 100644 vms/example/xsvm/tx/import.go create mode 100644 vms/example/xsvm/tx/payload.go create mode 100644 vms/example/xsvm/tx/transfer.go create mode 100644 vms/example/xsvm/tx/tx.go create mode 100644 vms/example/xsvm/tx/unsigned.go create mode 100644 vms/example/xsvm/tx/visitor.go create mode 100644 vms/example/xsvm/vm.go diff --git a/scripts/build_xsvm.sh b/scripts/build_xsvm.sh new file mode 100755 index 000000000000..d67e2cbc2075 --- /dev/null +++ b/scripts/build_xsvm.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! [[ "$0" =~ scripts/build_xsvm.sh ]]; then + echo "must be run from repository root" + exit 255 +fi + +source ./scripts/constants.sh + +echo "Building xsvm plugin..." +go build -o ./build/xsvm ./vms/example/xsvm/cmd/xsvm/ + +PLUGIN_DIR="$HOME/.avalanchego/plugins" +PLUGIN_PATH="${PLUGIN_DIR}/v3m4wPxaHpvGr8qfMeyK6PRW3idZrPHmYcMTt7oXdK47yurVH" +echo "Symlinking ./build/xsvm to ${PLUGIN_PATH}" +mkdir -p "${PLUGIN_DIR}" +ln -sf "${PWD}/build/xsvm" "${PLUGIN_PATH}" diff --git a/vms/example/xsvm/README.md b/vms/example/xsvm/README.md new file mode 100644 index 000000000000..6f30f7b7e756 --- /dev/null +++ b/vms/example/xsvm/README.md @@ -0,0 +1,369 @@ +# Cross Subnet Virtual Machine (XSVM) + +Cross Subnet Asset Transfers README Overview + +[Background](#avalanche-subnets-and-custom-vms) + +[Introduction](#introduction) + +[Usage](#how-it-works) + +[Running](#running-the-vm) + +[Demo](#cross-subnet-transaction-example) + +## Avalanche Subnets and Custom VMs + +Avalanche is a network composed of multiple sub-networks (called [subnets][Subnet]) that each contain any number of blockchains. Each blockchain is an instance of a [Virtual Machine (VM)](https://docs.avax.network/learn/platform-overview#virtual-machines), much like an object in an object-oriented language is an instance of a class. That is, the VM defines the behavior of the blockchain where it is instantiated. For example, [Coreth (EVM)][Coreth] is a VM that is instantiated by the [C-Chain]. Likewise, one could deploy another instance of the EVM as their own blockchain (to take this to its logical conclusion). + +## Introduction + +Just as [Coreth] powers the [C-Chain], XSVM can be used to power its own blockchain in an Avalanche [Subnet]. Instead of providing a place to execute Solidity smart contracts, however, XSVM enables asset transfers for assets originating on it's own chain or other XSVM chains on other subnets. + +## How it Works + +XSVM utilizes AvalancheGo's [teleporter] package to create and authenticate Subnet Messages. + +### Transfer + +If you want to send an asset to someone, you can use a `tx.Transfer` to send to any address. + +### Export + +If you want to send this chain's native asset to a different subnet, you can use a `tx.Export` to send to any address on a destination chain. You may also use a `tx.Export` to return the destination chain's native asset. + +### Import + +To receive assets from another chain's `tx.Export`, you must issue a `tx.Import`. Note that, similarly to a bridge, the security of the other chain's native asset is tied to the other chain. The security of all other assets on this chain are unrelated to the other chain. + +### Fees + +Currently there are no fees enforced in the XSVM. + +### xsvm + +#### Install + +```bash +git clone https://github.com/ava-labs/avalanchego.git; +cd avalanchego; +go install -v ./vms/example/xsvm/cmd/xsvm; +``` + +#### Usage + +``` +Runs an XSVM plugin + +Usage: + xsvm [flags] + xsvm [command] + +Available Commands: + account Displays the state of the requested account + chain Manages XS chains + completion Generate the autocompletion script for the specified shell + help Help about any command + issue Issues transactions + version Prints out the version + +Flags: + -h, --help help for xsvm + +Use "xsvm [command] --help" for more information about a command. +``` + +### [Golang SDK](https://github.com/ava-labs/avalanchego/blob/master/vms/example/xsvm/client/client.go) + +```golang +// Client defines xsvm client operations. +type Client interface { + Network( + ctx context.Context, + options ...rpc.Option, + ) (uint32, ids.ID, ids.ID, error) + Genesis( + ctx context.Context, + options ...rpc.Option, + ) (*genesis.Genesis, error) + Nonce( + ctx context.Context, + address ids.ShortID, + options ...rpc.Option, + ) (uint64, error) + Balance( + ctx context.Context, + address ids.ShortID, + assetID ids.ID, + options ...rpc.Option, + ) (uint64, error) + Loan( + ctx context.Context, + chainID ids.ID, + options ...rpc.Option, + ) (uint64, error) + IssueTx( + ctx context.Context, + tx *tx.Tx, + options ...rpc.Option, + ) (ids.ID, error) + LastAccepted( + ctx context.Context, + options ...rpc.Option, + ) (ids.ID, *block.Stateless, error) + Block( + ctx context.Context, + blkID ids.ID, + options ...rpc.Option, + (*block.Stateless, error) + Message( + ctx context.Context, + txID ids.ID, + options ...rpc.Option, + ) (*teleporter.UnsignedMessage, []byte, error) +} +``` + +### Public Endpoints + +#### xsvm.network + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.network", + "params":{}, + "id": 1 +} +>>> {"networkID":, "subnetID":, "chainID":} +``` + +For example: + +```bash +curl --location --request POST 'http://34.235.54.228:9650/ext/bc/28iioW2fYMBnKv24VG5nw9ifY2PsFuwuhxhyzxZB5MmxDd3rnT' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc": "2.0", + "method": "xsvm.network", + "params":{}, + "id": 1 +}' +``` + +> `{"jsonrpc":"2.0","result":{"networkID":1000000,"subnetID":"2gToFoYXURMQ6y4ZApFuRZN1HurGcDkwmtvkcMHNHcYarvsJN1","chainID":"28iioW2fYMBnKv24VG5nw9ifY2PsFuwuhxhyzxZB5MmxDd3rnT"},"id":1}` + +#### xsvm.genesis + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.genesis", + "params":{}, + "id": 1 +} +>>> {"genesis":} +``` + +#### xsvm.nonce + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.nonce", + "params":{ + "address": + }, + "id": 1 +} +>>> {"nonce":} +``` + +#### xsvm.balance + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.balance", + "params":{ + "address":, + "assetID": + }, + "id": 1 +} +>>> {"balance":} +``` + +#### xsvm.loan + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.loan", + "params":{ + "chainID": + }, + "id": 1 +} +>>> {"amount":} +``` + +#### xsvm.issueTx + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.issueTx", + "params":{ + "tx": + }, + "id": 1 +} +>>> {"txID":} +``` + +#### xsvm.lastAccepted + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.lastAccepted", + "params":{}, + "id": 1 +} +>>> {"blockID":, "block":} +``` + +#### xsvm.block + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.block", + "params":{ + "blockID": + }, + "id": 1 +} +>>> {"block":} +``` + +#### xsvm.message + +``` +<<< POST +{ + "jsonrpc": "2.0", + "method": "xsvm.message", + "params":{ + "txID": + }, + "id": 1 +} +>>> {"message":, "signature":} +``` + +## Running the VM + +To build the VM, run `./scripts/build_xsvm.sh`. + +### Deploying Your Own Network + +Anyone can deploy their own instance of the XSVM as a subnet on Avalanche. All you need to do is compile it, create a genesis, and send a few txs to the +P-Chain. + +You can do this by following the [subnet tutorial] or by using the [subnet-cli]. + +[teleporter]: https://github.com/ava-labs/avalanchego/tree/master/vms/platformvm/teleporter +[subnet tutorial]: https://docs.avax.network/build/tutorials/platform/subnets/create-a-subnet +[subnet-cli]: https://github.com/ava-labs/subnet-cli +[Coreth]: https://github.com/ava-labs/coreth +[C-Chain]: https://docs.avax.network/learn/platform-overview/#contract-chain-c-chain +[Subnet]: https://docs.avax.network/learn/platform-overview/#subnets + +## Cross Subnet Transaction Example + +The following example shows how to interact with the XSVM to send and receive native assets across subnets. + +### Overview of Steps + +1. Create & deploy Subnet A +2. Create & deploy Subnet B +3. Issue an **export** Tx on Subnet A +4. Issue an **import** Tx on Subnet B +5. Confirm Txs processed correctly + +> **Note:** This demo requires [avalanche-cli](https://github.com/ava-labs/avalanche-cli) version > 1.0.5, [xsvm](https://github.com/ava-labs/xsvm) version > 1.0.2 and [avalanche-network-runner](https://github.com/ava-labs/avalanche-network-runner) v1.3.5. + +### Create and Deploy Subnet A, Subnet B + +Using the avalanche-cli, this step deploys two subnets running the XSVM. Subnet A will act as the sender in this demo, and Subnet B will act as the receiver. + +Steps + +Build the [XSVM](https://github.com/ava-labs/xsvm) + +### Create a genesis file + +```bash +xsvm chain genesis --encoding binary > xsvm.genesis +``` + +### Create Subnet A and Subnet B + +```bash +avalanche subnet create subnetA --custom --genesis --vm +avalanche subnet create subnetB --custom --genesis --vm +``` + +### Deploy Subnet A and Subnet B + +```bash +avalanche subnet deploy subnetA --local +avalanche subnet deploy subnetB --local +``` + +### Issue Export Tx from Subnet A + +The SubnetID and ChainIDs are stored in the sidecar.json files in your avalanche-cli directory. Typically this is located at $HOME/.avalanche/subnets/ + +```bash +xsvm issue export --source-chain-id --amount --destination-chain-id +``` + +Save the TxID printed out by running the export command. + +### Issue Import Tx from Subnet B + +> Note: The import tx requires **snowman++** consensus to be activated on the importing chain. A chain requires ~3 blocks to be produced for snowman++ to start. +> Run `xsvm issue transfer --chain-id --amount 1000` to issue simple Txs on SubnetB + +```bash +xsvm issue import --source-chain-id --destination-chain-id --tx-id --source-uris +``` + +> The can be found by running `avalanche network status`. The default URIs are +"http://localhost:9650,http://localhost:9652,http://localhost:9654,http://localhost:9656,http://localhost:9658" + +**Account Values** +To check proper execution, use the `xsvm account` command to check balances. + +Verify the balance on SubnetA decreased by your export amount using + +```bash +xsvm account --chain-id +``` + +Now verify chain A's assets were successfully imported to SubnetB + +```bash +xsvm account --chain-id --asset-id +``` diff --git a/vms/example/xsvm/api/client.go b/vms/example/xsvm/api/client.go new file mode 100644 index 000000000000..785b092faed1 --- /dev/null +++ b/vms/example/xsvm/api/client.go @@ -0,0 +1,243 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package api + +import ( + "context" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/rpc" + "github.com/ava-labs/avalanchego/vms/example/xsvm/block" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +// Client defines the xsvm API client. +type Client interface { + Network( + ctx context.Context, + options ...rpc.Option, + ) (uint32, ids.ID, ids.ID, error) + Genesis( + ctx context.Context, + options ...rpc.Option, + ) (*genesis.Genesis, error) + Nonce( + ctx context.Context, + address ids.ShortID, + options ...rpc.Option, + ) (uint64, error) + Balance( + ctx context.Context, + address ids.ShortID, + assetID ids.ID, + options ...rpc.Option, + ) (uint64, error) + Loan( + ctx context.Context, + chainID ids.ID, + options ...rpc.Option, + ) (uint64, error) + IssueTx( + ctx context.Context, + tx *tx.Tx, + options ...rpc.Option, + ) (ids.ID, error) + LastAccepted( + ctx context.Context, + options ...rpc.Option, + ) (ids.ID, *block.Stateless, error) + Block( + ctx context.Context, + blkID ids.ID, + options ...rpc.Option, + ) (*block.Stateless, error) + Message( + ctx context.Context, + txID ids.ID, + options ...rpc.Option, + ) (*warp.UnsignedMessage, []byte, error) +} + +func NewClient(uri, chain string) Client { + path := fmt.Sprintf( + "%s/ext/%s/%s", + uri, + constants.ChainAliasPrefix, + chain, + ) + return &client{ + req: rpc.NewEndpointRequester(path), + } +} + +type client struct { + req rpc.EndpointRequester +} + +func (c *client) Network( + ctx context.Context, + options ...rpc.Option, +) (uint32, ids.ID, ids.ID, error) { + resp := new(NetworkReply) + err := c.req.SendRequest( + ctx, + "xsvm.network", + nil, + resp, + options..., + ) + return resp.NetworkID, resp.SubnetID, resp.ChainID, err +} + +func (c *client) Genesis( + ctx context.Context, + options ...rpc.Option, +) (*genesis.Genesis, error) { + resp := new(GenesisReply) + err := c.req.SendRequest( + ctx, + "xsvm.genesis", + nil, + resp, + options..., + ) + return resp.Genesis, err +} + +func (c *client) Nonce( + ctx context.Context, + address ids.ShortID, + options ...rpc.Option, +) (uint64, error) { + resp := new(NonceReply) + err := c.req.SendRequest( + ctx, + "xsvm.nonce", + &NonceArgs{ + Address: address, + }, + resp, + options..., + ) + return resp.Nonce, err +} + +func (c *client) Balance( + ctx context.Context, + address ids.ShortID, + assetID ids.ID, + options ...rpc.Option, +) (uint64, error) { + resp := new(BalanceReply) + err := c.req.SendRequest( + ctx, + "xsvm.balance", + &BalanceArgs{ + Address: address, + AssetID: assetID, + }, + resp, + options..., + ) + return resp.Balance, err +} + +func (c *client) Loan( + ctx context.Context, + chainID ids.ID, + options ...rpc.Option, +) (uint64, error) { + resp := new(LoanReply) + err := c.req.SendRequest( + ctx, + "xsvm.loan", + &LoanArgs{ + ChainID: chainID, + }, + resp, + options..., + ) + return resp.Amount, err +} + +func (c *client) IssueTx( + ctx context.Context, + newTx *tx.Tx, + options ...rpc.Option, +) (ids.ID, error) { + txBytes, err := tx.Codec.Marshal(tx.Version, newTx) + if err != nil { + return ids.Empty, err + } + + resp := new(IssueTxReply) + err = c.req.SendRequest( + ctx, + "xsvm.issueTx", + &IssueTxArgs{ + Tx: txBytes, + }, + resp, + options..., + ) + return resp.TxID, err +} + +func (c *client) LastAccepted( + ctx context.Context, + options ...rpc.Option, +) (ids.ID, *block.Stateless, error) { + resp := new(LastAcceptedReply) + err := c.req.SendRequest( + ctx, + "xsvm.lastAccepted", + nil, + resp, + options..., + ) + return resp.BlockID, resp.Block, err +} + +func (c *client) Block( + ctx context.Context, + blkID ids.ID, + options ...rpc.Option, +) (*block.Stateless, error) { + resp := new(BlockReply) + err := c.req.SendRequest( + ctx, + "xsvm.lastAccepted", + &BlockArgs{ + BlockID: blkID, + }, + resp, + options..., + ) + return resp.Block, err +} + +func (c *client) Message( + ctx context.Context, + txID ids.ID, + options ...rpc.Option, +) (*warp.UnsignedMessage, []byte, error) { + resp := new(MessageReply) + err := c.req.SendRequest( + ctx, + "xsvm.message", + &MessageArgs{ + TxID: txID, + }, + resp, + options..., + ) + if err != nil { + return nil, nil, err + } + return resp.Message, resp.Signature, resp.Message.Initialize() +} diff --git a/vms/example/xsvm/api/server.go b/vms/example/xsvm/api/server.go new file mode 100644 index 000000000000..e094563108df --- /dev/null +++ b/vms/example/xsvm/api/server.go @@ -0,0 +1,199 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package api + +import ( + "net/http" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/example/xsvm/block" + "github.com/ava-labs/avalanchego/vms/example/xsvm/builder" + "github.com/ava-labs/avalanchego/vms/example/xsvm/chain" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/example/xsvm/state" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +// Server defines the xsvm API server. +type Server interface { + Network(r *http.Request, args *struct{}, reply *NetworkReply) error + Genesis(r *http.Request, args *struct{}, reply *GenesisReply) error + Nonce(r *http.Request, args *NonceArgs, reply *NonceReply) error + Balance(r *http.Request, args *BalanceArgs, reply *BalanceReply) error + Loan(r *http.Request, args *LoanArgs, reply *LoanReply) error + IssueTx(r *http.Request, args *IssueTxArgs, reply *IssueTxReply) error + LastAccepted(r *http.Request, args *struct{}, reply *LastAcceptedReply) error + Block(r *http.Request, args *BlockArgs, reply *BlockReply) error + Message(r *http.Request, args *MessageArgs, reply *MessageReply) error +} + +func NewServer( + ctx *snow.Context, + genesis *genesis.Genesis, + state database.KeyValueReader, + chain chain.Chain, + builder builder.Builder, +) Server { + return &server{ + ctx: ctx, + genesis: genesis, + state: state, + chain: chain, + builder: builder, + } +} + +type server struct { + ctx *snow.Context + genesis *genesis.Genesis + state database.KeyValueReader + chain chain.Chain + builder builder.Builder +} + +type NetworkReply struct { + NetworkID uint32 `json:"networkID"` + SubnetID ids.ID `json:"subnetID"` + ChainID ids.ID `json:"chainID"` +} + +func (s *server) Network(_ *http.Request, _ *struct{}, reply *NetworkReply) error { + reply.NetworkID = s.ctx.NetworkID + reply.SubnetID = s.ctx.SubnetID + reply.ChainID = s.ctx.ChainID + return nil +} + +type GenesisReply struct { + Genesis *genesis.Genesis `json:"genesis"` +} + +func (s *server) Genesis(_ *http.Request, _ *struct{}, reply *GenesisReply) error { + reply.Genesis = s.genesis + return nil +} + +type NonceArgs struct { + Address ids.ShortID `json:"address"` +} + +type NonceReply struct { + Nonce uint64 `json:"nonce"` +} + +func (s *server) Nonce(_ *http.Request, args *NonceArgs, reply *NonceReply) error { + nonce, err := state.GetNonce(s.state, args.Address) + reply.Nonce = nonce + return err +} + +type BalanceArgs struct { + Address ids.ShortID `json:"address"` + AssetID ids.ID `json:"assetID"` +} + +type BalanceReply struct { + Balance uint64 `json:"balance"` +} + +func (s *server) Balance(_ *http.Request, args *BalanceArgs, reply *BalanceReply) error { + balance, err := state.GetBalance(s.state, args.Address, args.AssetID) + reply.Balance = balance + return err +} + +type LoanArgs struct { + ChainID ids.ID `json:"chainID"` +} + +type LoanReply struct { + Amount uint64 `json:"amount"` +} + +func (s *server) Loan(_ *http.Request, args *LoanArgs, reply *LoanReply) error { + amount, err := state.GetLoan(s.state, args.ChainID) + reply.Amount = amount + return err +} + +type IssueTxArgs struct { + Tx []byte `json:"tx"` +} + +type IssueTxReply struct { + TxID ids.ID `json:"txID"` +} + +func (s *server) IssueTx(r *http.Request, args *IssueTxArgs, reply *IssueTxReply) error { + newTx, err := tx.Parse(args.Tx) + if err != nil { + return err + } + + ctx := r.Context() + if err := s.builder.AddTx(ctx, newTx); err != nil { + return err + } + + txID, err := newTx.ID() + reply.TxID = txID + return err +} + +type LastAcceptedReply struct { + BlockID ids.ID `json:"blockID"` + Block *block.Stateless `json:"block"` +} + +func (s *server) LastAccepted(_ *http.Request, _ *struct{}, reply *LastAcceptedReply) error { + reply.BlockID = s.chain.LastAccepted() + blkBytes, err := state.GetBlock(s.state, reply.BlockID) + if err != nil { + return err + } + + reply.Block, err = block.Parse(blkBytes) + return err +} + +type BlockArgs struct { + BlockID ids.ID `json:"blockID"` +} + +type BlockReply struct { + Block *block.Stateless `json:"block"` +} + +func (s *server) Block(_ *http.Request, args *BlockArgs, reply *BlockReply) error { + blkBytes, err := state.GetBlock(s.state, args.BlockID) + if err != nil { + return err + } + + reply.Block, err = block.Parse(blkBytes) + return err +} + +type MessageArgs struct { + TxID ids.ID `json:"txID"` +} + +type MessageReply struct { + Message *warp.UnsignedMessage `json:"message"` + Signature []byte `json:"signature"` +} + +func (s *server) Message(_ *http.Request, args *MessageArgs, reply *MessageReply) error { + message, err := state.GetMessage(s.state, args.TxID) + if err != nil { + return err + } + + reply.Message = message + reply.Signature, err = s.ctx.WarpSigner.Sign(message) + return err +} diff --git a/vms/example/xsvm/block/block.go b/vms/example/xsvm/block/block.go new file mode 100644 index 000000000000..2db314ea1a5a --- /dev/null +++ b/vms/example/xsvm/block/block.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package block + +import ( + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" +) + +// Stateless blocks are blocks as they are marshalled/unmarshalled and sent over +// the p2p network. The stateful blocks which can be executed are built from +// Stateless blocks. +type Stateless struct { + ParentID ids.ID `serialize:"true" json:"parentID"` + Timestamp int64 `serialize:"true" json:"timestamp"` + Height uint64 `serialize:"true" json:"height"` + Txs []*tx.Tx `serialize:"true" json:"txs"` +} + +func (b *Stateless) Time() time.Time { + return time.Unix(b.Timestamp, 0) +} + +func (b *Stateless) ID() (ids.ID, error) { + bytes, err := Codec.Marshal(Version, b) + return hashing.ComputeHash256Array(bytes), err +} + +func Parse(bytes []byte) (*Stateless, error) { + blk := &Stateless{} + _, err := Codec.Unmarshal(bytes, blk) + return blk, err +} diff --git a/vms/example/xsvm/block/codec.go b/vms/example/xsvm/block/codec.go new file mode 100644 index 000000000000..0ffbc98d58e7 --- /dev/null +++ b/vms/example/xsvm/block/codec.go @@ -0,0 +1,11 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package block + +import "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" + +// Version is the current default codec version +const Version = tx.Version + +var Codec = tx.Codec diff --git a/vms/example/xsvm/builder/builder.go b/vms/example/xsvm/builder/builder.go new file mode 100644 index 000000000000..7135985d3707 --- /dev/null +++ b/vms/example/xsvm/builder/builder.go @@ -0,0 +1,139 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package builder + +import ( + "context" + "time" + + "github.com/ava-labs/avalanchego/database/versiondb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/vms/example/xsvm/chain" + "github.com/ava-labs/avalanchego/vms/example/xsvm/execute" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" + + smblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + xsblock "github.com/ava-labs/avalanchego/vms/example/xsvm/block" +) + +const MaxTxsPerBlock = 10 + +var _ Builder = (*builder)(nil) + +type Builder interface { + SetPreference(preferred ids.ID) + AddTx(ctx context.Context, tx *tx.Tx) error + BuildBlock(ctx context.Context, blockContext *smblock.Context) (chain.Block, error) +} + +type builder struct { + chainContext *snow.Context + engineChan chan<- common.Message + chain chain.Chain + + pendingTxs linkedhashmap.LinkedHashmap[ids.ID, *tx.Tx] + preference ids.ID +} + +func New(chainContext *snow.Context, engineChan chan<- common.Message, chain chain.Chain) Builder { + return &builder{ + chainContext: chainContext, + engineChan: engineChan, + chain: chain, + + pendingTxs: linkedhashmap.New[ids.ID, *tx.Tx](), + preference: chain.LastAccepted(), + } +} + +func (b *builder) SetPreference(preferred ids.ID) { + b.preference = preferred +} + +func (b *builder) AddTx(_ context.Context, newTx *tx.Tx) error { + // TODO: verify [tx] against the currently preferred state + txID, err := newTx.ID() + if err != nil { + return err + } + b.pendingTxs.Put(txID, newTx) + select { + case b.engineChan <- common.PendingTxs: + default: + } + return nil +} + +func (b *builder) BuildBlock(ctx context.Context, blockContext *smblock.Context) (chain.Block, error) { + preferredBlk, err := b.chain.GetBlock(b.preference) + if err != nil { + return nil, err + } + + preferredState, err := preferredBlk.State() + if err != nil { + return nil, err + } + + defer func() { + if b.pendingTxs.Len() == 0 { + return + } + select { + case b.engineChan <- common.PendingTxs: + default: + } + }() + + parentTimestamp := preferredBlk.Timestamp() + timestamp := time.Now().Truncate(time.Second) + if timestamp.Before(parentTimestamp) { + timestamp = parentTimestamp + } + + wipBlock := xsblock.Stateless{ + ParentID: b.preference, + Timestamp: timestamp.Unix(), + Height: preferredBlk.Height() + 1, + } + + currentState := versiondb.New(preferredState) + for len(wipBlock.Txs) < MaxTxsPerBlock { + txID, currentTx, exists := b.pendingTxs.Oldest() + if !exists { + break + } + b.pendingTxs.Delete(txID) + + sender, err := currentTx.SenderID() + if err != nil { + // This tx was invalid, drop it and continue block building + continue + } + + txState := versiondb.New(currentState) + txExecutor := execute.Tx{ + Context: ctx, + ChainContext: b.chainContext, + Database: txState, + BlockContext: blockContext, + TxID: txID, + Sender: sender, + // TODO: populate fees + } + if err := currentTx.Unsigned.Visit(&txExecutor); err != nil { + // This tx was invalid, drop it and continue block building + continue + } + if err := txState.Commit(); err != nil { + return nil, err + } + + wipBlock.Txs = append(wipBlock.Txs, currentTx) + } + return b.chain.NewBlock(&wipBlock) +} diff --git a/vms/example/xsvm/chain/block.go b/vms/example/xsvm/chain/block.go new file mode 100644 index 000000000000..8eb660e23c12 --- /dev/null +++ b/vms/example/xsvm/chain/block.go @@ -0,0 +1,219 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chain + +import ( + "context" + "errors" + "time" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/versiondb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/example/xsvm/execute" + "github.com/ava-labs/avalanchego/vms/example/xsvm/state" + + smblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + xsblock "github.com/ava-labs/avalanchego/vms/example/xsvm/block" +) + +const maxClockSkew = 10 * time.Second + +var ( + _ Block = (*block)(nil) + + errMissingParent = errors.New("missing parent block") + errMissingChild = errors.New("missing child block") + errParentNotVerified = errors.New("parent block has not been verified") + errMissingState = errors.New("missing state") + errFutureTimestamp = errors.New("future timestamp") + errTimestampBeforeParent = errors.New("timestamp before parent") + errWrongHeight = errors.New("wrong height") +) + +type Block interface { + snowman.Block + smblock.WithVerifyContext + + // State intends to return the new chain state following this block's + // acceptance. The new chain state is built (but not persisted) following a + // block's verification to allow block's descendants verification before + // being accepted. + State() (database.Database, error) +} + +type block struct { + *xsblock.Stateless + + chain *chain + + id ids.ID + status choices.Status + bytes []byte + + state *versiondb.Database + verifiedChildrenIDs set.Set[ids.ID] +} + +func (b *block) ID() ids.ID { + return b.id +} + +func (b *block) Status() choices.Status { + if !b.status.Decided() { + b.status = b.calculateStatus() + } + return b.status +} + +func (b *block) Parent() ids.ID { + return b.ParentID +} + +func (b *block) Bytes() []byte { + return b.bytes +} + +func (b *block) Height() uint64 { + return b.Stateless.Height +} + +func (b *block) Timestamp() time.Time { + return b.Time() +} + +func (b *block) Verify(ctx context.Context) error { + return b.VerifyWithContext(ctx, nil) +} + +func (b *block) Accept(context.Context) error { + if err := b.state.Commit(); err != nil { + return err + } + + // Following this block's acceptance, make sure that it's direct children + // point to the base state, which now also contains this block's changes. + for childID := range b.verifiedChildrenIDs { + child, exists := b.chain.verifiedBlocks[childID] + if !exists { + return errMissingChild + } + if err := child.state.SetDatabase(b.chain.acceptedState); err != nil { + return err + } + } + + b.status = choices.Accepted + b.chain.lastAccepted = b.id + delete(b.chain.verifiedBlocks, b.ParentID) + return nil +} + +func (b *block) Reject(context.Context) error { + b.status = choices.Rejected + delete(b.chain.verifiedBlocks, b.id) + + // TODO: push transactions back into the mempool + return nil +} + +func (b *block) ShouldVerifyWithContext(context.Context) (bool, error) { + return execute.ExpectsContext(b.Stateless) +} + +func (b *block) VerifyWithContext(ctx context.Context, blockContext *smblock.Context) error { + timestamp := b.Time() + if time.Until(timestamp) > maxClockSkew { + return errFutureTimestamp + } + + // parent block must be verified or accepted + parent, exists := b.chain.verifiedBlocks[b.ParentID] + if !exists { + return errMissingParent + } + + if b.Stateless.Height != parent.Stateless.Height+1 { + return errWrongHeight + } + + parentTimestamp := parent.Time() + if timestamp.Before(parentTimestamp) { + return errTimestampBeforeParent + } + + parentState, err := parent.State() + if err != nil { + return err + } + + // This block's state is a versionDB built on top of it's parent state. This + // block's changes are pushed atomically to the parent state when accepted. + blkState := versiondb.New(parentState) + err = execute.Block( + ctx, + b.chain.chainContext, + blkState, + b.chain.chainState == snow.Bootstrapping, + blockContext, + b.Stateless, + ) + if err != nil { + return err + } + + // Make sure to only state the state the first time we verify this block. + if b.state == nil { + b.state = blkState + parent.verifiedChildrenIDs.Add(b.id) + b.chain.verifiedBlocks[b.id] = b + } + + return nil +} + +func (b *block) State() (database.Database, error) { + if b.id == b.chain.lastAccepted { + return b.chain.acceptedState, nil + } + + // States of accepted blocks other than the lastAccepted are undefined. + if b.Status() == choices.Accepted { + return nil, errMissingState + } + + // We should not be calling State on an unverified block. + if b.state == nil { + return nil, errParentNotVerified + } + + return b.state, nil +} + +func (b *block) calculateStatus() choices.Status { + if b.chain.lastAccepted == b.id { + return choices.Accepted + } + if _, ok := b.chain.verifiedBlocks[b.id]; ok { + return choices.Processing + } + + _, err := state.GetBlock(b.chain.acceptedState, b.id) + switch { + case err == nil: + return choices.Accepted + + case errors.Is(err, database.ErrNotFound): + // This block hasn't been verified yet. + return choices.Processing + + default: + // TODO: correctly report this error to the consensus engine. + return choices.Processing + } +} diff --git a/vms/example/xsvm/chain/chain.go b/vms/example/xsvm/chain/chain.go new file mode 100644 index 000000000000..ef6a59de2dbb --- /dev/null +++ b/vms/example/xsvm/chain/chain.go @@ -0,0 +1,117 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chain + +import ( + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/vms/example/xsvm/state" + + xsblock "github.com/ava-labs/avalanchego/vms/example/xsvm/block" +) + +var _ Chain = (*chain)(nil) + +type Chain interface { + LastAccepted() ids.ID + SetChainState(state snow.State) + GetBlock(blkID ids.ID) (Block, error) + + // Creates a fully verifiable and executable block, which can be processed + // by the consensus engine, from a stateless block. + NewBlock(blk *xsblock.Stateless) (Block, error) +} + +type chain struct { + chainContext *snow.Context + acceptedState database.Database + + // chain state as driven by the consensus engine + chainState snow.State + + lastAccepted ids.ID + verifiedBlocks map[ids.ID]*block +} + +func New(ctx *snow.Context, db database.Database) (Chain, error) { + // Load the last accepted block data. For a newly created VM, this will be + // the genesis. It is assumed the genesis was processed and stored + // previously during VM initialization. + lastAcceptedID, err := state.GetLastAccepted(db) + if err != nil { + return nil, err + } + + c := &chain{ + chainContext: ctx, + acceptedState: db, + lastAccepted: lastAcceptedID, + } + + lastAccepted, err := c.getBlock(lastAcceptedID) + c.verifiedBlocks = map[ids.ID]*block{ + lastAcceptedID: lastAccepted, + } + return c, err +} + +func (c *chain) LastAccepted() ids.ID { + return c.lastAccepted +} + +func (c *chain) SetChainState(state snow.State) { + c.chainState = state +} + +func (c *chain) GetBlock(blkID ids.ID) (Block, error) { + return c.getBlock(blkID) +} + +func (c *chain) NewBlock(blk *xsblock.Stateless) (Block, error) { + blkID, err := blk.ID() + if err != nil { + return nil, err + } + + if blk, exists := c.verifiedBlocks[blkID]; exists { + return blk, nil + } + + blkBytes, err := xsblock.Codec.Marshal(xsblock.Version, blk) + if err != nil { + return nil, err + } + + return &block{ + Stateless: blk, + chain: c, + id: blkID, + bytes: blkBytes, + }, nil +} + +func (c *chain) getBlock(blkID ids.ID) (*block, error) { + if blk, exists := c.verifiedBlocks[blkID]; exists { + return blk, nil + } + + blkBytes, err := state.GetBlock(c.acceptedState, blkID) + if err != nil { + return nil, err + } + + stateless, err := xsblock.Parse(blkBytes) + if err != nil { + return nil, err + } + return &block{ + Stateless: stateless, + chain: c, + id: blkID, + status: choices.Accepted, + bytes: blkBytes, + }, nil +} diff --git a/vms/example/xsvm/cmd/account/cmd.go b/vms/example/xsvm/cmd/account/cmd.go new file mode 100644 index 000000000000..3436c3fea1b5 --- /dev/null +++ b/vms/example/xsvm/cmd/account/cmd.go @@ -0,0 +1,47 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package account + +import ( + "log" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/vms/example/xsvm/api" +) + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "account", + Short: "Displays the state of the requested account", + RunE: accountFunc, + } + flags := c.Flags() + AddFlags(flags) + return c +} + +func accountFunc(c *cobra.Command, args []string) error { + flags := c.Flags() + config, err := ParseFlags(flags, args) + if err != nil { + return err + } + + ctx := c.Context() + + client := api.NewClient(config.URI, config.ChainID) + + nonce, err := client.Nonce(ctx, config.Address) + if err != nil { + return err + } + + balance, err := client.Balance(ctx, config.Address, config.AssetID) + if err != nil { + return err + } + log.Printf("%s has %d of %s with nonce %d\n", config.Address, balance, config.AssetID, nonce) + return nil +} diff --git a/vms/example/xsvm/cmd/account/flags.go b/vms/example/xsvm/cmd/account/flags.go new file mode 100644 index 000000000000..17092bbe1a21 --- /dev/null +++ b/vms/example/xsvm/cmd/account/flags.go @@ -0,0 +1,83 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package account + +import ( + "github.com/spf13/pflag" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +const ( + URIKey = "uri" + ChainIDKey = "chain-id" + AddressKey = "address" + AssetIDKey = "asset-id" +) + +func AddFlags(flags *pflag.FlagSet) { + flags.String(URIKey, primary.LocalAPIURI, "API URI to use to fetch the account state") + flags.String(ChainIDKey, "", "Chain to fetch the account state on") + flags.String(AddressKey, genesis.EWOQKey.Address().String(), "Address of the account to fetch") + flags.String(AssetIDKey, "[chain-id]", "Asset balance to fetch") +} + +type Config struct { + URI string + ChainID string + Address ids.ShortID + AssetID ids.ID +} + +func ParseFlags(flags *pflag.FlagSet, args []string) (*Config, error) { + if err := flags.Parse(args); err != nil { + return nil, err + } + + if err := flags.Parse(args); err != nil { + return nil, err + } + + uri, err := flags.GetString(URIKey) + if err != nil { + return nil, err + } + + chainID, err := flags.GetString(ChainIDKey) + if err != nil { + return nil, err + } + + addrStr, err := flags.GetString(AddressKey) + if err != nil { + return nil, err + } + + addr, err := ids.ShortFromString(addrStr) + if err != nil { + return nil, err + } + + assetIDStr := chainID + if flags.Changed(AssetIDKey) { + assetIDStr, err = flags.GetString(AssetIDKey) + if err != nil { + return nil, err + } + } + + assetID, err := ids.FromString(assetIDStr) + if err != nil { + return nil, err + } + + return &Config{ + URI: uri, + ChainID: chainID, + Address: addr, + AssetID: assetID, + }, nil +} diff --git a/vms/example/xsvm/cmd/chain/cmd.go b/vms/example/xsvm/cmd/chain/cmd.go new file mode 100644 index 000000000000..a87e6911b8a8 --- /dev/null +++ b/vms/example/xsvm/cmd/chain/cmd.go @@ -0,0 +1,23 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chain + +import ( + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/chain/create" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/chain/genesis" +) + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "chain", + Short: "Manages XS chains", + } + c.AddCommand( + create.Command(), + genesis.Command(), + ) + return c +} diff --git a/vms/example/xsvm/cmd/chain/create/cmd.go b/vms/example/xsvm/cmd/chain/create/cmd.go new file mode 100644 index 000000000000..1f00491a4ce6 --- /dev/null +++ b/vms/example/xsvm/cmd/chain/create/cmd.go @@ -0,0 +1,85 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package create + +import ( + "log" + "time" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/example/xsvm" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" +) + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "create", + Short: "Creates a new chain", + RunE: createFunc, + } + flags := c.Flags() + AddFlags(flags) + return c +} + +func createFunc(c *cobra.Command, args []string) error { + flags := c.Flags() + config, err := ParseFlags(flags, args) + if err != nil { + return err + } + + ctx := c.Context() + kc := secp256k1fx.NewKeychain(config.PrivateKey) + + // NewWalletFromURI fetches the available UTXOs owned by [kc] on the network + // that [uri] is hosting. + walletSyncStartTime := time.Now() + wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{ + URI: config.URI, + AVAXKeychain: kc, + EthKeychain: kc, + PChainTxsToFetch: set.Of(config.SubnetID), + }) + if err != nil { + return err + } + log.Printf("synced wallet in %s\n", time.Since(walletSyncStartTime)) + + // Get the P-chain wallet + pWallet := wallet.P() + + genesisBytes, err := genesis.Codec.Marshal(genesis.Version, &genesis.Genesis{ + Timestamp: 0, + Allocations: []genesis.Allocation{ + { + Address: config.Address, + Balance: config.Balance, + }, + }, + }) + if err != nil { + return err + } + + createChainStartTime := time.Now() + createChainTxID, err := pWallet.IssueCreateChainTx( + config.SubnetID, + genesisBytes, + xsvm.ID, + nil, + config.Name, + common.WithContext(ctx), + ) + if err != nil { + return err + } + log.Printf("created chain %s in %s\n", createChainTxID, time.Since(createChainStartTime)) + return nil +} diff --git a/vms/example/xsvm/cmd/chain/create/flags.go b/vms/example/xsvm/cmd/chain/create/flags.go new file mode 100644 index 000000000000..80b1eefd67cf --- /dev/null +++ b/vms/example/xsvm/cmd/chain/create/flags.go @@ -0,0 +1,107 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package create + +import ( + "math" + + "github.com/spf13/pflag" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +const ( + URIKey = "uri" + SubnetIDKey = "subnet-id" + AddressKey = "address" + BalanceKey = "balance" + NameKey = "name" + PrivateKeyKey = "private-key" +) + +func AddFlags(flags *pflag.FlagSet) { + flags.String(URIKey, primary.LocalAPIURI, "API URI to use to issue the chain creation transaction") + flags.String(SubnetIDKey, "", "Subnet to create the chain under") + flags.String(AddressKey, genesis.EWOQKey.Address().String(), "Address to fund in the genesis") + flags.Uint64(BalanceKey, math.MaxUint64, "Amount to provide the funded address in the genesis") + flags.String(NameKey, "xs", "Name of the chain to create") + flags.String(PrivateKeyKey, genesis.EWOQKeyFormattedStr, "Private key to use when creating the new chain") +} + +type Config struct { + URI string + SubnetID ids.ID + Address ids.ShortID + Balance uint64 + Name string + PrivateKey *secp256k1.PrivateKey +} + +func ParseFlags(flags *pflag.FlagSet, args []string) (*Config, error) { + if err := flags.Parse(args); err != nil { + return nil, err + } + + if err := flags.Parse(args); err != nil { + return nil, err + } + + uri, err := flags.GetString(URIKey) + if err != nil { + return nil, err + } + + subnetIDStr, err := flags.GetString(SubnetIDKey) + if err != nil { + return nil, err + } + + subnetID, err := ids.FromString(subnetIDStr) + if err != nil { + return nil, err + } + + addrStr, err := flags.GetString(AddressKey) + if err != nil { + return nil, err + } + + addr, err := ids.ShortFromString(addrStr) + if err != nil { + return nil, err + } + + balance, err := flags.GetUint64(BalanceKey) + if err != nil { + return nil, err + } + + name, err := flags.GetString(NameKey) + if err != nil { + return nil, err + } + + skStr, err := flags.GetString(PrivateKeyKey) + if err != nil { + return nil, err + } + + var sk secp256k1.PrivateKey + err = sk.UnmarshalText([]byte(`"` + skStr + `"`)) + if err != nil { + return nil, err + } + + return &Config{ + URI: uri, + SubnetID: subnetID, + Address: addr, + Balance: balance, + Name: name, + PrivateKey: &sk, + }, nil +} diff --git a/vms/example/xsvm/cmd/chain/genesis/cmd.go b/vms/example/xsvm/cmd/chain/genesis/cmd.go new file mode 100644 index 000000000000..ae18e1db85e3 --- /dev/null +++ b/vms/example/xsvm/cmd/chain/genesis/cmd.go @@ -0,0 +1,56 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package genesis + +import ( + "errors" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" +) + +var errUnknownEncoding = errors.New("unknown encoding") + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "genesis", + Short: "Creates a chain's genesis and prints it to stdout", + RunE: genesisFunc, + } + flags := c.Flags() + AddFlags(flags) + return c +} + +func genesisFunc(c *cobra.Command, args []string) error { + flags := c.Flags() + config, err := ParseFlags(flags, args) + if err != nil { + return err + } + + genesisBytes, err := genesis.Codec.Marshal(genesis.Version, config.Genesis) + if err != nil { + return err + } + + switch config.Encoding { + case binaryEncoding: + _, err = os.Stdout.Write(genesisBytes) + return err + case hexEncoding: + encoded, err := formatting.Encode(formatting.Hex, genesisBytes) + if err != nil { + return err + } + _, err = fmt.Println(encoded) + return err + default: + return fmt.Errorf("%w: %q", errUnknownEncoding, config.Encoding) + } +} diff --git a/vms/example/xsvm/cmd/chain/genesis/flags.go b/vms/example/xsvm/cmd/chain/genesis/flags.go new file mode 100644 index 000000000000..5291327197d5 --- /dev/null +++ b/vms/example/xsvm/cmd/chain/genesis/flags.go @@ -0,0 +1,83 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package genesis + +import ( + "fmt" + "math" + "time" + + "github.com/spf13/pflag" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + + xsgenesis "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" +) + +const ( + TimeKey = "time" + AddressKey = "address" + BalanceKey = "balance" + EncodingKey = "encoding" + + binaryEncoding = "binary" + hexEncoding = "hex" +) + +func AddFlags(flags *pflag.FlagSet) { + flags.Int64(TimeKey, time.Now().Unix(), "Unix timestamp to include in the genesis") + flags.String(AddressKey, genesis.EWOQKey.Address().String(), "Address to fund in the genesis") + flags.Uint64(BalanceKey, math.MaxUint64, "Amount to provide the funded address in the genesis") + flags.String(EncodingKey, hexEncoding, fmt.Sprintf("Encoding to use for the genesis. Available values: %s or %s", hexEncoding, binaryEncoding)) +} + +type Config struct { + Genesis *xsgenesis.Genesis + Encoding string +} + +func ParseFlags(flags *pflag.FlagSet, args []string) (*Config, error) { + if err := flags.Parse(args); err != nil { + return nil, err + } + + timestamp, err := flags.GetInt64(TimeKey) + if err != nil { + return nil, err + } + + addrStr, err := flags.GetString(AddressKey) + if err != nil { + return nil, err + } + + addr, err := ids.ShortFromString(addrStr) + if err != nil { + return nil, err + } + + balance, err := flags.GetUint64(BalanceKey) + if err != nil { + return nil, err + } + + encoding, err := flags.GetString(EncodingKey) + if err != nil { + return nil, err + } + + return &Config{ + Genesis: &xsgenesis.Genesis{ + Timestamp: timestamp, + Allocations: []xsgenesis.Allocation{ + { + Address: addr, + Balance: balance, + }, + }, + }, + Encoding: encoding, + }, nil +} diff --git a/vms/example/xsvm/cmd/issue/cmd.go b/vms/example/xsvm/cmd/issue/cmd.go new file mode 100644 index 000000000000..e973efc31d5b --- /dev/null +++ b/vms/example/xsvm/cmd/issue/cmd.go @@ -0,0 +1,25 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package issue + +import ( + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/export" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/importtx" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/transfer" +) + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "issue", + Short: "Issues transactions", + } + c.AddCommand( + transfer.Command(), + export.Command(), + importtx.Command(), + ) + return c +} diff --git a/vms/example/xsvm/cmd/issue/export/cmd.go b/vms/example/xsvm/cmd/issue/export/cmd.go new file mode 100644 index 000000000000..c0a8cd11008c --- /dev/null +++ b/vms/example/xsvm/cmd/issue/export/cmd.go @@ -0,0 +1,70 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package export + +import ( + "encoding/json" + "log" + "time" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/vms/example/xsvm/api" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" +) + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "export", + Short: "Issues an export transaction", + RunE: exportFunc, + } + flags := c.Flags() + AddFlags(flags) + return c +} + +func exportFunc(c *cobra.Command, args []string) error { + flags := c.Flags() + config, err := ParseFlags(flags, args) + if err != nil { + return err + } + + ctx := c.Context() + + client := api.NewClient(config.URI, config.SourceChainID.String()) + + nonce, err := client.Nonce(ctx, config.PrivateKey.Address()) + if err != nil { + return err + } + + utx := &tx.Export{ + ChainID: config.SourceChainID, + Nonce: nonce, + MaxFee: config.MaxFee, + PeerChainID: config.DestinationChainID, + IsReturn: config.IsReturn, + Amount: config.Amount, + To: config.To, + } + stx, err := tx.Sign(utx, config.PrivateKey) + if err != nil { + return err + } + + txJSON, err := json.MarshalIndent(stx, "", " ") + if err != nil { + return err + } + + issueTxStartTime := time.Now() + txID, err := client.IssueTx(ctx, stx) + if err != nil { + return err + } + log.Printf("issued tx %s in %s\n%s\n", txID, time.Since(issueTxStartTime), string(txJSON)) + return nil +} diff --git a/vms/example/xsvm/cmd/issue/export/flags.go b/vms/example/xsvm/cmd/issue/export/flags.go new file mode 100644 index 000000000000..f14c21ef8f6f --- /dev/null +++ b/vms/example/xsvm/cmd/issue/export/flags.go @@ -0,0 +1,125 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package export + +import ( + "github.com/spf13/pflag" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +const ( + URIKey = "uri" + SourceChainIDKey = "source-chain-id" + DestinationChainIDKey = "destination-chain-id" + MaxFeeKey = "max-fee" + IsReturnKey = "is-return" + AmountKey = "amount" + ToKey = "to" + PrivateKeyKey = "private-key" +) + +func AddFlags(flags *pflag.FlagSet) { + flags.String(URIKey, primary.LocalAPIURI, "API URI to use during issuance") + flags.String(SourceChainIDKey, "", "Chain to issue the transaction on") + flags.String(DestinationChainIDKey, "", "Chain to send the asset to") + flags.Uint64(MaxFeeKey, 0, "Maximum fee to spend") + flags.Bool(IsReturnKey, false, "Mark this transaction as returning funds") + flags.Uint64(AmountKey, units.Schmeckle, "Amount to send") + flags.String(ToKey, genesis.EWOQKey.Address().String(), "Destination address") + flags.String(PrivateKeyKey, genesis.EWOQKeyFormattedStr, "Private key to sign the transaction") +} + +type Config struct { + URI string + SourceChainID ids.ID + DestinationChainID ids.ID + MaxFee uint64 + IsReturn bool + Amount uint64 + To ids.ShortID + PrivateKey *secp256k1.PrivateKey +} + +func ParseFlags(flags *pflag.FlagSet, args []string) (*Config, error) { + if err := flags.Parse(args); err != nil { + return nil, err + } + + uri, err := flags.GetString(URIKey) + if err != nil { + return nil, err + } + + sourceChainIDStr, err := flags.GetString(SourceChainIDKey) + if err != nil { + return nil, err + } + + sourceChainID, err := ids.FromString(sourceChainIDStr) + if err != nil { + return nil, err + } + + destinationChainIDStr, err := flags.GetString(DestinationChainIDKey) + if err != nil { + return nil, err + } + + destinationChainID, err := ids.FromString(destinationChainIDStr) + if err != nil { + return nil, err + } + + maxFee, err := flags.GetUint64(MaxFeeKey) + if err != nil { + return nil, err + } + + isReturn, err := flags.GetBool(IsReturnKey) + if err != nil { + return nil, err + } + + amount, err := flags.GetUint64(AmountKey) + if err != nil { + return nil, err + } + + toStr, err := flags.GetString(ToKey) + if err != nil { + return nil, err + } + + to, err := ids.ShortFromString(toStr) + if err != nil { + return nil, err + } + + skStr, err := flags.GetString(PrivateKeyKey) + if err != nil { + return nil, err + } + + var sk secp256k1.PrivateKey + err = sk.UnmarshalText([]byte(`"` + skStr + `"`)) + if err != nil { + return nil, err + } + + return &Config{ + URI: uri, + SourceChainID: sourceChainID, + DestinationChainID: destinationChainID, + MaxFee: maxFee, + IsReturn: isReturn, + Amount: amount, + To: to, + PrivateKey: &sk, + }, nil +} diff --git a/vms/example/xsvm/cmd/issue/importtx/cmd.go b/vms/example/xsvm/cmd/issue/importtx/cmd.go new file mode 100644 index 000000000000..2c8fbb7edb27 --- /dev/null +++ b/vms/example/xsvm/cmd/issue/importtx/cmd.go @@ -0,0 +1,135 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package importtx + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/example/xsvm/api" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "import", + Short: "Issues an import transaction", + RunE: importFunc, + } + flags := c.Flags() + AddFlags(flags) + return c +} + +func importFunc(c *cobra.Command, args []string) error { + flags := c.Flags() + config, err := ParseFlags(flags, args) + if err != nil { + return err + } + + ctx := c.Context() + + var ( + // Note: here we assume the unsigned message is correct from the last + // URI in sourceURIs. In practice this shouldn't be done. + unsignedMessage *warp.UnsignedMessage + // Note: assumes that sourceURIs are all of the validators of the subnet + // and that they do not share public keys. + signatures = make([]*bls.Signature, len(config.SourceURIs)) + ) + for i, uri := range config.SourceURIs { + xsClient := api.NewClient(uri, config.SourceChainID) + + fetchStartTime := time.Now() + var rawSignature []byte + unsignedMessage, rawSignature, err = xsClient.Message(ctx, config.TxID) + if err != nil { + return fmt.Errorf("failed to fetch BLS signature from %s with: %w", uri, err) + } + + sig, err := bls.SignatureFromBytes(rawSignature) + if err != nil { + return fmt.Errorf("failed to parse BLS signature from %s with: %w", uri, err) + } + + // Note: the public key should not be fetched from the node in practice. + // The public key should be fetched from the P-chain directly. + infoClient := info.NewClient(uri) + _, nodePOP, err := infoClient.GetNodeID(ctx) + if err != nil { + return fmt.Errorf("failed to fetch BLS public key from %s with: %w", uri, err) + } + + pk := nodePOP.Key() + if !bls.Verify(pk, sig, unsignedMessage.Bytes()) { + return fmt.Errorf("failed to verify BLS signature against public key from %s", uri) + } + + log.Printf("fetched BLS signature from %s in %s\n", uri, time.Since(fetchStartTime)) + signatures[i] = sig + } + + signers := set.NewBits() + for i := range signatures { + signers.Add(i) + } + signature := &warp.BitSetSignature{ + Signers: signers.Bytes(), + } + + aggSignature, err := bls.AggregateSignatures(signatures) + if err != nil { + return err + } + + aggSignatureBytes := bls.SignatureToBytes(aggSignature) + copy(signature.Signature[:], aggSignatureBytes) + + message, err := warp.NewMessage( + unsignedMessage, + signature, + ) + if err != nil { + return err + } + + client := api.NewClient(config.URI, config.DestinationChainID) + + nonce, err := client.Nonce(ctx, config.PrivateKey.Address()) + if err != nil { + return err + } + + utx := &tx.Import{ + Nonce: nonce, + MaxFee: config.MaxFee, + Message: message.Bytes(), + } + stx, err := tx.Sign(utx, config.PrivateKey) + if err != nil { + return err + } + + txJSON, err := json.MarshalIndent(stx, "", " ") + if err != nil { + return err + } + + issueTxStartTime := time.Now() + txID, err := client.IssueTx(ctx, stx) + if err != nil { + return err + } + log.Printf("issued tx %s in %s\n%s\n", txID, time.Since(issueTxStartTime), string(txJSON)) + return nil +} diff --git a/vms/example/xsvm/cmd/issue/importtx/flags.go b/vms/example/xsvm/cmd/issue/importtx/flags.go new file mode 100644 index 000000000000..486dfa1a05ea --- /dev/null +++ b/vms/example/xsvm/cmd/issue/importtx/flags.go @@ -0,0 +1,105 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package importtx + +import ( + "github.com/spf13/pflag" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +const ( + URIKey = "uri" + SourceURIsKey = "source-uris" + SourceChainIDKey = "source-chain-id" + DestinationChainIDKey = "destination-chain-id" + TxIDKey = "tx-id" + MaxFeeKey = "max-fee" + PrivateKeyKey = "private-key" +) + +func AddFlags(flags *pflag.FlagSet) { + flags.String(URIKey, primary.LocalAPIURI, "API URI to use during issuance") + flags.StringSlice(SourceURIsKey, []string{primary.LocalAPIURI}, "API URIs to use during the fetching of signatures") + flags.String(SourceChainIDKey, "", "Chain the export transaction was issued on") + flags.String(DestinationChainIDKey, "", "Chain to send the asset to") + flags.String(TxIDKey, "", "ID of the export transaction") + flags.Uint64(MaxFeeKey, 0, "Maximum fee to spend") + flags.String(PrivateKeyKey, genesis.EWOQKeyFormattedStr, "Private key to sign the transaction") +} + +type Config struct { + URI string + SourceURIs []string + SourceChainID string + DestinationChainID string + TxID ids.ID + MaxFee uint64 + PrivateKey *secp256k1.PrivateKey +} + +func ParseFlags(flags *pflag.FlagSet, args []string) (*Config, error) { + if err := flags.Parse(args); err != nil { + return nil, err + } + + uri, err := flags.GetString(URIKey) + if err != nil { + return nil, err + } + + sourceURIs, err := flags.GetStringSlice(SourceURIsKey) + if err != nil { + return nil, err + } + + sourceChainID, err := flags.GetString(SourceChainIDKey) + if err != nil { + return nil, err + } + + destinationChainID, err := flags.GetString(DestinationChainIDKey) + if err != nil { + return nil, err + } + + txIDStr, err := flags.GetString(TxIDKey) + if err != nil { + return nil, err + } + + txID, err := ids.FromString(txIDStr) + if err != nil { + return nil, err + } + + maxFee, err := flags.GetUint64(MaxFeeKey) + if err != nil { + return nil, err + } + + skStr, err := flags.GetString(PrivateKeyKey) + if err != nil { + return nil, err + } + + var sk secp256k1.PrivateKey + err = sk.UnmarshalText([]byte(`"` + skStr + `"`)) + if err != nil { + return nil, err + } + + return &Config{ + URI: uri, + SourceURIs: sourceURIs, + SourceChainID: sourceChainID, + DestinationChainID: destinationChainID, + TxID: txID, + MaxFee: maxFee, + PrivateKey: &sk, + }, nil +} diff --git a/vms/example/xsvm/cmd/issue/transfer/cmd.go b/vms/example/xsvm/cmd/issue/transfer/cmd.go new file mode 100644 index 000000000000..5dd15bc4ea26 --- /dev/null +++ b/vms/example/xsvm/cmd/issue/transfer/cmd.go @@ -0,0 +1,69 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transfer + +import ( + "encoding/json" + "log" + "time" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/vms/example/xsvm/api" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" +) + +func Command() *cobra.Command { + c := &cobra.Command{ + Use: "transfer", + Short: "Issues a transfer transaction", + RunE: transferFunc, + } + flags := c.Flags() + AddFlags(flags) + return c +} + +func transferFunc(c *cobra.Command, args []string) error { + flags := c.Flags() + config, err := ParseFlags(flags, args) + if err != nil { + return err + } + + ctx := c.Context() + + client := api.NewClient(config.URI, config.ChainID.String()) + + nonce, err := client.Nonce(ctx, config.PrivateKey.Address()) + if err != nil { + return err + } + + utx := &tx.Transfer{ + ChainID: config.ChainID, + Nonce: nonce, + MaxFee: config.MaxFee, + AssetID: config.AssetID, + Amount: config.Amount, + To: config.To, + } + stx, err := tx.Sign(utx, config.PrivateKey) + if err != nil { + return err + } + + txJSON, err := json.MarshalIndent(stx, "", " ") + if err != nil { + return err + } + + issueTxStartTime := time.Now() + txID, err := client.IssueTx(ctx, stx) + if err != nil { + return err + } + log.Printf("issued tx %s in %s\n%s\n", txID, time.Since(issueTxStartTime), string(txJSON)) + return nil +} diff --git a/vms/example/xsvm/cmd/issue/transfer/flags.go b/vms/example/xsvm/cmd/issue/transfer/flags.go new file mode 100644 index 000000000000..5d4667b47458 --- /dev/null +++ b/vms/example/xsvm/cmd/issue/transfer/flags.go @@ -0,0 +1,119 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transfer + +import ( + "github.com/spf13/pflag" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +const ( + URIKey = "uri" + ChainIDKey = "chain-id" + MaxFeeKey = "max-fee" + AssetIDKey = "asset-id" + AmountKey = "amount" + ToKey = "to" + PrivateKeyKey = "private-key" +) + +func AddFlags(flags *pflag.FlagSet) { + flags.String(URIKey, primary.LocalAPIURI, "API URI to use during issuance") + flags.String(ChainIDKey, "", "Chain to issue the transaction on") + flags.Uint64(MaxFeeKey, 0, "Maximum fee to spend") + flags.String(AssetIDKey, "[chain-id]", "Asset to send") + flags.Uint64(AmountKey, units.Schmeckle, "Amount to send") + flags.String(ToKey, genesis.EWOQKey.Address().String(), "Destination address") + flags.String(PrivateKeyKey, genesis.EWOQKeyFormattedStr, "Private key to sign the transaction") +} + +type Config struct { + URI string + ChainID ids.ID + MaxFee uint64 + AssetID ids.ID + Amount uint64 + To ids.ShortID + PrivateKey *secp256k1.PrivateKey +} + +func ParseFlags(flags *pflag.FlagSet, args []string) (*Config, error) { + if err := flags.Parse(args); err != nil { + return nil, err + } + + uri, err := flags.GetString(URIKey) + if err != nil { + return nil, err + } + + chainIDStr, err := flags.GetString(ChainIDKey) + if err != nil { + return nil, err + } + + chainID, err := ids.FromString(chainIDStr) + if err != nil { + return nil, err + } + + maxFee, err := flags.GetUint64(MaxFeeKey) + if err != nil { + return nil, err + } + + assetID := chainID + if flags.Changed(AssetIDKey) { + assetIDStr, err := flags.GetString(AssetIDKey) + if err != nil { + return nil, err + } + + assetID, err = ids.FromString(assetIDStr) + if err != nil { + return nil, err + } + } + + amount, err := flags.GetUint64(AmountKey) + if err != nil { + return nil, err + } + + toStr, err := flags.GetString(ToKey) + if err != nil { + return nil, err + } + + to, err := ids.ShortFromString(toStr) + if err != nil { + return nil, err + } + + skStr, err := flags.GetString(PrivateKeyKey) + if err != nil { + return nil, err + } + + var sk secp256k1.PrivateKey + err = sk.UnmarshalText([]byte(`"` + skStr + `"`)) + if err != nil { + return nil, err + } + + return &Config{ + URI: uri, + ChainID: chainID, + MaxFee: maxFee, + AssetID: assetID, + Amount: amount, + To: to, + PrivateKey: &sk, + }, nil +} diff --git a/vms/example/xsvm/cmd/run/cmd.go b/vms/example/xsvm/cmd/run/cmd.go new file mode 100644 index 000000000000..9776b15eb3a7 --- /dev/null +++ b/vms/example/xsvm/cmd/run/cmd.go @@ -0,0 +1,25 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package run + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/vms/example/xsvm" + "github.com/ava-labs/avalanchego/vms/rpcchainvm" +) + +func Command() *cobra.Command { + return &cobra.Command{ + Use: "xsvm", + Short: "Runs an XSVM plugin", + RunE: runFunc, + } +} + +func runFunc(*cobra.Command, []string) error { + return rpcchainvm.Serve(context.Background(), &xsvm.VM{}) +} diff --git a/vms/example/xsvm/cmd/version/cmd.go b/vms/example/xsvm/cmd/version/cmd.go new file mode 100644 index 000000000000..0827a9e800b7 --- /dev/null +++ b/vms/example/xsvm/cmd/version/cmd.go @@ -0,0 +1,38 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package version + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/version" + "github.com/ava-labs/avalanchego/vms/example/xsvm" +) + +const format = `%s: + VMID: %s + Version: %s + Plugin Version: %d +` + +func Command() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Prints out the version", + RunE: versionFunc, + } +} + +func versionFunc(*cobra.Command, []string) error { + fmt.Printf( + format, + xsvm.Name, + xsvm.ID, + xsvm.Version, + version.RPCChainVMProtocol, + ) + return nil +} diff --git a/vms/example/xsvm/cmd/xsvm/main.go b/vms/example/xsvm/cmd/xsvm/main.go new file mode 100644 index 000000000000..ac370a4a9df2 --- /dev/null +++ b/vms/example/xsvm/cmd/xsvm/main.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/account" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/chain" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/run" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/version" +) + +func init() { + cobra.EnablePrefixMatching = true +} + +func main() { + cmd := run.Command() + cmd.AddCommand( + account.Command(), + chain.Command(), + issue.Command(), + version.Command(), + ) + ctx := context.Background() + if err := cmd.ExecuteContext(ctx); err != nil { + fmt.Fprintf(os.Stderr, "command failed %v\n", err) + os.Exit(1) + } +} diff --git a/vms/example/xsvm/constants.go b/vms/example/xsvm/constants.go new file mode 100644 index 000000000000..e4b22b742450 --- /dev/null +++ b/vms/example/xsvm/constants.go @@ -0,0 +1,21 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package xsvm + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/version" +) + +const Name = "xsvm" + +var ( + ID = ids.ID{'x', 's', 'v', 'm'} + + Version = &version.Semantic{ + Major: 1, + Minor: 0, + Patch: 4, + } +) diff --git a/vms/example/xsvm/execute/block.go b/vms/example/xsvm/execute/block.go new file mode 100644 index 000000000000..dc9de45af19e --- /dev/null +++ b/vms/example/xsvm/execute/block.go @@ -0,0 +1,71 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package execute + +import ( + "context" + "errors" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/example/xsvm/state" + + smblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + xsblock "github.com/ava-labs/avalanchego/vms/example/xsvm/block" +) + +var errNoTxs = errors.New("no transactions") + +func Block( + ctx context.Context, + chainContext *snow.Context, + db database.KeyValueReaderWriterDeleter, + skipVerify bool, + blockContext *smblock.Context, + blk *xsblock.Stateless, +) error { + if len(blk.Txs) == 0 { + return errNoTxs + } + + for _, currentTx := range blk.Txs { + txID, err := currentTx.ID() + if err != nil { + return err + } + sender, err := currentTx.SenderID() + if err != nil { + return err + } + txExecutor := Tx{ + Context: ctx, + ChainContext: chainContext, + Database: db, + SkipVerify: skipVerify, + BlockContext: blockContext, + TxID: txID, + Sender: sender, + // TODO: populate fees + } + if err := currentTx.Unsigned.Visit(&txExecutor); err != nil { + return err + } + } + + blkID, err := blk.ID() + if err != nil { + return err + } + + if err := state.SetLastAccepted(db, blkID); err != nil { + return err + } + + blkBytes, err := xsblock.Codec.Marshal(xsblock.Version, blk) + if err != nil { + return err + } + + return state.AddBlock(db, blk.Height, blkID, blkBytes) +} diff --git a/vms/example/xsvm/execute/expects_context.go b/vms/example/xsvm/execute/expects_context.go new file mode 100644 index 000000000000..0109cadc3731 --- /dev/null +++ b/vms/example/xsvm/execute/expects_context.go @@ -0,0 +1,38 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package execute + +import ( + "github.com/ava-labs/avalanchego/vms/example/xsvm/block" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" +) + +var _ tx.Visitor = (*TxExpectsContext)(nil) + +func ExpectsContext(blk *block.Stateless) (bool, error) { + t := TxExpectsContext{} + for _, tx := range blk.Txs { + if err := tx.Unsigned.Visit(&t); err != nil { + return false, err + } + } + return t.Result, nil +} + +type TxExpectsContext struct { + Result bool +} + +func (*TxExpectsContext) Transfer(*tx.Transfer) error { + return nil +} + +func (*TxExpectsContext) Export(*tx.Export) error { + return nil +} + +func (t *TxExpectsContext) Import(*tx.Import) error { + t.Result = true + return nil +} diff --git a/vms/example/xsvm/execute/genesis.go b/vms/example/xsvm/execute/genesis.go new file mode 100644 index 000000000000..312a7bd0b73c --- /dev/null +++ b/vms/example/xsvm/execute/genesis.go @@ -0,0 +1,51 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package execute + +import ( + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/example/xsvm/block" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/example/xsvm/state" +) + +func Genesis(db database.KeyValueReaderWriterDeleter, chainID ids.ID, g *genesis.Genesis) error { + isInitialized, err := state.IsInitialized(db) + if err != nil { + return err + } + if isInitialized { + return nil + } + + blk, err := genesis.Block(g) + if err != nil { + return err + } + + for _, allocation := range g.Allocations { + if err := state.SetBalance(db, allocation.Address, chainID, allocation.Balance); err != nil { + return err + } + } + + blkID, err := blk.ID() + if err != nil { + return err + } + + blkBytes, err := block.Codec.Marshal(block.Version, blk) + if err != nil { + return err + } + + if err := state.AddBlock(db, blk.Height, blkID, blkBytes); err != nil { + return err + } + if err := state.SetLastAccepted(db, blkID); err != nil { + return err + } + return state.SetInitialized(db) +} diff --git a/vms/example/xsvm/execute/tx.go b/vms/example/xsvm/execute/tx.go new file mode 100644 index 000000000000..6c8276af8e79 --- /dev/null +++ b/vms/example/xsvm/execute/tx.go @@ -0,0 +1,178 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package execute + +import ( + "context" + "errors" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/avalanchego/vms/example/xsvm/state" + "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +const ( + QuorumNumerator = 2 + QuorumDenominator = 3 +) + +var ( + _ tx.Visitor = (*Tx)(nil) + + errFeeTooHigh = errors.New("fee too high") + errWrongChainID = errors.New("wrong chainID") + errMissingBlockContext = errors.New("missing block context") + errDuplicateImport = errors.New("duplicate import") +) + +type Tx struct { + Context context.Context + ChainContext *snow.Context + Database database.KeyValueReaderWriterDeleter + + SkipVerify bool + BlockContext *block.Context + + TxID ids.ID + Sender ids.ShortID + TransferFee uint64 + ExportFee uint64 + ImportFee uint64 +} + +func (t *Tx) Transfer(tf *tx.Transfer) error { + if tf.MaxFee < t.TransferFee { + return errFeeTooHigh + } + if tf.ChainID != t.ChainContext.ChainID { + return errWrongChainID + } + + var errs wrappers.Errs + errs.Add( + state.IncrementNonce(t.Database, t.Sender, tf.Nonce), + state.DecreaseBalance(t.Database, t.Sender, tf.ChainID, t.TransferFee), + state.DecreaseBalance(t.Database, t.Sender, tf.AssetID, tf.Amount), + state.IncreaseBalance(t.Database, tf.To, tf.AssetID, tf.Amount), + ) + return errs.Err +} + +func (t *Tx) Export(e *tx.Export) error { + if e.MaxFee < t.ExportFee { + return errFeeTooHigh + } + if e.ChainID != t.ChainContext.ChainID { + return errWrongChainID + } + + payload, err := tx.NewPayload( + t.Sender, + e.Nonce, + e.IsReturn, + e.Amount, + e.To, + ) + if err != nil { + return err + } + + message, err := warp.NewUnsignedMessage( + t.ChainContext.NetworkID, + e.ChainID, + payload.Bytes(), + ) + if err != nil { + return err + } + + var errs wrappers.Errs + errs.Add( + state.IncrementNonce(t.Database, t.Sender, e.Nonce), + state.DecreaseBalance(t.Database, t.Sender, e.ChainID, t.ExportFee), + ) + + if e.IsReturn { + errs.Add( + state.DecreaseBalance(t.Database, t.Sender, e.PeerChainID, e.Amount), + ) + } else { + errs.Add( + state.DecreaseBalance(t.Database, t.Sender, e.ChainID, e.Amount), + state.IncreaseLoan(t.Database, e.PeerChainID, e.Amount), + ) + } + + errs.Add( + state.SetMessage(t.Database, t.TxID, message), + ) + return errs.Err +} + +func (t *Tx) Import(i *tx.Import) error { + if i.MaxFee < t.ImportFee { + return errFeeTooHigh + } + if t.BlockContext == nil { + return errMissingBlockContext + } + + message, err := warp.ParseMessage(i.Message) + if err != nil { + return err + } + + var errs wrappers.Errs + errs.Add( + state.IncrementNonce(t.Database, t.Sender, i.Nonce), + state.DecreaseBalance(t.Database, t.Sender, t.ChainContext.ChainID, t.ImportFee), + ) + + payload, err := tx.ParsePayload(message.Payload) + if err != nil { + return err + } + + if payload.IsReturn { + errs.Add( + state.IncreaseBalance(t.Database, payload.To, t.ChainContext.ChainID, payload.Amount), + state.DecreaseLoan(t.Database, message.SourceChainID, payload.Amount), + ) + } else { + errs.Add( + state.IncreaseBalance(t.Database, payload.To, message.SourceChainID, payload.Amount), + ) + } + + var loanID ids.ID = hashing.ComputeHash256Array(message.UnsignedMessage.Bytes()) + hasLoanID, err := state.HasLoanID(t.Database, message.SourceChainID, loanID) + if hasLoanID { + return errDuplicateImport + } + + errs.Add( + err, + state.AddLoanID(t.Database, message.SourceChainID, loanID), + ) + + if t.SkipVerify || errs.Errored() { + return errs.Err + } + + return message.Signature.Verify( + t.Context, + &message.UnsignedMessage, + t.ChainContext.NetworkID, + t.ChainContext.ValidatorState, + t.BlockContext.PChainHeight, + QuorumNumerator, + QuorumDenominator, + ) +} diff --git a/vms/example/xsvm/factory.go b/vms/example/xsvm/factory.go new file mode 100644 index 000000000000..0531a1bcc7d0 --- /dev/null +++ b/vms/example/xsvm/factory.go @@ -0,0 +1,17 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package xsvm + +import ( + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms" +) + +var _ vms.Factory = (*Factory)(nil) + +type Factory struct{} + +func (*Factory) New(logging.Logger) (interface{}, error) { + return &VM{}, nil +} diff --git a/vms/example/xsvm/genesis/codec.go b/vms/example/xsvm/genesis/codec.go new file mode 100644 index 000000000000..6ef652864574 --- /dev/null +++ b/vms/example/xsvm/genesis/codec.go @@ -0,0 +1,11 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package genesis + +import "github.com/ava-labs/avalanchego/vms/example/xsvm/block" + +// Version is the current default codec version +const Version = block.Version + +var Codec = block.Codec diff --git a/vms/example/xsvm/genesis/genesis.go b/vms/example/xsvm/genesis/genesis.go new file mode 100644 index 000000000000..e8580ffef7f6 --- /dev/null +++ b/vms/example/xsvm/genesis/genesis.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package genesis + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/vms/example/xsvm/block" +) + +type Genesis struct { + Timestamp int64 `serialize:"true" json:"timestamp"` + Allocations []Allocation `serialize:"true" json:"allocations"` +} + +type Allocation struct { + Address ids.ShortID `serialize:"true" json:"address"` + Balance uint64 `serialize:"true" json:"balance"` +} + +func Parse(bytes []byte) (*Genesis, error) { + genesis := &Genesis{} + _, err := Codec.Unmarshal(bytes, genesis) + return genesis, err +} + +func Block(genesis *Genesis) (*block.Stateless, error) { + bytes, err := Codec.Marshal(Version, genesis) + if err != nil { + return nil, err + } + return &block.Stateless{ + ParentID: hashing.ComputeHash256Array(bytes), + Timestamp: genesis.Timestamp, + }, nil +} diff --git a/vms/example/xsvm/genesis/genesis_test.go b/vms/example/xsvm/genesis/genesis_test.go new file mode 100644 index 000000000000..511c5c9b0b8b --- /dev/null +++ b/vms/example/xsvm/genesis/genesis_test.go @@ -0,0 +1,35 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package genesis + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" +) + +func TestGenesis(t *testing.T) { + require := require.New(t) + + id, err := ids.ShortFromString("6Y3kysjF9jnHnYkdS9yGAuoHyae2eNmeV") + require.NoError(err) + id2, err := ids.ShortFromString("LeKrndtsMxcLMzHz3w4uo1XtLDpfi66c") + require.NoError(err) + + genesis := Genesis{ + Timestamp: 123, + Allocations: []Allocation{ + {Address: id, Balance: 1000000000}, + {Address: id2, Balance: 3000000000}, + }, + } + bytes, err := Codec.Marshal(Version, genesis) + require.NoError(err) + + parsed, err := Parse(bytes) + require.NoError(err) + require.Equal(genesis, *parsed) +} diff --git a/vms/example/xsvm/state/keys.go b/vms/example/xsvm/state/keys.go new file mode 100644 index 000000000000..5e78f1b95a0e --- /dev/null +++ b/vms/example/xsvm/state/keys.go @@ -0,0 +1,25 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +var ( + initializedKey = []byte{} + blockPrefix = []byte{0x00} + addressPrefix = []byte{0x01} + chainPrefix = []byte{0x02} + messagePrefix = []byte{0x03} +) + +func Flatten[T any](slices ...[]T) []T { + var size int + for _, slice := range slices { + size += len(slice) + } + + result := make([]T, 0, size) + for _, slice := range slices { + result = append(result, slice...) + } + return result +} diff --git a/vms/example/xsvm/state/storage.go b/vms/example/xsvm/state/storage.go new file mode 100644 index 000000000000..c9b6bf1ae65d --- /dev/null +++ b/vms/example/xsvm/state/storage.go @@ -0,0 +1,210 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "errors" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +var ( + errWrongNonce = errors.New("wrong nonce") + errInsufficientBalance = errors.New("insufficient balance") +) + +/* + * VMDB + * |-- initializedKey -> nil + * |-. blocks + * | |-- lastAcceptedKey -> blockID + * | |-- height -> blockID + * | '-- blockID -> block bytes + * |-. addresses + * | '-- addressID -> nonce + * | '-- addressID + chainID -> balance + * |-. chains + * | |-- chainID -> balance + * | '-- chainID + loanID -> nil + * '-. message + * '-- txID -> message bytes + */ + +// Chain state + +func IsInitialized(db database.KeyValueReader) (bool, error) { + return db.Has(initializedKey) +} + +func SetInitialized(db database.KeyValueWriter) error { + return db.Put(initializedKey, nil) +} + +// Block state + +func GetLastAccepted(db database.KeyValueReader) (ids.ID, error) { + return database.GetID(db, blockPrefix) +} + +func SetLastAccepted(db database.KeyValueWriter, blkID ids.ID) error { + return database.PutID(db, blockPrefix, blkID) +} + +func GetBlockIDByHeight(db database.KeyValueReader, height uint64) (ids.ID, error) { + key := Flatten(blockPrefix, database.PackUInt64(height)) + return database.GetID(db, key) +} + +func GetBlock(db database.KeyValueReader, blkID ids.ID) ([]byte, error) { + key := Flatten(blockPrefix, blkID[:]) + return db.Get(key) +} + +func AddBlock(db database.KeyValueWriter, height uint64, blkID ids.ID, blk []byte) error { + heightToIDKey := Flatten(blockPrefix, database.PackUInt64(height)) + if err := database.PutID(db, heightToIDKey, blkID); err != nil { + return err + } + idToBlockKey := Flatten(blockPrefix, blkID[:]) + return db.Put(idToBlockKey, blk) +} + +// Address state + +func GetNonce(db database.KeyValueReader, address ids.ShortID) (uint64, error) { + key := Flatten(addressPrefix, address[:]) + nonce, err := database.GetUInt64(db, key) + if errors.Is(err, database.ErrNotFound) { + return 0, nil + } + return nonce, err +} + +func SetNonce(db database.KeyValueWriter, address ids.ShortID, nonce uint64) error { + key := Flatten(addressPrefix, address[:]) + return database.PutUInt64(db, key, nonce) +} + +func IncrementNonce(db database.KeyValueReaderWriter, address ids.ShortID, nonce uint64) error { + expectedNonce, err := GetNonce(db, address) + if err != nil { + return err + } + if nonce != expectedNonce { + return errWrongNonce + } + return SetNonce(db, address, nonce+1) +} + +func GetBalance(db database.KeyValueReader, address ids.ShortID, chainID ids.ID) (uint64, error) { + key := Flatten(addressPrefix, address[:], chainID[:]) + balance, err := database.GetUInt64(db, key) + if errors.Is(err, database.ErrNotFound) { + return 0, nil + } + return balance, err +} + +func SetBalance(db database.KeyValueWriterDeleter, address ids.ShortID, chainID ids.ID, balance uint64) error { + key := Flatten(addressPrefix, address[:], chainID[:]) + if balance == 0 { + return db.Delete(key) + } + return database.PutUInt64(db, key, balance) +} + +func DecreaseBalance(db database.KeyValueReaderWriterDeleter, address ids.ShortID, chainID ids.ID, amount uint64) error { + balance, err := GetBalance(db, address, chainID) + if err != nil { + return err + } + if balance < amount { + return errInsufficientBalance + } + return SetBalance(db, address, chainID, balance-amount) +} + +func IncreaseBalance(db database.KeyValueReaderWriterDeleter, address ids.ShortID, chainID ids.ID, amount uint64) error { + balance, err := GetBalance(db, address, chainID) + if err != nil { + return err + } + balance, err = math.Add64(balance, amount) + if err != nil { + return err + } + return SetBalance(db, address, chainID, balance) +} + +// Chain state + +func HasLoanID(db database.KeyValueReader, chainID ids.ID, loanID ids.ID) (bool, error) { + key := Flatten(chainPrefix, chainID[:], loanID[:]) + return db.Has(key) +} + +func AddLoanID(db database.KeyValueWriter, chainID ids.ID, loanID ids.ID) error { + key := Flatten(chainPrefix, chainID[:], loanID[:]) + return db.Put(key, nil) +} + +func GetLoan(db database.KeyValueReader, chainID ids.ID) (uint64, error) { + key := Flatten(chainPrefix, chainID[:]) + balance, err := database.GetUInt64(db, key) + if errors.Is(err, database.ErrNotFound) { + return 0, nil + } + return balance, err +} + +func SetLoan(db database.KeyValueWriterDeleter, chainID ids.ID, balance uint64) error { + key := Flatten(chainPrefix, chainID[:]) + if balance == 0 { + return db.Delete(key) + } + return database.PutUInt64(db, key, balance) +} + +func DecreaseLoan(db database.KeyValueReaderWriterDeleter, chainID ids.ID, amount uint64) error { + balance, err := GetLoan(db, chainID) + if err != nil { + return err + } + if balance < amount { + return errInsufficientBalance + } + return SetLoan(db, chainID, balance-amount) +} + +func IncreaseLoan(db database.KeyValueReaderWriterDeleter, chainID ids.ID, amount uint64) error { + balance, err := GetLoan(db, chainID) + if err != nil { + return err + } + balance, err = math.Add64(balance, amount) + if err != nil { + return err + } + return SetLoan(db, chainID, balance) +} + +// Message state + +func GetMessage(db database.KeyValueReader, txID ids.ID) (*warp.UnsignedMessage, error) { + key := Flatten(messagePrefix, txID[:]) + bytes, err := db.Get(key) + if err != nil { + return nil, err + } + return warp.ParseUnsignedMessage(bytes) +} + +func SetMessage(db database.KeyValueWriter, txID ids.ID, message *warp.UnsignedMessage) error { + key := Flatten(messagePrefix, txID[:]) + bytes := message.Bytes() + return db.Put(key, bytes) +} diff --git a/vms/example/xsvm/tx/codec.go b/vms/example/xsvm/tx/codec.go new file mode 100644 index 000000000000..aa8ce5738415 --- /dev/null +++ b/vms/example/xsvm/tx/codec.go @@ -0,0 +1,33 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "math" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils/wrappers" +) + +// Version is the current default codec version +const Version = 0 + +var Codec codec.Manager + +func init() { + c := linearcodec.NewCustomMaxLength(math.MaxInt32) + Codec = codec.NewManager(math.MaxInt32) + + errs := wrappers.Errs{} + errs.Add( + c.RegisterType(&Transfer{}), + c.RegisterType(&Export{}), + c.RegisterType(&Import{}), + Codec.RegisterCodec(Version, c), + ) + if errs.Errored() { + panic(errs.Err) + } +} diff --git a/vms/example/xsvm/tx/export.go b/vms/example/xsvm/tx/export.go new file mode 100644 index 000000000000..1192952c4dca --- /dev/null +++ b/vms/example/xsvm/tx/export.go @@ -0,0 +1,24 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import "github.com/ava-labs/avalanchego/ids" + +var _ Unsigned = (*Export)(nil) + +type Export struct { + // ChainID provides cross chain replay protection + ChainID ids.ID `serialize:"true" json:"chainID"` + // Nonce provides internal chain replay protection + Nonce uint64 `serialize:"true" json:"nonce"` + MaxFee uint64 `serialize:"true" json:"maxFee"` + PeerChainID ids.ID `serialize:"true" json:"peerChainID"` + IsReturn bool `serialize:"true" json:"isReturn"` + Amount uint64 `serialize:"true" json:"amount"` + To ids.ShortID `serialize:"true" json:"to"` +} + +func (e *Export) Visit(v Visitor) error { + return v.Export(e) +} diff --git a/vms/example/xsvm/tx/import.go b/vms/example/xsvm/tx/import.go new file mode 100644 index 000000000000..8449c79c55b2 --- /dev/null +++ b/vms/example/xsvm/tx/import.go @@ -0,0 +1,18 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +var _ Unsigned = (*Import)(nil) + +type Import struct { + // Nonce provides internal chain replay protection + Nonce uint64 `serialize:"true" json:"nonce"` + MaxFee uint64 `serialize:"true" json:"maxFee"` + // Message includes the chainIDs to provide cross chain replay protection + Message []byte `serialize:"true" json:"message"` +} + +func (i *Import) Visit(v Visitor) error { + return v.Import(i) +} diff --git a/vms/example/xsvm/tx/payload.go b/vms/example/xsvm/tx/payload.go new file mode 100644 index 000000000000..e93e5f560ac5 --- /dev/null +++ b/vms/example/xsvm/tx/payload.go @@ -0,0 +1,48 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import "github.com/ava-labs/avalanchego/ids" + +type Payload struct { + // Sender + Nonce provides replay protection + Sender ids.ShortID `serialize:"true" json:"sender"` + Nonce uint64 `serialize:"true" json:"nonce"` + IsReturn bool `serialize:"true" json:"isReturn"` + Amount uint64 `serialize:"true" json:"amount"` + To ids.ShortID `serialize:"true" json:"to"` + + bytes []byte +} + +func (p *Payload) Bytes() []byte { + return p.bytes +} + +func NewPayload( + sender ids.ShortID, + nonce uint64, + isReturn bool, + amount uint64, + to ids.ShortID, +) (*Payload, error) { + p := &Payload{ + Sender: sender, + Nonce: nonce, + IsReturn: isReturn, + Amount: amount, + To: to, + } + bytes, err := Codec.Marshal(Version, p) + p.bytes = bytes + return p, err +} + +func ParsePayload(bytes []byte) (*Payload, error) { + p := &Payload{ + bytes: bytes, + } + _, err := Codec.Unmarshal(bytes, p) + return p, err +} diff --git a/vms/example/xsvm/tx/transfer.go b/vms/example/xsvm/tx/transfer.go new file mode 100644 index 000000000000..c895b5e5cc51 --- /dev/null +++ b/vms/example/xsvm/tx/transfer.go @@ -0,0 +1,23 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import "github.com/ava-labs/avalanchego/ids" + +var _ Unsigned = (*Transfer)(nil) + +type Transfer struct { + // ChainID provides cross chain replay protection + ChainID ids.ID `serialize:"true" json:"chainID"` + // Nonce provides internal chain replay protection + Nonce uint64 `serialize:"true" json:"nonce"` + MaxFee uint64 `serialize:"true" json:"maxFee"` + AssetID ids.ID `serialize:"true" json:"assetID"` + Amount uint64 `serialize:"true" json:"amount"` + To ids.ShortID `serialize:"true" json:"to"` +} + +func (t *Transfer) Visit(v Visitor) error { + return v.Transfer(t) +} diff --git a/vms/example/xsvm/tx/tx.go b/vms/example/xsvm/tx/tx.go new file mode 100644 index 000000000000..5ada3bf79129 --- /dev/null +++ b/vms/example/xsvm/tx/tx.go @@ -0,0 +1,64 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/hashing" +) + +var secp256k1r = secp256k1.Factory{ + Cache: cache.LRU[ids.ID, *secp256k1.PublicKey]{ + Size: 2048, + }, +} + +type Tx struct { + Unsigned `serialize:"true" json:"unsigned"` + Signature [secp256k1.SignatureLen]byte `serialize:"true" json:"signature"` +} + +func Parse(bytes []byte) (*Tx, error) { + tx := &Tx{} + _, err := Codec.Unmarshal(bytes, tx) + return tx, err +} + +func Sign(utx Unsigned, key *secp256k1.PrivateKey) (*Tx, error) { + unsignedBytes, err := Codec.Marshal(Version, &utx) + if err != nil { + return nil, err + } + + sig, err := key.Sign(unsignedBytes) + if err != nil { + return nil, err + } + + tx := &Tx{ + Unsigned: utx, + } + copy(tx.Signature[:], sig) + return tx, nil +} + +func (tx *Tx) ID() (ids.ID, error) { + bytes, err := Codec.Marshal(Version, tx) + return hashing.ComputeHash256Array(bytes), err +} + +func (tx *Tx) SenderID() (ids.ShortID, error) { + unsignedBytes, err := Codec.Marshal(Version, &tx.Unsigned) + if err != nil { + return ids.ShortEmpty, err + } + + pk, err := secp256k1r.RecoverPublicKey(unsignedBytes, tx.Signature[:]) + if err != nil { + return ids.ShortEmpty, err + } + return pk.Address(), nil +} diff --git a/vms/example/xsvm/tx/unsigned.go b/vms/example/xsvm/tx/unsigned.go new file mode 100644 index 000000000000..2c57f91e26fb --- /dev/null +++ b/vms/example/xsvm/tx/unsigned.go @@ -0,0 +1,8 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +type Unsigned interface { + Visit(Visitor) error +} diff --git a/vms/example/xsvm/tx/visitor.go b/vms/example/xsvm/tx/visitor.go new file mode 100644 index 000000000000..b411e06f15b0 --- /dev/null +++ b/vms/example/xsvm/tx/visitor.go @@ -0,0 +1,10 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +type Visitor interface { + Transfer(*Transfer) error + Export(*Export) error + Import(*Import) error +} diff --git a/vms/example/xsvm/vm.go b/vms/example/xsvm/vm.go new file mode 100644 index 000000000000..7e50d3b4c349 --- /dev/null +++ b/vms/example/xsvm/vm.go @@ -0,0 +1,190 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package xsvm + +import ( + "context" + "fmt" + "net/http" + + "github.com/gorilla/rpc/v2" + + "go.uber.org/zap" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/manager" + "github.com/ava-labs/avalanchego/database/versiondb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils/json" + "github.com/ava-labs/avalanchego/version" + "github.com/ava-labs/avalanchego/vms/example/xsvm/api" + "github.com/ava-labs/avalanchego/vms/example/xsvm/builder" + "github.com/ava-labs/avalanchego/vms/example/xsvm/chain" + "github.com/ava-labs/avalanchego/vms/example/xsvm/execute" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/example/xsvm/state" + + smblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + xsblock "github.com/ava-labs/avalanchego/vms/example/xsvm/block" +) + +var ( + _ smblock.ChainVM = (*VM)(nil) + _ smblock.BuildBlockWithContextChainVM = (*VM)(nil) +) + +type VM struct { + common.AppHandler + + chainContext *snow.Context + db database.Database + genesis *genesis.Genesis + engineChan chan<- common.Message + + chain chain.Chain + builder builder.Builder +} + +func (vm *VM) Initialize( + _ context.Context, + chainContext *snow.Context, + dbManager manager.Manager, + genesisBytes []byte, + _ []byte, + _ []byte, + engineChan chan<- common.Message, + _ []*common.Fx, + _ common.AppSender, +) error { + vm.AppHandler = common.NewNoOpAppHandler(chainContext.Log) + + chainContext.Log.Info("initializing xsvm", + zap.Stringer("version", Version), + ) + + vm.chainContext = chainContext + vm.db = dbManager.Current().Database + + g, err := genesis.Parse(genesisBytes) + if err != nil { + return fmt.Errorf("failed to parse genesis bytes: %w", err) + } + + vdb := versiondb.New(vm.db) + if err := execute.Genesis(vdb, chainContext.ChainID, g); err != nil { + return fmt.Errorf("failed to initialize genesis state: %w", err) + } + if err := vdb.Commit(); err != nil { + return err + } + + vm.genesis = g + vm.engineChan = engineChan + + vm.chain, err = chain.New(chainContext, vm.db) + if err != nil { + return fmt.Errorf("failed to initialize chain manager: %w", err) + } + + vm.builder = builder.New(chainContext, engineChan, vm.chain) + + chainContext.Log.Info("initialized xsvm", + zap.Stringer("lastAcceptedID", vm.chain.LastAccepted()), + ) + return nil +} + +func (vm *VM) SetState(_ context.Context, state snow.State) error { + vm.chain.SetChainState(state) + return nil +} + +func (vm *VM) Shutdown(context.Context) error { + if vm.chainContext == nil { + return nil + } + return vm.db.Close() +} + +func (*VM) Version(context.Context) (string, error) { + return Version.String(), nil +} + +func (*VM) CreateStaticHandlers(context.Context) (map[string]*common.HTTPHandler, error) { + return nil, nil +} + +func (vm *VM) CreateHandlers(_ context.Context) (map[string]*common.HTTPHandler, error) { + server := rpc.NewServer() + server.RegisterCodec(json.NewCodec(), "application/json") + server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") + api := api.NewServer( + vm.chainContext, + vm.genesis, + vm.db, + vm.chain, + vm.builder, + ) + if err := server.RegisterService(api, Name); err != nil { + return nil, err + } + return map[string]*common.HTTPHandler{ + "": { + LockOptions: common.WriteLock, + Handler: server, + }, + }, nil +} + +func (*VM) HealthCheck(context.Context) (interface{}, error) { + return http.StatusOK, nil +} + +func (*VM) Connected(context.Context, ids.NodeID, *version.Application) error { + return nil +} + +func (*VM) Disconnected(context.Context, ids.NodeID) error { + return nil +} + +func (vm *VM) GetBlock(_ context.Context, blkID ids.ID) (snowman.Block, error) { + return vm.chain.GetBlock(blkID) +} + +func (vm *VM) ParseBlock(_ context.Context, blkBytes []byte) (snowman.Block, error) { + blk, err := xsblock.Parse(blkBytes) + if err != nil { + return nil, err + } + return vm.chain.NewBlock(blk) +} + +func (vm *VM) BuildBlock(ctx context.Context) (snowman.Block, error) { + return vm.builder.BuildBlock(ctx, nil) +} + +func (vm *VM) SetPreference(_ context.Context, preferred ids.ID) error { + vm.builder.SetPreference(preferred) + return nil +} + +func (vm *VM) LastAccepted(context.Context) (ids.ID, error) { + return vm.chain.LastAccepted(), nil +} + +func (vm *VM) BuildBlockWithContext(ctx context.Context, blockContext *smblock.Context) (snowman.Block, error) { + return vm.builder.BuildBlock(ctx, blockContext) +} + +func (*VM) VerifyHeightIndex(context.Context) error { + return nil +} + +func (vm *VM) GetBlockIDAtHeight(_ context.Context, height uint64) (ids.ID, error) { + return state.GetBlockIDByHeight(vm.db, height) +}