From 43b410e20325e5d786c83be393cf96b6d5a73f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Tue, 5 Mar 2019 15:49:56 +0100 Subject: [PATCH 01/10] wip: #232 --- Makefile | 1 + x/msgfee/codec.pb.go | 366 ++++++++++++++++++++++++++++++++++++++++++ x/msgfee/codec.proto | 13 ++ x/msgfee/decorator.go | 50 ++++++ x/msgfee/doc.go | 1 + x/msgfee/init.go | 43 +++++ x/msgfee/model.go | 68 ++++++++ 7 files changed, 542 insertions(+) create mode 100644 x/msgfee/codec.pb.go create mode 100644 x/msgfee/codec.proto create mode 100644 x/msgfee/decorator.go create mode 100644 x/msgfee/doc.go create mode 100644 x/msgfee/init.go create mode 100644 x/msgfee/model.go diff --git a/Makefile b/Makefile index ea3cdb35..ce6943f1 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,7 @@ protoc: protoc --gogofaster_out=. -I=. -I=./vendor -I=$(GOPATH)/src cmd/bnsd/x/nft/username/*.proto protoc --gogofaster_out=. -I=. -I=$(GOPATH)/src x/cash/*.proto protoc --gogofaster_out=. -I=. -I=$(GOPATH)/src x/sigs/*.proto + protoc --gogofaster_out=. -I=. -I=$(GOPATH)/src x/msgfee/*.proto protoc --gogofaster_out=. -I=. -I=./vendor -I=$(GOPATH)/src x/multisig/*.proto protoc --gogofaster_out=. -I=. -I=./vendor -I=$(GOPATH)/src x/validators/*.proto protoc --gogofaster_out=. -I=. -I=$(GOPATH)/src x/batch/*.proto diff --git a/x/msgfee/codec.pb.go b/x/msgfee/codec.pb.go new file mode 100644 index 00000000..04b20f91 --- /dev/null +++ b/x/msgfee/codec.pb.go @@ -0,0 +1,366 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: x/msgfee/codec.proto + +/* + Package msgfee is a generated protocol buffer package. + + It is generated from these files: + x/msgfee/codec.proto + + It has these top-level messages: + MsgFee +*/ +package msgfee + +import proto "github.com/gogo/protobuf/proto" +import fmt "fmt" +import math "math" +import coin "github.com/iov-one/weave/coin" +import _ "github.com/gogo/protobuf/gogoproto" + +import io "io" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package + +// MsgFee represents a fee for a single message that must be paid in order for +// the message to be processed. +type MsgFee struct { + MsgPath string `protobuf:"bytes,1,opt,name=msg_path,json=msgPath,proto3" json:"msg_path,omitempty"` + Fee *coin.Coin `protobuf:"bytes,2,opt,name=fee" json:"fee,omitempty"` +} + +func (m *MsgFee) Reset() { *m = MsgFee{} } +func (m *MsgFee) String() string { return proto.CompactTextString(m) } +func (*MsgFee) ProtoMessage() {} +func (*MsgFee) Descriptor() ([]byte, []int) { return fileDescriptorCodec, []int{0} } + +func (m *MsgFee) GetMsgPath() string { + if m != nil { + return m.MsgPath + } + return "" +} + +func (m *MsgFee) GetFee() *coin.Coin { + if m != nil { + return m.Fee + } + return nil +} + +func init() { + proto.RegisterType((*MsgFee)(nil), "msgfee.MsgFee") +} +func (m *MsgFee) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgFee) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.MsgPath) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintCodec(dAtA, i, uint64(len(m.MsgPath))) + i += copy(dAtA[i:], m.MsgPath) + } + if m.Fee != nil { + dAtA[i] = 0x12 + i++ + i = encodeVarintCodec(dAtA, i, uint64(m.Fee.Size())) + n1, err := m.Fee.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n1 + } + return i, nil +} + +func encodeVarintCodec(dAtA []byte, offset int, v uint64) int { + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return offset + 1 +} +func (m *MsgFee) Size() (n int) { + var l int + _ = l + l = len(m.MsgPath) + if l > 0 { + n += 1 + l + sovCodec(uint64(l)) + } + if m.Fee != nil { + l = m.Fee.Size() + n += 1 + l + sovCodec(uint64(l)) + } + return n +} + +func sovCodec(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} +func sozCodec(x uint64) (n int) { + return sovCodec(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgFee) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCodec + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgFee: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgFee: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MsgPath", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCodec + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthCodec + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MsgPath = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Fee", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCodec + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthCodec + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Fee == nil { + m.Fee = &coin.Coin{} + } + if err := m.Fee.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipCodec(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthCodec + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipCodec(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCodec + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCodec + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCodec + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + iNdEx += length + if length < 0 { + return 0, ErrInvalidLengthCodec + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCodec + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipCodec(dAtA[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthCodec = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowCodec = fmt.Errorf("proto: integer overflow") +) + +func init() { proto.RegisterFile("x/msgfee/codec.proto", fileDescriptorCodec) } + +var fileDescriptorCodec = []byte{ + // 189 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xa9, 0xd0, 0xcf, 0x2d, + 0x4e, 0x4f, 0x4b, 0x4d, 0xd5, 0x4f, 0xce, 0x4f, 0x49, 0x4d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, + 0x17, 0x62, 0x83, 0x88, 0x49, 0x69, 0xa6, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, 0xe7, + 0xea, 0x67, 0xe6, 0x97, 0xe9, 0xe6, 0xe7, 0xa5, 0xea, 0x97, 0xa7, 0x26, 0x96, 0x81, 0x54, 0x67, + 0xe6, 0x21, 0x6b, 0x91, 0xd2, 0x45, 0x52, 0x9a, 0x9e, 0x9f, 0x9e, 0xaf, 0x0f, 0x16, 0x4e, 0x2a, + 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x5c, 0xc9, 0x91, 0x8b, 0xcd, 0xb7, 0x38, 0xdd, + 0x2d, 0x35, 0x55, 0x48, 0x92, 0x8b, 0x23, 0xb7, 0x38, 0x3d, 0xbe, 0x20, 0xb1, 0x24, 0x43, 0x82, + 0x51, 0x81, 0x51, 0x83, 0x33, 0x88, 0x3d, 0xb7, 0x38, 0x3d, 0x20, 0xb1, 0x24, 0x43, 0x48, 0x86, + 0x8b, 0x39, 0x2d, 0x35, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0xdb, 0x88, 0x4b, 0x0f, 0x64, 0xa7, + 0x9e, 0x73, 0x7e, 0x66, 0x5e, 0x10, 0x48, 0xd8, 0x49, 0xe0, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, + 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0x21, 0x89, 0x0d, 0x6c, 0xb6, 0x31, + 0x20, 0x00, 0x00, 0xff, 0xff, 0x91, 0x10, 0x19, 0xc3, 0xd5, 0x00, 0x00, 0x00, +} diff --git a/x/msgfee/codec.proto b/x/msgfee/codec.proto new file mode 100644 index 00000000..f0893c59 --- /dev/null +++ b/x/msgfee/codec.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package msgfee; + +import "github.com/iov-one/weave/coin/codec.proto"; +import "github.com/gogo/protobuf/gogoproto/gogo.proto"; + +// MsgFee represents a fee for a single message that must be paid in order for +// the message to be processed. +message MsgFee { + string msg_path = 1; + coin.Coin fee = 2; +} diff --git a/x/msgfee/decorator.go b/x/msgfee/decorator.go new file mode 100644 index 00000000..891231b1 --- /dev/null +++ b/x/msgfee/decorator.go @@ -0,0 +1,50 @@ +package msgfee + +import ( + "github.com/iov-one/weave" + "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/errors" +) + +type FeeDecorator struct { + bucket *MsgFeeBucket +} + +var _ weave.Decorator = (*FeeDecorator)(nil) + +// NewFeeDecorator returns a decorator that is upading the cost of processing +// each message according to the fee configured per each message type. +func NewFeeDecorator() *FeeDecorator { + return &FeeDecorator{ + bucket: NewMsgFeeBucket(), + } +} + +func (d *FeeDecorator) Check(ctx weave.Context, store weave.KVStore, tx weave.Tx, next weave.Checker) (weave.CheckResult, error) { + return next.Check(ctx, store, tx) +} + +func (d *FeeDecorator) Deliver(ctx weave.Context, store weave.KVStore, tx weave.Tx, next weave.Deliverer) (weave.DeliverResult, error) { + res, err := next.Deliver(ctx, store, tx) + if err != nil { + return res, err + } + + msg, err := tx.GetMsg() + if err != nil { + return res, errors.Wrap(err, "cannot get message") + } + fee, err := d.bucket.MessageFee(store, msg.Path()) + if err != nil { + return res, errors.Wrap(err, "cannot get fee") + } + + if !coin.IsEmpty(fee) { + total, err := res.RequiredFee.Add(*fee) + if err != nil { + return res, errors.Wrap(err, "cannot apply message type fee") + } + res.RequiredFee = total + } + return res, nil +} diff --git a/x/msgfee/doc.go b/x/msgfee/doc.go new file mode 100644 index 00000000..6d65c0d6 --- /dev/null +++ b/x/msgfee/doc.go @@ -0,0 +1 @@ +package msgfee diff --git a/x/msgfee/init.go b/x/msgfee/init.go new file mode 100644 index 00000000..13a8fed2 --- /dev/null +++ b/x/msgfee/init.go @@ -0,0 +1,43 @@ +package msgfee + +import ( + "fmt" + + "github.com/iov-one/weave" + "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/errors" +) + +// Initializer fulfils the Initializer interface to load data from the genesis +// file +type Initializer struct{} + +var _ weave.Initializer = (*Initializer)(nil) + +// FromGenesis will parse initial account info from genesis and save it to the +// database +func (*Initializer) FromGenesis(opts weave.Options, db weave.KVStore) error { + type msgfee struct { + MsgPath string `json:"msg_path"` + Fee coin.Coin `json:"fee"` + } + var fees []*msgfee + if err := opts.ReadOptions("msgfee", &fees); err != nil { + return errors.Wrap(err, "cannot load fees") + } + + bucket := NewMsgFeeBucket() + for i, f := range fees { + fee := MsgFee{ + MsgPath: f.MsgPath, + Fee: &f.Fee, + } + if err := fee.Validate(); err != nil { + return errors.Wrap(err, fmt.Sprintf("fee #%d is invalid", i)) + } + if _, err := bucket.Create(db, &fee); err != nil { + return errors.Wrap(err, fmt.Sprintf("cannot store #%d fee", i)) + } + } + return nil +} diff --git a/x/msgfee/model.go b/x/msgfee/model.go new file mode 100644 index 00000000..78d73e1f --- /dev/null +++ b/x/msgfee/model.go @@ -0,0 +1,68 @@ +package msgfee + +import ( + "github.com/iov-one/weave" + "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/errors" + "github.com/iov-one/weave/orm" +) + +var _ orm.CloneableData = (*MsgFee)(nil) + +func (mf *MsgFee) Validate() error { + panic("todo") +} + +func (mf *MsgFee) Copy() orm.CloneableData { + return &MsgFee{ + MsgPath: mf.MsgPath, + Fee: mf.Fee.Clone(), + } +} + +type MsgFeeBucket struct { + orm.Bucket +} + +// NewMsgFeeBucket returns a bucket for keeping track of fees for eeach message +// type. Message fees are indexed by the corresponding message path. +func NewMsgFeeBucket() *MsgFeeBucket { + b := orm.NewBucket("msgfee", orm.NewSimpleObj(nil, &MsgFee{})) + return &MsgFeeBucket{ + Bucket: b, + } +} + +// Create adds given message fee instance to the store. +func (b *MsgFeeBucket) Create(db weave.KVStore, mf *MsgFee) (orm.Object, error) { + key := []byte(mf.MsgPath) + obj := orm.NewSimpleObj(key, mf) + return obj, b.Bucket.Save(db, obj) +} + +// Save persists the state of a given revenue entity. +func (b *MsgFeeBucket) Save(db weave.KVStore, obj orm.Object) error { + if _, ok := obj.Value().(*MsgFee); !ok { + return errors.ErrInvalidModel.Newf("invalid type: %T", obj.Value()) + } + return b.Bucket.Save(db, obj) +} + +// Fee returns the fee value for a given message path. It returns an empty fee +// and no error if the message fee is not declared. +func (b *MsgFeeBucket) MessageFee(db weave.KVStore, msgPath string) (*coin.Coin, error) { + obj, err := b.Get(db, []byte(msgPath)) + switch { + case err == nil: + // All good. + case obj == nil || obj.Value() == nil: + return nil, nil + default: + return nil, errors.Wrap(err, "cannot get fee definition") + } + mf, ok := obj.Value().(*MsgFee) + if !ok { + return nil, errors.ErrInvalidModel.Newf("invalid type: %T", obj.Value()) + } + return mf.Fee, nil +} From ac5d8a402699ab8c365f9ecdd8f48bed78102abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 11:09:45 +0100 Subject: [PATCH 02/10] update coin package - new function `NewCoinp` to create and return a pointer to a coin - `Coin.Add` method allows to add zero value coin without a ticker - update coin tests --- coin/coin.go | 23 +++++++++++-- coin/coin_test.go | 87 +++++++++++++++++++++++++++++------------------ 2 files changed, 73 insertions(+), 37 deletions(-) diff --git a/coin/coin.go b/coin/coin.go index 79eba2dd..a6e473ba 100644 --- a/coin/coin.go +++ b/coin/coin.go @@ -26,9 +26,7 @@ const ( ) // NewCoin creates a new coin object -func NewCoin(whole int64, fractional int64, - ticker string) Coin { - +func NewCoin(whole int64, fractional int64, ticker string) Coin { return Coin{ Whole: whole, Fractional: fractional, @@ -36,6 +34,12 @@ func NewCoin(whole int64, fractional int64, } } +// NewCoinp returns a pointer to a new coin. +func NewCoinp(whole, fractional int64, ticker string) *Coin { + c := NewCoin(whole, fractional, ticker) + return &c +} + // ID returns a coin ticker name. func (c Coin) ID() string { return c.Ticker @@ -128,10 +132,20 @@ func mul64(a, b int64) (int64, error) { // currencies, or if the combination would cause // an overflow func (c Coin) Add(o Coin) (Coin, error) { + // If any of the coins represents no value and does not have a ticker + // set then it has no influence on the addition result. + if c.Ticker == "" && c.IsZero() { + return o, nil + } + if o.Ticker == "" && o.IsZero() { + return c, nil + } + if !c.SameType(o) { err := ErrInvalidCurrency.Newf("adding %s to %s", c.Ticker, o.Ticker) return Coin{}, err } + c.Whole += o.Whole c.Fractional += o.Fractional return c.normalize() @@ -225,6 +239,9 @@ func (c Coin) SameType(o Coin) bool { // Clone provides an independent copy of a coin pointer func (c *Coin) Clone() *Coin { + if c == nil { + return nil + } return &Coin{ Ticker: c.Ticker, Whole: c.Whole, diff --git a/coin/coin_test.go b/coin/coin_test.go index 61382059..d51245d2 100644 --- a/coin/coin_test.go +++ b/coin/coin_test.go @@ -155,44 +155,63 @@ func TestValidCoin(t *testing.T) { func TestAddCoin(t *testing.T) { base := NewCoin(17, 2345566, "DEF") - cases := []struct { - a, b Coin - res Coin - bad bool + cases := map[string]struct { + a, b Coin + wantRes Coin + wantErr error }{ - // plus and minus equals 0 - {base, base.Negative(), NewCoin(0, 0, "DEF"), false}, - // wrong types - { - NewCoin(1, 2, "FOO"), - NewCoin(2, 3, "BAR"), - Coin{}, - true, - }, - // normal math - { - NewCoin(7, 5000, "ABC"), - NewCoin(-4, -12000, "ABC"), - NewCoin(2, 999993000, "ABC"), - false, - }, - // overflow - { - NewCoin(500500500123456, 0, "SEE"), - NewCoin(500500500123456, 0, "SEE"), - Coin{}, - true, + "plus and minus equals 0": { + a: base, + b: base.Negative(), + wantRes: NewCoin(0, 0, "DEF"), + }, + "wrong types": { + a: NewCoin(1, 2, "FOO"), + b: NewCoin(2, 3, "BAR"), + wantRes: Coin{}, + wantErr: ErrInvalidCurrency, + }, + "normal math": { + a: NewCoin(7, 5000, "ABC"), + b: NewCoin(-4, -12000, "ABC"), + wantRes: NewCoin(2, 999993000, "ABC"), + }, + "overflow": { + a: NewCoin(500500500123456, 0, "SEE"), + b: NewCoin(500500500123456, 0, "SEE"), + wantRes: NewCoin(0, 0, ""), + wantErr: ErrInvalidCoin, + }, + "adding to zero coin": { + a: NewCoin(0, 0, ""), + b: NewCoin(1, 0, "DOGE"), + wantRes: NewCoin(1, 0, "DOGE"), + }, + "adding a zero coin": { + a: NewCoin(1, 0, "DOGE"), + b: NewCoin(0, 0, ""), + wantRes: NewCoin(1, 0, "DOGE"), + }, + "adding a non zero coin without a ticker": { + a: NewCoin(1, 0, "DOGE"), + b: NewCoin(1, 0, ""), + wantErr: ErrInvalidCurrency, + }, + "adding to non zero coin without a ticker": { + a: NewCoin(1, 0, ""), + b: NewCoin(1, 0, "DOGE"), + wantErr: ErrInvalidCurrency, }, } - for idx, tc := range cases { - t.Run(fmt.Sprintf("case-%d", idx), func(t *testing.T) { - c, err := tc.a.Add(tc.b) - if tc.bad { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tc.res, c) + for testName, tc := range cases { + t.Run(testName, func(t *testing.T) { + res, err := tc.a.Add(tc.b) + if !errors.Is(tc.wantErr, err) { + t.Fatalf("got error: %v", err) + } + if tc.wantErr == nil { + assert.Equal(t, tc.wantRes, res) } }) } From c74db3db43b0d28c5870c461cebee0674b7df310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 11:11:58 +0100 Subject: [PATCH 03/10] improve msgfee pakage Finish the implementation and add tests. resolve #232 --- x/msgfee/decorator.go | 52 ++++++++++++++++++------ x/msgfee/decorator_test.go | 70 ++++++++++++++++++++++++++++++++ x/msgfee/doc.go | 15 +++++++ x/msgfee/init_test.go | 54 +++++++++++++++++++++++++ x/msgfee/model.go | 21 ++++++---- x/msgfee/model_test.go | 83 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 276 insertions(+), 19 deletions(-) create mode 100644 x/msgfee/decorator_test.go create mode 100644 x/msgfee/init_test.go create mode 100644 x/msgfee/model_test.go diff --git a/x/msgfee/decorator.go b/x/msgfee/decorator.go index 891231b1..e70be20e 100644 --- a/x/msgfee/decorator.go +++ b/x/msgfee/decorator.go @@ -6,6 +6,12 @@ import ( "github.com/iov-one/weave/errors" ) +// FeeDecorator implements a decorator that for each processed transaction +// attach an additional fee to the result. Each fee is declarated per +// transaction type. If fee is not set (zero value) then this decorator does +// not increase the required fee value. +// Additional fee is attached to only those transaction results that represent +// a success. type FeeDecorator struct { bucket *MsgFeeBucket } @@ -21,30 +27,52 @@ func NewFeeDecorator() *FeeDecorator { } func (d *FeeDecorator) Check(ctx weave.Context, store weave.KVStore, tx weave.Tx, next weave.Checker) (weave.CheckResult, error) { - return next.Check(ctx, store, tx) -} - -func (d *FeeDecorator) Deliver(ctx weave.Context, store weave.KVStore, tx weave.Tx, next weave.Deliverer) (weave.DeliverResult, error) { - res, err := next.Deliver(ctx, store, tx) + res, err := next.Check(ctx, store, tx) if err != nil { return res, err } - msg, err := tx.GetMsg() - if err != nil { - return res, errors.Wrap(err, "cannot get message") + switch fee, err := txFee(d.bucket, store, tx); { + case err != nil: + return res, err + case !coin.IsEmpty(fee): + total, err := res.RequiredFee.Add(*fee) + if err != nil { + return res, errors.Wrap(err, "cannot apply message type fee") + } + res.RequiredFee = total } - fee, err := d.bucket.MessageFee(store, msg.Path()) + return res, nil +} + +func (d *FeeDecorator) Deliver(ctx weave.Context, store weave.KVStore, tx weave.Tx, next weave.Deliverer) (weave.DeliverResult, error) { + res, err := next.Deliver(ctx, store, tx) if err != nil { - return res, errors.Wrap(err, "cannot get fee") + return res, err } - if !coin.IsEmpty(fee) { + switch fee, err := txFee(d.bucket, store, tx); { + case err != nil: + return res, err + case !coin.IsEmpty(fee): total, err := res.RequiredFee.Add(*fee) if err != nil { - return res, errors.Wrap(err, "cannot apply message type fee") + return res, errors.Wrap(err, "cannot apply message type fee to %v") } res.RequiredFee = total } return res, nil } + +// txFee returns the fee value for a given transaction as configured in the store. +func txFee(bucket *MsgFeeBucket, store weave.KVStore, tx weave.Tx) (*coin.Coin, error) { + msg, err := tx.GetMsg() + if err != nil { + return nil, errors.Wrap(err, "cannot get message") + } + fee, err := bucket.MessageFee(store, msg.Path()) + if err != nil { + return nil, errors.Wrap(err, "cannot get fee") + } + return fee, nil +} diff --git a/x/msgfee/decorator_test.go b/x/msgfee/decorator_test.go new file mode 100644 index 00000000..7ccb9afa --- /dev/null +++ b/x/msgfee/decorator_test.go @@ -0,0 +1,70 @@ +package msgfee + +import ( + "testing" + + "github.com/iov-one/weave" + "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/store" + "github.com/iov-one/weave/x" +) + +func TestFeeDecorator(t *testing.T) { + handler := helpers.CountingHandler() + decorator := NewFeeDecorator() + bucket := NewMsgFeeBucket() + db := store.MemStore() + + _, err := bucket.Create(db, &MsgFee{ + MsgPath: "foo/bar", + Fee: coin.NewCoinp(0, 1234, "DOGE"), + }) + if err != nil { + t.Fatalf("cannot create a transaction fee: %s", err) + } + + tx := &txMock{ + msg: &msgMock{path: "foo/bar"}, + } + if res, err := decorator.Check(nil, db, tx, handler); err != nil { + t.Fatalf("check failed: %s", err) + } else if !res.RequiredFee.Equals(coin.NewCoin(0, 1234, "DOGE")) { + t.Fatalf("unexpected check fee: %v", res.RequiredFee) + } + + if c := handler.GetCount(); c != 1 { + t.Fatalf("want count=1, got %d", c) + } + + if res, err := decorator.Deliver(nil, db, tx, handler); err != nil { + t.Fatalf("check failed: %s", err) + } else if !res.RequiredFee.Equals(coin.NewCoin(0, 1234, "DOGE")) { + t.Fatalf("unexpected deliver fee: %v", res.RequiredFee) + } + + if c := handler.GetCount(); c != 2 { + t.Fatalf("want count=2, got %d", c) + } +} + +var helpers x.TestHelpers + +type txMock struct { + weave.Tx + + msg weave.Msg + err error +} + +func (tx *txMock) GetMsg() (weave.Msg, error) { + return tx.msg, tx.err +} + +type msgMock struct { + weave.Msg + path string +} + +func (m *msgMock) Path() string { + return m.path +} diff --git a/x/msgfee/doc.go b/x/msgfee/doc.go index 6d65c0d6..68724a47 100644 --- a/x/msgfee/doc.go +++ b/x/msgfee/doc.go @@ -1 +1,16 @@ +/* + +Package msgfee allows to define and charge an additional fee per transaction +type. + +With this extension it is possible to declare a fee for each transaction +message type. For each message path a coin value can be declared. After +successful processing of a transaction the result required fee value is +increased by the declared coin value. + +This extension does not know of supported (installed) message paths and +therefore cannot validate for their existence. Make sure that when registering +a new message fee the path is set correctly. + +*/ package msgfee diff --git a/x/msgfee/init_test.go b/x/msgfee/init_test.go new file mode 100644 index 00000000..5c64d7ff --- /dev/null +++ b/x/msgfee/init_test.go @@ -0,0 +1,54 @@ +package msgfee + +import ( + "encoding/json" + "testing" + + "github.com/iov-one/weave" + "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/store" +) + +func TestGenesis(t *testing.T) { + const genesis = ` +{ + "msgfee": [ + { + "msg_path": "foo/bar", + "fee": {"whole": 1, "fractional": 2, "ticker": "DOGE"} + }, + { + "msg_path": "a/b", + "fee": {"whole": 2, "fractional": 0, "ticker": "ETH"} + } + ] +} + ` + var opts weave.Options + if err := json.Unmarshal([]byte(genesis), &opts); err != nil { + t.Fatalf("cannot unmarshal genesis: %s", err) + } + + db := store.MemStore() + var ini Initializer + if err := ini.FromGenesis(opts, db); err != nil { + t.Fatalf("cannot load genesis: %s", err) + } + + bucket := NewMsgFeeBucket() + fee, err := bucket.MessageFee(db, "foo/bar") + if err != nil { + t.Fatalf("cannot fetch fee: %s", err) + } + if !fee.Equals(coin.NewCoin(1, 2, "DOGE")) { + t.Fatalf("got an unexpected fee value: %s", fee) + } + + fee, err = bucket.MessageFee(db, "a/b") + if err != nil { + t.Fatalf("cannot fetch fee: %s", err) + } + if !fee.Equals(coin.NewCoin(2, 0, "ETH")) { + t.Fatalf("got an unexpected fee value: %s", fee) + } +} diff --git a/x/msgfee/model.go b/x/msgfee/model.go index 78d73e1f..c1cf30b8 100644 --- a/x/msgfee/model.go +++ b/x/msgfee/model.go @@ -10,7 +10,16 @@ import ( var _ orm.CloneableData = (*MsgFee)(nil) func (mf *MsgFee) Validate() error { - panic("todo") + if mf.MsgPath == "" { + return errors.Wrap(errors.ErrInvalidModel, "invalid message path") + } + if coin.IsEmpty(mf.Fee) { + return errors.Wrap(errors.ErrInvalidModel, "invalid fee") + } + if err := mf.Fee.Validate(); err != nil { + return errors.Wrap(err, "invalid fee") + } + return nil } func (mf *MsgFee) Copy() orm.CloneableData { @@ -52,14 +61,12 @@ func (b *MsgFeeBucket) Save(db weave.KVStore, obj orm.Object) error { // and no error if the message fee is not declared. func (b *MsgFeeBucket) MessageFee(db weave.KVStore, msgPath string) (*coin.Coin, error) { obj, err := b.Get(db, []byte(msgPath)) - switch { - case err == nil: - // All good. - case obj == nil || obj.Value() == nil: - return nil, nil - default: + if err != nil { return nil, errors.Wrap(err, "cannot get fee definition") } + if obj == nil || obj.Value() == nil { + return nil, nil + } mf, ok := obj.Value().(*MsgFee) if !ok { return nil, errors.ErrInvalidModel.Newf("invalid type: %T", obj.Value()) diff --git a/x/msgfee/model_test.go b/x/msgfee/model_test.go new file mode 100644 index 00000000..4b45162d --- /dev/null +++ b/x/msgfee/model_test.go @@ -0,0 +1,83 @@ +package msgfee + +import ( + "testing" + + "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/errors" + "github.com/iov-one/weave/store" +) + +func TestMsgFeeValidate(t *testing.T) { + cases := map[string]struct { + mf MsgFee + wantErr error + }{ + "all good": { + mf: MsgFee{ + MsgPath: "foo/bar", + Fee: coin.NewCoinp(1, 2, "DOGE"), + }, + wantErr: nil, + }, + "empty path": { + mf: MsgFee{ + MsgPath: "", + Fee: coin.NewCoinp(1, 2, "DOGE"), + }, + wantErr: errors.ErrInvalidModel, + }, + "zero value fee": { + mf: MsgFee{ + MsgPath: "foo/bar", + Fee: coin.NewCoinp(0, 0, "DOGE"), + }, + wantErr: errors.ErrInvalidModel, + }, + "nil fee": { + mf: MsgFee{ + MsgPath: "foo/bar", + Fee: nil, + }, + wantErr: errors.ErrInvalidModel, + }, + } + + for testName, tc := range cases { + t.Run(testName, func(t *testing.T) { + err := tc.mf.Validate() + if !errors.Is(tc.wantErr, err) { + t.Fatalf("got %v", err) + } + }) + } +} + +func TestBucketMessageFee(t *testing.T) { + b := NewMsgFeeBucket() + db := store.MemStore() + + _, err := b.Create(db, &MsgFee{ + MsgPath: "a/b", + Fee: coin.NewCoinp(1, 2, "DOGE"), + }) + if err != nil { + t.Fatalf("cannot create a fee: %s", err) + } + + fee, err := b.MessageFee(db, "a/b") + if err != nil { + t.Fatalf("got error: %v", err) + } + if !fee.Equals(coin.NewCoin(1, 2, "DOGE")) { + t.Fatalf("got an unexpected fee: %v", fee) + } + + nofee, err := b.MessageFee(db, "does-not/exist") + if err != nil { + t.Fatalf("got error: %v", err) + } + if nofee != nil { + t.Fatalf("want nil, got %v", nofee) + } +} From 215557c1e36ad79246a4a10a7500d19738963f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 14:03:25 +0100 Subject: [PATCH 04/10] Use coin.Coin as a non nullable attribute. --- x/msgfee/codec.pb.go | 46 +++++++++++++++++--------------------- x/msgfee/codec.proto | 2 +- x/msgfee/decorator_test.go | 2 +- x/msgfee/init.go | 2 +- x/msgfee/model.go | 6 ++--- x/msgfee/model_test.go | 14 ++++++------ 6 files changed, 33 insertions(+), 39 deletions(-) diff --git a/x/msgfee/codec.pb.go b/x/msgfee/codec.pb.go index 04b20f91..5ebf9986 100644 --- a/x/msgfee/codec.pb.go +++ b/x/msgfee/codec.pb.go @@ -34,8 +34,8 @@ const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package // MsgFee represents a fee for a single message that must be paid in order for // the message to be processed. type MsgFee struct { - MsgPath string `protobuf:"bytes,1,opt,name=msg_path,json=msgPath,proto3" json:"msg_path,omitempty"` - Fee *coin.Coin `protobuf:"bytes,2,opt,name=fee" json:"fee,omitempty"` + MsgPath string `protobuf:"bytes,1,opt,name=msg_path,json=msgPath,proto3" json:"msg_path,omitempty"` + Fee coin.Coin `protobuf:"bytes,2,opt,name=fee" json:"fee"` } func (m *MsgFee) Reset() { *m = MsgFee{} } @@ -50,11 +50,11 @@ func (m *MsgFee) GetMsgPath() string { return "" } -func (m *MsgFee) GetFee() *coin.Coin { +func (m *MsgFee) GetFee() coin.Coin { if m != nil { return m.Fee } - return nil + return coin.Coin{} } func init() { @@ -81,16 +81,14 @@ func (m *MsgFee) MarshalTo(dAtA []byte) (int, error) { i = encodeVarintCodec(dAtA, i, uint64(len(m.MsgPath))) i += copy(dAtA[i:], m.MsgPath) } - if m.Fee != nil { - dAtA[i] = 0x12 - i++ - i = encodeVarintCodec(dAtA, i, uint64(m.Fee.Size())) - n1, err := m.Fee.MarshalTo(dAtA[i:]) - if err != nil { - return 0, err - } - i += n1 + dAtA[i] = 0x12 + i++ + i = encodeVarintCodec(dAtA, i, uint64(m.Fee.Size())) + n1, err := m.Fee.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err } + i += n1 return i, nil } @@ -110,10 +108,8 @@ func (m *MsgFee) Size() (n int) { if l > 0 { n += 1 + l + sovCodec(uint64(l)) } - if m.Fee != nil { - l = m.Fee.Size() - n += 1 + l + sovCodec(uint64(l)) - } + l = m.Fee.Size() + n += 1 + l + sovCodec(uint64(l)) return n } @@ -214,9 +210,6 @@ func (m *MsgFee) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if m.Fee == nil { - m.Fee = &coin.Coin{} - } if err := m.Fee.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } @@ -350,17 +343,18 @@ var ( func init() { proto.RegisterFile("x/msgfee/codec.proto", fileDescriptorCodec) } var fileDescriptorCodec = []byte{ - // 189 bytes of a gzipped FileDescriptorProto + // 196 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xa9, 0xd0, 0xcf, 0x2d, 0x4e, 0x4f, 0x4b, 0x4d, 0xd5, 0x4f, 0xce, 0x4f, 0x49, 0x4d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x83, 0x88, 0x49, 0x69, 0xa6, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x67, 0xe6, 0x97, 0xe9, 0xe6, 0xe7, 0xa5, 0xea, 0x97, 0xa7, 0x26, 0x96, 0x81, 0x54, 0x67, 0xe6, 0x21, 0x6b, 0x91, 0xd2, 0x45, 0x52, 0x9a, 0x9e, 0x9f, 0x9e, 0xaf, 0x0f, 0x16, 0x4e, 0x2a, - 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x5c, 0xc9, 0x91, 0x8b, 0xcd, 0xb7, 0x38, 0xdd, + 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x5c, 0xc9, 0x9d, 0x8b, 0xcd, 0xb7, 0x38, 0xdd, 0x2d, 0x35, 0x55, 0x48, 0x92, 0x8b, 0x23, 0xb7, 0x38, 0x3d, 0xbe, 0x20, 0xb1, 0x24, 0x43, 0x82, - 0x51, 0x81, 0x51, 0x83, 0x33, 0x88, 0x3d, 0xb7, 0x38, 0x3d, 0x20, 0xb1, 0x24, 0x43, 0x48, 0x86, + 0x51, 0x81, 0x51, 0x83, 0x33, 0x88, 0x3d, 0xb7, 0x38, 0x3d, 0x20, 0xb1, 0x24, 0x43, 0x48, 0x89, 0x8b, 0x39, 0x2d, 0x35, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0xdb, 0x88, 0x4b, 0x0f, 0x64, 0xa7, - 0x9e, 0x73, 0x7e, 0x66, 0x5e, 0x10, 0x48, 0xd8, 0x49, 0xe0, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, - 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0x21, 0x89, 0x0d, 0x6c, 0xb6, 0x31, - 0x20, 0x00, 0x00, 0xff, 0xff, 0x91, 0x10, 0x19, 0xc3, 0xd5, 0x00, 0x00, 0x00, + 0x9e, 0x73, 0x7e, 0x66, 0x9e, 0x13, 0xcb, 0x89, 0x7b, 0xf2, 0x0c, 0x41, 0x20, 0x49, 0x27, 0x81, + 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, + 0x86, 0x24, 0x36, 0xb0, 0x0d, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xde, 0xd4, 0xfb, 0xb8, + 0xdb, 0x00, 0x00, 0x00, } diff --git a/x/msgfee/codec.proto b/x/msgfee/codec.proto index f0893c59..5fb9ac5a 100644 --- a/x/msgfee/codec.proto +++ b/x/msgfee/codec.proto @@ -9,5 +9,5 @@ import "github.com/gogo/protobuf/gogoproto/gogo.proto"; // the message to be processed. message MsgFee { string msg_path = 1; - coin.Coin fee = 2; + coin.Coin fee = 2 [(gogoproto.nullable) = false]; } diff --git a/x/msgfee/decorator_test.go b/x/msgfee/decorator_test.go index 7ccb9afa..1d22988c 100644 --- a/x/msgfee/decorator_test.go +++ b/x/msgfee/decorator_test.go @@ -17,7 +17,7 @@ func TestFeeDecorator(t *testing.T) { _, err := bucket.Create(db, &MsgFee{ MsgPath: "foo/bar", - Fee: coin.NewCoinp(0, 1234, "DOGE"), + Fee: coin.NewCoin(0, 1234, "DOGE"), }) if err != nil { t.Fatalf("cannot create a transaction fee: %s", err) diff --git a/x/msgfee/init.go b/x/msgfee/init.go index 13a8fed2..6f016840 100644 --- a/x/msgfee/init.go +++ b/x/msgfee/init.go @@ -30,7 +30,7 @@ func (*Initializer) FromGenesis(opts weave.Options, db weave.KVStore) error { for i, f := range fees { fee := MsgFee{ MsgPath: f.MsgPath, - Fee: &f.Fee, + Fee: f.Fee, } if err := fee.Validate(); err != nil { return errors.Wrap(err, fmt.Sprintf("fee #%d is invalid", i)) diff --git a/x/msgfee/model.go b/x/msgfee/model.go index c1cf30b8..55861ece 100644 --- a/x/msgfee/model.go +++ b/x/msgfee/model.go @@ -13,7 +13,7 @@ func (mf *MsgFee) Validate() error { if mf.MsgPath == "" { return errors.Wrap(errors.ErrInvalidModel, "invalid message path") } - if coin.IsEmpty(mf.Fee) { + if mf.Fee.IsZero() { return errors.Wrap(errors.ErrInvalidModel, "invalid fee") } if err := mf.Fee.Validate(); err != nil { @@ -25,7 +25,7 @@ func (mf *MsgFee) Validate() error { func (mf *MsgFee) Copy() orm.CloneableData { return &MsgFee{ MsgPath: mf.MsgPath, - Fee: mf.Fee.Clone(), + Fee: *mf.Fee.Clone(), } } @@ -71,5 +71,5 @@ func (b *MsgFeeBucket) MessageFee(db weave.KVStore, msgPath string) (*coin.Coin, if !ok { return nil, errors.ErrInvalidModel.Newf("invalid type: %T", obj.Value()) } - return mf.Fee, nil + return &mf.Fee, nil } diff --git a/x/msgfee/model_test.go b/x/msgfee/model_test.go index 4b45162d..642fdffe 100644 --- a/x/msgfee/model_test.go +++ b/x/msgfee/model_test.go @@ -16,28 +16,28 @@ func TestMsgFeeValidate(t *testing.T) { "all good": { mf: MsgFee{ MsgPath: "foo/bar", - Fee: coin.NewCoinp(1, 2, "DOGE"), + Fee: coin.NewCoin(1, 2, "DOGE"), }, wantErr: nil, }, "empty path": { mf: MsgFee{ MsgPath: "", - Fee: coin.NewCoinp(1, 2, "DOGE"), + Fee: coin.NewCoin(1, 2, "DOGE"), }, wantErr: errors.ErrInvalidModel, }, - "zero value fee": { + "zero value fee with a ticker": { mf: MsgFee{ MsgPath: "foo/bar", - Fee: coin.NewCoinp(0, 0, "DOGE"), + Fee: coin.NewCoin(0, 0, "DOGE"), }, wantErr: errors.ErrInvalidModel, }, - "nil fee": { + "zero value fee with no ticker": { mf: MsgFee{ MsgPath: "foo/bar", - Fee: nil, + Fee: coin.Coin{}, }, wantErr: errors.ErrInvalidModel, }, @@ -59,7 +59,7 @@ func TestBucketMessageFee(t *testing.T) { _, err := b.Create(db, &MsgFee{ MsgPath: "a/b", - Fee: coin.NewCoinp(1, 2, "DOGE"), + Fee: coin.NewCoin(1, 2, "DOGE"), }) if err != nil { t.Fatalf("cannot create a fee: %s", err) From ead18fc79123fe870bfb33db233509d6889ebaed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 15:18:17 +0100 Subject: [PATCH 05/10] pull request review changes --- x/msgfee/decorator.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/x/msgfee/decorator.go b/x/msgfee/decorator.go index e70be20e..943eccd5 100644 --- a/x/msgfee/decorator.go +++ b/x/msgfee/decorator.go @@ -32,10 +32,11 @@ func (d *FeeDecorator) Check(ctx weave.Context, store weave.KVStore, tx weave.Tx return res, err } - switch fee, err := txFee(d.bucket, store, tx); { - case err != nil: + fee, err := txFee(d.bucket, store, tx) + if err != nil { return res, err - case !coin.IsEmpty(fee): + } + if !coin.IsEmpty(fee) { total, err := res.RequiredFee.Add(*fee) if err != nil { return res, errors.Wrap(err, "cannot apply message type fee") @@ -51,10 +52,11 @@ func (d *FeeDecorator) Deliver(ctx weave.Context, store weave.KVStore, tx weave. return res, err } - switch fee, err := txFee(d.bucket, store, tx); { - case err != nil: + fee, err := txFee(d.bucket, store, tx) + if err != nil { return res, err - case !coin.IsEmpty(fee): + } + if !coin.IsEmpty(fee) { total, err := res.RequiredFee.Add(*fee) if err != nil { return res, errors.Wrap(err, "cannot apply message type fee to %v") From 28b3caba166a78df4a3826a3a4030007db190b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 15:25:19 +0100 Subject: [PATCH 06/10] add errors.Wrapf function --- errors/errors.go | 9 +++++++++ x/msgfee/decorator.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/errors/errors.go b/errors/errors.go index bf4b3b22..968e7663 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -185,6 +185,15 @@ func Wrap(err error, description string) TMError { } } +// Wrapf extends given error with an additional information. +// +// This function works like Wrap function with additional funtionality of +// formatting the input as specified. +func Wrapf(err error, format string, args ...interface{}) TMError { + desc := fmt.Sprintf(format, args...) + return Wrap(err, desc) +} + type wrappedError struct { // This error layer description. msg string diff --git a/x/msgfee/decorator.go b/x/msgfee/decorator.go index 943eccd5..fef5dd02 100644 --- a/x/msgfee/decorator.go +++ b/x/msgfee/decorator.go @@ -59,7 +59,7 @@ func (d *FeeDecorator) Deliver(ctx weave.Context, store weave.KVStore, tx weave. if !coin.IsEmpty(fee) { total, err := res.RequiredFee.Add(*fee) if err != nil { - return res, errors.Wrap(err, "cannot apply message type fee to %v") + return res, errors.Wrapf(err, "cannot apply message type fee to %v", res.RequiredFee) } res.RequiredFee = total } From c4ee0bfdf12a58f20ab18169820e0ca7516ea1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 15:45:43 +0100 Subject: [PATCH 07/10] extend genesis tests --- x/msgfee/init_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/x/msgfee/init_test.go b/x/msgfee/init_test.go index 5c64d7ff..defce0b6 100644 --- a/x/msgfee/init_test.go +++ b/x/msgfee/init_test.go @@ -52,3 +52,28 @@ func TestGenesis(t *testing.T) { t.Fatalf("got an unexpected fee value: %s", fee) } } + +func TestGenesisWithInvalidFee(t *testing.T) { + cases := map[string]string{ + "zero fee": `[{"msg_path": "foo/bar", "fee": {"whole": 0, "fractional": 0, "ticker": "DOGE"}}]`, + "no ticker": `[{"msg_path": "foo/bar", "fee": {"whole": 1, "fractional": 0, "ticker": ""}}]`, + "no path": `[{"fee": {"whole": 1, "fractional": 1, "ticker": "DOGE"}}]`, + "no fee": `[{"msg_path": "foo/bar"}]`, + } + for testName, content := range cases { + t.Run(testName, func(t *testing.T) { + genesis := `{"msgfee": ` + content + `}` + var opts weave.Options + if err := json.Unmarshal([]byte(genesis), &opts); err != nil { + t.Fatalf("cannot unmarshal genesis: %s", err) + } + + db := store.MemStore() + var ini Initializer + if err := ini.FromGenesis(opts, db); err == nil { + t.Fatal("no error") + } + }) + } + +} From a274bf5ce69e3705aaa72a3d3c5423f1a701d84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 16:27:33 +0100 Subject: [PATCH 08/10] extend decorator tests --- x/msgfee/decorator_test.go | 135 +++++++++++++++++++++++++++++-------- 1 file changed, 106 insertions(+), 29 deletions(-) diff --git a/x/msgfee/decorator_test.go b/x/msgfee/decorator_test.go index 1d22988c..f5651c72 100644 --- a/x/msgfee/decorator_test.go +++ b/x/msgfee/decorator_test.go @@ -5,45 +5,104 @@ import ( "github.com/iov-one/weave" "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/errors" "github.com/iov-one/weave/store" "github.com/iov-one/weave/x" ) func TestFeeDecorator(t *testing.T) { - handler := helpers.CountingHandler() - decorator := NewFeeDecorator() - bucket := NewMsgFeeBucket() - db := store.MemStore() - - _, err := bucket.Create(db, &MsgFee{ - MsgPath: "foo/bar", - Fee: coin.NewCoin(0, 1234, "DOGE"), - }) - if err != nil { - t.Fatalf("cannot create a transaction fee: %s", err) + cases := map[string]struct { + InitFees []MsgFee + Tx weave.Tx + Handler weave.Handler + WantCheckErr error + WantCheckFee coin.Coin + WantDeliverErr error + WantDeliverFee coin.Coin + }{ + "all good": { + InitFees: []MsgFee{ + {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 1234, "DOGE")}, + }, + Handler: &handlerMock{}, + Tx: &txMock{msg: &msgMock{path: "foo/bar"}}, + WantCheckFee: coin.NewCoin(0, 1234, "DOGE"), + WantDeliverFee: coin.NewCoin(0, 1234, "DOGE"), + }, + "delivery failure": { + InitFees: []MsgFee{ + {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 1234, "DOGE")}, + }, + Handler: &handlerMock{ + deliverErr: errors.ErrUnauthorized, + }, + Tx: &txMock{msg: &msgMock{path: "foo/bar"}}, + WantCheckFee: coin.NewCoin(0, 1234, "DOGE"), + WantDeliverErr: errors.ErrUnauthorized, + WantDeliverFee: coin.Coin{}, + }, + "check failure": { + InitFees: []MsgFee{ + {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 1234, "DOGE")}, + }, + Handler: &handlerMock{ + checkErr: errors.ErrUnauthorized, + }, + Tx: &txMock{msg: &msgMock{path: "foo/bar"}}, + WantCheckErr: errors.ErrUnauthorized, + WantDeliverFee: coin.NewCoin(0, 1234, "DOGE"), + }, + "no fee for the transaction message": { + InitFees: []MsgFee{}, + Handler: &handlerMock{}, + Tx: &txMock{msg: &msgMock{path: "foo/bar"}}, + WantCheckFee: coin.Coin{}, + WantDeliverFee: coin.Coin{}, + }, + "message fee with a different ticker than the existing fee": { + InitFees: []MsgFee{ + {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 1234, "DOGE")}, + }, + Handler: &handlerMock{ + checkRes: weave.CheckResult{RequiredFee: coin.NewCoin(1, 0, "BTC")}, + deliverRes: weave.DeliverResult{RequiredFee: coin.NewCoin(1, 0, "BTC")}, + }, + Tx: &txMock{msg: &msgMock{path: "foo/bar"}}, + WantCheckErr: coin.ErrInvalidCurrency, + WantCheckFee: coin.NewCoin(1, 0, "BTC"), + WantDeliverErr: coin.ErrInvalidCurrency, + WantDeliverFee: coin.NewCoin(1, 0, "BTC"), + }, } - tx := &txMock{ - msg: &msgMock{path: "foo/bar"}, - } - if res, err := decorator.Check(nil, db, tx, handler); err != nil { - t.Fatalf("check failed: %s", err) - } else if !res.RequiredFee.Equals(coin.NewCoin(0, 1234, "DOGE")) { - t.Fatalf("unexpected check fee: %v", res.RequiredFee) - } + for testName, tc := range cases { + t.Run(testName, func(t *testing.T) { + decorator := NewFeeDecorator() + bucket := NewMsgFeeBucket() + db := store.MemStore() - if c := handler.GetCount(); c != 1 { - t.Fatalf("want count=1, got %d", c) - } + for _, f := range tc.InitFees { + if i, err := bucket.Create(db, &f); err != nil { + t.Fatalf("cannot create #%d transaction fee: %s", i, err) + } + } - if res, err := decorator.Deliver(nil, db, tx, handler); err != nil { - t.Fatalf("check failed: %s", err) - } else if !res.RequiredFee.Equals(coin.NewCoin(0, 1234, "DOGE")) { - t.Fatalf("unexpected deliver fee: %v", res.RequiredFee) - } + cres, err := decorator.Check(nil, db, tc.Tx, tc.Handler) + if !errors.Is(tc.WantCheckErr, err) { + t.Fatalf("check returned an unexpected error: %v", err) + } + if !tc.WantCheckFee.Equals(cres.RequiredFee) { + t.Fatalf("unexpected check fee: %v", cres.RequiredFee) + } - if c := handler.GetCount(); c != 2 { - t.Fatalf("want count=2, got %d", c) + dres, err := decorator.Deliver(nil, db, tc.Tx, tc.Handler) + if !errors.Is(tc.WantDeliverErr, err) { + t.Fatalf("deliver returned an unexpected error: %v", err) + } + if !tc.WantDeliverFee.Equals(dres.RequiredFee) { + t.Fatalf("unexpected deliver fee: %v", dres.RequiredFee) + } + }) } } @@ -68,3 +127,21 @@ type msgMock struct { func (m *msgMock) Path() string { return m.path } + +type handlerMock struct { + checkRes weave.CheckResult + checkErr error + + deliverRes weave.DeliverResult + deliverErr error +} + +var _ weave.Handler = (*handlerMock)(nil) + +func (m *handlerMock) Check(weave.Context, weave.KVStore, weave.Tx) (weave.CheckResult, error) { + return m.checkRes, m.checkErr +} + +func (m *handlerMock) Deliver(weave.Context, weave.KVStore, weave.Tx) (weave.DeliverResult, error) { + return m.deliverRes, m.deliverErr +} From 49c792ea0ba3e90e37453d0df167176596ef8cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 16:35:56 +0100 Subject: [PATCH 09/10] one moar test --- x/msgfee/decorator_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/x/msgfee/decorator_test.go b/x/msgfee/decorator_test.go index f5651c72..a9b3948d 100644 --- a/x/msgfee/decorator_test.go +++ b/x/msgfee/decorator_test.go @@ -20,7 +20,7 @@ func TestFeeDecorator(t *testing.T) { WantDeliverErr error WantDeliverFee coin.Coin }{ - "all good": { + "message fee with no previous fee": { InitFees: []MsgFee{ {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 1234, "DOGE")}, }, @@ -29,6 +29,18 @@ func TestFeeDecorator(t *testing.T) { WantCheckFee: coin.NewCoin(0, 1234, "DOGE"), WantDeliverFee: coin.NewCoin(0, 1234, "DOGE"), }, + "message fee with addded to existing value with the same currency": { + InitFees: []MsgFee{ + {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 22, "BTC")}, + }, + Handler: &handlerMock{ + checkRes: weave.CheckResult{RequiredFee: coin.NewCoin(1, 0, "BTC")}, + deliverRes: weave.DeliverResult{RequiredFee: coin.NewCoin(1, 0, "BTC")}, + }, + Tx: &txMock{msg: &msgMock{path: "foo/bar"}}, + WantCheckFee: coin.NewCoin(1, 22, "BTC"), + WantDeliverFee: coin.NewCoin(1, 22, "BTC"), + }, "delivery failure": { InitFees: []MsgFee{ {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 1234, "DOGE")}, From 36f162fdaa6bc9b4a60d4b9f7b816a837d62e08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Wed, 6 Mar 2019 16:40:21 +0100 Subject: [PATCH 10/10] typo --- x/msgfee/decorator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/msgfee/decorator_test.go b/x/msgfee/decorator_test.go index a9b3948d..1972edd1 100644 --- a/x/msgfee/decorator_test.go +++ b/x/msgfee/decorator_test.go @@ -29,7 +29,7 @@ func TestFeeDecorator(t *testing.T) { WantCheckFee: coin.NewCoin(0, 1234, "DOGE"), WantDeliverFee: coin.NewCoin(0, 1234, "DOGE"), }, - "message fee with addded to existing value with the same currency": { + "message fee added to an existing value with the same currency": { InitFees: []MsgFee{ {MsgPath: "foo/bar", Fee: coin.NewCoin(0, 22, "BTC")}, },