From 52cf3282962576869525069a953ae2f902308af2 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 18 Nov 2020 14:26:13 +0300 Subject: [PATCH] core: add NotaryAssisted transaction attribute --- pkg/core/blockchain.go | 11 ++++++ pkg/core/blockchain_test.go | 45 +++++++++++++++++++++++-- pkg/core/transaction/attribute.go | 7 +++- pkg/core/transaction/attribute_test.go | 32 +++++++++++++++++- pkg/core/transaction/attrtype.go | 1 + pkg/core/transaction/attrtype_string.go | 7 ++-- pkg/core/transaction/notary_assisted.go | 37 ++++++++++++++++++++ 7 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 pkg/core/transaction/notary_assisted.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 2c58a3a3a..bfe44143b 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1280,6 +1280,13 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. return fmt.Errorf("%w: (%d > MaxTransactionSize %d)", ErrTxTooBig, size, transaction.MaxTransactionSize) } needNetworkFee := int64(size) * bc.FeePerByte() + if bc.P2PSigExtensionsEnabled() { + attrs := t.GetAttributes(transaction.NotaryAssistedT) + if len(attrs) != 0 { + na := attrs[0].Value.(*transaction.NotaryAssisted) + needNetworkFee += (int64(na.NKeys) + 1) * transaction.NotaryServiceFeePerKey + } + } netFee := t.NetworkFee - needNetworkFee if netFee < 0 { return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee) @@ -1375,6 +1382,10 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { if err := bc.dao.HasTransaction(conflicts.Hash); errors.Is(err, dao.ErrAlreadyExists) { return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE()) } + case transaction.NotaryAssistedT: + if !bc.config.P2PSigExtensions { + return fmt.Errorf("%w: NotaryAssisted attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) + } default: if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound { return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 1a0b208a5..e771b0135 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -652,12 +652,12 @@ func TestVerifyTx(t *testing.T) { return tx } t.Run("Disabled", func(t *testing.T) { - tx := getReservedTx(transaction.ReservedLowerBound + 2) + tx := getReservedTx(transaction.ReservedLowerBound + 3) require.Error(t, bc.VerifyTx(tx)) }) t.Run("Enabled", func(t *testing.T) { bc.config.ReservedAttributes = true - tx := getReservedTx(transaction.ReservedLowerBound + 2) + tx := getReservedTx(transaction.ReservedLowerBound + 3) require.NoError(t, bc.VerifyTx(tx)) }) }) @@ -719,6 +719,47 @@ func TestVerifyTx(t *testing.T) { }) }) }) + t.Run("NotaryAssisted", func(t *testing.T) { + getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction { + tx := bc.newTestTx(h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ + NKeys: signaturesCount, + }}) + tx.NetworkFee += serviceFee // additional fee for NotaryAssisted attribute + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }} + rawScript := testchain.CommitteeVerificationScript() + require.NoError(t, err) + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{{ + InvocationScript: testchain.SignCommittee(data), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + bc.config.P2PSigExtensions = false + tx := getNotaryAssistedTx(0, 0) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Enabled, insufficient network fee", func(t *testing.T) { + bc.config.P2PSigExtensions = true + tx := getNotaryAssistedTx(1, 0) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Enabled, positive", func(t *testing.T) { + bc.config.P2PSigExtensions = true + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) }) } diff --git a/pkg/core/transaction/attribute.go b/pkg/core/transaction/attribute.go index 5ef14dd17..c81dbb1b5 100644 --- a/pkg/core/transaction/attribute.go +++ b/pkg/core/transaction/attribute.go @@ -39,6 +39,8 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) { attr.Value = new(NotValidBefore) case ConflictsT: attr.Value = new(Conflicts) + case NotaryAssistedT: + attr.Value = new(NotaryAssisted) default: if t >= ReservedLowerBound && t <= ReservedUpperBound { attr.Value = new(Reserved) @@ -55,7 +57,7 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) { bw.WriteB(byte(attr.Type)) switch t := attr.Type; t { case HighPriority: - case OracleResponseT, NotValidBeforeT, ConflictsT: + case OracleResponseT, NotValidBeforeT, ConflictsT, NotaryAssistedT: attr.Value.EncodeBinary(bw) default: if t >= ReservedLowerBound && t <= ReservedUpperBound { @@ -97,6 +99,9 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error { case ConflictsT.String(): attr.Type = ConflictsT attr.Value = new(Conflicts) + case NotaryAssistedT.String(): + attr.Type = NotaryAssistedT + attr.Value = new(NotaryAssisted) default: return errors.New("wrong Type") } diff --git a/pkg/core/transaction/attribute_test.go b/pkg/core/transaction/attribute_test.go index 1474ebfca..4d43df95f 100644 --- a/pkg/core/transaction/attribute_test.go +++ b/pkg/core/transaction/attribute_test.go @@ -61,7 +61,7 @@ func TestAttribute_EncodeBinary(t *testing.T) { } } t.Run("lower bound", func(t *testing.T) { - testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedLowerBound+2), new(Attribute)) + testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedLowerBound+3), new(Attribute)) }) t.Run("upper bound", func(t *testing.T) { testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedUpperBound), new(Attribute)) @@ -95,6 +95,27 @@ func TestAttribute_EncodeBinary(t *testing.T) { require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(Conflicts))) }) }) + t.Run("NotaryAssisted", func(t *testing.T) { + t.Run("positive", func(t *testing.T) { + attr := &Attribute{ + Type: NotaryAssistedT, + Value: &NotaryAssisted{ + NKeys: 3, + }, + } + testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) + }) + t.Run("bad format: too long", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes(make([]byte, 2)) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(NotaryAssisted))) + }) + t.Run("bad format: too short", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes([]byte{}) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(NotaryAssisted))) + }) + }) } func TestAttribute_MarshalJSON(t *testing.T) { @@ -149,4 +170,13 @@ func TestAttribute_MarshalJSON(t *testing.T) { } testserdes.MarshalUnmarshalJSON(t, attr, new(Attribute)) }) + t.Run("NotaryAssisted", func(t *testing.T) { + attr := &Attribute{ + Type: NotaryAssistedT, + Value: &NotaryAssisted{ + NKeys: 3, + }, + } + testserdes.MarshalUnmarshalJSON(t, attr, new(Attribute)) + }) } diff --git a/pkg/core/transaction/attrtype.go b/pkg/core/transaction/attrtype.go index fa68ae5cc..c6879fefa 100644 --- a/pkg/core/transaction/attrtype.go +++ b/pkg/core/transaction/attrtype.go @@ -18,6 +18,7 @@ const ( OracleResponseT AttrType = 0x11 // OracleResponse NotValidBeforeT AttrType = ReservedLowerBound // NotValidBefore ConflictsT AttrType = ReservedLowerBound + 1 // Conflicts + NotaryAssistedT AttrType = ReservedLowerBound + 2 // NotaryAssisted ) func (a AttrType) allowMultiple() bool { diff --git a/pkg/core/transaction/attrtype_string.go b/pkg/core/transaction/attrtype_string.go index 70db86618..1f2eb06ba 100644 --- a/pkg/core/transaction/attrtype_string.go +++ b/pkg/core/transaction/attrtype_string.go @@ -12,16 +12,17 @@ func _() { _ = x[OracleResponseT-17] _ = x[NotValidBeforeT-224] _ = x[ConflictsT-225] + _ = x[NotaryAssistedT-226] } const ( _AttrType_name_0 = "HighPriority" _AttrType_name_1 = "OracleResponse" - _AttrType_name_2 = "NotValidBeforeConflicts" + _AttrType_name_2 = "NotValidBeforeConflictsNotaryAssisted" ) var ( - _AttrType_index_2 = [...]uint8{0, 14, 23} + _AttrType_index_2 = [...]uint8{0, 14, 23, 37} ) func (i AttrType) String() string { @@ -30,7 +31,7 @@ func (i AttrType) String() string { return _AttrType_name_0 case i == 17: return _AttrType_name_1 - case 224 <= i && i <= 225: + case 224 <= i && i <= 226: i -= 224 return _AttrType_name_2[_AttrType_index_2[i]:_AttrType_index_2[i+1]] default: diff --git a/pkg/core/transaction/notary_assisted.go b/pkg/core/transaction/notary_assisted.go new file mode 100644 index 000000000..119dc6a3b --- /dev/null +++ b/pkg/core/transaction/notary_assisted.go @@ -0,0 +1,37 @@ +package transaction + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/io" +) + +// NotaryServiceFeePerKey is a reward per key for notary nodes. +const NotaryServiceFeePerKey = 1000_0000 // 0.1 GAS + +// NotaryAssisted represents attribute for notary service transactions. +type NotaryAssisted struct { + NKeys uint8 `json:"nkeys"` +} + +// DecodeBinary implements io.Serializable interface. +func (n *NotaryAssisted) DecodeBinary(br *io.BinReader) { + bytes := br.ReadVarBytes(1) + if br.Err != nil { + return + } + if len(bytes) != 1 { + br.Err = fmt.Errorf("expected 1 byte, got %d", len(bytes)) + return + } + n.NKeys = bytes[0] +} + +// EncodeBinary implements io.Serializable interface. +func (n *NotaryAssisted) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes([]byte{n.NKeys}) +} + +func (n *NotaryAssisted) toJSONMap(m map[string]interface{}) { + m["nkeys"] = n.NKeys +}