diff --git a/README.md b/README.md index d97d8b36..01ad090f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,14 @@ KVVM is served over RPC with [go-plugin](https://github.com/hashicorp/go-plugin) At its core, the Avalanche protocol still maintains the immutable ordered sequence of states in a fully permissionless settings. And KVVM defines the rules and data structures to store key-value pairs. -To interact with Avalanche network RPC chain APIs, download and run a [AvalancheGo](https://github.com/ava-labs/avalanchego#installation) node locally, as follows: +Build quarkvm: + +```bash +cd ${HOME}/go/src/github.com/ava-labs/quarkvm +./scripts/build.sh +``` + +*Step 1.* To interact with Avalanche network RPC chain APIs, download and run a [AvalancheGo](https://github.com/ava-labs/avalanchego#installation) node locally, as follows: ```bash # run 1 avalanchego node in local network @@ -36,6 +43,110 @@ curl -X POST --data '{ }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/health ``` -TODO: example commands +*Step 2.* Create a user: + +```bash +curl --location --request POST '127.0.0.1:9650/ext/keystore' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc":"2.0", + "id" :1, + "method" :"keystore.createUser", + "params" :{ + "username":"testusername123", + "password":"insecurestring789" + } +}' +``` + +*Step 3.* Import the pre-funded key for the P-chain: + +```bash +curl --location --request POST '127.0.0.1:9650/ext/P' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc":"2.0", + "id" :1, + "method" :"platform.importKey", + "params" :{ + "username":"testusername123", + "password":"insecurestring789", + "privateKey":"PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN" + } +}' +# {"jsonrpc":"2.0","result":{"address":"P-local18jma8ppw3nhx5r4ap8clazz0dps7rv5u00z96u"},"id":1} +``` + +*Step 4.* Get the list of P-chain addresses: + +```bash +curl -X POST --data '{ + "jsonrpc": "2.0", + "method": "platform.listAddresses", + "params": { + "username":"testusername123", + "password":"insecurestring789" + }, + "id": 1 +}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/P +# {"jsonrpc":"2.0","result":{"addresses":["P-local18jma8ppw3nhx5r4ap8clazz0dps7rv5u00z96u"]},"id":1} +``` + +*Step 5.* Create a subnet: + +```bash +curl --location --request POST '127.0.0.1:9650/ext/P' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc":"2.0", + "id" :1, + "method" :"platform.createSubnet", + "params" :{ + "username":"testusername123", + "password":"insecurestring789", + "threshold":1, + "controlKeys":["P-local18jma8ppw3nhx5r4ap8clazz0dps7rv5u00z96u"] + } +}' +# {"jsonrpc":"2.0","result":{"txID":"29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL","changeAddr":"P-local18jma8ppw3nhx5r4ap8clazz0dps7rv5u00z96u"},"id":1} +# 29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL is the subnet blockchain ID +``` + +*Step 6.* Create a blockchain: +```bash +# TODO: where to get vmID +curl --location --request POST '127.0.0.1:9650/ext/P' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc":"2.0", + "id" :1, + "method" :"platform.createBlockchain", + "params" :{ + "username":"testusername123", + "password":"insecurestring789", + "vmID":"tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH", + "subnetID":"29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL", + "name":"quarkvm", + "genesisData":"", + "controlKeys":["P-local18jma8ppw3nhx5r4ap8clazz0dps7rv5u00z96u"] + } +}' +# +``` + +Connect to quarkvm: +```bash +curl --location --request POST '127.0.0.1:9650/ext/vm/tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc":"2.0", + "id" :1, + "method" :"quarkvm.put", + "params" :{ + "key":"foo", + "value":"bar" + } +}' +``` diff --git a/agent/agent.go b/agent/agent.go new file mode 100644 index 00000000..317ca043 --- /dev/null +++ b/agent/agent.go @@ -0,0 +1,137 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package agent implements KVVM agent. +package agent + +import ( + "context" + "fmt" + "math/big" + "math/rand" + "time" + + "ekyu.moe/cryptonight" + "github.com/ava-labs/quarkvm/chain" + "github.com/ava-labs/quarkvm/crypto/ed25519" + "github.com/ava-labs/quarkvm/transaction" +) + +type Agent interface { + Run() +} + +type agent struct { + ctx context.Context + chain chain.Chain + + privateKey ed25519.PrivateKey + publicKey ed25519.PublicKey +} + +func New(ctx context.Context, chain chain.Chain) Agent { + prv, err := ed25519.NewPrivateKey() + if err != nil { + panic(err) + } + pub := prv.PublicKey() + + fmt.Println("new agent:", pub.Address()) + return &agent{ + ctx: ctx, + chain: chain, + + privateKey: prv, + publicKey: pub, + } +} + +func (a *agent) Run() { + for a.ctx.Err() == nil { + prefix := randString(16) + if rand.Intn(100) < 20 { + // claim own address key + prefix = a.publicKey.Address() + fmt.Println("attempting to claim address prefix", prefix) + } + utx := a.claim(prefix) + a.mine(utx) + stx := a.sign(utx) + a.chain.Submit(stx) + + // wait for claim to be set or abandon + confirmed := a.confirm(stx) + if !confirmed { + // TODO: try again with same prefix + continue + } + owner, _, err := a.chain.GetPrefixInfo([]byte(prefix)) + if err != nil { + panic(err) + } + fmt.Println("prefix claimed:", prefix, "expires:", owner.Expiry, "keys:", owner.Keys) + // TODO: print out "rate of decay" + // TODO: set 2 keys + // TODO: delete 1 key + // TODO: wait for key expiry + // TODO: attempt to set key + // TODO: add lifeline + } +} + +func (a *agent) claim(prefix string) transaction.Unsigned { + return transaction.NewClaim(a.publicKey, []byte(prefix)) +} + +func (a *agent) mine(utx transaction.Unsigned) { + for { + cbID := a.chain.CurrentBlock().ID() + utx.SetBlockID(cbID) + graffiti := big.NewInt(0) + for a.chain.ValidBlockID(cbID) { + utx.SetGraffiti(graffiti.Bytes()) + h := cryptonight.Sum(transaction.UnsignedBytes(utx), 2) + if cryptonight.CheckHash(h, a.chain.DifficultyEstimate()) { + return + } + graffiti.Add(graffiti, big.NewInt(1)) + } + // Get new block hash if no longer valid + } +} + +func (a *agent) sign(utx transaction.Unsigned) *transaction.Transaction { + sig, err := a.privateKey.Sign(transaction.UnsignedBytes(utx)) + if err != nil { + panic(err) + } + return transaction.New(utx, sig) +} + +func (a *agent) confirm(stx *transaction.Transaction) bool { + loops := 0 + for a.ctx.Err() == nil && a.chain.ValidBlockID(stx.Unsigned.GetBlockID()) { + if a.chain.TxConfirmed(stx.ID()) { + return true + } + time.Sleep(1 * time.Second) + loops++ + + // Resubmit if pending for a while but still valid + if loops%5 == 0 && !a.chain.MempoolContains(stx.ID()) { + a.chain.Submit(stx) + } + } + return false +} + +func randString(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + l := rand.Intn(n) + 1 // ensure never 0 + + s := make([]rune, l) + for i := range s { + s[i] = letters[rand.Intn(len(letters))] + } + return string(s) +} diff --git a/block/block.go b/block/block.go new file mode 100644 index 00000000..129e73cc --- /dev/null +++ b/block/block.go @@ -0,0 +1,136 @@ +package block + +import ( + "errors" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/storage" + "github.com/ava-labs/quarkvm/transaction" +) + +func init() { + codec.RegisterType(&Block{}) +} + +var _ snowman.Block = &Block{} + +var ( + ErrTimestampTooEarly = errors.New("block timestamp too early") + ErrTimestampTooLate = errors.New("block timestamp too late") +) + +type Block struct { + Prnt ids.ID `serialize:"true" json:"parentID"` + Tmstmp int64 `serialize:"true" json:"timestamp"` + Hght uint64 `serialize:"true" json:"height"` + + MinDifficulty uint64 `serialize:"true" json:"minDifficulty"` + BlockCost uint64 `serialize:"true" json:"blockCost"` + Txs []*transaction.Transaction `serialize:"true" json:"txs"` + + raw []byte + id ids.ID + st choices.Status + s storage.Storage + lookup func(ids.ID) (*Block, error) + onVerify func(*Block) error + onAccept func(*Block) error + onReject func(*Block) error +} + +func (b *Block) Update( + source []byte, + status choices.Status, + s storage.Storage, + lookup func(ids.ID) (*Block, error), + onVerify func(*Block) error, + onAccept func(*Block) error, + onReject func(*Block) error, +) { + id, err := ids.ToID(hashing.ComputeHash256(source)) + if err != nil { + panic(err) + } + b.raw = source + b.id = id + b.st = status + b.s = s + b.lookup = lookup + b.onVerify = onVerify + b.onAccept = onAccept + b.onReject = onReject +} + +// implements "snowman.Block.choices.Decidable" +func (b *Block) ID() ids.ID { return b.id } + +// implements "snowman.Block" +func (b *Block) Verify() error { + if b.st == choices.Accepted { + return nil + } + + parentID := b.Parent() + parentBlock, err := b.lookup(parentID) + if err != nil { + return err + } + + if b.Timestamp().Unix() < parentBlock.Timestamp().Unix() { + return ErrTimestampTooEarly + } + if b.Timestamp().Unix() >= time.Now().Add(time.Hour).Unix() { + return ErrTimestampTooLate + } + + return b.onVerify(b) +} + +// implements "snowman.Block.choices.Decidable" +func (b *Block) Accept() error { + for _, tx := range b.Txs { + if err := tx.Accept(b.s, b.Tmstmp); err != nil { + return err + } + } + b.st = choices.Accepted + return b.onAccept(b) +} + +// implements "snowman.Block.choices.Decidable" +func (b *Block) Reject() error { + b.st = choices.Rejected + return b.onReject(b) +} + +// implements "snowman.Block.choices.Decidable" +func (b *Block) Status() choices.Status { + return b.st +} + +// implements "snowman.Block" +func (b *Block) Parent() ids.ID { return b.Prnt } + +// implements "snowman.Block" +func (b *Block) Bytes() []byte { + d, err := codec.Marshal(b) + if err != nil { + panic(err) + } + return d +} + +// implements "snowman.Block" +func (b *Block) Height() uint64 { + return b.Hght +} + +// implements "snowman.Block" +func (b *Block) Timestamp() time.Time { + return time.Unix(b.Tmstmp, 0) +} diff --git a/chain/chain.go b/chain/chain.go new file mode 100644 index 00000000..8d5bf2d3 --- /dev/null +++ b/chain/chain.go @@ -0,0 +1,330 @@ +package chain + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/quarkvm/block" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/owner" + "github.com/ava-labs/quarkvm/storage" + "github.com/ava-labs/quarkvm/transaction" +) + +type Chain interface { + GetBlock(id ids.ID) (*block.Block, error) + AddBlock(b *block.Block) + CurrentBlock() *block.Block + SetLastAccepted(id ids.ID) + GetLastAccepted() ids.ID + PutBlock(*block.Block) error + + // TODO: separate mempool interface? + Produce() *block.Block + Submit(*transaction.Transaction) + ValidBlockID(ids.ID) bool + GetPrefixInfo(prefix []byte) (*owner.Owner, bool, error) + GetValue(key []byte) ([]byte, bool, error) + Pending() int + MempoolContains(ids.ID) bool + TxConfirmed(ids.ID) bool + DifficultyEstimate() uint64 +} + +const ( + lookbackWindow = 10 + blockTarget = 1 + targetTransactions = 10 * lookbackWindow / blockTarget // TODO: can be higher on real network + blockTimer = 250 * time.Millisecond // TODO: set to be block target on real network + minDifficulty = 1 // TODO: set much higher on real network + minBlockCost = 0 // in units of tx surplus + mempoolSize = 1024 + maxKeyLength = 256 + expiryTime = 30 // TODO: set much longer on real network +) + +var _ Chain = &chain{} + +type chain struct { + mu sync.RWMutex + + s storage.Storage + + mempool transaction.Mempool + + // add when a block is produced + // TODO: cache blocks + blocks []*block.Block + lastAccepted ids.ID +} + +func New(s storage.Storage) Chain { + return &chain{ + s: s, + mempool: transaction.NewMempool(mempoolSize), + blocks: []*block.Block{ + // for initial produce + { + MinDifficulty: minDifficulty, + BlockCost: minBlockCost, + }, + }, + } +} + +func (c *chain) GetBlock(id ids.ID) (*block.Block, error) { + // TODO: read from cache + blkBytes, err := c.s.Block().Get(id[:]) + if err != nil { + return nil, err + } + + blk := new(block.Block) + if _, err := codec.Unmarshal(blkBytes, blk); err != nil { + return nil, err + } + return blk, nil +} + +func (c *chain) AddBlock(b *block.Block) { + c.mu.Lock() + defer c.mu.Unlock() + c.blocks = append(c.blocks, b) +} + +func (c *chain) CurrentBlock() *block.Block { + return c.blocks[len(c.blocks)-1] +} + +func (c *chain) SetLastAccepted(id ids.ID) { + c.mu.Lock() + defer c.mu.Unlock() + c.lastAccepted = id +} + +func (c *chain) GetLastAccepted() ids.ID { + return c.lastAccepted +} + +func (c *chain) PutBlock(b *block.Block) error { + id := b.ID() + d, err := codec.Marshal(b) + if err != nil { + return err + } + return c.s.Block().Put(id[:], d) +} + +func (c *chain) ValidBlockID(blockID ids.ID) bool { + now := time.Now().Unix() + for i := len(c.blocks) - 1; i >= 0; i-- { + ob := c.blocks[i] + sinceBlock := now - ob.Timestamp().Unix() + if sinceBlock > lookbackWindow && i != len(c.blocks)-1 { + break + } + if ob.ID() == blockID { + return true + } + } + return false +} + +func (c *chain) Submit(tx *transaction.Transaction) { + // Important to cache the difficulty of the transaction prior to block + // production otherwise the block prod loop will be SUPER slow. + c.mu.Lock() + defer c.mu.Unlock() + c.mempool.Push(tx) +} + +func (c *chain) GetPrefixInfo(prefix []byte) (*owner.Owner, bool, error) { + has, err := c.s.Owner().Has(prefix) + if err != nil { + return nil, false, err + } + if !has { + return nil, false, nil + } + v, err := c.s.Owner().Get(prefix) + if err != nil { + return nil, false, err + } + iv := new(owner.Owner) + if _, err := codec.Unmarshal(v, iv); err != nil { + return nil, false, err + } + return iv, true, nil +} + +func (c *chain) GetValue(key []byte) ([]byte, bool, error) { + return c.s.Get(key) +} + +func (c *chain) DifficultyEstimate() uint64 { + totalDifficulty := uint64(0) + totalBlocks := uint64(0) + currTime := time.Now().Unix() + for i := len(c.blocks) - 1; i >= 0; i-- { + ob := c.blocks[i] + sinceBlock := currTime - ob.Timestamp().Unix() + if sinceBlock > lookbackWindow/2 && i != len(c.blocks)-1 { + break + } + totalDifficulty += ob.MinDifficulty + totalBlocks++ + } + return totalDifficulty/totalBlocks + 1 +} + +func (c *chain) RecentData(currTime int64, lastBlock *block.Block) (ids.Set, ids.Set, uint64, uint64) { + // get tx count over lookback + recentBlockIDs := ids.NewSet(256) // TODO: figure out right sizes here, keep track of dynamically + recentTxIDs := ids.NewSet(256) + for i := len(c.blocks) - 1; i >= 0; i-- { + ob := c.blocks[i] + sinceBlock := currTime - ob.Timestamp().Unix() + if sinceBlock > lookbackWindow && i != len(c.blocks)-1 { + break + } + recentBlockIDs.Add(ob.ID()) + for _, tx := range ob.Txs { + recentTxIDs.Add(tx.ID()) + } + } + + // compute new block cost + secondsSinceLast := currTime - lastBlock.Timestamp().Unix() + newBlockCost := lastBlock.BlockCost + if secondsSinceLast < blockTarget { + newBlockCost += uint64(blockTarget - secondsSinceLast) + } else { + possibleDiff := uint64(secondsSinceLast - blockTarget) + if possibleDiff < newBlockCost-minBlockCost { + newBlockCost -= possibleDiff + } else { + newBlockCost = minBlockCost + } + } + + // compute new min difficulty + newMinDifficulty := lastBlock.MinDifficulty + recentTxs := recentTxIDs.Len() + if recentTxs > targetTransactions { + newMinDifficulty++ + } else if recentTxs < targetTransactions { + elapsedWindows := uint64(secondsSinceLast/lookbackWindow) + 1 // account for current window being less + if elapsedWindows < newMinDifficulty-minDifficulty { + newMinDifficulty -= elapsedWindows + } else { + newMinDifficulty = minDifficulty + } + } + + return recentBlockIDs, recentTxIDs, newBlockCost, newMinDifficulty +} + +func (c *chain) Produce() *block.Block { + lb := c.blocks[len(c.blocks)-1] + now := time.Now().Unix() + recentBlockIDs, recentTxIDs, blockCost, minDifficulty := c.RecentData(now, lb) + + // TODO: should be from ParseBlock + b := &block.Block{ + Tmstmp: now, + Prnt: lb.ID(), + BlockCost: blockCost, + MinDifficulty: minDifficulty, + } + + // select new transactions + c.mu.Lock() + defer c.mu.Unlock() + b.Txs = []*transaction.Transaction{} + c.mempool.Prune(recentBlockIDs) + prefixes := ids.NewSet(targetTransactions) + for len(b.Txs) < targetTransactions && c.mempool.Len() > 0 { + next, diff := c.mempool.PopMax() + if diff < b.MinDifficulty { + c.mempool.Push(next) + break + } + p := next.PrefixID() + if prefixes.Contains(p) { + continue + } + if err := next.Verify(c.s, b.Timestamp().Unix(), recentBlockIDs, recentTxIDs, b.MinDifficulty); err != nil { + fmt.Println("dropping tx", "id:", next.ID(), "err:", err) + continue + } + // Wait to add prefix until after verification + prefixes.Add(p) + b.Txs = append(b.Txs, next) + } + + return b +} + +func (c *chain) Pending() int { + c.mu.RLock() + cnt := c.mempool.Len() + c.mu.RUnlock() + return cnt +} + +func (c *chain) MempoolContains(txID ids.ID) bool { + c.mu.RLock() + has := c.mempool.Has(txID) + c.mu.RUnlock() + return has +} + +func (c *chain) TxConfirmed(txID ids.ID) bool { + id := append([]byte{}, txID[:]...) + has, err := c.s.Tx().Has(id) + if err != nil { + panic(err) + } + return has +} + +func (c *chain) Verify(b *block.Block) error { + if len(b.Txs) == 0 { + return errors.New("no fee-paying transactions") + } + lastBlock := c.blocks[len(c.blocks)-1] + if b.Timestamp().Unix() < lastBlock.Timestamp().Unix() { + return errors.New("invalid block time") + } + recentBlockIDs, recentTxIDs, blockCost, minDifficulty := c.RecentData(b.Timestamp().Unix(), lastBlock) + if b.BlockCost != blockCost { + return errors.New("invalid block cost") + } + if b.MinDifficulty != minDifficulty { + return errors.New("invalid difficulty") + } + // Ensure only 1 claim per prefix per block (otherwise both may pass Verify + // and one will fail on accept) + prefixes := ids.NewSet(len(b.Txs)) + surplusDifficulty := uint64(0) + for _, tx := range b.Txs { + p := tx.PrefixID() + if prefixes.Contains(p) { + return errors.New("only 1 operation per prefix allowed per block") + } + prefixes.Add(p) + if err := tx.Verify(c.s, b.Timestamp().Unix(), recentBlockIDs, recentTxIDs, minDifficulty); err != nil { + return err + } + surplusDifficulty += tx.Difficulty() - minDifficulty + } + + // Ensure enough work is performed to compensate for block production speed + if surplusDifficulty < minDifficulty*b.BlockCost { + return errors.New("insufficient block burn") + } + return nil +} diff --git a/cmd/quarkvm/main.go b/cmd/quarkvm/main.go index c9cd928d..d1e507df 100644 --- a/cmd/quarkvm/main.go +++ b/cmd/quarkvm/main.go @@ -4,35 +4,74 @@ package main import ( + "context" "fmt" "os" + "os/signal" + "syscall" "github.com/ava-labs/avalanchego/vms/rpcchainvm" - "github.com/ava-labs/quarkvm/quarkvm" + "github.com/ava-labs/quarkvm/version" + "github.com/ava-labs/quarkvm/vm" "github.com/hashicorp/go-plugin" log "github.com/inconshreveable/log15" + "golang.org/x/sync/errgroup" ) func main() { - version, err := PrintVersion() + printVersion, err := PrintVersion() if err != nil { fmt.Printf("couldn't get config: %s", err) os.Exit(1) } // Print VM ID and exit - if version { - fmt.Printf("%s@%s\n", quarkvm.Name, quarkvm.Version) + if printVersion { + fmt.Printf("%s@%s\n", vm.Name, version.Version) os.Exit(0) } - log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat()))) - plugin.Serve(&plugin.ServeConfig{ - HandshakeConfig: rpcchainvm.Handshake, - Plugins: map[string]plugin.Plugin{ - "vm": rpcchainvm.New(&quarkvm.VM{}), - }, - - // A non-nil value here enables gRPC serving for this plugin... - GRPCServer: plugin.DefaultGRPCServer, + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + // When we get a SIGINT or SIGTERM, stop the network. + signalsCh := make(chan os.Signal, 1) + signal.Notify(signalsCh, syscall.SIGINT) + signal.Notify(signalsCh, syscall.SIGTERM) + go func() { + select { + case <-ctx.Done(): + return + case sig := <-signalsCh: + fmt.Println("received OS signal:", sig) + cancel() + } + }() + + g, gctx := errgroup.WithContext(ctx) + _ = gctx + + // TODO create agent + // TODO create vm + + g.Go(func() error { + plugin.Serve(&plugin.ServeConfig{ + // HandshakeConfig: rpcchainvm.Handshake, + HandshakeConfig: plugin.HandshakeConfig{ + ProtocolVersion: 9, + MagicCookieKey: "VM_PLUGIN", + MagicCookieValue: "dynamic", + }, + Plugins: map[string]plugin.Plugin{ + "vm": rpcchainvm.New(&vm.VM{}), + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) + return nil }) + if err := g.Wait(); err != nil { + panic(err) + } } diff --git a/cmd/quarkvm/params.go b/cmd/quarkvm/params.go index cb2545a1..966e4e70 100644 --- a/cmd/quarkvm/params.go +++ b/cmd/quarkvm/params.go @@ -6,7 +6,7 @@ package main import ( "flag" - "github.com/ava-labs/quarkvm/quarkvm" + "github.com/ava-labs/quarkvm/vm" "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -16,7 +16,7 @@ const ( ) func buildFlagSet() *flag.FlagSet { - fs := flag.NewFlagSet(quarkvm.Name, flag.ContinueOnError) + fs := flag.NewFlagSet(vm.Name, flag.ContinueOnError) fs.Bool(versionKey, false, "If true, prints Version and quit") diff --git a/codec/init.go b/codec/init.go new file mode 100644 index 00000000..315f4851 --- /dev/null +++ b/codec/init.go @@ -0,0 +1,47 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package codec imports default message codec managers. +package codec + +import ( + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" +) + +const ( + // CodecVersion is the current default codec version + codecVersion = 0 +) + +var ( + // Codecs do serialization and deserialization + codecManager codec.Manager + c linearcodec.Codec +) + +func init() { + c = linearcodec.NewDefault() + codecManager = codec.NewDefaultManager() + + if err := codecManager.RegisterCodec(codecVersion, c); err != nil { + panic(err) + } +} + +// Manager returns the initialized codec manager. +func Manager() codec.Manager { + return codecManager +} + +func Marshal(source interface{}) ([]byte, error) { + return codecManager.Marshal(codecVersion, source) +} + +func Unmarshal(source []byte, destination interface{}) (uint16, error) { + return codecManager.Unmarshal(source, destination) +} + +func RegisterType(t interface{}) { + c.RegisterType(t) +} diff --git a/crypto/ed25519/crypto.go b/crypto/ed25519/crypto.go new file mode 100644 index 00000000..265f82b0 --- /dev/null +++ b/crypto/ed25519/crypto.go @@ -0,0 +1,93 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package ed25519 implements cryptography utilities +// with Edwards-curve Digital Signature Algorithm (EdDSA). +package ed25519 + +import ( + "crypto/ed25519" + + "github.com/ava-labs/avalanchego/utils/formatting" +) + +type PublicKey interface { + Verify(message, signature []byte) bool + VerifyHash(hash, signature []byte) bool + + Address() string + Bytes() []byte +} + +type PrivateKey interface { + PublicKey() PublicKey + + Sign(message []byte) ([]byte, error) + SignHash(hash []byte) ([]byte, error) + + Bytes() []byte +} + +// NewPrivateKey implements the Factory interface +func NewPrivateKey() (PrivateKey, error) { + _, k, err := ed25519.GenerateKey(nil) + return &PrivateKeyED25519{sk: k}, err +} + +type PublicKeyED25519 struct { + pk ed25519.PublicKey + addr string +} + +// Verify implements the PublicKey interface +func (k *PublicKeyED25519) Verify(msg, sig []byte) bool { + return ed25519.Verify(k.pk, msg, sig) +} + +// VerifyHash implements the PublicKey interface +func (k *PublicKeyED25519) VerifyHash(hash, sig []byte) bool { + return k.Verify(hash, sig) +} + +// Address implements the PublicKey interface +func (k *PublicKeyED25519) Address() string { + if len(k.addr) == 0 { + addr, err := formatting.EncodeWithChecksum(formatting.CB58, k.pk) + if err != nil { + panic(err) + } + k.addr = addr + } + return k.addr +} + +// Bytes implements the PublicKey interface +func (k *PublicKeyED25519) Bytes() []byte { return k.pk } + +type PrivateKeyED25519 struct { + sk ed25519.PrivateKey + pk *PublicKeyED25519 +} + +// PublicKey implements the PrivateKey interface +func (k *PrivateKeyED25519) PublicKey() PublicKey { + if k.pk == nil { + k.pk = &PublicKeyED25519{ + pk: k.sk.Public().(ed25519.PublicKey), + } + } + return k.pk +} + +// Sign implements the PrivateKey interface +func (k *PrivateKeyED25519) Sign(msg []byte) ([]byte, error) { + return ed25519.Sign(k.sk, msg), nil +} + +// SignHash implements the PrivateKey interface +func (k PrivateKeyED25519) SignHash(hash []byte) ([]byte, error) { + return k.Sign(hash) +} + +// Bytes implements the PrivateKey interface +func (k PrivateKeyED25519) Bytes() []byte { return k.sk } diff --git a/quarkvm/factory.go b/factory.go similarity index 83% rename from quarkvm/factory.go rename to factory.go index 2a611d6a..9982ba54 100644 --- a/quarkvm/factory.go +++ b/factory.go @@ -1,17 +1,16 @@ // Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package quarkvm +package vm import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms" + "github.com/ava-labs/quarkvm/vm" ) var _ vms.Factory = &Factory{} -// Factory ... type Factory struct{} -// New ... -func (f *Factory) New(*snow.Context) (interface{}, error) { return &VM{}, nil } +func (f *Factory) New(*snow.Context) (interface{}, error) { return &vm.VM{}, nil } diff --git a/go.mod b/go.mod index 8fa8798b..808e767b 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,27 @@ module github.com/ava-labs/quarkvm go 1.17 require ( + ekyu.moe/cryptonight v0.3.0 github.com/ava-labs/avalanchego v1.6.5 - github.com/ava-labs/timestampvm v1.1.1 github.com/gorilla/rpc v1.2.0 github.com/hashicorp/go-plugin v1.4.3 github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.9.0 + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c ) require ( github.com/AppsFlyer/go-sundheit v0.2.0 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/aead/skein v0.0.0-20160722084837-9365ae6e95d2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcutil v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200627015759-01fd2de07837 // indirect + github.com/dchest/blake256 v1.0.0 // indirect github.com/fatih/color v1.9.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-stack/stack v1.8.0 // indirect @@ -58,7 +61,6 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect go.opencensus.io v0.23.0 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect diff --git a/go.sum b/go.sum index 695cd4c4..6db09cf3 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +ekyu.moe/cryptonight v0.3.0 h1:Ur4LA+koVyo52/G7GF/5QKyVcloXkIQWjsw0vNvSKEA= +ekyu.moe/cryptonight v0.3.0/go.mod h1:ZSoDow0HyP4oPny5lywoY1Szuo4eH+fpZn7J5YRCVBc= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= github.com/AppsFlyer/go-sundheit v0.2.0 h1:FArqX+HbqZ6U32RC3giEAWRUpkggqxHj91KIvxNgwjU= github.com/AppsFlyer/go-sundheit v0.2.0/go.mod h1:rCRkVTMQo7/krF7xQ9X0XEF1an68viFR6/Gy02q+4ds= @@ -53,6 +55,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/aead/skein v0.0.0-20160722084837-9365ae6e95d2 h1:q5TSngwXJdajCyZPQR+eKyRRgI3/ZXC/Nq1ZxZ4Zxu8= +github.com/aead/skein v0.0.0-20160722084837-9365ae6e95d2/go.mod h1:4JBZEId5BaLqvA2DGU53phvwkn2WpeLhNSF79/uKBPs= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -63,11 +67,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/ava-labs/avalanchego v1.6.2/go.mod h1:Dir4TnnbmIzrl5/Gnqxv+89qGmwIPjqKjLBWXw2lu9M= github.com/ava-labs/avalanchego v1.6.5 h1:xzta2OGAjF7ka06Cl1IoB4jtrqhAuxjI1onVDGWwdsk= github.com/ava-labs/avalanchego v1.6.5/go.mod h1:M21LNXMX5AcvUfQ0x1wKewOEgJjOcq8W08m3gmnGJAU= -github.com/ava-labs/timestampvm v1.1.1 h1:PUD6vn63rdNfwp+7u4huF6ibGD/Cj5ZyRRTx2D5n9Y8= -github.com/ava-labs/timestampvm v1.1.1/go.mod h1:XS/hDSZT3pLAGIJky+BFlkHj7Po2BkR5Onq4EsQEiBY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -111,11 +112,10 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= +github.com/dchest/blake256 v1.0.0 h1:6gUgI5MHdz9g0TdrgKqXsoDX+Zjxmm1Sc6OsoGru50I= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200627015759-01fd2de07837 h1:g2cyFTu5FKWhCo7L4hVJ797Q506B4EywA7L9I6OebgA= github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200627015759-01fd2de07837/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -634,7 +634,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -719,7 +721,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/owner/owner.go b/owner/owner.go new file mode 100644 index 00000000..24f2741c --- /dev/null +++ b/owner/owner.go @@ -0,0 +1,25 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package owner defines the key owner information. +package owner + +import ( + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/crypto/ed25519" +) + +func init() { + codec.RegisterType(&Owner{}) +} + +type Owner struct { + PublicKey ed25519.PublicKey `serialize:"true" json:"publicKey"` + + Namespace string `serialize:"true" json:"namespace"` + LastUpdated int64 `serialize:"true" json:"lastUpdated"` + Expiry int64 `serialize:"true" json:"expiry"` + + // decays faster the more keys you have + Keys int64 `serialize:"true" json:"keys"` +} diff --git a/quarkvm/block.go b/quarkvm/block.go deleted file mode 100644 index 78c5310b..00000000 --- a/quarkvm/block.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "errors" - "time" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/choices" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/utils/hashing" -) - -var ( - errTimestampTooEarly = errors.New("block's timestamp is earlier than its parent's timestamp") - errDatabaseGet = errors.New("error while retrieving data from database") - errTimestampTooLate = errors.New("block's timestamp is more than 1 hour ahead of local time") - errBlockType = errors.New("unexpected block type") - - _ Block = &timeBlock{} -) - -type Block interface { - snowman.Block - Initialize(bytes []byte, status choices.Status, vm *VM) - Data() [dataLen]byte -} - -// Block is a block on the chain. -// Each block contains: -// 1) A piece of data (a string) -// 2) A timestamp -type timeBlock struct { - PrntID ids.ID `serialize:"true" json:"parentID"` // parent's ID - Hght uint64 `serialize:"true" json:"height"` // This block's height. The genesis block is at height 0. - Tmstmp int64 `serialize:"true" json:"timestamp"` // Time this block was proposed at - Dt [dataLen]byte `serialize:"true" json:"data"` // Arbitrary data - - id ids.ID - bytes []byte - status choices.Status - vm *VM -} - -// Verify returns nil iff this block is valid. -// To be valid, it must be that: -// b.parent.Timestamp < b.Timestamp <= [local time] + 1 hour -func (b *timeBlock) Verify() error { - // TODO: need to use versionDB (on accept/reject state) - if b.Status() == choices.Accepted { - return nil - } - - // Get [b]'s parent - parentID := b.Parent() - parentIntf, err := b.vm.GetBlock(parentID) - if err != nil { - return errDatabaseGet - } - parent, ok := parentIntf.(*timeBlock) - if !ok { - return errBlockType - } - - // Ensure [b]'s timestamp is after its parent's timestamp. - if b.Timestamp().Unix() < parent.Timestamp().Unix() { - return errTimestampTooEarly - } - - // Ensure [b]'s timestamp is not more than an hour - // ahead of this node's time - if b.Timestamp().Unix() >= time.Now().Add(time.Hour).Unix() { - return errTimestampTooLate - } - - b.vm.currentBlocks[b.id] = b - - return nil -} - -// Initialize sets [b.bytes] to [bytes], sets [b.id] to hash([b.bytes]) -// Checks if [b]'s status is already stored in state. If so, [b] gets that status. -// Otherwise [b]'s status is Unknown. -func (b *timeBlock) Initialize(bytes []byte, status choices.Status, vm *VM) { - b.vm = vm - b.bytes = bytes - b.id = hashing.ComputeHash256Array(b.bytes) - b.status = status -} - -// Accept sets this block's status to Accepted and sets lastAccepted to this -// block's ID and saves this info to b.vm.DB -func (b *timeBlock) Accept() error { - b.SetStatus(choices.Accepted) // Change state of this block - blkID := b.ID() - - // Persist data - if err := b.vm.state.PutBlock(b); err != nil { - return err - } - - b.vm.state.SetLastAccepted(blkID) // Change state of VM - if err := b.vm.state.Commit(); err != nil { - return err - } - delete(b.vm.currentBlocks, b.ID()) - return nil -} - -// Reject sets this block's status to Rejected and saves the status in state -// Recall that b.vm.DB.Commit() must be called to persist to the DB -func (b *timeBlock) Reject() error { - b.SetStatus(choices.Rejected) - if err := b.vm.state.PutBlock(b); err != nil { - return err - } - if err := b.vm.state.Commit(); err != nil { - return err - } - delete(b.vm.currentBlocks, b.ID()) - return nil -} - -// ID returns the ID of this block -func (b *timeBlock) ID() ids.ID { return b.id } - -// ParentID returns [b]'s parent's ID -func (b *timeBlock) Parent() ids.ID { return b.PrntID } - -// Height returns this block's height. The genesis block has height 0. -func (b *timeBlock) Height() uint64 { return b.Hght } - -// Timestamp returns this block's time. The genesis block has time 0. -func (b *timeBlock) Timestamp() time.Time { return time.Unix(b.Tmstmp, 0) } - -// Status returns the status of this block -func (b *timeBlock) Status() choices.Status { return b.status } - -// Bytes returns the byte repr. of this block -func (b *timeBlock) Bytes() []byte { return b.bytes } - -// Data returns the data of this block -func (b *timeBlock) Data() [dataLen]byte { return b.Dt } - -// SetStatus sets the status of this block -func (b *timeBlock) SetStatus(status choices.Status) { b.status = status } - -func newTimeBlock(parentID ids.ID, height uint64, data [dataLen]byte, timestamp time.Time) *timeBlock { - // Create our new block - return &timeBlock{ - PrntID: parentID, - Hght: height, - Tmstmp: timestamp.Unix(), - Dt: data, - } -} diff --git a/quarkvm/block_state.go b/quarkvm/block_state.go deleted file mode 100644 index d76fe008..00000000 --- a/quarkvm/block_state.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "github.com/ava-labs/avalanchego/cache" - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/choices" -) - -const ( - blockCacheSize = 8192 -) - -var _ BlockState = &blockState{} - -type BlockState interface { - GetBlock(blkID ids.ID) (Block, error) - PutBlock(blk Block) error - - GetLastAccepted() ids.ID - SetLastAccepted(ids.ID) - - ClearCache() -} - -type blockState struct { - blkCache cache.Cacher - blockDB database.Database - vm *VM - - lastAccepted ids.ID -} - -type blkWrapper struct { - Blk []byte `serialize:"true"` - Status choices.Status `serialize:"true"` - - block Block -} - -func NewBlockState(db database.Database, vm *VM) BlockState { - return &blockState{ - blkCache: &cache.LRU{Size: blockCacheSize}, - blockDB: db, - vm: vm, - } -} - -func (s *blockState) GetBlock(blkID ids.ID) (Block, error) { - blkBytes, err := s.blockDB.Get(blkID[:]) - if err != nil { - return nil, err - } - - blkw := blkWrapper{} - if _, err := Codec.Unmarshal(blkBytes, &blkw); err != nil { - return nil, err - } - - blk := timeBlock{} - if _, err := Codec.Unmarshal(blkw.Blk, &blk); err != nil { - return nil, err - } - - blk.Initialize(blkw.Blk, blkw.Status, s.vm) - - s.blkCache.Put(blkID, blk) - - return &blk, nil -} - -func (s *blockState) PutBlock(blk Block) error { - blkw := blkWrapper{ - Blk: blk.Bytes(), - Status: blk.Status(), - block: blk, - } - - bytes, err := Codec.Marshal(codecVersion, &blkw) - if err != nil { - return err - } - - blkID := blk.ID() - s.blkCache.Put(blkID, &blk) - return s.blockDB.Put(blkID[:], bytes) -} - -func (s *blockState) DeleteBlock(blkID ids.ID) error { - s.blkCache.Put(blkID, nil) - return s.blockDB.Delete(blkID[:]) -} - -func (s *blockState) GetLastAccepted() ids.ID { return s.lastAccepted } -func (s *blockState) SetLastAccepted(lastAccepted ids.ID) { s.lastAccepted = lastAccepted } - -func (s *blockState) ClearCache() { - s.blkCache.Flush() -} diff --git a/quarkvm/codec.go b/quarkvm/codec.go deleted file mode 100644 index e202d81a..00000000 --- a/quarkvm/codec.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "github.com/ava-labs/avalanchego/codec" - "github.com/ava-labs/avalanchego/codec/linearcodec" - "github.com/ava-labs/avalanchego/utils/wrappers" -) - -const ( - // CodecVersion is the current default codec version - codecVersion = 0 -) - -// Codecs do serialization and deserialization -var ( - Codec codec.Manager -) - -func init() { - c := linearcodec.NewDefault() - Codec = codec.NewDefaultManager() - - errs := wrappers.Errs{} - - errs.Add( - Codec.RegisterCodec(codecVersion, c), - ) - if errs.Errored() { - panic(errs.Err) - } -} diff --git a/quarkvm/mempool.go b/quarkvm/mempool.go deleted file mode 100644 index b77a0a7d..00000000 --- a/quarkvm/mempool.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import "github.com/ava-labs/avalanchego/ids" - -// mempool defines in-memory transaction pool. -type mempool interface { - push(tx transaction) - peekMax() (transaction, uint64) - peekMin() (transaction, uint64) - popMax() (transaction, uint64) - popMin() transaction - remove(id ids.ID) transaction - prune(validHashes ids.Set) - len() int - get(id ids.ID) (transaction, bool) - has(id ids.ID) bool -} diff --git a/quarkvm/service.go b/quarkvm/service.go deleted file mode 100644 index a7d71f15..00000000 --- a/quarkvm/service.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "errors" - "net/http" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/formatting" - "github.com/ava-labs/avalanchego/utils/json" -) - -var ( - errBadData = errors.New("data must be base 58 repr. of 32 bytes") - errNoSuchBlock = errors.New("couldn't get block from database. Does it exist?") -) - -// Service is the API service for this VM -type Service struct{ vm *VM } - -// ProposeBlockArgs are the arguments to function ProposeValue -type ProposeBlockArgs struct { - // Data in the block. Must be base 58 encoding of 32 bytes. - Data string `json:"data"` -} - -// ProposeBlockReply is the reply from function ProposeBlock -type ProposeBlockReply struct{ Success bool } - -// ProposeBlock is an API method to propose a new block whose data is [args].Data. -// [args].Data must be a string repr. of a 32 byte array -func (s *Service) ProposeBlock(_ *http.Request, args *ProposeBlockArgs, reply *ProposeBlockReply) error { - bytes, err := formatting.Decode(formatting.CB58, args.Data) - if err != nil || len(bytes) != dataLen { - return errBadData - } - var data [dataLen]byte // The data as an array of bytes - copy(data[:], bytes[:dataLen]) // Copy the bytes in dataSlice to data - s.vm.proposeBlock(data) - reply.Success = true - return nil -} - -// APIBlock is the API representation of a block -type APIBlock struct { - Timestamp json.Uint64 `json:"timestamp"` // Timestamp of most recent block - Data string `json:"data"` // Data in the most recent block. Base 58 repr. of 5 bytes. - ID string `json:"id"` // String repr. of ID of the most recent block - ParentID string `json:"parentID"` // String repr. of ID of the most recent block's parent -} - -// GetBlockArgs are the arguments to GetBlock -type GetBlockArgs struct { - // ID of the block we're getting. - // If left blank, gets the latest block - ID string -} - -// GetBlockReply is the reply from GetBlock -type GetBlockReply struct { - APIBlock -} - -// GetBlock gets the block whose ID is [args.ID] -// If [args.ID] is empty, get the latest block -func (s *Service) GetBlock(_ *http.Request, args *GetBlockArgs, reply *GetBlockReply) error { - // If an ID is given, parse its string representation to an ids.ID - // If no ID is given, ID becomes the ID of last accepted block - var id ids.ID - var err error - if args.ID == "" { - id = s.vm.state.GetLastAccepted() - } else { - id, err = ids.FromString(args.ID) - if err != nil { - return errors.New("problem parsing ID") - } - } - - // Get the block from the database - blockInterface, err := s.vm.GetBlock(id) - if err != nil { - return errNoSuchBlock - } - - block, ok := blockInterface.(*timeBlock) - if !ok { // Should never happen but better to check than to panic - return errBadData - } - - // Fill out the response with the block's data - reply.APIBlock.ID = block.ID().String() - reply.APIBlock.Timestamp = json.Uint64(block.Timestamp().Unix()) - reply.APIBlock.ParentID = block.Parent().String() - data := block.Data() - reply.Data, err = formatting.EncodeWithChecksum(formatting.CB58, data[:]) - - return err -} diff --git a/quarkvm/state.go b/quarkvm/state.go deleted file mode 100644 index 54bf24db..00000000 --- a/quarkvm/state.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/prefixdb" - "github.com/ava-labs/avalanchego/database/versiondb" - "github.com/ava-labs/avalanchego/vms/components/avax" -) - -var ( - singletonStatePrefix = []byte("singleton") - blockStatePrefix = []byte("block") - - _ State = &state{} -) - -type State interface { - avax.SingletonState - BlockState - - Commit() error - Close() error -} - -type state struct { - avax.SingletonState - BlockState - - baseDB *versiondb.Database -} - -func NewState(db database.Database, vm *VM) State { - baseDB := versiondb.New(db) - - blockDB := prefixdb.New(blockStatePrefix, baseDB) - singletonDB := prefixdb.New(singletonStatePrefix, baseDB) - - return &state{ - BlockState: NewBlockState(blockDB, vm), - SingletonState: avax.NewSingletonState(singletonDB), - baseDB: baseDB, - } -} - -func (s *state) Commit() error { - return s.baseDB.Commit() -} - -func (s *state) Close() error { - // close underlying database - return s.baseDB.Close() -} diff --git a/quarkvm/transaction.go b/quarkvm/transaction.go deleted file mode 100644 index 1e60fec0..00000000 --- a/quarkvm/transaction.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import "github.com/ava-labs/avalanchego/ids" - -type transaction interface { - ID() ids.ID - Difficulty() uint64 - GetBlockID() ids.ID - Bytes() [32]byte -} - -func newTransaction(d [32]byte) transaction { - return &tx{} -} - -type tx struct { - // TODO -} - -func (tx *tx) ID() ids.ID { - return ids.Empty -} - -func (tx *tx) Difficulty() uint64 { - return 0 -} - -func (tx *tx) GetBlockID() ids.ID { - return ids.Empty -} - -func (tx *tx) Bytes() [32]byte { - return ids.Empty -} diff --git a/quarkvm/tx_heap.go b/quarkvm/tx_heap.go deleted file mode 100644 index 94206a74..00000000 --- a/quarkvm/tx_heap.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "container/heap" - - "github.com/ava-labs/avalanchego/ids" -) - -// txEntry is used to track the work transactions pay to be included in -// the mempool. -type txEntry struct { - id ids.ID - tx transaction - difficulty uint64 - index int -} - -var _ heap.Interface = &internalTxHeap{} - -// internalTxHeap is used to track pending transactions by [difficulty] -type internalTxHeap struct { - // min-heap pops the lowest difficulty transaction - // max-heap pops the highest difficulty transaction - isMinHeap bool - - items []*txEntry - lookup map[ids.ID]*txEntry -} - -func newInternalTxHeap(items int, isMinHeap bool) *internalTxHeap { - return &internalTxHeap{ - isMinHeap: isMinHeap, - items: make([]*txEntry, 0, items), - lookup: map[ids.ID]*txEntry{}, - } -} - -func (th internalTxHeap) Len() int { return len(th.items) } - -func (th internalTxHeap) Less(i, j int) bool { - if th.isMinHeap { - // min-heap pops the lowest difficulty transaction - return th.items[i].difficulty < th.items[j].difficulty - } - // max-heap pops the highest difficulty transaction - return th.items[i].difficulty > th.items[j].difficulty -} - -func (th internalTxHeap) Swap(i, j int) { - th.items[i], th.items[j] = th.items[j], th.items[i] - th.items[i].index = i - th.items[j].index = j -} - -func (th *internalTxHeap) Push(x interface{}) { - entry := x.(*txEntry) - if th.Has(entry.id) { - return - } - th.items = append(th.items, entry) - th.lookup[entry.id] = entry -} - -func (th *internalTxHeap) Pop() interface{} { - n := len(th.items) - item := th.items[n-1] - th.items[n-1] = nil // avoid memory leak - th.items = th.items[0 : n-1] - delete(th.lookup, item.id) - return item -} - -func (th *internalTxHeap) get(id ids.ID) (*txEntry, bool) { - entry, ok := th.lookup[id] - if !ok { - return nil, false - } - return entry, true -} - -func (th *internalTxHeap) Has(id ids.ID) bool { - _, has := th.get(id) - return has -} - -var _ mempool = &txHeap{} - -// implementing double-ended priority queue -type txHeap struct { - maxSize int - maxHeap *internalTxHeap - minHeap *internalTxHeap -} - -func newTxHeap(maxSize int) *txHeap { - return &txHeap{ - maxSize: maxSize, - maxHeap: newInternalTxHeap(maxSize, false), - minHeap: newInternalTxHeap(maxSize, true), - } -} - -func (th *txHeap) push(tx transaction) { - txID := tx.ID() - // Don't add duplicates - if th.has(txID) { - return - } - // Remove the lowest paying tx - if th.len() >= th.maxSize { - _ = th.popMin() - } - difficulty := tx.Difficulty() - oldLen := th.len() - heap.Push(th.maxHeap, &txEntry{ - id: txID, - difficulty: difficulty, - tx: tx, - index: oldLen, - }) - heap.Push(th.minHeap, &txEntry{ - id: txID, - difficulty: difficulty, - tx: tx, - index: oldLen, - }) -} - -// Assumes there is non-zero items in [txHeap] -func (th *txHeap) peekMax() (transaction, uint64) { - txEntry := th.maxHeap.items[0] - return txEntry.tx, txEntry.difficulty -} - -// Assumes there is non-zero items in [txHeap] -func (th *txHeap) peekMin() (transaction, uint64) { - txEntry := th.minHeap.items[0] - return txEntry.tx, txEntry.difficulty -} - -// Assumes there is non-zero items in [txHeap] -func (th *txHeap) popMax() (transaction, uint64) { - item := th.maxHeap.items[0] - return th.remove(item.id), item.difficulty -} - -// Assumes there is non-zero items in [txHeap] -func (th *txHeap) popMin() transaction { - return th.remove(th.minHeap.items[0].id) -} - -func (th *txHeap) remove(id ids.ID) transaction { - maxEntry, ok := th.maxHeap.get(id) - if !ok { - return nil - } - heap.Remove(th.maxHeap, maxEntry.index) - - minEntry, ok := th.minHeap.get(id) - if !ok { - // This should never happen, as that would mean the heaps are out of - // sync. - return nil - } - return heap.Remove(th.minHeap, minEntry.index).(*txEntry).tx -} - -func (th *txHeap) prune(validHashes ids.Set) { - toRemove := []ids.ID{} - for _, txE := range th.maxHeap.items { - if !validHashes.Contains(txE.tx.GetBlockID()) { - toRemove = append(toRemove, txE.id) - } - } - for _, txID := range toRemove { - th.remove(txID) - } -} - -func (th *txHeap) len() int { - return th.maxHeap.Len() -} - -func (th *txHeap) get(id ids.ID) (transaction, bool) { - txEntry, ok := th.maxHeap.get(id) - if !ok { - return nil, false - } - return txEntry.tx, true -} - -func (th *txHeap) has(id ids.ID) bool { - return th.maxHeap.Has(id) -} diff --git a/quarkvm/vm.go b/quarkvm/vm.go deleted file mode 100644 index 4b9efb8b..00000000 --- a/quarkvm/vm.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "errors" - "fmt" - "time" - - "github.com/gorilla/rpc/v2" - log "github.com/inconshreveable/log15" - - "github.com/ava-labs/avalanchego/database/manager" - "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/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/utils/json" - "github.com/ava-labs/avalanchego/version" -) - -const ( - dataLen = 32 - Name = "quarkvm" - - mempoolSize = 512 -) - -var ( - errNoPendingBlocks = errors.New("there is no block to propose") - Version = version.NewDefaultVersion(0, 0, 1) - - _ block.ChainVM = &VM{} -) - -// VM implements the snowman.VM interface -// Each block in this chain contains a Unix timestamp -// and a piece of data (a string) -type VM struct { - // The context of this vm - ctx *snow.Context - dbManager manager.Manager - - state State - - // ID of the preferred block - preferred ids.ID - - // channel to send messages to the consensus engine - toEngine chan<- common.Message - - // Proposed pieces of data that haven't been put into a block and proposed yet - mempool mempool - currentBlocks map[ids.ID]Block -} - -// Initialize this vm -// [ctx] is this vm's context -// [dbManager] is the manager of this vm's database -// [toEngine] is used to notify the consensus engine that new blocks are -// ready to be added to consensus -// The data in the genesis block is [genesisData] -func (vm *VM) Initialize( - ctx *snow.Context, - dbManager manager.Manager, - genesisData []byte, - upgradeData []byte, - configData []byte, - toEngine chan<- common.Message, - _ []*common.Fx, - _ common.AppSender, -) error { - version, err := vm.Version() - if err != nil { - log.Error("error initializing Timestamp VM: %v", err) - return err - } - log.Info("Initializing Timestamp VM", "Version", version) - - vm.dbManager = dbManager - vm.ctx = ctx - vm.toEngine = toEngine - vm.currentBlocks = make(map[ids.ID]Block) - vm.mempool = newTxHeap(mempoolSize) - - vm.state = NewState(vm.dbManager.Current().Database, vm) - - if err := vm.initGenesis(genesisData); err != nil { - return err - } - - ctx.Log.Info("initializing last accepted block as %s", vm.state.GetLastAccepted()) - - // Build off the most recently accepted block - return vm.SetPreference(vm.state.GetLastAccepted()) -} - -var errBadGenesisBytes = errors.New("bad genesis bytes") - -// SetDBInitialized marks the database as initialized -func (vm *VM) initGenesis(genesisData []byte) error { - stateInitialized, err := vm.state.IsInitialized() - if err != nil { - return err - } - - if stateInitialized { - return nil - } - - if len(genesisData) > dataLen { - return errBadGenesisBytes - } - - // genesisData is a byte slice but each block contains an byte array - // Take the first [dataLen] bytes from genesisData and put them in an array - var genesisDataArr [dataLen]byte - copy(genesisDataArr[:], genesisData) - - // Create the genesis block - // Timestamp of genesis block is 0. It has no parent. - genesisBlock, err := vm.NewBlock(ids.Empty, 0, genesisDataArr, time.Unix(0, 0)) - if err != nil { - log.Error("error while creating genesis block: %v", err) - return err - } - - if err := vm.state.PutBlock(genesisBlock); err != nil { - log.Error("error while saving genesis block: %v", err) - return err - } - - // Accept the genesis block - // Sets [vm.lastAccepted] and [vm.preferred] - if err := genesisBlock.Accept(); err != nil { - return fmt.Errorf("error accepting genesis block: %w", err) - } - - if err := vm.state.SetInitialized(); err != nil { - return fmt.Errorf("error while setting db to initialized: %w", err) - } - - vm.state.SetLastAccepted(genesisBlock.ID()) - - // Flush VM's database to underlying db - return vm.state.Commit() -} - -// CreateHandlers returns a map where: -// Keys: The path extension for this VM's API (empty in this case) -// Values: The handler for the API -func (vm *VM) CreateHandlers() (map[string]*common.HTTPHandler, error) { - server := rpc.NewServer() - server.RegisterCodec(json.NewCodec(), "application/json") - server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") - if err := server.RegisterService(&Service{vm: vm}, Name); err != nil { - return nil, err - } - - return map[string]*common.HTTPHandler{ - "": { - Handler: server, - }, - }, nil -} - -// CreateStaticHandlers returns a map where: -// Keys: The path extension for this VM's static API -// Values: The handler for that static API -func (vm *VM) CreateStaticHandlers() (map[string]*common.HTTPHandler, error) { - server := rpc.NewServer() - server.RegisterCodec(json.NewCodec(), "application/json") - server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") - if err := server.RegisterService(&StaticService{}, Name); err != nil { - return nil, err - } - - return map[string]*common.HTTPHandler{ - "": { - LockOptions: common.NoLock, - Handler: server, - }, - }, nil -} - -// Health implements the common.VM interface -func (vm *VM) HealthCheck() (interface{}, error) { return nil, nil } - -// BuildBlock returns a block that this vm wants to add to consensus -func (vm *VM) BuildBlock() (snowman.Block, error) { - if vm.mempool.len() == 0 { // There is no block to be built - return nil, errNoPendingBlocks - } - - // Get the value to put in the new block - // TODO: iterate over txs in pool until tx can't pay more fees - // (more than just single tx addition) - // e.g., for len(b.Txs) < targetTransactions && c.mempool.Len() > 0 { - maxTx, _ := vm.mempool.popMax() - value := maxTx.Bytes() - - // Notify consensus engine that there are more pending data for blocks - // (if that is the case) when done building this block - if vm.mempool.len() > 0 { - defer vm.NotifyBlockReady() - } - - // Gets Preferred Block - preferredIntf, err := vm.GetBlock(vm.preferred) - if err != nil { - return nil, fmt.Errorf("couldn't get preferred block: %w", err) - } - preferredHeight := preferredIntf.Height() - - // Build the block with preferred height - block, err := vm.NewBlock(vm.preferred, preferredHeight+1, value, time.Now()) - if err != nil { - return nil, fmt.Errorf("couldn't build block: %w", err) - } - - // Verifies block - if err := block.Verify(); err != nil { - return nil, err - } - return block, nil -} - -// NotifyBlockReady tells the consensus engine that a new block -// is ready to be created -func (vm *VM) NotifyBlockReady() { - select { - case vm.toEngine <- common.PendingTxs: - default: - vm.ctx.Log.Debug("dropping message to consensus engine") - } -} - -// GetBlock implements the snowman.ChainVM interface -func (vm *VM) GetBlock(blkID ids.ID) (snowman.Block, error) { return vm.getBlock(blkID) } - -func (vm *VM) getBlock(blkID ids.ID) (Block, error) { - // If block is in memory, return it. - if blk, exists := vm.currentBlocks[blkID]; exists { - return blk, nil - } - - return vm.state.GetBlock(blkID) -} - -// LastAccepted returns the block most recently accepted -func (vm *VM) LastAccepted() (ids.ID, error) { return vm.state.GetLastAccepted(), nil } - -// TODO -// instead use a block-based timer -// https://github.com/ava-labs/coreth/blob/master/plugin/evm/block_builder.go - -// proposeBlock appends [data] to [p.mempool]. -// Then it notifies the consensus engine -// that a new block is ready to be added to consensus -// (namely, a block with data [data]) -func (vm *VM) proposeBlock(data [dataLen]byte) { - vm.mempool.push(newTransaction(ids.Empty)) - vm.NotifyBlockReady() -} - -// ParseBlock parses [bytes] to a snowman.Block -// This function is used by the vm's state to unmarshal blocks saved in state -// and by the consensus layer when it receives the byte representation of a block -// from another node -func (vm *VM) ParseBlock(bytes []byte) (snowman.Block, error) { - // A new empty block - block := &timeBlock{} - - // Unmarshal the byte repr. of the block into our empty block - _, err := Codec.Unmarshal(bytes, block) - if err != nil { - return nil, err - } - - // Initialize the block - // (Block inherits Initialize from its embedded *core.Block) - block.Initialize(bytes, choices.Processing, vm) - - // Return the block - return block, nil -} - -// NewBlock returns a new Block where: -// - the block's parent is [parentID] -// - the block's data is [data] -// - the block's timestamp is [timestamp] -func (vm *VM) NewBlock(parentID ids.ID, height uint64, data [dataLen]byte, timestamp time.Time) (Block, error) { - block := newTimeBlock(parentID, height, data, timestamp) - - // Get the byte representation of the block - blockBytes, err := Codec.Marshal(codecVersion, block) - if err != nil { - return nil, err - } - - // Initialize the block by providing it with its byte representation - // and a reference to this VM - block.Initialize(blockBytes, choices.Processing, vm) - return block, nil -} - -// Shutdown this vm -func (vm *VM) Shutdown() error { - if vm.ctx == nil { - return nil - } - - return vm.state.Close() -} - -// SetPreference sets the block with ID [ID] as the preferred block -func (vm *VM) SetPreference(id ids.ID) error { - vm.preferred = id - return nil -} - -// Bootstrapped marks this VM as bootstrapped -func (vm *VM) Bootstrapped() error { - vm.ctx.Bootstrapped() - return nil -} - -// Bootstrapping marks this VM as bootstrapping -func (vm *VM) Bootstrapping() error { return nil } - -// Returns this VM's version -func (vm *VM) Version() (string, error) { - return Version.String(), nil -} - -func (vm *VM) Connected(id ids.ShortID) error { - return nil // noop -} - -func (vm *VM) Disconnected(id ids.ShortID) error { - return nil // noop -} - -// This VM doesn't (currently) have any app-specific messages -func (vm *VM) AppGossip(nodeID ids.ShortID, msg []byte) error { - return nil -} - -// This VM doesn't (currently) have any app-specific messages -func (vm *VM) AppRequest(nodeID ids.ShortID, requestID uint32, deadline time.Time, request []byte) error { - return nil -} - -// This VM doesn't (currently) have any app-specific messages -func (vm *VM) AppResponse(nodeID ids.ShortID, requestID uint32, response []byte) error { - return nil -} - -// This VM doesn't (currently) have any app-specific messages -func (vm *VM) AppRequestFailed(nodeID ids.ShortID, requestID uint32) error { - return nil -} diff --git a/quarkvm/vm_test.go b/quarkvm/vm_test.go deleted file mode 100644 index 2c59e88d..00000000 --- a/quarkvm/vm_test.go +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package quarkvm - -import ( - "fmt" - "testing" - - "github.com/ava-labs/avalanchego/database/manager" - "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/version" -) - -var blockchainID = ids.ID{1, 2, 3} - -// Utility function to assert that [block] has: -// * Parent with ID [parentID] -// * Data [expectedData] -// * Verify() returns nil iff passesVerify == true -func assertBlock(block Block, parentID ids.ID, expectedData [dataLen]byte, passesVerify bool) error { - if block.Parent() != parentID { - return fmt.Errorf("expect parent ID to be %s but was %s", parentID, block.Parent()) - } - if block.Data() != expectedData { - return fmt.Errorf("expected data to be %v but was %v", expectedData, block.Data()) - } - err := block.Verify() - if err != nil && passesVerify { - return fmt.Errorf("expected block to pass verification but it fails: %w", err) - } - if err == nil && !passesVerify { - return fmt.Errorf("expected block to fail verification but it passes") - } - return nil -} - -// Assert that after initialization, the vm has the state we expect -func TestGenesis(t *testing.T) { - // Initialize the vm - dbManager := manager.NewMemDB(version.DefaultVersion1_0_0) - msgChan := make(chan common.Message, 1) - vm := &VM{} - ctx := snow.DefaultContextTest() - ctx.ChainID = blockchainID - - if err := vm.Initialize(ctx, dbManager, []byte{0, 0, 0, 0, 0}, nil, nil, msgChan, nil, nil); err != nil { - t.Fatal(err) - } - - // Verify that the db is initialized - if ok, err := vm.state.IsInitialized(); err != nil || !ok { - t.Fatal("db should be initialized") - } - - // Get lastAccepted - lastAccepted, err := vm.LastAccepted() - if err != nil { - t.Fatal(err) - } - if lastAccepted == ids.Empty { - t.Fatal("lastAccepted should not be empty") - } - - // Verify that getBlock returns the genesis block, and the genesis block - // is the type we expect - genesisSnowmanBlock, err := vm.GetBlock(lastAccepted) // genesisBlock as snowman.Block - if err != nil { - t.Fatalf("couldn't get genesisBlock: %s", err) - } - - genesisBlock, ok := genesisSnowmanBlock.(*timeBlock) // type assert that genesisBlock is a *Block - if !ok { - t.Fatal("type of genesisBlock should be *Block") - } - - // Verify that the genesis block has the data we expect - if err := assertBlock(genesisBlock, ids.Empty, ids.ID{0, 0, 0, 0, 0}, true); err != nil { - t.Fatal(err) - } -} - -func TestHappyPath(t *testing.T) { - // Initialize the vm - dbManager := manager.NewMemDB(version.DefaultVersion1_0_0) - msgChan := make(chan common.Message, 1) - vm := &VM{} - ctx := snow.DefaultContextTest() - ctx.ChainID = blockchainID - if err := vm.Initialize(ctx, dbManager, []byte{0, 0, 0, 0, 0}, nil, nil, msgChan, nil, nil); err != nil { - t.Fatal(err) - } - - lastAcceptedID, err := vm.LastAccepted() - if err != nil { - t.Fatal(err) - } - genesisBlock, err := vm.GetBlock(lastAcceptedID) - if err != nil { - t.Fatal("could not get genesis block") - } - // in an actual execution, the engine would set the preference - if err := vm.SetPreference(genesisBlock.ID()); err != nil { - t.Fatal(err) - } - - ctx.Lock.Lock() - vm.proposeBlock([dataLen]byte{0, 0, 0, 0, 1}) // propose a value - ctx.Lock.Unlock() - - select { // assert there is a pending tx message to the engine - case msg := <-msgChan: - if msg != common.PendingTxs { - t.Fatal("Wrong message") - } - default: - t.Fatal("should have been pendingTxs message on channel") - } - - // build the block - ctx.Lock.Lock() - snowmanBlock2, err := vm.BuildBlock() - if err != nil { - t.Fatalf("problem building block: %s", err) - } - if err := snowmanBlock2.Verify(); err != nil { - t.Fatal(err) - } - if err := snowmanBlock2.Accept(); err != nil { // accept the block - t.Fatal(err) - } - if err := vm.SetPreference(snowmanBlock2.ID()); err != nil { - t.Fatal(err) - } - - lastAcceptedID, err = vm.LastAccepted() - if err != nil { - t.Fatal(err) - } - // Should be the block we just accepted - snowmanBlock2, err = vm.GetBlock(lastAcceptedID) - if err != nil { - t.Fatal("couldn't get block") - } - block2, ok := snowmanBlock2.(*timeBlock) - if !ok { - t.Fatal("genesis block should be type *Block") - } - // Assert the block we accepted has the data we expect - if err := assertBlock(block2, genesisBlock.ID(), [dataLen]byte{0, 0, 0, 0, 1}, true); err != nil { - t.Fatal(err) - } - - vm.proposeBlock([dataLen]byte{0, 0, 0, 0, 2}) // propose a block - ctx.Lock.Unlock() - - select { // verify there is a pending tx message to the engine - case msg := <-msgChan: - if msg != common.PendingTxs { - t.Fatal("Wrong message") - } - default: - t.Fatal("should have been pendingTxs message on channel") - } - - ctx.Lock.Lock() - - // build the block - if block, err := vm.BuildBlock(); err != nil { - t.Fatalf("problem building block: %s", err) - } else { - if err := block.Verify(); err != nil { - t.Fatal(err) - } - if err := block.Accept(); err != nil { // accept the block - t.Fatal(err) - } - if err := vm.SetPreference(block.ID()); err != nil { - t.Fatal(err) - } - } - - lastAcceptedID, err = vm.LastAccepted() - if err != nil { - t.Fatal(err) - } - // The block we just accepted - snowmanBlock3, err := vm.GetBlock(lastAcceptedID) - if err != nil { - t.Fatal("couldn't get block") - } - block3, ok := snowmanBlock3.(*timeBlock) - if !ok { - t.Fatal("genesis block should be type *Block") - } - // Assert the block we accepted has the data we expect - if err := assertBlock(block3, snowmanBlock2.ID(), [dataLen]byte{0, 0, 0, 0, 2}, true); err != nil { - t.Fatal(err) - } - - // Next, check the blocks we added are there - if block2FromState, err := vm.GetBlock(block2.ID()); err != nil { - t.Fatal(err) - } else if block2FromState.ID() != block2.ID() { - t.Fatal("expected IDs to match but they don't") - } - if block3FromState, err := vm.GetBlock(block3.ID()); err != nil { - t.Fatal(err) - } else if block3FromState.ID() != block3.ID() { - t.Fatal("expected IDs to match but they don't") - } - - ctx.Lock.Unlock() -} - -func TestService(t *testing.T) { - // Initialize the vm - dbManager := manager.NewMemDB(version.DefaultVersion1_0_0) - msgChan := make(chan common.Message, 1) - vm := &VM{} - ctx := snow.DefaultContextTest() - ctx.ChainID = blockchainID - if err := vm.Initialize(ctx, dbManager, []byte{0, 0, 0, 0, 0}, nil, nil, msgChan, nil, nil); err != nil { - t.Fatal(err) - } - - service := Service{vm} - if err := service.GetBlock(nil, &GetBlockArgs{}, &GetBlockReply{}); err != nil { - t.Fatal(err) - } -} diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 00000000..a3e3df11 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail +set -e + +if ! [[ "$0" =~ scripts/lint.sh ]]; then + echo "must be run from repository root" + exit 255 +fi + +if [ "$#" -eq 0 ]; then + # by default, check all source code + # to test only "vm" package + # ./scripts/lint.sh ./vm/... + TARGET="./..." +else + TARGET="${1}" +fi + +# by default, "./scripts/lint.sh" runs all lint tests +# to run only "license_header" test +# TESTS='license_header' ./scripts/lint.sh +TESTS=${TESTS:-"golangci_lint license_header"} + +function test_golangci_lint { + golangci-lint run --max-same-issues=0 --timeout=2m +} + +# find_go_files [package] +# all go files except generated ones +function find_go_files { + local target="${1}" + go fmt -n "${target}" | grep -Eo "([^ ]*)$" | grep -vE "(\\.pb\\.go|\\.pb\\.gw.go)" +} + +# automatically checks license headers +# to modify the file headers (if missing), remove "--check" flag +# TESTS='license_header' ADDLICENSE_FLAGS="-v" ./scripts/lint.sh +_addlicense_flags=${ADDLICENSE_FLAGS:-"--check -v"} +function test_license_header { + go install -v github.com/google/addlicense@latest + local target="${1}" + local files=() + while IFS= read -r line; do files+=("$line"); done < <(find_go_files "${target}") + + # ignore 3rd party code + addlicense \ + -f ./LICENSE.header \ + ${_addlicense_flags} \ + --ignore 'utils/ip_test.go' \ + --ignore 'utils/logging/highlight.go' \ + --ignore 'utils/ulimit/ulimit_non_unix.go.go' \ + --ignore 'utils/ulimit/ulimit_unix.go' \ + "${files[@]}" +} + +function run { + local test="${1}" + shift 1 + echo "START: '${test}' at $(date)" + if "test_${test}" "$@" ; then + echo "SUCCESS: '${test}' completed at $(date)" + else + echo "FAIL: '${test}' failed at $(date)" + exit 255 + fi +} + +echo "Running '$TESTS' at: $(date)" +for test in $TESTS; do + run "${test}" "${TARGET}" +done + +echo "ALL SUCCESS!" diff --git a/storage/prefix.go b/storage/prefix.go new file mode 100644 index 00000000..5d29aea8 --- /dev/null +++ b/storage/prefix.go @@ -0,0 +1,50 @@ +package storage + +import ( + "bytes" + "errors" +) + +var ( + delimiter = byte(0x2f) // '/' + noPrefixEnd = []byte{0} +) + +var ( + ErrInvalidKeyDelimiter = errors.New("key has unexpected delimiters; only flat key or sub-key is supported") +) + +// getPrefix returns the prefixed key and range query end key for list calls. +// put "foo" becomes "foo/" for its own namespace, and range ends with "foo0" +// put "fop" becomes "fop/" for its own namespace, and range ends with "fop0" +// put "foo1" becomes "foo1/" for its own namespace, and range ends with "foo10" +// For now, the storage itself does not implement key hierarchy, just flat prefix namespace. +func getPrefix(key []byte) (pfx []byte, end []byte, err error) { + if bytes.Count(key, []byte{delimiter}) > 1 { + return nil, nil, ErrInvalidKeyDelimiter + } + + idx := bytes.IndexRune(key, rune(delimiter)) + switch { + case idx == -1: // "foo" + pfx = append(key, delimiter) + case idx == len(key)-1: // "foo/" + pfx = key + default: // "a/b", then "a/" becomes prefix + pfx = append(bytes.Split(key, []byte{delimiter})[0], delimiter) + } + + // next lexicographical key (range end) for prefix queries + end = make([]byte, len(pfx)) + copy(end, pfx) + for i := len(end) - 1; i >= 0; i-- { + if end[i] < 0xff { + end[i] = end[i] + 1 + end = end[:i+1] + return pfx, end, nil + } + } + // next prefix does not exist (e.g., 0xffff); + // default to special end key + return pfx, noPrefixEnd, nil +} diff --git a/storage/prefix_test.go b/storage/prefix_test.go new file mode 100644 index 00000000..b7829998 --- /dev/null +++ b/storage/prefix_test.go @@ -0,0 +1,52 @@ +package storage + +import ( + "bytes" + "testing" +) + +func Test_getPrefix(t *testing.T) { + tt := []struct { + key []byte + pfx []byte + end []byte + err error + }{ + { + key: []byte("foo"), + pfx: []byte("foo/"), + end: []byte("foo0"), + err: nil, + }, + { + key: []byte("foo/"), + pfx: []byte("foo/"), + end: []byte("foo0"), + err: nil, + }, + { + key: []byte("a/b"), + pfx: []byte("a/"), + end: []byte("a0"), + err: nil, + }, + { + key: []byte("a/b/"), + pfx: nil, + end: nil, + err: ErrInvalidKeyDelimiter, + }, + } + for i, tv := range tt { + pfx, end, err := getPrefix(tv.key) + if !bytes.Equal(pfx, tv.pfx) { + t.Fatalf("#%d: pfx expected %q, got %q", i, string(tv.pfx), string(pfx)) + } + if !bytes.Equal(end, tv.end) { + t.Fatalf("#%d: end expected %q, got %q", i, string(tv.end), string(end)) + } + if err != tv.err { + t.Fatalf("#%d: err expected %v, got %v", i, tv.err, err) + } + } +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 00000000..86f34794 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,266 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package storage + +import ( + "bytes" + "errors" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/prefixdb" + "github.com/ava-labs/avalanchego/database/versiondb" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/crypto/ed25519" + "github.com/ava-labs/quarkvm/owner" +) + +var ( + blockStateBucket = []byte("block") + ownerBucket = []byte("owner") + txBucket = []byte("tx") + keyBucket = []byte("key") +) + +var ( + ErrNoPubKey = errors.New("caller must provide the public key to make modification or overwrite") + ErrInvalidSig = errors.New("invalid signature") + ErrKeyExists = errors.New("key already exists") + ErrKeyNotExist = errors.New("key not exists") + ErrInvalidKeyLength = errors.New("invalid key length") + ErrInvalidValueLength = errors.New("invalid value length") +) + +const ( + maxKeyLength = 256 + maxValueLength = 1024 // limit to 1 KiB for now +) + +type Storage interface { + Block() database.Database + Owner() database.Database + Tx() database.Database + Key() database.Database + Commit() error + Close() error + + // Finds the underlying info based on the key. + // The method should handle the prefix extraction. + // Returns an error if the prefix is non-existent. + FindOwner(k []byte) (prefix []byte, ov *owner.Owner, err error) + Put(k []byte, v []byte, opts ...OpOption) error + Get(k []byte, opts ...OpOption) ([]byte, bool, error) +} + +type storage struct { + ctx *snow.Context + baseDB *versiondb.Database + blockDB *prefixdb.Database + ownerDB *prefixdb.Database + txDB *prefixdb.Database + keyDB *prefixdb.Database +} + +func New(ctx *snow.Context, db database.Database) Storage { + baseDB := versiondb.New(db) + return &storage{ + baseDB: baseDB, + blockDB: prefixdb.New(blockStateBucket, baseDB), + ownerDB: prefixdb.New(ownerBucket, baseDB), + txDB: prefixdb.New(txBucket, baseDB), + keyDB: prefixdb.New(keyBucket, baseDB), + } +} + +func (s *storage) Block() database.Database { + return s.blockDB +} + +func (s *storage) Owner() database.Database { + return s.ownerDB +} + +func (s *storage) Tx() database.Database { + return s.txDB +} + +func (s *storage) Key() database.Database { + return s.keyDB +} + +func (s *storage) Commit() error { + return s.baseDB.Commit() +} + +func (s *storage) Close() error { + return s.baseDB.Close() +} + +func (s *storage) FindOwner(k []byte) (pfx []byte, ov *owner.Owner, err error) { + pfx, _, err = getPrefix(k) + if err != nil { + return pfx, nil, err + } + + // TODO: do this in one db call (e.g., Get) + exist, err := s.ownerDB.Has(pfx) + if err != nil { + return pfx, nil, err + } + if !exist { + return pfx, nil, ErrKeyNotExist + } + + src, err := s.ownerDB.Get(pfx) + if err != nil { + return pfx, nil, err + } + + ov = new(owner.Owner) + if _, err := codec.Unmarshal(src, ov); err != nil { + return pfx, nil, err + } + return pfx, ov, nil +} + +func (s *storage) Put(k []byte, v []byte, opts ...OpOption) error { + if len(k) > maxKeyLength || len(k) == 0 { + return ErrInvalidKeyLength + } + if len(v) > maxValueLength { + return ErrInvalidValueLength + } + + ret := &Op{} + ret.applyOpts(opts) + if ret.pub == nil { + return ErrNoPubKey + } + + // value must be signe with signature + if !ret.pub.Verify(v, ret.sig) { + return ErrInvalidSig + } + + if exist, _ := s.keyDB.Has(k); exist && !ret.overwrite { + return ErrKeyExists + } + + // check the ownership of the key + // any non-existent/expired key can be claimed by anyone + // that submits a sufficient PoW + exists := true + pfx, prevOwner, err := s.FindOwner(k) + if err != nil { + if err != ErrKeyNotExist { + return err + } + exists = false + } + if exists && prevOwner == nil { // should never happen + panic("key exists but owner not found?") + } + + needNewOwner := true + if exists { + // prefix already claimed + expired := prevOwner.Expiry < time.Now().Unix() + sameOwner := bytes.Equal(prevOwner.PublicKey.Bytes(), ret.pub.Bytes()) + switch { + case !expired && !sameOwner: + return fmt.Errorf("%q is not expired and already owned by %q", prevOwner.Namespace, prevOwner.PublicKey.Address()) + case !expired && sameOwner: + needNewOwner = false + s.ctx.Log.Info("%q has an active owner", prevOwner.Namespace) + case expired: + s.ctx.Log.Info("%q has an expired owner; allowing put for new owner", prevOwner.Namespace) + } + } + + // prefix expired or not claimed yet + newOwner := prevOwner + if needNewOwner { + newOwner = &owner.Owner{ + PublicKey: ret.pub, + Namespace: string(pfx), + } + } + + // TODO: define save owner method + // TODO: update other fields + // TODO: make this configurable + lastUpdated := time.Now() + newOwner.LastUpdated = lastUpdated.Unix() + newOwner.Expiry = lastUpdated.Add(time.Hour).Unix() + newOwner.Keys++ + newOwnerBytes, err := codec.Marshal(newOwner) + if err != nil { + return nil + } + if err := s.ownerDB.Put(pfx, newOwnerBytes); err != nil { + return err + } + + // if validated or new key, + // the owner is allowed to write the key + // TODO: encrypt value + return s.keyDB.Put(k, v) +} + +func (s *storage) Get(k []byte, opts ...OpOption) ([]byte, bool, error) { + ret := &Op{} + ret.applyOpts(opts) + if ret.pub == nil { + return nil, false, ErrNoPubKey + } + + pfx, _, err := getPrefix(k) + if err != nil { + return nil, false, err + } + _ = pfx + + // just check ownership with prefix + // for now just return the value + + has, err := s.keyDB.Has(k) + if err != nil { + return nil, false, err + } + if !has { + return nil, false, nil + } + v, err := s.keyDB.Get(k) + if err != nil { + return nil, false, err + } + return v, true, nil +} + +type Op struct { + overwrite bool + pub ed25519.PublicKey + sig []byte +} + +type OpOption func(*Op) + +func (op *Op) applyOpts(opts []OpOption) { + for _, opt := range opts { + opt(op) + } +} + +func WithOverwrite(b bool) OpOption { + return func(op *Op) { op.overwrite = b } +} + +func WithSignature(pub ed25519.PublicKey, sig []byte) OpOption { + return func(op *Op) { + op.pub = pub + op.sig = sig + } +} diff --git a/storage/storage_test.go b/storage/storage_test.go new file mode 100644 index 00000000..88836ef9 --- /dev/null +++ b/storage/storage_test.go @@ -0,0 +1,72 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package storage + +import ( + "bytes" + "testing" + + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/quarkvm/crypto/ed25519" +) + +func TestPut(t *testing.T) { + s := New(snow.DefaultContextTest(), memdb.New()) + defer s.Close() + + priv, err := ed25519.NewPrivateKey() + if err != nil { + t.Fatal(err) + } + pub := priv.PublicKey() + sig, err := priv.Sign([]byte("bar")) + if err != nil { + panic(err) + } + + tt := []struct { + k []byte + v []byte + opts []OpOption + err error + }{ + { + k: []byte("foo"), + v: []byte("bar"), + opts: []OpOption{WithSignature(pub, sig)}, + err: nil, + }, + { + k: []byte("foo"), + v: []byte("bar"), + opts: []OpOption{WithSignature(pub, []byte("bar"))}, + err: ErrInvalidSig, + }, + { + k: []byte(""), + v: []byte("bar"), + opts: nil, + err: ErrInvalidKeyLength, + }, + { + k: bytes.Repeat([]byte(" "), maxKeyLength+1), + v: []byte("bar"), + opts: nil, + err: ErrInvalidKeyLength, + }, + { + k: []byte("foo"), + v: bytes.Repeat([]byte(" "), maxValueLength+1), + opts: nil, + err: ErrInvalidValueLength, + }, + } + for i, tv := range tt { + err := s.Put(tv.k, tv.v, tv.opts...) + if err != tv.err { + t.Fatalf("#%d: put err expected %v, got %v", i, tv.err, err) + } + } +} diff --git a/transaction/heap.go b/transaction/heap.go new file mode 100644 index 00000000..284fba36 --- /dev/null +++ b/transaction/heap.go @@ -0,0 +1,87 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transaction + +import ( + "container/heap" + + "github.com/ava-labs/avalanchego/ids" +) + +// txEntry is used to track the work transactions pay to be included in +// the mempool. +type txEntry struct { + id ids.ID + tx *Transaction + difficulty uint64 + index int +} + +var _ heap.Interface = &txHeap{} + +// txHeap is used to track pending transactions by [difficulty] +type txHeap struct { + // min-heap pops the lowest difficulty transaction + // max-heap pops the highest difficulty transaction + isMinHeap bool + + items []*txEntry + lookup map[ids.ID]*txEntry +} + +func newTxHeap(items int, isMinHeap bool) *txHeap { + return &txHeap{ + isMinHeap: isMinHeap, + items: make([]*txEntry, 0, items), + lookup: map[ids.ID]*txEntry{}, + } +} + +func (th txHeap) Len() int { return len(th.items) } + +func (th txHeap) Less(i, j int) bool { + if th.isMinHeap { + // min-heap pops the lowest difficulty transaction + return th.items[i].difficulty < th.items[j].difficulty + } + // max-heap pops the highest difficulty transaction + return th.items[i].difficulty > th.items[j].difficulty +} + +func (th txHeap) Swap(i, j int) { + th.items[i], th.items[j] = th.items[j], th.items[i] + th.items[i].index = i + th.items[j].index = j +} + +func (th *txHeap) Push(x interface{}) { + entry := x.(*txEntry) + if th.has(entry.id) { + return + } + th.items = append(th.items, entry) + th.lookup[entry.id] = entry +} + +func (th *txHeap) Pop() interface{} { + n := len(th.items) + item := th.items[n-1] + th.items[n-1] = nil // avoid memory leak + th.items = th.items[0 : n-1] + delete(th.lookup, item.id) + return item +} + +func (th *txHeap) get(id ids.ID) (*txEntry, bool) { + entry, ok := th.lookup[id] + if !ok { + return nil, false + } + return entry, true +} + +func (th *txHeap) has(id ids.ID) bool { + _, has := th.get(id) + return has +} diff --git a/transaction/mempool.go b/transaction/mempool.go new file mode 100644 index 00000000..7815e205 --- /dev/null +++ b/transaction/mempool.go @@ -0,0 +1,134 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transaction + +import ( + "container/heap" + + "github.com/ava-labs/avalanchego/ids" +) + +// Mempool defines in-memory transaction pool. +type Mempool interface { + Push(tx *Transaction) + PeekMax() (*Transaction, uint64) + PeekMin() (*Transaction, uint64) + PopMax() (*Transaction, uint64) + PopMin() *Transaction + Remove(id ids.ID) *Transaction + Prune(validHashes ids.Set) + Len() int + Get(id ids.ID) (*Transaction, bool) + Has(id ids.ID) bool +} + +var _ Mempool = &txMempool{} + +// implementing double-ended priority queue +type txMempool struct { + maxSize int + maxHeap *txHeap + minHeap *txHeap +} + +func NewMempool(maxSize int) Mempool { + return &txMempool{ + maxSize: maxSize, + maxHeap: newTxHeap(maxSize, false), + minHeap: newTxHeap(maxSize, true), + } +} + +func (txm *txMempool) Push(tx *Transaction) { + txID := tx.ID() + // Don't add duplicates + if txm.Has(txID) { + return + } + // Remove the lowest paying tx + if txm.Len() >= txm.maxSize { + _ = txm.PopMin() + } + difficulty := tx.Difficulty() + oldLen := txm.Len() + heap.Push(txm.maxHeap, &txEntry{ + id: txID, + difficulty: difficulty, + tx: tx, + index: oldLen, + }) + heap.Push(txm.minHeap, &txEntry{ + id: txID, + difficulty: difficulty, + tx: tx, + index: oldLen, + }) +} + +// Assumes there is non-zero items in [txHeap] +func (txm *txMempool) PeekMax() (*Transaction, uint64) { + txEntry := txm.maxHeap.items[0] + return txEntry.tx, txEntry.difficulty +} + +// Assumes there is non-zero items in [txHeap] +func (txm *txMempool) PeekMin() (*Transaction, uint64) { + txEntry := txm.minHeap.items[0] + return txEntry.tx, txEntry.difficulty +} + +// Assumes there is non-zero items in [txHeap] +func (txm *txMempool) PopMax() (*Transaction, uint64) { + item := txm.maxHeap.items[0] + return txm.Remove(item.id), item.difficulty +} + +// Assumes there is non-zero items in [txHeap] +func (txm *txMempool) PopMin() *Transaction { + return txm.Remove(txm.minHeap.items[0].id) +} + +func (txm *txMempool) Remove(id ids.ID) *Transaction { + maxEntry, ok := txm.maxHeap.get(id) + if !ok { + return nil + } + heap.Remove(txm.maxHeap, maxEntry.index) + + minEntry, ok := txm.minHeap.get(id) + if !ok { + // This should never happen, as that would mean the heaps are out of + // sync. + return nil + } + return heap.Remove(txm.minHeap, minEntry.index).(*txEntry).tx +} + +func (txm *txMempool) Prune(validHashes ids.Set) { + toRemove := []ids.ID{} + for _, txE := range txm.maxHeap.items { + if !validHashes.Contains(txE.tx.Unsigned.GetBlockID()) { + toRemove = append(toRemove, txE.id) + } + } + for _, txID := range toRemove { + txm.Remove(txID) + } +} + +func (txm *txMempool) Len() int { + return txm.maxHeap.Len() +} + +func (txm *txMempool) Get(id ids.ID) (*Transaction, bool) { + txEntry, ok := txm.maxHeap.get(id) + if !ok { + return nil, false + } + return txEntry.tx, true +} + +func (txm *txMempool) Has(id ids.ID) bool { + return txm.maxHeap.has(id) +} diff --git a/transaction/mempool_test.go b/transaction/mempool_test.go new file mode 100644 index 00000000..7e1ef19e --- /dev/null +++ b/transaction/mempool_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transaction + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/crypto/ed25519" + "github.com/ava-labs/quarkvm/storage" +) + +func TestMempool(t *testing.T) { + codec.RegisterType(&testUnsigned{}) + + tx1 := newTestTransaction(ids.GenerateTestID(), 1, ids.GenerateTestID(), []byte("sig1")) // difficulty 1 + tx2 := newTestTransaction(ids.GenerateTestID(), 2, ids.GenerateTestID(), []byte("sig2")) // difficulty 2 + tx3 := newTestTransaction(ids.GenerateTestID(), 3, ids.GenerateTestID(), []byte("sig3")) // difficulty 3 + + txm := NewMempool(3) + txm.Push(tx1) + txm.Push(tx2) + txm.Push(tx3) + + _, diff := txm.PeekMax() + if diff != 3 { + t.Fatalf("difficulty expected 3, got %d", diff) + } + _, diff = txm.PeekMin() + if diff != 1 { + t.Fatalf("difficulty expected 1, got %d", diff) + } +} + +func newTestTransaction(id ids.ID, difficulty uint64, blockID ids.ID, sig []byte) *Transaction { + return &Transaction{ + Unsigned: newTestUnsigned(id, difficulty, blockID), + Signature: sig, + difficulty: difficulty, + } +} + +func newTestUnsigned(id ids.ID, difficulty uint64, blockID ids.ID) Unsigned { + return &testUnsigned{ + id: id, + difficulty: difficulty, + blockID: blockID, + } +} + +type testUnsigned struct { + id ids.ID + difficulty uint64 + blockID ids.ID +} + +func (utx *testUnsigned) ID() ids.ID { return utx.id } +func (utx *testUnsigned) Difficulty() uint64 { return utx.difficulty } +func (utx *testUnsigned) Bytes() [32]byte { return ids.Empty } +func (utx *testUnsigned) GetBlockID() ids.ID { return utx.blockID } +func (utx *testUnsigned) SetBlockID(block ids.ID) { utx.blockID = block } +func (utx *testUnsigned) SetGraffiti(graffiti []byte) {} +func (utx *testUnsigned) GetSender() ed25519.PublicKey { return nil } +func (utx *testUnsigned) GetPrefix() []byte { return nil } +func (utx *testUnsigned) VerifyBase() error { return nil } +func (utx *testUnsigned) Verify(storage.Storage, int64) error { return nil } +func (utx *testUnsigned) Accept(storage.Storage, int64) error { return nil } diff --git a/transaction/transaction.go b/transaction/transaction.go new file mode 100644 index 00000000..9c00cd96 --- /dev/null +++ b/transaction/transaction.go @@ -0,0 +1,112 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package transaction defines the transaction interface and objects. +package transaction + +import ( + "errors" + + "ekyu.moe/cryptonight" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/storage" +) + +func init() { + codec.RegisterType(&Transaction{}) +} + +type Transaction struct { + Unsigned Unsigned `serialize:"true" json:"unsigned"` + Signature []byte `serialize:"true" json:"signature"` + + difficulty uint64 // populate in mempool +} + +func New(utx Unsigned, sig []byte) *Transaction { + return &Transaction{ + Unsigned: utx, + Signature: sig, + } +} + +func UnsignedBytes(utx Unsigned) []byte { + v, err := codec.Marshal(utx) + if err != nil { + panic(err) + } + return v +} + +func (t *Transaction) Bytes() []byte { + v, err := codec.Marshal(t) + if err != nil { + panic(err) + } + return v +} + +func (t *Transaction) Size() uint64 { + return uint64(len(t.Bytes())) +} + +func (t *Transaction) ID() ids.ID { + h, err := ids.ToID(hashing.ComputeHash256(t.Bytes())) + if err != nil { + panic(err) + } + return h +} + +func (t *Transaction) Difficulty() uint64 { + if t.difficulty == 0 { + h := cryptonight.Sum(UnsignedBytes(t.Unsigned), 2) + t.difficulty = cryptonight.Difficulty(h) + } + return t.difficulty +} + +func (t *Transaction) Verify(s storage.Storage, blockTime int64, recentBlockIDs ids.Set, recentTxIDs ids.Set, minDifficulty uint64) error { + if err := t.Unsigned.VerifyBase(); err != nil { + return err + } + if !recentBlockIDs.Contains(t.Unsigned.GetBlockID()) { + // Hash must be recent to be any good + // Should not happen beause of mempool cleanup + return errors.New("invalid block id") + } + if recentTxIDs.Contains(t.ID()) { + // Tx hash must not be recently executed (otherwise could be replayed) + // + // NOTE: We only need to keep cached tx hashes around as long as the + // block hash referenced in the tx is valid + return errors.New("duplicate tx") + } + if t.Difficulty() < minDifficulty { + return errors.New("invalid difficulty") + } + if !t.Unsigned.GetSender().Verify(UnsignedBytes(t.Unsigned), t.Signature) { + return errors.New("invalid signature") + } + return t.Unsigned.Verify(s, blockTime) +} + +func (t *Transaction) Accept(s storage.Storage, blockTime int64) error { + if err := t.Unsigned.Accept(s, blockTime); err != nil { + return err + } + + // persists in prefixed db + id := t.ID() + return s.Tx().Put(append([]byte{}, id[:]...), nil) +} + +func (t *Transaction) PrefixID() ids.ID { + h, err := ids.ToID(hashing.ComputeHash256(t.Unsigned.GetPrefix())) + if err != nil { + panic(err) + } + return h +} diff --git a/transaction/unsigned.go b/transaction/unsigned.go new file mode 100644 index 00000000..57a33224 --- /dev/null +++ b/transaction/unsigned.go @@ -0,0 +1,79 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transaction + +import ( + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/crypto/ed25519" + "github.com/ava-labs/quarkvm/storage" +) + +var ( + ErrPrefixNotExist = errors.New("prefix does not exist") + ErrPrefixOwnerMismatch = errors.New("prefix owner mismatch") + ErrPrefixNotExpired = errors.New("prefix not expired") + ErrPrefixExpired = errors.New("prefix expired") + ErrKeyNotExist = errors.New("key does not exist") +) + +type Unsigned interface { + GetBlockID() ids.ID + SetBlockID(block ids.ID) + SetGraffiti(graffiti []byte) + GetSender() ed25519.PublicKey + GetPrefix() []byte + VerifyBase() error + Verify(storage.Storage, int64) error + Accept(storage.Storage, int64) error +} + +const maxKeyLength = 256 + +func init() { + codec.RegisterType(&ed25519.PublicKeyED25519{}) + codec.RegisterType(&base{}) +} + +type base struct { + Sender ed25519.PublicKey `serialize:"true"` + Graffiti []byte `serialize:"true"` + BlockID ids.ID `serialize:"true"` + Prefix []byte `serialize:"true"` +} + +func (b *base) SetBlockID(blockID ids.ID) { + b.BlockID = blockID +} + +func (b *base) SetGraffiti(graffiti []byte) { + b.Graffiti = graffiti +} + +func (b *base) GetBlockID() ids.ID { + return b.BlockID +} + +func (b *base) GetSender() ed25519.PublicKey { + return b.Sender +} + +func (b *base) GetPrefix() []byte { + return b.Prefix +} + +func (b *base) VerifyBase() error { + if len(b.Prefix) > maxKeyLength || len(b.Prefix) == 0 { + return errors.New("invalid length") + } + if b.Sender == nil { + return errors.New("invalid sender") + } + if b.BlockID == ids.Empty { + return errors.New("invalid blockID") + } + return nil +} diff --git a/transaction/unsigned_claim.go b/transaction/unsigned_claim.go new file mode 100644 index 00000000..8ad47eb8 --- /dev/null +++ b/transaction/unsigned_claim.go @@ -0,0 +1,79 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transaction + +import ( + "bytes" + "errors" + + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/crypto/ed25519" + "github.com/ava-labs/quarkvm/owner" + "github.com/ava-labs/quarkvm/storage" +) + +func NewClaim(sender ed25519.PublicKey, prefix []byte) Unsigned { + return &claim{ + base: &base{ + Sender: sender, + Prefix: prefix, + }, + } +} + +func init() { + codec.RegisterType(&claim{}) +} + +var _ Unsigned = &claim{} + +type claim struct { + *base `serialize:"true"` +} + +func (c *claim) Verify(s storage.Storage, blockTime int64) error { + // Restrict address prefix to be owned by pk + if decodedPrefix, err := formatting.Decode(formatting.CB58, string(c.Prefix)); err == nil { + if !bytes.Equal(c.Sender.Bytes(), decodedPrefix) { + return errors.New("public key does not match decoded prefix") + } + } + + has, err := s.Owner().Has(c.Prefix) + if err != nil { + return err + } + if !has { + return ErrPrefixNotExist + } + v, err := s.Owner().Get(c.Prefix) + if err != nil { + return err + } + iv := new(owner.Owner) + if _, err := codec.Unmarshal(v, iv); err != nil { + return err + } + if iv.Expiry >= blockTime { + return ErrPrefixNotExpired + } + return nil +} + +const expiryTime = 30 // TODO: set much longer on real network + +func (c *claim) Accept(s storage.Storage, blockTime int64) error { + iv := &owner.Owner{PublicKey: c.Sender, LastUpdated: blockTime, Expiry: blockTime + expiryTime, Keys: 1} + b, err := codec.Marshal(iv) + if err != nil { + return err + } + if err := s.Owner().Put(c.Prefix, b); err != nil { + return err + } + // Remove anything that is stored in value prefix + // return database.ClearPrefix(db, db, PrefixValueKey(c.Prefix, nil)) + return nil +} diff --git a/transaction/unsigned_key_value.go b/transaction/unsigned_key_value.go new file mode 100644 index 00000000..6fd19898 --- /dev/null +++ b/transaction/unsigned_key_value.go @@ -0,0 +1,100 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transaction + +import ( + "errors" + + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/owner" + "github.com/ava-labs/quarkvm/storage" +) + +func init() { + codec.RegisterType(&keyValue{}) +} + +var _ Unsigned = &keyValue{} + +type keyValue struct { + *base `serialize:"true"` + Key []byte `serialize:"true"` + Value []byte `serialize:"true"` +} + +func (kv *keyValue) Verify(s storage.Storage, blockTime int64) error { + if len(kv.Key) > maxKeyLength || len(kv.Key) == 0 { + return errors.New("invalid key length") + } + if len(kv.Value) > maxKeyLength { + return errors.New("invalid value length") + } + + has, err := s.Owner().Has(kv.Prefix) + if err != nil { + return err + } + if !has { + return ErrPrefixNotExist + } + v, err := s.Owner().Get(kv.Prefix) + if err != nil { + return err + } + iv := new(owner.Owner) + if _, err := codec.Unmarshal(v, iv); err != nil { + return err + } + if iv.PublicKey != kv.Sender { + return ErrPrefixOwnerMismatch + } + if iv.Expiry < blockTime { + return ErrPrefixExpired + } + + if len(kv.Value) == 0 { + return nil + } + + has, err = s.Key().Has(kv.Key) + if err != nil { + return err + } + if !has { + return ErrKeyNotExist + } + return nil +} + +func (kv *keyValue) Accept(s storage.Storage, blockTime int64) error { + v, err := s.Owner().Get(kv.Prefix) + if err != nil { + return err + } + iv := new(owner.Owner) + if _, err := codec.Unmarshal(v, iv); err != nil { + return err + } + + timeRemaining := (iv.Expiry - iv.LastUpdated) * iv.Keys + if len(kv.Value) == 0 { + iv.Keys-- + if err := s.Key().Delete(kv.Key); err != nil { + return err + } + } else { + iv.Keys++ + if err := s.Key().Put(kv.Key, kv.Value); err != nil { + return err + } + } + newTimeRemaining := timeRemaining / iv.Keys + iv.LastUpdated = blockTime + iv.Expiry = blockTime + newTimeRemaining + b, err := codec.Marshal(iv) + if err != nil { + return err + } + return s.Owner().Put(kv.Prefix, b) +} diff --git a/transaction/unsigned_lifeline.go b/transaction/unsigned_lifeline.go new file mode 100644 index 00000000..ac40dae0 --- /dev/null +++ b/transaction/unsigned_lifeline.go @@ -0,0 +1,50 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transaction + +import ( + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/owner" + "github.com/ava-labs/quarkvm/storage" +) + +func init() { + codec.RegisterType(&lifeline{}) +} + +var _ Unsigned = &lifeline{} + +type lifeline struct { + *base `serialize:"true"` +} + +func (kv *lifeline) Verify(s storage.Storage, blockTime int64) error { + has, err := s.Owner().Has(kv.Prefix) + if err != nil { + return err + } + if !has { + return ErrPrefixNotExist + } + // Anyone can choose to support a prefix (not just owner) + return nil +} + +func (kv *lifeline) Accept(s storage.Storage, blockTime int64) error { + v, err := s.Owner().Get(kv.Prefix) + if err != nil { + return err + } + iv := new(owner.Owner) + if _, err := codec.Unmarshal(v, iv); err != nil { + return err + } + // If you are "in debt", lifeline only adds but doesn't reset to new + iv.Expiry += expiryTime / iv.Keys + b, err := codec.Marshal(iv) + if err != nil { + return err + } + return s.Owner().Put(kv.Prefix, b) +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 00000000..086bcc9d --- /dev/null +++ b/version/version.go @@ -0,0 +1,9 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package version defines version variables. +package version + +import "github.com/ava-labs/avalanchego/version" + +var Version = version.NewDefaultVersion(0, 0, 1) diff --git a/vm/quarkvm.go b/vm/quarkvm.go new file mode 100644 index 00000000..71aa3ac4 --- /dev/null +++ b/vm/quarkvm.go @@ -0,0 +1,43 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +import ( + "errors" + + "github.com/ava-labs/quarkvm/crypto/ed25519" + "github.com/ava-labs/quarkvm/storage" +) + +const Name = "quarkvm" + +// Service is the API service for this VM +type Service struct { + vm *VM +} + +type PutArgs struct { + PublicKey ed25519.PublicKey `serialize:"true" json:"publicKey"` + Signature string `serialize:"true" json:"signature"` + Key string `serialize:"true" json:"key"` + Value string `serialize:"true" json:"value"` +} + +type PutReply struct { + Success bool `serialize:"true" json:"success"` +} + +func (vm *VM) Put(args *PutArgs) error { + if args.PublicKey == nil { + return errors.New("the caller must provide a public key") + } + + // TODO: check PoW + return vm.s.Put( + []byte(args.Key), + []byte(args.Value), + storage.WithOverwrite(true), + storage.WithSignature(args.PublicKey, []byte(args.Signature)), + ) +} diff --git a/quarkvm/static_service.go b/vm/rpc.go similarity index 99% rename from quarkvm/static_service.go rename to vm/rpc.go index 0add08f4..f324c0c1 100644 --- a/quarkvm/static_service.go +++ b/vm/rpc.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package quarkvm +package vm import ( "fmt" diff --git a/vm/vm.go b/vm/vm.go new file mode 100644 index 00000000..e3bfd78b --- /dev/null +++ b/vm/vm.go @@ -0,0 +1,276 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package vm implements custom VM. +package vm + +import ( + "errors" + "net/http" + "time" + + "github.com/ava-labs/avalanchego/database/manager" + "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/snow/engine/common" + snowmanblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/utils/json" + "github.com/ava-labs/quarkvm/block" + "github.com/ava-labs/quarkvm/chain" + "github.com/ava-labs/quarkvm/codec" + "github.com/ava-labs/quarkvm/storage" + "github.com/ava-labs/quarkvm/version" + "github.com/gorilla/rpc/v2" +) + +var ( + ErrNoPendingBlock = errors.New("no pending block") +) + +var _ snowmanblock.ChainVM = &VM{} + +type VM struct { + ctx *snow.Context + s storage.Storage + toEngine chan<- common.Message + chain chain.Chain + + preferred ids.ID +} + +// implements "snowmanblock.ChainVM.common.VM" +func (vm *VM) Initialize( + ctx *snow.Context, + dbManager manager.Manager, + genesisBytes []byte, + upgradeBytes []byte, + configBytes []byte, + toEngine chan<- common.Message, + _ []*common.Fx, + _ common.AppSender, +) error { + // TODO: check initialize from singleton store + + vm.ctx = ctx + vm.s = storage.New(ctx, dbManager.Current().Database) + vm.toEngine = toEngine + vm.chain = chain.New(vm.s) + + // parent ID, height, timestamp all zero by default + genesisBlk := new(block.Block) + genesisBlk.Update( + genesisBytes, + choices.Processing, + vm.s, + func(id ids.ID) (*block.Block, error) { // lookup + return vm.chain.GetBlock(id) + }, + func(b *block.Block) error { // onVerify + // TODO: store in the vm.chain block cache + return nil + }, + func(b *block.Block) error { // onAccept + vm.chain.SetLastAccepted(b.ID()) + if err := vm.chain.PutBlock(b); err != nil { + return err + } + return vm.s.Commit() + }, + func(b *block.Block) error { // onReject + if err := vm.chain.PutBlock(b); err != nil { + return err + } + return vm.s.Commit() + }, + ) + if err := vm.chain.PutBlock(genesisBlk); err != nil { + return err + } + if err := genesisBlk.Accept(); err != nil { + return err + } + vm.chain.SetLastAccepted(genesisBlk.ID()) + + // TODO: set initialize for singleton store + return vm.s.Commit() +} + +// implements "snowmanblock.ChainVM.common.VM" +func (vm *VM) Bootstrapping() error { + return nil +} + +// implements "snowmanblock.ChainVM.common.VM" +func (vm *VM) Bootstrapped() error { + vm.ctx.Bootstrapped() + return nil +} + +// implements "snowmanblock.ChainVM.common.VM" +func (vm *VM) Shutdown() error { + if vm.ctx == nil { + return nil + } + + if err := vm.s.Commit(); err != nil { + return err + } + return vm.s.Close() +} + +// implements "snowmanblock.ChainVM.common.VM" +func (vm *VM) Version() (string, error) { return version.Version.String(), nil } + +// implements "snowmanblock.ChainVM.common.VM" +func (vm *VM) CreateStaticHandlers() (map[string]*common.HTTPHandler, error) { + server := rpc.NewServer() + server.RegisterCodec(json.NewCodec(), "application/json") + server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") + if err := server.RegisterService(&Service{vm: vm}, Name); err != nil { + return nil, err + } + return map[string]*common.HTTPHandler{ + "": { + Handler: server, + }, + }, nil +} + +// implements "snowmanblock.ChainVM.common.VM" +func (vm *VM) CreateHandlers() (map[string]*common.HTTPHandler, error) { + server := rpc.NewServer() + server.RegisterCodec(json.NewCodec(), "application/json") + server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") + if err := server.RegisterService(&StaticService{}, Name); err != nil { + return nil, err + } + return map[string]*common.HTTPHandler{ + "": { + LockOptions: common.NoLock, + Handler: server, + }, + }, nil +} + +// implements "snowmanblock.ChainVM.commom.VM.AppHandler" +func (vm *VM) AppRequest(nodeID ids.ShortID, requestID uint32, deadline time.Time, request []byte) error { + // (currently) no app-specific messages + return nil +} + +// implements "snowmanblock.ChainVM.commom.VM.AppHandler" +func (vm *VM) AppRequestFailed(nodeID ids.ShortID, requestID uint32) error { + // (currently) no app-specific messages + return nil +} + +// implements "snowmanblock.ChainVM.commom.VM.AppHandler" +func (vm *VM) AppResponse(nodeID ids.ShortID, requestID uint32, response []byte) error { + // (currently) no app-specific messages + return nil +} + +// implements "snowmanblock.ChainVM.commom.VM.AppHandler" +func (vm *VM) AppGossip(nodeID ids.ShortID, msg []byte) error { + // (currently) no app-specific messages + return nil +} + +// implements "snowmanblock.ChainVM.commom.VM.health.Checkable" +func (vm *VM) HealthCheck() (interface{}, error) { return "", nil } + +// implements "snowmanblock.ChainVM.commom.VM.validators.Connector" +func (vm *VM) Connected(id ids.ShortID) error { + // no-op + return nil +} + +// implements "snowmanblock.ChainVM.commom.VM.validators.Connector" +func (vm *VM) Disconnected(id ids.ShortID) error { + // no-op + return nil +} + +// implements "snowmanblock.ChainVM.commom.VM.Getter" +func (vm *VM) GetBlock(id ids.ID) (snowman.Block, error) { + // TODO: add cache + return vm.chain.GetBlock(id) +} + +// implements "snowmanblock.ChainVM.commom.VM.Parser" +func (vm *VM) ParseBlock(source []byte) (snowman.Block, error) { + blk := new(block.Block) + if _, err := codec.Unmarshal(source, blk); err != nil { + return nil, err + } + blk.Update( + source, + choices.Processing, + vm.s, + func(id ids.ID) (*block.Block, error) { // lookup + return vm.chain.GetBlock(id) + }, + func(b *block.Block) error { // onVerify + // TODO: store in the vm.chain block cache + return nil + }, + func(b *block.Block) error { // onAccept + vm.chain.SetLastAccepted(b.ID()) + if err := vm.chain.PutBlock(b); err != nil { + return err + } + return vm.s.Commit() + }, + func(b *block.Block) error { // onReject + if err := vm.chain.PutBlock(b); err != nil { + return err + } + return vm.s.Commit() + }, + ) + return blk, nil +} + +// implements "snowmanblock.ChainVM" +func (vm *VM) BuildBlock() (snowman.Block, error) { + // TODO; check pending transactions + // if vm.chain.Pending() == 0 { + // return nil, ErrNoPendingBlock + // } + + b := vm.chain.Produce() + if err := b.Verify(); err != nil { + return nil, err + } + vm.chain.AddBlock(b) + go func() { + select { + case vm.toEngine <- common.PendingTxs: + default: + vm.ctx.Log.Debug("dropping message to consensus engine") + } + }() + return b, nil +} + +// implements "snowmanblock.ChainVM" +func (vm *VM) SetPreference(id ids.ID) error { + vm.preferred = id + return nil +} + +// implements "snowmanblock.ChainVM" +func (vm *VM) LastAccepted() (ids.ID, error) { + return vm.chain.GetLastAccepted(), nil +} + +func (s *Service) Put(_ *http.Request, args *PutArgs, reply *PutReply) error { + if err := s.vm.Put(args); err != nil { + s.vm.ctx.Log.Warn("failed put for %q (%v)", args.Key, err) + return err + } + reply.Success = true + return nil +}