diff --git a/README.md b/README.md index 11f735ca..b6ac4bac 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ TODO: Extend on * Prefixes (address prefixes reserved) * Hashed Value Keys * Prefix Expiry (based on weight of all key-values) +* Load Units vs Fee Units +* Lifeline Rewards (why run a node -> don't need to mine) + +# RPC +## /public +* range query + +## /private +* set beneficiary # Quick start diff --git a/chain/base_tx.go b/chain/base_tx.go index 76882084..6f6ece2e 100644 --- a/chain/base_tx.go +++ b/chain/base_tx.go @@ -58,6 +58,25 @@ func (b *BaseTx) ExecuteBase() error { return nil } -func (b *BaseTx) Units() uint64 { +func (b *BaseTx) FeeUnits() uint64 { return BaseTxUnits } + +func (b *BaseTx) LoadUnits() uint64 { + return b.FeeUnits() +} + +func (b *BaseTx) Copy() *BaseTx { + sender := [crypto.SECP256K1RPKLen]byte{} + copy(sender[:], b.Sender[:]) + blockID := ids.ID{} + copy(blockID[:], b.BlockID[:]) + prefix := make([]byte, len(b.Prefix)) + copy(prefix, b.Prefix) + return &BaseTx{ + Sender: sender, + BlockID: blockID, + Graffiti: b.Graffiti, + Prefix: prefix, + } +} diff --git a/chain/block.go b/chain/block.go index 73654af9..4c8d5840 100644 --- a/chain/block.go +++ b/chain/block.go @@ -21,12 +21,13 @@ const futureBound = 10 * time.Second var _ snowman.Block = &StatelessBlock{} type StatefulBlock struct { - Prnt ids.ID `serialize:"true" json:"parent"` - Tmstmp int64 `serialize:"true" json:"timestamp"` - Hght uint64 `serialize:"true" json:"height"` - Difficulty uint64 `serialize:"true" json:"difficulty"` // difficulty per unit - Cost uint64 `serialize:"true" json:"cost"` - Txs []*Transaction `serialize:"true" json:"txs"` + Prnt ids.ID `serialize:"true" json:"parent"` + Tmstmp int64 `serialize:"true" json:"timestamp"` + Hght uint64 `serialize:"true" json:"height"` + Difficulty uint64 `serialize:"true" json:"difficulty"` // difficulty per unit + Cost uint64 `serialize:"true" json:"cost"` + Txs []*Transaction `serialize:"true" json:"txs"` + Beneficiary []byte `serialize:"true" json:"beneficiary"` // prefix to reward } // Stateless is defined separately from "Block" @@ -45,14 +46,15 @@ type StatelessBlock struct { onAcceptDB *versiondb.Database } -func NewBlock(vm VM, parent snowman.Block, tmstp int64, context *Context) *StatelessBlock { +func NewBlock(vm VM, parent snowman.Block, tmstp int64, beneficiary []byte, context *Context) *StatelessBlock { return &StatelessBlock{ StatefulBlock: &StatefulBlock{ - Tmstmp: tmstp, - Prnt: parent.ID(), - Hght: parent.Height() + 1, - Difficulty: context.NextDifficulty, - Cost: context.NextCost, + Tmstmp: tmstp, + Prnt: parent.ID(), + Hght: parent.Height() + 1, + Difficulty: context.NextDifficulty, + Cost: context.NextCost, + Beneficiary: beneficiary, }, vm: vm, st: choices.Processing, @@ -117,15 +119,11 @@ func (b *StatelessBlock) ID() ids.ID { return b.id } // verify checks the correctness of a block and then returns the // *versiondb.Database computed during execution. func (b *StatelessBlock) verify() (*StatelessBlock, *versiondb.Database, error) { - prnt, err := b.vm.GetBlock(b.Prnt) + parent, err := b.vm.GetStatelessBlock(b.Prnt) if err != nil { log.Debug("could not get parent", "id", b.Prnt) return nil, nil, err } - parent, ok := prnt.(*StatelessBlock) - if !ok { - return nil, nil, fmt.Errorf("unexpected snowman.Block %T, expected *StatelessBlock", prnt) - } if len(b.Txs) == 0 { return nil, nil, ErrNoTxs @@ -146,6 +144,7 @@ func (b *StatelessBlock) verify() (*StatelessBlock, *versiondb.Database, error) if b.Difficulty != context.NextDifficulty { return nil, nil, ErrInvalidDifficulty } + parentState, err := parent.onAccept() if err != nil { return nil, nil, err @@ -156,6 +155,10 @@ func (b *StatelessBlock) verify() (*StatelessBlock, *versiondb.Database, error) if err := ExpireNext(onAcceptDB, parent.Tmstmp, b.Tmstmp); err != nil { return nil, nil, err } + // Reward producer (if [b.Beneficiary] is non-nil) + if err := Reward(onAcceptDB, b.Beneficiary); err != nil { + return nil, nil, err + } // Process new transactions log.Debug("build context", "height", b.Hght, "difficulty", b.Difficulty, "cost", b.Cost) @@ -164,7 +167,7 @@ func (b *StatelessBlock) verify() (*StatelessBlock, *versiondb.Database, error) if err := tx.Execute(onAcceptDB, b.Tmstmp, context); err != nil { return nil, nil, err } - surplusWork += (tx.Difficulty() - b.Difficulty) * tx.Units() + surplusWork += (tx.Difficulty() - b.Difficulty) * tx.FeeUnits() } // Ensure enough work is performed to compensate for block production speed requiredSurplus := b.Difficulty * b.Cost @@ -178,6 +181,7 @@ func (b *StatelessBlock) verify() (*StatelessBlock, *versiondb.Database, error) func (b *StatelessBlock) Verify() error { parent, onAcceptDB, err := b.verify() if err != nil { + log.Debug("block verification failed", "blkID", b.ID(), "error", err) return err } b.onAcceptDB = onAcceptDB diff --git a/chain/block_test.go b/chain/block_test.go index 12b7da28..d7fe40bd 100644 --- a/chain/block_test.go +++ b/chain/block_test.go @@ -173,9 +173,9 @@ func createTestBlk( if err := parentBlk.init(); err != nil { t.Fatal(err) } - vm.EXPECT().GetBlock(parentBlk.ID()).Return(parentBlk, nil) + vm.EXPECT().GetStatelessBlock(parentBlk.ID()).Return(parentBlk, nil) - blk := NewBlock(vm, parentBlk, blkTmpstp, blkCtx) + blk := NewBlock(vm, parentBlk, blkTmpstp, nil, blkCtx) if uint64(blk.StatefulBlock.Tmstmp) != uint64(blkTmpstp) { t.Fatalf("blk.StatefulBlock.Tmstmp expected %d, got %d", blkTmpstp, blk.StatefulBlock.Tmstmp) } diff --git a/chain/builder.go b/chain/builder.go index 77faaab2..dca3958d 100644 --- a/chain/builder.go +++ b/chain/builder.go @@ -4,7 +4,6 @@ package chain import ( - "fmt" "time" "github.com/ava-labs/avalanchego/database/versiondb" @@ -17,31 +16,38 @@ func BuildBlock(vm VM, preferred ids.ID) (snowman.Block, error) { log.Debug("attempting block building") nextTime := time.Now().Unix() - prnt, err := vm.GetBlock(preferred) + parent, err := vm.GetStatelessBlock(preferred) if err != nil { log.Debug("block building failed: couldn't get parent", "err", err) return nil, err } - parent, ok := prnt.(*StatelessBlock) - if !ok { - return nil, fmt.Errorf("unexpected snowman.Block %T, expected *StatelessBlock", prnt) - } context, err := vm.ExecutionContext(nextTime, parent) if err != nil { log.Debug("block building failed: couldn't get execution context", "err", err) return nil, err } - b := NewBlock(vm, parent, nextTime, context) + b := NewBlock(vm, parent, nextTime, vm.Beneficiary(), context) + + // Clean out invalid txs + mempool := vm.Mempool() + mempool.Prune(context.RecentBlockIDs) - // Select new transactions parentDB, err := parent.onAccept() if err != nil { log.Debug("block building failed: couldn't get parent db", "err", err) return nil, err } - mempool := vm.Mempool() - mempool.Prune(context.RecentBlockIDs) // clean out invalid txs vdb := versiondb.New(parentDB) + + // Remove all expired prefixes + if err := ExpireNext(vdb, parent.Tmstmp, b.Tmstmp); err != nil { + return nil, err + } + // Reward producer (if [b.Beneficiary] is non-nil) + if err := Reward(vdb, b.Beneficiary); err != nil { + return nil, err + } + b.Txs = []*Transaction{} units := uint64(0) for units < TargetUnits && mempool.Len() > 0 { @@ -62,8 +68,9 @@ func BuildBlock(vm VM, preferred ids.ID) (snowman.Block, error) { } // Wait to add prefix until after verification b.Txs = append(b.Txs, next) - units += next.Units() + units += next.LoadUnits() } + vdb.Abort() // Compute block hash and marshaled representation if err := b.init(); err != nil { diff --git a/chain/chain.go b/chain/chain.go index 65af0ba8..f998a3e9 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -5,21 +5,36 @@ package chain import ( "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/units" ) // TODO: load from genesis const ( - ExpiryTime = 60 * 60 * 24 * 30 // 30 Days - ValueUnitSize = 256 // 256B - MaxValueSize = 1 << 10 * 128 // 128KB (500 Units) - BaseTxUnits = 10 - + // Tx params + BaseTxUnits = 10 + + // SetTx params + ValueUnitSize = 256 // 256B + MaxValueSize = 128 * units.KiB // (500 Units) + + // Claim Params + ClaimFeeMultiplier = 5 + ExpiryTime = 60 * 60 * 24 * 30 // 30 Days + ClaimTier3Multiplier = 1 + ClaimTier2Size = 36 + ClaimTier2Multiplier = 5 + ClaimTier1Size = 12 + ClaimTier1Multiplier = 25 + + // Lifeline Params + PrefixRenewalDiscount = 5 + + // Fee Mechanism Params LookbackWindow = 60 // 60 Seconds BlockTarget = 1 // 1 Block per Second - TargetUnits = BaseTxUnits * 512 * LookbackWindow / BlockTarget // 512 Units Per Block - - MinDifficulty = 10 // ~10ms per unit (100 ms for claim) - MinBlockCost = 1 // Minimum Unit Overhead + TargetUnits = BaseTxUnits * 512 * LookbackWindow / BlockTarget // 5012 Units Per Block (~1.2MB of SetTx) + MinDifficulty = 100 // ~100ms per unit (~5s for easiest claim) + MinBlockCost = 1 // Minimum Unit Overhead ) type Context struct { diff --git a/chain/claim_tx.go b/chain/claim_tx.go index bdc7f986..e951e1ee 100644 --- a/chain/claim_tx.go +++ b/chain/claim_tx.go @@ -8,6 +8,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/utils/crypto" + "github.com/ava-labs/quarkvm/parser" ) var _ UnsignedTransaction = &ClaimTx{} @@ -45,3 +46,34 @@ func (c *ClaimTx) Execute(db database.Database, blockTime uint64) error { } return nil } + +// [prefixUnits] requires the caller to produce more work to get prefixes of +// a shorter length because they are more desirable. This creates a "lottery" +// mechanism where the people that spend the most mining power will win the +// prefix. +// +// [prefixUnits] should only be called on a prefix that is valid +func prefixUnits(p []byte) uint64 { + desirability := parser.MaxKeySize - len(p) + if len(p) > ClaimTier2Size { + return uint64(desirability * ClaimTier3Multiplier) + } + if len(p) > ClaimTier1Size { + return uint64(desirability * ClaimTier2Multiplier) + } + return uint64(desirability * ClaimTier1Multiplier) +} + +func (c *ClaimTx) FeeUnits() uint64 { + return c.LoadUnits() + prefixUnits(c.Prefix) +} + +func (c *ClaimTx) LoadUnits() uint64 { + return c.BaseTx.LoadUnits() * ClaimFeeMultiplier +} + +func (c *ClaimTx) Copy() UnsignedTransaction { + return &ClaimTx{ + BaseTx: c.BaseTx.Copy(), + } +} diff --git a/chain/codec.go b/chain/codec.go index 1d2979c7..2964cdcf 100644 --- a/chain/codec.go +++ b/chain/codec.go @@ -14,8 +14,8 @@ const ( // codecVersion is the current default codec version codecVersion = 0 - // maxSize is 1MB to support large blocks (~9 large key settings) - maxSize = 1 * units.MiB + // maxSize is 2MB to support large values + maxSize = 2 * units.MiB ) var codecManager codec.Manager diff --git a/chain/lifeline_tx.go b/chain/lifeline_tx.go index b4084ad7..98a3fd2a 100644 --- a/chain/lifeline_tx.go +++ b/chain/lifeline_tx.go @@ -13,17 +13,32 @@ type LifelineTx struct { *BaseTx `serialize:"true" json:"baseTx"` } -func (l *LifelineTx) Execute(db database.Database, blockTime uint64) error { - i, has, err := GetPrefixInfo(db, l.Prefix) +func addLife(db database.KeyValueReaderWriter, prefix []byte) error { + i, has, err := GetPrefixInfo(db, prefix) if err != nil { return err } - // Cannot add lifeline to missing prefix + // Cannot add time to missing prefix if !has { return ErrPrefixMissing } // Lifeline spread across all units lastExpiry := i.Expiry - i.Expiry += ExpiryTime / i.Units - return PutPrefixInfo(db, l.Prefix, i, lastExpiry) + prefixPenalty := prefixUnits(prefix) / PrefixRenewalDiscount + if prefixPenalty < 1 { // avoid division by 0 + prefixPenalty = 1 + } + + i.Expiry += ExpiryTime / i.Units / prefixPenalty + return PutPrefixInfo(db, prefix, i, lastExpiry) +} + +func (l *LifelineTx) Execute(db database.Database, blockTime uint64) error { + return addLife(db, l.Prefix) +} + +func (l *LifelineTx) Copy() UnsignedTransaction { + return &LifelineTx{ + BaseTx: l.BaseTx.Copy(), + } } diff --git a/chain/mempool_mock.go b/chain/mempool_mock.go index 4239ce14..e21657e6 100644 --- a/chain/mempool_mock.go +++ b/chain/mempool_mock.go @@ -64,6 +64,20 @@ func (mr *MockMempoolMockRecorder) Len() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Len", reflect.TypeOf((*MockMempool)(nil).Len)) } +// NewTxs mocks base method. +func (m *MockMempool) NewTxs(arg0 uint64) []*Transaction { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewTxs", arg0) + ret0, _ := ret[0].([]*Transaction) + return ret0 +} + +// NewTxs indicates an expected call of NewTxs. +func (mr *MockMempoolMockRecorder) NewTxs(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTxs", reflect.TypeOf((*MockMempool)(nil).NewTxs), arg0) +} + // PopMax mocks base method. func (m *MockMempool) PopMax() (*Transaction, uint64) { m.ctrl.T.Helper() diff --git a/chain/reward.go b/chain/reward.go new file mode 100644 index 00000000..5d696798 --- /dev/null +++ b/chain/reward.go @@ -0,0 +1,21 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chain + +import ( + "github.com/ava-labs/avalanchego/database" + + "github.com/ava-labs/quarkvm/parser" +) + +func Reward(db database.KeyValueReaderWriter, prefix []byte) error { + // If there is no one to reward, do nothing + if len(prefix) == 0 { + return nil + } + if err := parser.CheckPrefix(prefix); err != nil { + return err + } + return addLife(db, prefix) +} diff --git a/chain/set_tx.go b/chain/set_tx.go index 2571f193..ed1f1ced 100644 --- a/chain/set_tx.go +++ b/chain/set_tx.go @@ -79,15 +79,15 @@ func (s *SetTx) updatePrefix(db database.Database, blockTime uint64, i *PrefixIn if !exists { return ErrKeyMissing } - i.Units -= lengthOverhead(v) + i.Units -= valueUnits(v) if err := DeletePrefixKey(db, s.Prefix, s.Key); err != nil { return err } } else { if exists { - i.Units -= lengthOverhead(v) + i.Units -= valueUnits(v) } - i.Units += lengthOverhead(s.Value) + i.Units += valueUnits(s.Value) if err := PutPrefixKey(db, s.Prefix, s.Key, s.Value); err != nil { return err } @@ -99,12 +99,28 @@ func (s *SetTx) updatePrefix(db database.Database, blockTime uint64, i *PrefixIn return PutPrefixInfo(db, s.Prefix, i, lastExpiry) } -func lengthOverhead(b []byte) uint64 { +func valueUnits(b []byte) uint64 { return uint64(len(b)/ValueUnitSize + 1) } -func (s *SetTx) Units() uint64 { +func (s *SetTx) FeeUnits() uint64 { // We don't subtract by 1 here because we want to charge extra for any - // value-based interaction (even if it is small). - return s.BaseTx.Units() + lengthOverhead(s.Value) + // value-based interaction (even if it is small or a delete). + return s.BaseTx.FeeUnits() + valueUnits(s.Value) +} + +func (s *SetTx) LoadUnits() uint64 { + return s.FeeUnits() +} + +func (s *SetTx) Copy() UnsignedTransaction { + key := make([]byte, len(s.Key)) + copy(key, s.Key) + value := make([]byte, len(s.Value)) + copy(value, s.Value) + return &SetTx{ + BaseTx: s.BaseTx.Copy(), + Key: key, + Value: value, + } } diff --git a/chain/storage.go b/chain/storage.go index 41ffb7d6..96397169 100644 --- a/chain/storage.go +++ b/chain/storage.go @@ -203,6 +203,9 @@ func GetBlock(db database.KeyValueReader, bid ids.ID) ([]byte, error) { // ExpireNext queries "expiryPrefix" key space to find expiring keys, // deletes their prefixInfos, and schedules its key pruning with its raw prefix. +// +// TODO: all records should just be deleted if we are still bootstrapping +// (instead of doing it async) func ExpireNext(db database.Database, rparent int64, rcurrent int64) (err error) { parent, current := uint64(rparent), uint64(rcurrent) startKey := RangeTimeKey(expiryPrefix, parent) diff --git a/chain/tx.go b/chain/tx.go index 9c26193c..9f0b2729 100644 --- a/chain/tx.go +++ b/chain/tx.go @@ -79,7 +79,7 @@ func CalcDifficulty(utx UnsignedTransaction) ([]byte, uint64, error) { if err != nil { return nil, 0, err } - return b, pow.Difficulty(b) / utx.Units(), nil + return b, pow.Difficulty(b) / utx.FeeUnits(), nil } func (t *Transaction) Execute(db database.Database, blockTime int64, context *Context) error { diff --git a/chain/unsigned_tx.go b/chain/unsigned_tx.go index 734297bb..1597f3fd 100644 --- a/chain/unsigned_tx.go +++ b/chain/unsigned_tx.go @@ -10,11 +10,13 @@ import ( ) type UnsignedTransaction interface { + Copy() UnsignedTransaction SetBlockID(block ids.ID) SetGraffiti(graffiti uint64) GetSender() [crypto.SECP256K1RPKLen]byte GetBlockID() ids.ID - Units() uint64 // number of units to mine tx + FeeUnits() uint64 // number of units to mine tx + LoadUnits() uint64 // units that should impact fee rate ExecuteBase() error Execute(database.Database, uint64) error diff --git a/chain/unsigned_tx_mock.go b/chain/unsigned_tx_mock.go index 9aa38908..5aff5c35 100644 --- a/chain/unsigned_tx_mock.go +++ b/chain/unsigned_tx_mock.go @@ -37,6 +37,20 @@ func (m *MockUnsignedTransaction) EXPECT() *MockUnsignedTransactionMockRecorder return m.recorder } +// Copy mocks base method. +func (m *MockUnsignedTransaction) Copy() UnsignedTransaction { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Copy") + ret0, _ := ret[0].(UnsignedTransaction) + return ret0 +} + +// Copy indicates an expected call of Copy. +func (mr *MockUnsignedTransactionMockRecorder) Copy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockUnsignedTransaction)(nil).Copy)) +} + // Execute mocks base method. func (m *MockUnsignedTransaction) Execute(arg0 database.Database, arg1 uint64) error { m.ctrl.T.Helper() @@ -65,6 +79,20 @@ func (mr *MockUnsignedTransactionMockRecorder) ExecuteBase() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBase", reflect.TypeOf((*MockUnsignedTransaction)(nil).ExecuteBase)) } +// FeeUnits mocks base method. +func (m *MockUnsignedTransaction) FeeUnits() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FeeUnits") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// FeeUnits indicates an expected call of FeeUnits. +func (mr *MockUnsignedTransactionMockRecorder) FeeUnits() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FeeUnits", reflect.TypeOf((*MockUnsignedTransaction)(nil).FeeUnits)) +} + // GetBlockID mocks base method. func (m *MockUnsignedTransaction) GetBlockID() ids.ID { m.ctrl.T.Helper() @@ -93,6 +121,20 @@ func (mr *MockUnsignedTransactionMockRecorder) GetSender() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSender", reflect.TypeOf((*MockUnsignedTransaction)(nil).GetSender)) } +// LoadUnits mocks base method. +func (m *MockUnsignedTransaction) LoadUnits() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadUnits") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// LoadUnits indicates an expected call of LoadUnits. +func (mr *MockUnsignedTransactionMockRecorder) LoadUnits() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUnits", reflect.TypeOf((*MockUnsignedTransaction)(nil).LoadUnits)) +} + // SetBlockID mocks base method. func (m *MockUnsignedTransaction) SetBlockID(arg0 ids.ID) { m.ctrl.T.Helper() @@ -116,17 +158,3 @@ func (mr *MockUnsignedTransactionMockRecorder) SetGraffiti(arg0 interface{}) *go mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGraffiti", reflect.TypeOf((*MockUnsignedTransaction)(nil).SetGraffiti), arg0) } - -// Units mocks base method. -func (m *MockUnsignedTransaction) Units() uint64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Units") - ret0, _ := ret[0].(uint64) - return ret0 -} - -// Units indicates an expected call of Units. -func (mr *MockUnsignedTransactionMockRecorder) Units() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Units", reflect.TypeOf((*MockUnsignedTransaction)(nil).Units)) -} diff --git a/chain/vm.go b/chain/vm.go index 32822612..6f80a732 100644 --- a/chain/vm.go +++ b/chain/vm.go @@ -6,13 +6,14 @@ package chain import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" ) type VM interface { State() database.Database Mempool() Mempool - GetBlock(ids.ID) (snowman.Block, error) + GetStatelessBlock(ids.ID) (*StatelessBlock, error) + Beneficiary() []byte + SetBeneficiary(prefix []byte) ExecutionContext(currentTime int64, parent *StatelessBlock) (*Context, error) Verified(*StatelessBlock) Rejected(*StatelessBlock) diff --git a/chain/vm_mock.go b/chain/vm_mock.go index b5df4e2f..78f9bf33 100644 --- a/chain/vm_mock.go +++ b/chain/vm_mock.go @@ -12,7 +12,6 @@ import ( database "github.com/ava-labs/avalanchego/database" ids "github.com/ava-labs/avalanchego/ids" - snowman "github.com/ava-labs/avalanchego/snow/consensus/snowman" gomock "github.com/golang/mock/gomock" ) @@ -51,6 +50,20 @@ func (mr *MockVMMockRecorder) Accepted(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accepted", reflect.TypeOf((*MockVM)(nil).Accepted), arg0) } +// Beneficiary mocks base method. +func (m *MockVM) Beneficiary() []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Beneficiary") + ret0, _ := ret[0].([]byte) + return ret0 +} + +// Beneficiary indicates an expected call of Beneficiary. +func (mr *MockVMMockRecorder) Beneficiary() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Beneficiary", reflect.TypeOf((*MockVM)(nil).Beneficiary)) +} + // ExecutionContext mocks base method. func (m *MockVM) ExecutionContext(currentTime int64, parent *StatelessBlock) (*Context, error) { m.ctrl.T.Helper() @@ -66,19 +79,19 @@ func (mr *MockVMMockRecorder) ExecutionContext(currentTime, parent interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionContext", reflect.TypeOf((*MockVM)(nil).ExecutionContext), currentTime, parent) } -// GetBlock mocks base method. -func (m *MockVM) GetBlock(arg0 ids.ID) (snowman.Block, error) { +// GetStatelessBlock mocks base method. +func (m *MockVM) GetStatelessBlock(arg0 ids.ID) (*StatelessBlock, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlock", arg0) - ret0, _ := ret[0].(snowman.Block) + ret := m.ctrl.Call(m, "GetStatelessBlock", arg0) + ret0, _ := ret[0].(*StatelessBlock) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetBlock indicates an expected call of GetBlock. -func (mr *MockVMMockRecorder) GetBlock(arg0 interface{}) *gomock.Call { +// GetStatelessBlock indicates an expected call of GetStatelessBlock. +func (mr *MockVMMockRecorder) GetStatelessBlock(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockVM)(nil).GetBlock), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatelessBlock", reflect.TypeOf((*MockVM)(nil).GetStatelessBlock), arg0) } // Mempool mocks base method. @@ -107,6 +120,18 @@ func (mr *MockVMMockRecorder) Rejected(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rejected", reflect.TypeOf((*MockVM)(nil).Rejected), arg0) } +// SetBeneficiary mocks base method. +func (m *MockVM) SetBeneficiary(prefix []byte) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetBeneficiary", prefix) +} + +// SetBeneficiary indicates an expected call of SetBeneficiary. +func (mr *MockVMMockRecorder) SetBeneficiary(prefix interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBeneficiary", reflect.TypeOf((*MockVM)(nil).SetBeneficiary), prefix) +} + // State mocks base method. func (m *MockVM) State() database.Database { m.ctrl.T.Helper() diff --git a/client/client.go b/client/client.go index 15d4c66c..a64da420 100644 --- a/client/client.go +++ b/client/client.go @@ -12,10 +12,11 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/rpc" + "github.com/fatih/color" + "github.com/ava-labs/quarkvm/chain" "github.com/ava-labs/quarkvm/parser" "github.com/ava-labs/quarkvm/vm" - "github.com/fatih/color" ) // Client defines quarkvm client operations. @@ -41,12 +42,13 @@ type Client interface { Range(pfx, key []byte, opts ...OpOption) (kvs []chain.KeyValue, err error) // Performs Proof-of-Work (PoW) by enumerating the graffiti. Mine( - ctx context.Context, utx chain.UnsignedTransaction, difficulty uint64, minSurplus uint64, + ctx context.Context, utx chain.UnsignedTransaction, ) (chain.UnsignedTransaction, error) } // New creates a new client object. func New(uri string, endpoint string, reqTimeout time.Duration) Client { + // TODO: automatically append public/private based on method if !strings.HasPrefix(endpoint, "/") { endpoint = "/" + endpoint } @@ -196,43 +198,6 @@ done: return false, ctx.Err() } -func (cli *client) Mine( - ctx context.Context, utx chain.UnsignedTransaction, difficulty uint64, minSurplus uint64, -) (chain.UnsignedTransaction, error) { - for ctx.Err() == nil { - // TODO: only query this periodically - cbID, err := cli.Preferred() - if err != nil { - return nil, err - } - utx.SetBlockID(cbID) - - graffiti := uint64(0) - for ctx.Err() == nil { - // TODO: only query periodically - valid, err := cli.CheckBlock(cbID) - if err != nil { - return nil, err - } - if !valid { - color.Yellow("%v is no longer a valid block id", cbID) - break - } - utx.SetGraffiti(graffiti) - _, utxd, err := chain.CalcDifficulty(utx) - if err != nil { - return nil, err - } - if utxd >= difficulty && (utxd-difficulty)*utx.Units() >= minSurplus { - return utx, nil - } - graffiti++ - } - // TODO: get new block hash if no longer valid - } - return nil, ctx.Err() -} - type Op struct { key []byte rangeEnd []byte diff --git a/client/errors.go b/client/errors.go new file mode 100644 index 00000000..e48e6171 --- /dev/null +++ b/client/errors.go @@ -0,0 +1,8 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package client + +import "errors" + +var ErrSolution = errors.New("solution found") diff --git a/client/helper.go b/client/helper.go index 9620e2e7..2259badc 100644 --- a/client/helper.go +++ b/client/helper.go @@ -27,13 +27,7 @@ func MineSignIssueTx( ret := &Op{} ret.applyOpts(opts) - diff, cost, err := cli.EstimateDifficulty() - if err != nil { - return ids.Empty, err - } - color.Yellow("fetched estimated difficulty (diff=%d, cost=%d)", diff, cost) - - utx, err := cli.Mine(ctx, rtx, diff, cost) + utx, err := cli.Mine(ctx, rtx) if err != nil { return ids.Empty, err } @@ -56,10 +50,9 @@ func MineSignIssueTx( return ids.Empty, err } - surplusContribution := (tx.Difficulty() - diff) * tx.Units() color.Yellow( - "issuing tx %s (units=%d, difficulty=%d, surplus=%d, blkID=%s)", - tx.ID(), tx.Units(), tx.Difficulty(), surplusContribution, tx.GetBlockID(), + "issuing tx %s (fee units=%d, load units=%d, difficulty=%d, blkID=%s)", + tx.ID(), tx.FeeUnits(), tx.LoadUnits(), tx.Difficulty(), tx.GetBlockID(), ) txID, err = cli.IssueTx(tx.Bytes()) if err != nil { diff --git a/client/miner.go b/client/miner.go new file mode 100644 index 00000000..e31c1143 --- /dev/null +++ b/client/miner.go @@ -0,0 +1,181 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package client + +import ( + "context" + "runtime" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/fatih/color" + "golang.org/x/sync/errgroup" + + "github.com/ava-labs/quarkvm/chain" +) + +const ( + durPrecision = 10 * time.Millisecond + etaMultiplier = 3 +) + +var concurrency = uint64(runtime.NumCPU()) + +type miningData struct { + blockID ids.ID + minDifficulty uint64 + minCost uint64 +} + +// TODO: properly benchmark and optimize +func (cli *client) Mine(ctx context.Context, utx chain.UnsignedTransaction) (chain.UnsignedTransaction, error) { + now := time.Now() + g, gctx := errgroup.WithContext(ctx) + + // We purposely do not lock around any of these values because it makes the + // core mining loop inefficient. + var ( + ready = make(chan struct{}) + md *miningData + agraffiti uint64 // approximate graffiti (could be set by any thread) + solution chain.UnsignedTransaction + ) + + // Mine for solution + for i := uint64(0); i < concurrency; i++ { + j := i // i will get overwritten during loop iteration + jutx := utx.Copy() // ensure each thread is modifying own copy of tx + graffiti := j // need to offset graffiti by thread + g.Go(func() error { + // Wait for all vars to be initialized + select { + case <-ready: + case <-gctx.Done(): + return gctx.Err() + } + + lastBlk := md.blockID + for gctx.Err() == nil { + cmd := md + // Reset graffiti when block has been updated + // + // Note: We always want to use the newest BlockID when mining to maximize + // the probability our transaction will get into a block before it + // expires. + if cmd.blockID != lastBlk { + lastBlk = cmd.blockID + graffiti = j + } + + // Try new graffiti + jutx.SetBlockID(cmd.blockID) + jutx.SetGraffiti(graffiti) + _, utxd, err := chain.CalcDifficulty(jutx) + if err != nil { + return err + } + if utxd >= cmd.minDifficulty && + (utxd-cmd.minDifficulty)*jutx.FeeUnits() >= cmd.minDifficulty*cmd.minCost { + solution = jutx + color.Green( + "mining complete[%d] (difficulty=%d, surplus=%d, elapsed=%v)", + graffiti, utxd, (utxd-cmd.minDifficulty)*solution.FeeUnits(), time.Since(now).Round(durPrecision), + ) + return ErrSolution + } + + // Work is insufficient, try again + graffiti += concurrency // offset to avoid duplicate work + agraffiti = graffiti // approximate graffiti values + } + return gctx.Err() + }) + } + + // Periodically print ETA + g.Go(func() error { + // Wait for all vars to be initialized + select { + case <-ready: + case <-gctx.Done(): + return gctx.Err() + } + + // Inline function so that we don't need to copy variables around and/or + // make execution context with locks + printETA := func() { + // If we haven't returned yet, but have a solution, exit + if solution != nil { + return + } + + // Assumes each additional unit of difficulty is ~1ms of compute + cmd := md + eta := time.Duration(utx.FeeUnits()*cmd.minDifficulty) * time.Millisecond + eta = (eta / time.Duration(concurrency)) * etaMultiplier // account for threads and overestimate + diff := time.Since(now) + if diff > eta { + color.Yellow( + "mining in progress[%s/%d]... (elapsed=%v, threads=%d)", + cmd.blockID, agraffiti, time.Since(now).Round(durPrecision), concurrency, + ) + } else { + eta -= diff + color.Yellow( + "mining in progress[%s/%d]... (elapsed=%v, est. remaining=%v, threads=%d)", + cmd.blockID, agraffiti, time.Since(now).Round(durPrecision), eta.Round(durPrecision), concurrency, + ) + } + } + + t := time.NewTicker(2 * time.Second) + printETA() + for { + select { + case <-t.C: + printETA() + case <-gctx.Done(): + return gctx.Err() + } + } + }) + + // Periodically update blockID and required difficulty + g.Go(func() error { + t := time.NewTicker(time.Second) + readyClosed := false + for { + select { + case <-t.C: + blkID, err := cli.Preferred() + if err != nil { + return err + } + diff, cost, err := cli.EstimateDifficulty() + if err != nil { + return err + } + + md = &miningData{ + blockID: blkID, + minDifficulty: diff, + minCost: cost, + } + + if !readyClosed { + close(ready) + readyClosed = true + } + case <-gctx.Done(): + return nil + } + } + }) + err := g.Wait() + if solution != nil { + // If a solution was found, we don't care what the error was. + return solution, nil + } + return nil, err +} diff --git a/go.mod b/go.mod index 01dd4a10..63a7eaf3 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/onsi/gomega v1.17.0 github.com/spf13/cobra v1.2.1 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c sigs.k8s.io/yaml v1.3.0 ) diff --git a/go.sum b/go.sum index 6f57a314..61e3e151 100644 --- a/go.sum +++ b/go.sum @@ -589,6 +589,7 @@ 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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/mempool/mempool.go b/mempool/mempool.go index 82a01fa3..b302b29a 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -250,13 +250,13 @@ func (th *Mempool) NewTxs(maxUnits uint64) []*chain.Transaction { if !th.maxHeap.Has(tx.ID()) { continue } - if tx.Units()+units > maxUnits { + if tx.LoadUnits()+units > maxUnits { // Note: this algorithm preserves the ordering of new transactions th.newTxs = th.newTxs[i:] return selected } selected = append(selected, tx) - units += tx.Units() + units += tx.LoadUnits() } th.newTxs = nil return selected diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index 5a125fcf..11baf984 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -13,22 +13,25 @@ import ( func TestMempool(t *testing.T) { txm := mempool.New(4) - for _, i := range []int{1, 2, 3} { // difficulty 2, 3, 0 + for _, i := range []int{200, 220, 250} { tx := &chain.Transaction{ - Signature: bytes.Repeat([]byte{'a'}, i*10), - UnsignedTransaction: &chain.ClaimTx{ + Signature: bytes.Repeat([]byte{'a'}, i), + UnsignedTransaction: &chain.SetTx{ BaseTx: &chain.BaseTx{ - Prefix: bytes.Repeat([]byte{'k'}, i*10), + Prefix: bytes.Repeat([]byte{'b'}, i), + Graffiti: 28829, }, }, } if err := tx.Init(); err != nil { t.Fatal(err) } - txm.Add(tx) + if !txm.Add(tx) { + t.Fatalf("tx %s was not added", tx.ID()) + } } if _, diff := txm.PeekMax(); diff != 3 { - t.Fatalf("difficulty expected 4, got %d", diff) + t.Fatalf("difficulty expected 3, got %d", diff) } if _, diff := txm.PeekMin(); diff != 0 { t.Fatalf("difficulty expected 0, got %d", diff) diff --git a/pow/pow.go b/pow/pow.go index 2a1b0d74..7bb0ec24 100644 --- a/pow/pow.go +++ b/pow/pow.go @@ -12,10 +12,9 @@ import ( // Recommended reading to understand how this works: https://en.bitcoin.it/wiki/Difficulty var ( - // TODO: provide meaningful comment here // Bitcoin uses 2^208 for the [base] - // Each unit of difficulty at 2^240 adds ~1ms on a nice macbook - base = new(big.Int).Exp(big.NewInt(2), big.NewInt(240), nil) + // Each unit of difficulty at 2^230 adds ~1ms on a new laptop + base = new(big.Int).Exp(big.NewInt(2), big.NewInt(230), nil) scalingFactor = new(big.Int).Mul(big.NewInt(0xFFFF), base) ) diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 5dcb362f..fa7a857b 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -8,7 +8,8 @@ import ( "bytes" "context" "flag" - "fmt" + "path" + "strings" "syscall" "testing" "time" @@ -16,7 +17,9 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto" "github.com/ava-labs/quarkvm/chain" "github.com/ava-labs/quarkvm/client" + "github.com/ava-labs/quarkvm/parser" "github.com/ava-labs/quarkvm/tests" + "github.com/ava-labs/quarkvm/vm" "github.com/fatih/color" ginkgo "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" @@ -43,7 +46,7 @@ func init() { flag.DurationVar( &requestTimeout, "request-timeout", - 30*time.Second, + 60*time.Second, "timeout for transaction issuance and confirmation", ) flag.StringVar( @@ -96,7 +99,7 @@ var _ = ginkgo.BeforeSuite(func() { u := clusterInfo.URIs[i] instances[i] = instance{ uri: u, - cli: client.New(u, clusterInfo.Endpoint, requestTimeout), + cli: client.New(u, path.Join(clusterInfo.Endpoint, vm.PublicEndpoint), requestTimeout), } } color.Blue("created clients with %+v", clusterInfo) @@ -132,7 +135,7 @@ var _ = ginkgo.Describe("[Claim/SetTx]", func() { } }) - pfx := []byte(fmt.Sprintf("%10d", time.Now().UnixNano())) + pfx := []byte(strings.Repeat("a", parser.MaxPrefixSize)) ginkgo.It("Claim/SetTx with valid PoW in a single node", func() { ginkgo.By("mine and issue ClaimTx to the first node", func() { claimTx := &chain.ClaimTx{ diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index f56b2457..bcb67f7d 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -7,8 +7,8 @@ package integration_test import ( "context" "flag" - "fmt" "net/http/httptest" + "strings" "testing" "time" @@ -25,6 +25,7 @@ import ( "github.com/ava-labs/quarkvm/chain" "github.com/ava-labs/quarkvm/client" + "github.com/ava-labs/quarkvm/parser" "github.com/ava-labs/quarkvm/vm" ) @@ -50,7 +51,7 @@ func init() { flag.DurationVar( &requestTimeout, "request-timeout", - 30*time.Second, + 60*time.Second, "timeout for transaction issuance and confirmation", ) flag.IntVar( @@ -151,7 +152,7 @@ var _ = ginkgo.BeforeSuite(func() { hd, err = v.CreateHandlers() gomega.Ω(err).Should(gomega.BeNil()) - httpServer := httptest.NewServer(hd[""].Handler) + httpServer := httptest.NewServer(hd[vm.PublicEndpoint].Handler) instances[i] = instance{ nodeID: ctx.NodeID, vm: v, @@ -195,7 +196,7 @@ var _ = ginkgo.Describe("[ClaimTx]", func() { }) ginkgo.It("Gossip ClaimTx to a different node", func() { - pfx := []byte(fmt.Sprintf("%10d", time.Now().UnixNano())) + pfx := []byte(strings.Repeat("a", parser.MaxPrefixSize)) claimTx := &chain.ClaimTx{ BaseTx: &chain.BaseTx{ Sender: sender, @@ -265,7 +266,7 @@ var _ = ginkgo.Describe("[ClaimTx]", func() { }) ginkgo.It("Claim/SetTx with valid PoW in a single node", func() { - pfx := []byte(fmt.Sprintf("%10d", time.Now().UnixNano())) + pfx := []byte(strings.Repeat("b", parser.MaxPrefixSize)) claimTx := &chain.ClaimTx{ BaseTx: &chain.BaseTx{ Sender: sender, @@ -297,8 +298,21 @@ var _ = ginkgo.Describe("[ClaimTx]", func() { // to work around "ErrInsufficientSurplus" for mining too fast time.Sleep(5 * time.Second) - ginkgo.By("mine and accept block with a new SetTx", func() { + ginkgo.By("mine and accept block with a new SetTx (with beneficiary)", func() { + i, err := instances[0].cli.PrefixInfo(pfx) + gomega.Ω(err).To(gomega.BeNil()) + instances[0].vm.SetBeneficiary(pfx) + mineAndExpectBlkAccept(instances[0], setTx) + + i2, err := instances[0].cli.PrefixInfo(pfx) + gomega.Ω(err).To(gomega.BeNil()) + n := uint64(time.Now().Unix()) + irem := (i.Expiry - n) * i.Units + i2rem := (i2.Expiry - n) * i2.Units + gomega.Ω(i2rem > irem).To(gomega.BeTrue()) + + instances[0].vm.SetBeneficiary(nil) }) ginkgo.By("read back from VM with range query", func() { @@ -310,7 +324,7 @@ var _ = ginkgo.Describe("[ClaimTx]", func() { }) ginkgo.It("fail Gossip ClaimTx to a stale node when missing previous blocks", func() { - pfx := []byte(fmt.Sprintf("%10d", time.Now().UnixNano())) + pfx := []byte(strings.Repeat("c", parser.MaxPrefixSize)) claimTx := &chain.ClaimTx{ BaseTx: &chain.BaseTx{ Sender: sender, @@ -346,9 +360,7 @@ func mineAndExpectBlkAccept( rtx chain.UnsignedTransaction, ) { ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) - diff, cost, err := i.cli.EstimateDifficulty() - gomega.Ω(err).Should(gomega.BeNil()) - utx, err := i.cli.Mine(ctx, rtx, diff, cost) + utx, err := i.cli.Mine(ctx, rtx) cancel() gomega.Ω(err).Should(gomega.BeNil()) diff --git a/vm/chain_vm.go b/vm/chain_vm.go index c7a5efff..e92729f0 100644 --- a/vm/chain_vm.go +++ b/vm/chain_vm.go @@ -39,7 +39,19 @@ func (vm *VM) Accepted(b *chain.StatelessBlock) { vm.blocks.Put(b.ID(), b) delete(vm.verifiedBlocks, b.ID()) vm.lastAccepted = b - log.Debug("accepted block", "id", b.ID()) + log.Debug("accepted block", "blkID", b.ID(), "beneficiary", string(b.Beneficiary)) +} + +func (vm *VM) SetBeneficiary(prefix []byte) { + vm.beneficiaryLock.Lock() + defer vm.beneficiaryLock.Unlock() + vm.beneficiary = prefix +} + +func (vm *VM) Beneficiary() []byte { + vm.beneficiaryLock.RLock() + defer vm.beneficiaryLock.RUnlock() + return vm.beneficiary } func (vm *VM) ExecutionContext(currTime int64, lastBlock *chain.StatelessBlock) (*chain.Context, error) { @@ -52,7 +64,7 @@ func (vm *VM) ExecutionContext(currTime int64, lastBlock *chain.StatelessBlock) recentBlockIDs.Add(b.ID()) for _, tx := range b.StatefulBlock.Txs { recentTxIDs.Add(tx.ID()) - recentUnits += tx.Units() + recentUnits += tx.LoadUnits() } difficulties = append(difficulties, b.Difficulty) costs = append(costs, b.Cost) diff --git a/vm/helpers.go b/vm/helpers.go index b63064b0..1ac3bcaf 100644 --- a/vm/helpers.go +++ b/vm/helpers.go @@ -19,7 +19,7 @@ const ( // TODO: add caching + test func (vm *VM) lookback(currTime int64, lastID ids.ID, f func(b *chain.StatelessBlock) (bool, error)) error { - curr, err := vm.getBlock(lastID) + curr, err := vm.GetStatelessBlock(lastID) if err != nil { return err } @@ -31,7 +31,7 @@ func (vm *VM) lookback(currTime int64, lastID ids.ID, f func(b *chain.StatelessB if curr.Hght == 0 /* genesis */ { return nil } - b, err := vm.getBlock(curr.Prnt) + b, err := vm.GetStatelessBlock(curr.Prnt) if err != nil { return err } diff --git a/vm/network.go b/vm/network.go index dc60d73c..41e4d1eb 100644 --- a/vm/network.go +++ b/vm/network.go @@ -90,7 +90,7 @@ func (n *PushNetwork) RegossipTxs() error { // recently. n.gossipedTxs.Put(tx.ID(), nil) txs = append(txs, tx) - units += tx.Units() + units += tx.LoadUnits() } return n.sendTxs(txs) diff --git a/vm/private_service.go b/vm/private_service.go new file mode 100644 index 00000000..e503a590 --- /dev/null +++ b/vm/private_service.go @@ -0,0 +1,21 @@ +// Copyright (C) 2019-2021, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package vm + +import ( + "net/http" +) + +type PrivateService struct { + vm *VM +} + +type SetBeneficiaryArgs struct { + Beneficiary []byte `serialize:"true" json:"beneficiary"` +} + +func (svc *PrivateService) SetBeneficiary(_ *http.Request, args *SetBeneficiaryArgs, _ *struct{}) error { + svc.vm.SetBeneficiary(args.Beneficiary) + return nil +} diff --git a/vm/service.go b/vm/public_service.go similarity index 82% rename from vm/service.go rename to vm/public_service.go index 23d84c8a..ce691417 100644 --- a/vm/service.go +++ b/vm/public_service.go @@ -19,7 +19,7 @@ var ( ErrInvalidEmptyTx = errors.New("invalid empty transaction") ) -type Service struct { +type PublicService struct { vm *VM } @@ -29,7 +29,7 @@ type PingReply struct { Success bool `serialize:"true" json:"success"` } -func (svc *Service) Ping(_ *http.Request, args *PingArgs, reply *PingReply) (err error) { +func (svc *PublicService) Ping(_ *http.Request, args *PingArgs, reply *PingReply) (err error) { log.Info("ping") reply.Success = true return nil @@ -44,7 +44,7 @@ type IssueTxReply struct { Success bool `serialize:"true" json:"success"` } -func (svc *Service) IssueTx(_ *http.Request, args *IssueTxArgs, reply *IssueTxReply) error { +func (svc *PublicService) IssueTx(_ *http.Request, args *IssueTxArgs, reply *IssueTxReply) error { tx := new(chain.Transaction) if _, err := chain.Unmarshal(args.Tx, tx); err != nil { return err @@ -76,7 +76,7 @@ type CheckTxReply struct { Confirmed bool `serialize:"true" json:"confirmed"` } -func (svc *Service) CheckTx(_ *http.Request, args *CheckTxArgs, reply *CheckTxReply) error { +func (svc *PublicService) CheckTx(_ *http.Request, args *CheckTxArgs, reply *CheckTxReply) error { has, err := chain.HasTransaction(svc.vm.db, args.TxID) if err != nil { return err @@ -91,7 +91,7 @@ type CurrBlockReply struct { BlockID ids.ID `serialize:"true" json:"blockId"` } -func (svc *Service) CurrBlock(_ *http.Request, args *CurrBlockArgs, reply *CurrBlockReply) error { +func (svc *PublicService) CurrBlock(_ *http.Request, args *CurrBlockArgs, reply *CurrBlockReply) error { reply.BlockID = svc.vm.preferred return nil } @@ -104,7 +104,7 @@ type ValidBlockIDReply struct { Valid bool `serialize:"true" json:"valid"` } -func (svc *Service) ValidBlockID(_ *http.Request, args *ValidBlockIDArgs, reply *ValidBlockIDReply) error { +func (svc *PublicService) ValidBlockID(_ *http.Request, args *ValidBlockIDArgs, reply *ValidBlockIDReply) error { valid, err := svc.vm.ValidBlockID(args.BlockID) if err != nil { return err @@ -120,7 +120,7 @@ type DifficultyEstimateReply struct { Cost uint64 `serialize:"true" json:"cost"` } -func (svc *Service) DifficultyEstimate( +func (svc *PublicService) DifficultyEstimate( _ *http.Request, _ *DifficultyEstimateArgs, reply *DifficultyEstimateReply, @@ -142,7 +142,7 @@ type PrefixInfoReply struct { Info *chain.PrefixInfo `serialize:"true" json:"info"` } -func (svc *Service) PrefixInfo(_ *http.Request, args *PrefixInfoArgs, reply *PrefixInfoReply) error { +func (svc *PublicService) PrefixInfo(_ *http.Request, args *PrefixInfoArgs, reply *PrefixInfoReply) error { i, _, err := chain.GetPrefixInfo(svc.vm.db, args.Prefix) if err != nil { return err @@ -175,7 +175,7 @@ type RangeReply struct { KeyValues []chain.KeyValue `serialize:"true" json:"keyValues"` } -func (svc *Service) Range(_ *http.Request, args *RangeArgs, reply *RangeReply) (err error) { +func (svc *PublicService) Range(_ *http.Request, args *RangeArgs, reply *RangeReply) (err error) { log.Debug("range query", "key", string(args.Key), "rangeEnd", string(args.RangeEnd)) opts := make([]chain.OpOption, 0) if len(args.RangeEnd) > 0 { diff --git a/vm/vm.go b/vm/vm.go index 38aaa527..60dad780 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -7,6 +7,7 @@ package vm import ( "fmt" "net/http" + "sync" "time" "github.com/ava-labs/avalanchego/cache" @@ -29,7 +30,9 @@ import ( ) const ( - Name = "quarkvm" + Name = "quarkvm" + PublicEndpoint = "/public" + PrivateEndpoint = "/private" defaultBuildInterval = 500 * time.Millisecond defaultGossipInterval = 1 * time.Second @@ -66,7 +69,7 @@ type VM struct { appSender common.AppSender network *PushNetwork - // cache block objects to optimize "getBlock" + // cache block objects to optimize "GetBlockStateless" // only put when a block is accepted // key: block ID, value: *chain.StatelessBlock blocks *cache.LRU @@ -85,6 +88,11 @@ type VM struct { minDifficulty uint64 minBlockCost uint64 + // beneficiary is the prefix that will receive rewards if the node produces + // a block + beneficiaryLock sync.RWMutex + beneficiary []byte + stop chan struct{} builderStop chan struct{} @@ -155,7 +163,7 @@ func (vm *VM) Initialize( return err } - blk, err := vm.getBlock(blkID) + blk, err := vm.GetStatelessBlock(blkID) if err != nil { log.Error("could not load last accepted", "err", err) return err @@ -213,20 +221,43 @@ func (vm *VM) Shutdown() error { // implements "snowmanblock.ChainVM.common.VM" func (vm *VM) Version() (string, error) { return version.Version.String(), nil } -// implements "snowmanblock.ChainVM.common.VM" -// for "ext/vm/[chainID]" -func (vm *VM) CreateHandlers() (map[string]*common.HTTPHandler, error) { +// NewHandler returns a new Handler for a service where: +// * The handler's functionality is defined by [service] +// [service] should be a gorilla RPC service (see https://www.gorillatoolkit.org/pkg/rpc/v2) +// * The name of the service is [name] +// * The LockOption is the first element of [lockOption] +// By default the LockOption is WriteLock +// [lockOption] should have either 0 or 1 elements. Elements beside the first are ignored. +func newHandler(name string, service interface{}, lockOption ...common.LockOption) (*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 { + if err := server.RegisterService(service, name); err != nil { + return nil, err + } + + var lock common.LockOption = common.WriteLock + if len(lockOption) != 0 { + lock = lockOption[0] + } + return &common.HTTPHandler{LockOptions: lock, Handler: server}, nil +} + +// implements "snowmanblock.ChainVM.common.VM" +// for "ext/vm/[chainID]" +func (vm *VM) CreateHandlers() (map[string]*common.HTTPHandler, error) { + apis := map[string]*common.HTTPHandler{} + public, err := newHandler(Name, &PublicService{vm: vm}) + if err != nil { + return nil, err + } + apis[PublicEndpoint] = public + private, err := newHandler(Name, &PrivateService{vm: vm}) + if err != nil { return nil, err } - return map[string]*common.HTTPHandler{ - "": { - Handler: server, - }, - }, nil + apis[PrivateEndpoint] = private + return apis, nil } // implements "snowmanblock.ChainVM.common.VM" @@ -273,14 +304,14 @@ func (vm *VM) Disconnected(id ids.ShortID) error { // implements "snowmanblock.ChainVM.commom.VM.Getter" // replaces "core.SnowmanVM.GetBlock" func (vm *VM) GetBlock(id ids.ID) (snowman.Block, error) { - b, err := vm.getBlock(id) + b, err := vm.GetStatelessBlock(id) if err != nil { log.Warn("failed to get block", "err", err) } return b, err } -func (vm *VM) getBlock(blkID ids.ID) (*chain.StatelessBlock, error) { +func (vm *VM) GetStatelessBlock(blkID ids.ID) (*chain.StatelessBlock, error) { // has the block been cached from previous "Accepted" call bi, exist := vm.blocks.Get(blkID) if exist { @@ -316,7 +347,7 @@ func (vm *VM) ParseBlock(source []byte) (snowman.Block, error) { if err != nil { log.Error("could not parse block", "err", err) } else { - log.Debug("parsing block", "id", blk.ID()) + log.Debug("parsed block", "id", blk.ID()) } return blk, err } @@ -331,29 +362,31 @@ func (vm *VM) BuildBlock() (snowman.Block, error) { log.Debug("BuildBlock failed", "error", err) return nil, err } + sblk, ok := blk.(*chain.StatelessBlock) + if !ok { + return nil, fmt.Errorf("unexpected snowman.Block %T, expected *StatelessBlock", blk) + } - log.Debug("BuildBlock success", "blockId", blk.ID()) + log.Debug("BuildBlock success", + "blkID", blk.ID(), "txs", len(sblk.Txs), "beneficiary", string(sblk.Beneficiary), + ) return blk, nil } func (vm *VM) Submit(txs ...*chain.Transaction) (errs []error) { - blk, err := vm.GetBlock(vm.preferred) + blk, err := vm.GetStatelessBlock(vm.preferred) if err != nil { return []error{err} } - sblk, ok := blk.(*chain.StatelessBlock) - if !ok { - return []error{fmt.Errorf("unexpected snowman.Block %T, expected *StatelessBlock", blk)} - } now := time.Now().Unix() - ctx, err := vm.ExecutionContext(now, sblk) + ctx, err := vm.ExecutionContext(now, blk) if err != nil { return []error{err} } vdb := versiondb.New(vm.db) // Expire outdated prefixes before checking submission validity - if err := chain.ExpireNext(vdb, sblk.Tmstmp, now); err != nil { + if err := chain.ExpireNext(vdb, blk.Tmstmp, now); err != nil { return []error{err} } diff --git a/vm/vm_test.go b/vm/vm_test.go index f18dc6f0..5ce163c1 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -35,7 +35,7 @@ func TestBlockCache(t *testing.T) { // we have not set up any persistent db // so this must succeed from using cache - blk2, err := vm.getBlock(blkID) + blk2, err := vm.GetStatelessBlock(blkID) if err != nil { t.Fatal(err) }