diff --git a/docs/node-configuration.md b/docs/node-configuration.md index 9120be64d2..bae794d7d5 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -341,7 +341,7 @@ protocol-related settings described in the table below. | MaxValidUntilBlockIncrement | `uint32` | `5760` | Upper height increment limit for transaction's ValidUntilBlock field value relative to the current blockchain height, exceeding which a transaction will fail validation. It is set to estimated daily number of blocks with 15s interval by default. | | MemPoolSize | `int` | `50000` | Size of the node's memory pool where transactions are stored before they are added to block. | | P2PNotaryRequestPayloadPoolSize | `int` | `1000` | Size of the node's P2P Notary request payloads memory pool where P2P Notary requests are stored before main or fallback transaction is completed and added to the chain.
This option is valid only if `P2PSigExtensions` are enabled. | Not supported by the C# node, thus may affect heterogeneous networks functionality. | -| P2PSigExtensions | `bool` | `false` | Enables following additional Notary service related logic:
• Transaction attribute `NotaryAssisted`
• Network payload of the `P2PNotaryRequest` type
• Notary node module | Not supported by the C# node, thus may affect heterogeneous networks functionality. | +| P2PSigExtensions | `bool` | `false` | Enables following additional Notary service related logic:
• Network payload of the `P2PNotaryRequest` type
• Notary node module | Not supported by the C# node, thus may affect heterogeneous networks functionality. | | P2PStateExchangeExtensions | `bool` | `false` | Enables the following P2P MPT state data exchange logic:
• `StateSyncInterval` protocol setting
• P2P commands `GetMPTDataCMD` and `MPTDataCMD` | Not supported by the C# node, thus may affect heterogeneous networks functionality. Can be supported either on MPT-complete node (`KeepOnlyLatestState`=`false`) or on light GC-enabled node (`RemoveUntraceableBlocks=true`) in which case `KeepOnlyLatestState` setting doesn't change the behavior, an appropriate set of MPTs is always stored (see `RemoveUntraceableBlocks`). | | ReservedAttributes | `bool` | `false` | Allows to have reserved attributes range for experimental or private purposes. | | SeedList | `[]string` | [] | List of initial nodes addresses used to establish connectivity. | diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index c67ee5e430..8190ff6555 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2697,8 +2697,8 @@ func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transact 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) + if !bc.isHardforkEnabled(&transaction.NotaryAssistedActivation, bc.BlockHeight()) { + return fmt.Errorf("%w: NotaryAssisted attribute was found, but %s is not active yet", ErrInvalidAttribute, transaction.NotaryAssistedActivation) } if !tx.HasSigner(nativehashes.Notary) { return fmt.Errorf("%w: NotaryAssisted attribute was found, but transaction is not signed by the Notary native contract", ErrInvalidAttribute) diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index e6dabb5b41..482ec7b95e 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -2057,7 +2057,12 @@ func TestBlockchain_VerifyTx(t *testing.T) { } t.Run("Disabled", func(t *testing.T) { bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.Blockchain) { - c.P2PSigExtensions = false + c.P2PSigExtensions = true + c.Hardforks = map[string]uint32{ + config.HFAspidochelone.String(): 0, + config.HFBasilisk.String(): 0, + config.HFCockatrice.String(): 0, + } c.ReservedAttributes = false }) eBad := neotest.NewExecutor(t, bcBad, validatorBad, committeeBad) @@ -2069,7 +2074,7 @@ func TestBlockchain_VerifyTx(t *testing.T) { eBad.SignTx(t, tx, 1_0000_0000, eBad.Committee) err := bcBad.VerifyTx(tx) require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "invalid attribute: NotaryAssisted attribute was found, but P2PSigExtensions are disabled")) + require.True(t, strings.Contains(err.Error(), "invalid attribute: NotaryAssisted attribute was found, but Echidna is not active yet")) }) t.Run("Enabled, insufficient network fee", func(t *testing.T) { tx := getNotaryAssistedTx(e, 1, 0) diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index b0bd92b958..3949062518 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -72,9 +72,9 @@ func NewContracts(cfg config.ProtocolConfiguration) *Contracts { cs.Ledger = ledger cs.Contracts = append(cs.Contracts, ledger) - gas := newGAS(int64(cfg.InitialGASSupply), cfg.P2PSigExtensions) + gas := newGAS(int64(cfg.InitialGASSupply)) neo := newNEO(cfg) - policy := newPolicy(cfg.P2PSigExtensions) + policy := newPolicy() neo.GAS = gas neo.Policy = policy gas.NEO = neo diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go index 73859507b1..df10d4b868 100644 --- a/pkg/core/native/management_test.go +++ b/pkg/core/native/management_test.go @@ -17,7 +17,7 @@ import ( func TestDeployGetUpdateDestroyContract(t *testing.T) { mgmt := newManagement() - mgmt.Policy = newPolicy(false) + mgmt.Policy = newPolicy() d := dao.NewSimple(storage.NewMemoryStore(), false) ic := &interop.Context{DAO: d} err := mgmt.Initialize(ic, nil, nil) @@ -95,7 +95,7 @@ func TestManagement_Initialize(t *testing.T) { func TestManagement_GetNEP17Contracts(t *testing.T) { mgmt := newManagement() - mgmt.Policy = newPolicy(false) + mgmt.Policy = newPolicy() d := dao.NewSimple(storage.NewMemoryStore(), false) err := mgmt.Initialize(&interop.Context{DAO: d}, nil, nil) require.NoError(t, err) diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index 92f1aed7fb..7cfe34bce1 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -22,8 +22,7 @@ type GAS struct { NEO *NEO Policy *Policy - initialSupply int64 - p2pSigExtensionsEnabled bool + initialSupply int64 } const gasContractID = -6 @@ -32,10 +31,9 @@ const gasContractID = -6 const GASFactor = NEOTotalSupply // newGAS returns GAS native contract. -func newGAS(init int64, p2pSigExtensionsEnabled bool) *GAS { +func newGAS(init int64) *GAS { g := &GAS{ - initialSupply: init, - p2pSigExtensionsEnabled: p2pSigExtensionsEnabled, + initialSupply: init, } defer g.BuildHFSpecificMD(g.ActiveIn()) @@ -122,14 +120,12 @@ func (g *GAS) OnPersist(ic *interop.Context) error { var netFee int64 for _, tx := range ic.Block.Transactions { netFee += tx.NetworkFee - if g.p2pSigExtensionsEnabled { - // Reward for NotaryAssisted attribute will be minted to designated notary nodes - // by Notary contract. - attrs := tx.GetAttributes(transaction.NotaryAssistedT) - if len(attrs) != 0 { - na := attrs[0].Value.(*transaction.NotaryAssisted) - netFee -= (int64(na.NKeys) + 1) * g.Policy.GetAttributeFeeInternal(ic.DAO, transaction.NotaryAssistedT) - } + // Reward for NotaryAssisted attribute will be minted to designated notary nodes + // by Notary contract. + attrs := tx.GetAttributes(transaction.NotaryAssistedT) + if len(attrs) != 0 { + na := attrs[0].Value.(*transaction.NotaryAssisted) + netFee -= (int64(na.NKeys) + 1) * g.Policy.GetAttributeFeeInternal(ic.DAO, transaction.NotaryAssistedT) } } g.mint(ic, primary, big.NewInt(int64(netFee)), false) diff --git a/pkg/core/native/native_test/management_test.go b/pkg/core/native/native_test/management_test.go index 9adc7e5c5b..498eb19271 100644 --- a/pkg/core/native/native_test/management_test.go +++ b/pkg/core/native/native_test/management_test.go @@ -58,6 +58,7 @@ var ( // under assumption that hardforks from Aspidochelone to Echidna (included) are enabled. echidnaCSS = map[string]string{ nativenames.Notary: `{"id":-10,"hash":"0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"Notary","abi":{"methods":[{"name":"balanceOf","offset":0,"parameters":[{"name":"addr","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"expirationOf","offset":7,"parameters":[{"name":"addr","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"getMaxNotValidBeforeDelta","offset":14,"parameters":[],"returntype":"Integer","safe":true},{"name":"lockDepositUntil","offset":21,"parameters":[{"name":"address","type":"Hash160"},{"name":"till","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"onNEP17Payment","offset":28,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"setMaxNotValidBeforeDelta","offset":35,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"verify","offset":42,"parameters":[{"name":"signature","type":"Signature"}],"returntype":"Boolean","safe":true},{"name":"withdraw","offset":49,"parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}],"returntype":"Boolean","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, + nativenames.Policy: `{"id":-7,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1094259016},"manifest":{"name":"PolicyContract","abi":{"methods":[{"name":"blockAccount","offset":0,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","safe":false},{"name":"getAttributeFee","offset":7,"parameters":[{"name":"attributeType","type":"Integer"}],"returntype":"Integer","safe":true},{"name":"getExecFeeFactor","offset":14,"parameters":[],"returntype":"Integer","safe":true},{"name":"getFeePerByte","offset":21,"parameters":[],"returntype":"Integer","safe":true},{"name":"getStoragePrice","offset":28,"parameters":[],"returntype":"Integer","safe":true},{"name":"isBlocked","offset":35,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","safe":true},{"name":"setAttributeFee","offset":42,"parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setExecFeeFactor","offset":49,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setFeePerByte","offset":56,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setStoragePrice","offset":63,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"unblockAccount","offset":70,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, } ) @@ -255,6 +256,13 @@ func TestManagement_NativeDeployUpdateNotifications(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(aer)) expected = expected[:0] + expected = append(expected, state.NotificationEvent{ + ScriptHash: nativehashes.ContractManagement, + Name: "Update", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(nativehashes.PolicyContract), + }), + }) expected = append(expected, state.NotificationEvent{ ScriptHash: nativehashes.ContractManagement, Name: "Deploy", diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index 0591fe7b44..a8a823639e 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -63,9 +63,6 @@ var ( type Policy struct { interop.ContractMD NEO *NEO - - // p2pSigExtensionsEnabled defines whether the P2P signature extensions logic is relevant. - p2pSigExtensionsEnabled bool } type PolicyCache struct { @@ -100,11 +97,8 @@ func copyPolicyCache(src, dst *PolicyCache) { } // newPolicy returns Policy native contract. -func newPolicy(p2pSigExtensionsEnabled bool) *Policy { - p := &Policy{ - ContractMD: *interop.NewContractMD(nativenames.Policy, policyContractID), - p2pSigExtensionsEnabled: p2pSigExtensionsEnabled, - } +func newPolicy() *Policy { + p := &Policy{ContractMD: *interop.NewContractMD(nativenames.Policy, policyContractID)} defer p.BuildHFSpecificMD(p.ActiveIn()) desc := newDescriptor("getFeePerByte", smartcontract.IntegerType) @@ -136,13 +130,24 @@ func newPolicy(p2pSigExtensionsEnabled bool) *Policy { desc = newDescriptor("getAttributeFee", smartcontract.IntegerType, manifest.NewParameter("attributeType", smartcontract.IntegerType)) - md = newMethodAndPrice(p.getAttributeFee, 1<<15, callflag.ReadStates) + md = newMethodAndPrice(p.getAttributeFeeV0, 1<<15, callflag.ReadStates, config.HFDefault, transaction.NotaryAssistedActivation) + p.AddMethod(md, desc) + + desc = newDescriptor("getAttributeFee", smartcontract.IntegerType, + manifest.NewParameter("attributeType", smartcontract.IntegerType)) + md = newMethodAndPrice(p.getAttributeFeeV1, 1<<15, callflag.ReadStates, transaction.NotaryAssistedActivation) p.AddMethod(md, desc) desc = newDescriptor("setAttributeFee", smartcontract.VoidType, manifest.NewParameter("attributeType", smartcontract.IntegerType), manifest.NewParameter("value", smartcontract.IntegerType)) - md = newMethodAndPrice(p.setAttributeFee, 1<<15, callflag.States) + md = newMethodAndPrice(p.setAttributeFeeV0, 1<<15, callflag.States, config.HFDefault, transaction.NotaryAssistedActivation) + p.AddMethod(md, desc) + + desc = newDescriptor("setAttributeFee", smartcontract.VoidType, + manifest.NewParameter("attributeType", smartcontract.IntegerType), + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(p.setAttributeFeeV1, 1<<15, callflag.States, transaction.NotaryAssistedActivation) p.AddMethod(md, desc) desc = newDescriptor("setFeePerByte", smartcontract.VoidType, @@ -170,27 +175,27 @@ func (p *Policy) Metadata() *interop.ContractMD { // Initialize initializes Policy native contract and implements the Contract interface. func (p *Policy) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { - if hf != p.ActiveIn() { - return nil - } - - setIntWithKey(p.ID, ic.DAO, feePerByteKey, defaultFeePerByte) - setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor) - setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice) - - cache := &PolicyCache{ - execFeeFactor: defaultExecFeeFactor, - feePerByte: defaultFeePerByte, - maxVerificationGas: defaultMaxVerificationGas, - storagePrice: DefaultStoragePrice, - attributeFee: map[transaction.AttrType]uint32{}, - blockedAccounts: make([]util.Uint160, 0), + if hf == p.ActiveIn() { + setIntWithKey(p.ID, ic.DAO, feePerByteKey, defaultFeePerByte) + setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor) + setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice) + + cache := &PolicyCache{ + execFeeFactor: defaultExecFeeFactor, + feePerByte: defaultFeePerByte, + maxVerificationGas: defaultMaxVerificationGas, + storagePrice: DefaultStoragePrice, + attributeFee: map[transaction.AttrType]uint32{}, + blockedAccounts: make([]util.Uint160, 0), + } + ic.DAO.SetCache(p.ID, cache) } - if p.p2pSigExtensionsEnabled { + if hf != nil && *hf == transaction.NotaryAssistedActivation { setIntWithKey(p.ID, ic.DAO, []byte{attributeFeePrefix, byte(transaction.NotaryAssistedT)}, defaultNotaryAssistedFee) + + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) cache.attributeFee[transaction.NotaryAssistedT] = defaultNotaryAssistedFee } - ic.DAO.SetCache(p.ID, cache) return nil } @@ -363,9 +368,18 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta return stackitem.Null{} } -func (p *Policy) getAttributeFee(ic *interop.Context, args []stackitem.Item) stackitem.Item { +func (p *Policy) getAttributeFeeV0(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return p.getAttributeFeeGeneric(ic, args, false) +} + +func (p *Policy) getAttributeFeeV1(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return p.getAttributeFeeGeneric(ic, args, true) +} + +func (p *Policy) getAttributeFeeGeneric(ic *interop.Context, args []stackitem.Item, allowNotaryAssisted bool) stackitem.Item { t := transaction.AttrType(toUint8(args[0])) - if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) { + if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) || + (!allowNotaryAssisted && t == transaction.NotaryAssistedT) { panic(fmt.Errorf("invalid attribute type: %d", t)) } return stackitem.NewBigInteger(big.NewInt(p.GetAttributeFeeInternal(ic.DAO, t))) @@ -382,10 +396,19 @@ func (p *Policy) GetAttributeFeeInternal(d *dao.Simple, t transaction.AttrType) return int64(v) } -func (p *Policy) setAttributeFee(ic *interop.Context, args []stackitem.Item) stackitem.Item { +func (p *Policy) setAttributeFeeV0(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return p.setAttributeFeeGeneric(ic, args, false) +} + +func (p *Policy) setAttributeFeeV1(ic *interop.Context, args []stackitem.Item) stackitem.Item { + return p.setAttributeFeeGeneric(ic, args, true) +} + +func (p *Policy) setAttributeFeeGeneric(ic *interop.Context, args []stackitem.Item, allowNotaryAssisted bool) stackitem.Item { t := transaction.AttrType(toUint8(args[0])) value := toUint32(args[1]) - if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) { + if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) || + (!allowNotaryAssisted && t == transaction.NotaryAssistedT) { panic(fmt.Errorf("invalid attribute type: %d", t)) } if value > maxAttributeFee { diff --git a/pkg/core/native/policy_test.go b/pkg/core/native/policy_test.go index 9569dd6b83..a1d95fe568 100644 --- a/pkg/core/native/policy_test.go +++ b/pkg/core/native/policy_test.go @@ -4,9 +4,12 @@ import ( "fmt" "testing" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/stretchr/testify/require" @@ -65,3 +68,51 @@ func TestPolicy_BlockedAccounts(t *testing.T) { require.Equal(t, expectedErr, err.Error()) }) } + +func TestPolicy_GetNotaryFeePerKey(t *testing.T) { + const echidnaHeight = 4 + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) { + c.Hardforks = map[string]uint32{ + config.HFEchidna.String(): echidnaHeight, + } + }) + e := neotest.NewExecutor(t, bc, acc, acc) + p := e.CommitteeInvoker(nativehashes.PolicyContract) + + // Invoke before Echidna should fail. + p.InvokeFail(t, "invalid attribute type: 34", "getAttributeFee", int64(transaction.NotaryAssistedT)) + + for e.Chain.BlockHeight() < echidnaHeight-1 { + e.AddNewBlock(t) + } + + // Invoke at Echidna should return the default value. + p.Invoke(t, 1000_0000, "getAttributeFee", int64(transaction.NotaryAssistedT)) + + // Invoke after Echidna should return the default value. + p.Invoke(t, 1000_0000, "getAttributeFee", int64(transaction.NotaryAssistedT)) +} + +func TestPolicy_SetNotaryFeePerKey(t *testing.T) { + const echidnaHeight = 4 + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) { + c.Hardforks = map[string]uint32{ + config.HFEchidna.String(): echidnaHeight, + } + }) + e := neotest.NewExecutor(t, bc, acc, acc) + p := e.CommitteeInvoker(nativehashes.PolicyContract) + + // Invoke before Echidna should fail. + p.InvokeFail(t, "invalid attribute type: 34", "setAttributeFee", int64(transaction.NotaryAssistedT), 500_0000) + + for e.Chain.BlockHeight() < echidnaHeight-1 { + e.AddNewBlock(t) + } + + // Invoke at Echidna should return the default value. + p.Invoke(t, nil, "setAttributeFee", int64(transaction.NotaryAssistedT), 500_0000) + + // Invoke after Echidna should return the default value. + p.Invoke(t, nil, "setAttributeFee", int64(transaction.NotaryAssistedT), 510_0000) +} diff --git a/pkg/core/transaction/notary_assisted.go b/pkg/core/transaction/notary_assisted.go index 278aaea936..8f857bfd04 100644 --- a/pkg/core/transaction/notary_assisted.go +++ b/pkg/core/transaction/notary_assisted.go @@ -1,9 +1,14 @@ package transaction import ( + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/io" ) +// NotaryAssistedActivation stores the hardfork of NotaryAssisted transaction attribute +// activation. +var NotaryAssistedActivation = config.HFEchidna + // NotaryAssisted represents attribute for notary service transactions. type NotaryAssisted struct { NKeys uint8 `json:"nkeys"`