Merge pull request #3056 from nspcc-dev/revert-script-check-removal

core: create hardfork for strict script check
This commit is contained in:
Roman Khimov 2023-08-09 20:55:55 +03:00 committed by GitHub
commit 25f61b45dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 24 deletions

View file

@ -343,7 +343,7 @@ protocol-related settings described in the table below.
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| CommitteeHistory | map[uint32]uint32 | none | Number of committee members after the given height, for example `{0: 1, 20: 4}` sets up a chain with one committee member since the genesis and then changes the setting to 4 committee members at the height of 20. `StandbyCommittee` committee setting must have the number of keys equal or exceeding the highest value in this option. Blocks numbers where the change happens must be divisible by the old and by the new values simultaneously. If not set, committee size is derived from the `StandbyCommittee` setting and never changes. | | CommitteeHistory | map[uint32]uint32 | none | Number of committee members after the given height, for example `{0: 1, 20: 4}` sets up a chain with one committee member since the genesis and then changes the setting to 4 committee members at the height of 20. `StandbyCommittee` committee setting must have the number of keys equal or exceeding the highest value in this option. Blocks numbers where the change happens must be divisible by the old and by the new values simultaneously. If not set, committee size is derived from the `StandbyCommittee` setting and never changes. |
| GarbageCollectionPeriod | `uint32` | 10000 | Controls MPT garbage collection interval (in blocks) for configurations with `RemoveUntraceableBlocks` enabled and `KeepOnlyLatestState` disabled. In this mode the node stores a number of MPT trees (corresponding to `MaxTraceableBlocks` and `StateSyncInterval`), but the DB needs to be clean from old entries from time to time. Doing it too often will cause too much processing overhead, doing it too rarely will leave more useless data in the DB. This setting is deprecated in favor of the same setting in the ApplicationConfiguration and will be removed in future node versions. If both settings are used, ApplicationConfiguration is prioritized over this one. | | GarbageCollectionPeriod | `uint32` | 10000 | Controls MPT garbage collection interval (in blocks) for configurations with `RemoveUntraceableBlocks` enabled and `KeepOnlyLatestState` disabled. In this mode the node stores a number of MPT trees (corresponding to `MaxTraceableBlocks` and `StateSyncInterval`), but the DB needs to be clean from old entries from time to time. Doing it too often will cause too much processing overhead, doing it too rarely will leave more useless data in the DB. This setting is deprecated in favor of the same setting in the ApplicationConfiguration and will be removed in future node versions. If both settings are used, ApplicationConfiguration is prioritized over this one. |
| Hardforks | `map[string]uint32` | [] | The set of incompatible changes that affect node behaviour starting from the specified height. The default value is an empty set which should be interpreted as "each known hard-fork is applied from the zero blockchain height". The list of valid hard-fork names:<br>`Aspidochelone` represents hard-fork introduced in [#2469](https://github.com/nspcc-dev/neo-go/pull/2469) (ported from the [reference](https://github.com/neo-project/neo/pull/2712)). It adjusts the prices of `System.Contract.CreateStandardAccount` and `System.Contract.CreateMultisigAccount` interops so that the resulting prices are in accordance with `sha256` method of native `CryptoLib` contract. It also includes [#2519](https://github.com/nspcc-dev/neo-go/pull/2519) (ported from the [reference](https://github.com/neo-project/neo/pull/2749)) that adjusts the price of `System.Runtime.GetRandom` interop and fixes its vulnerability. A special NeoGo-specific change is included as well for ContractManagement's update/deploy call flags behaviour to be compatible with pre-0.99.0 behaviour that was changed because of the [3.2.0 protocol change](https://github.com/neo-project/neo/pull/2653). | | Hardforks | `map[string]uint32` | [] | The set of incompatible changes that affect node behaviour starting from the specified height. The default value is an empty set which should be interpreted as "each known hard-fork is applied from the zero blockchain height". The list of valid hard-fork names:<br>`Aspidochelone` represents hard-fork introduced in [#2469](https://github.com/nspcc-dev/neo-go/pull/2469) (ported from the [reference](https://github.com/neo-project/neo/pull/2712)). It adjusts the prices of `System.Contract.CreateStandardAccount` and `System.Contract.CreateMultisigAccount` interops so that the resulting prices are in accordance with `sha256` method of native `CryptoLib` contract. It also includes [#2519](https://github.com/nspcc-dev/neo-go/pull/2519) (ported from the [reference](https://github.com/neo-project/neo/pull/2749)) that adjusts the price of `System.Runtime.GetRandom` interop and fixes its vulnerability. A special NeoGo-specific change is included as well for ContractManagement's update/deploy call flags behaviour to be compatible with pre-0.99.0 behaviour that was changed because of the [3.2.0 protocol change](https://github.com/neo-project/neo/pull/2653).<br>`Basilisk` represents hard-fork introduced in [#3056](https://github.com/nspcc-dev/neo-go/pull/3056) (ported from the [reference](https://github.com/neo-project/neo/pull/2881)). It enables strict smart contract script check against a set of JMP instructions and against method boundaries enabled on contract deploy or update. |
| KeepOnlyLatestState | `bool` | `false` | Specifies if MPT should only store the latest state (or a set of latest states, see `P2PStateExcangeExtensions` section for details). If true, DB size will be smaller, but older roots won't be accessible. This value should remain the same for the same database. | This setting is deprecated in favor of the same setting in the ApplicationConfiguration and will be removed in future node versions. If both settings are used, setting any of them to true enables the function. | | KeepOnlyLatestState | `bool` | `false` | Specifies if MPT should only store the latest state (or a set of latest states, see `P2PStateExcangeExtensions` section for details). If true, DB size will be smaller, but older roots won't be accessible. This value should remain the same for the same database. | This setting is deprecated in favor of the same setting in the ApplicationConfiguration and will be removed in future node versions. If both settings are used, setting any of them to true enables the function. |
| Magic | `uint32` | `0` | Magic number which uniquely identifies Neo network. | | Magic | `uint32` | `0` | Magic number which uniquely identifies Neo network. |
| MaxBlockSize | `uint32` | `262144` | Maximum block size in bytes. | | MaxBlockSize | `uint32` | `262144` | Maximum block size in bytes. |

View file

@ -10,6 +10,9 @@ const (
// https://github.com/neo-project/neo/pull/2712) and #2519 (ported from // https://github.com/neo-project/neo/pull/2712) and #2519 (ported from
// https://github.com/neo-project/neo/pull/2749). // https://github.com/neo-project/neo/pull/2749).
HFAspidochelone Hardfork = 1 << iota // Aspidochelone HFAspidochelone Hardfork = 1 << iota // Aspidochelone
// HFBasilisk represents hard-fork introduced in #3056 (ported from
// https://github.com/neo-project/neo/pull/2881).
HFBasilisk // Basilisk
) )
// hardforks holds a map of Hardfork string representation to its type. // hardforks holds a map of Hardfork string representation to its type.
@ -17,7 +20,7 @@ var hardforks map[string]Hardfork
func init() { func init() {
hardforks = make(map[string]Hardfork) hardforks = make(map[string]Hardfork)
for _, hf := range []Hardfork{HFAspidochelone} { for _, hf := range []Hardfork{HFAspidochelone, HFBasilisk} {
hardforks[hf.String()] = hf hardforks[hf.String()] = hf
} }
} }

View file

@ -9,11 +9,12 @@ func _() {
// Re-run the stringer command to generate them again. // Re-run the stringer command to generate them again.
var x [1]struct{} var x [1]struct{}
_ = x[HFAspidochelone-1] _ = x[HFAspidochelone-1]
_ = x[HFBasilisk-2]
} }
const _Hardfork_name = "Aspidochelone" const _Hardfork_name = "AspidocheloneBasilisk"
var _Hardfork_index = [...]uint8{0, 13} var _Hardfork_index = [...]uint8{0, 13, 21}
func (i Hardfork) String() string { func (i Hardfork) String() string {
i -= 1 i -= 1

View file

@ -10,6 +10,7 @@ import (
"math/big" "math/big"
"unicode/utf8" "unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
@ -23,6 +24,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/util/bitfield"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
@ -351,7 +354,7 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item)
if ic.Tx == nil { if ic.Tx == nil {
panic(errors.New("no transaction provided")) panic(errors.New("no transaction provided"))
} }
newcontract, err := m.Deploy(ic.DAO, ic.Tx.Sender(), neff, manif) newcontract, err := m.Deploy(ic, ic.Tx.Sender(), neff, manif)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -373,16 +376,16 @@ func markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) {
// Deploy creates a contract's hash/ID and saves a new contract into the given DAO. // Deploy creates a contract's hash/ID and saves a new contract into the given DAO.
// It doesn't run _deploy method and doesn't emit notification. // It doesn't run _deploy method and doesn't emit notification.
func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) { func (m *Management) Deploy(ic *interop.Context, sender util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) {
h := state.CreateContractHash(sender, neff.Checksum, manif.Name) h := state.CreateContractHash(sender, neff.Checksum, manif.Name)
if m.Policy.IsBlocked(d, h) { if m.Policy.IsBlocked(ic.DAO, h) {
return nil, fmt.Errorf("the contract %s has been blocked", h.StringLE()) return nil, fmt.Errorf("the contract %s has been blocked", h.StringLE())
} }
_, err := GetContract(d, h) _, err := GetContract(ic.DAO, h)
if err == nil { if err == nil {
return nil, errors.New("contract already exists") return nil, errors.New("contract already exists")
} }
id, err := m.getNextContractID(d) id, err := m.getNextContractID(ic.DAO)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -390,7 +393,7 @@ func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File,
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid manifest: %w", err) return nil, fmt.Errorf("invalid manifest: %w", err)
} }
err = checkScriptAndMethods(neff.Script, manif.ABI.Methods) err = checkScriptAndMethods(ic, neff.Script, manif.ABI.Methods)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -402,7 +405,7 @@ func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File,
Manifest: *manif, Manifest: *manif,
}, },
} }
err = PutContractState(d, newcontract) err = PutContractState(ic.DAO, newcontract)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -423,7 +426,7 @@ func (m *Management) updateWithData(ic *interop.Context, args []stackitem.Item)
if neff == nil && manif == nil { if neff == nil && manif == nil {
panic(errors.New("both NEF and manifest are nil")) panic(errors.New("both NEF and manifest are nil"))
} }
contract, err := m.Update(ic.DAO, ic.VM.GetCallingScriptHash(), neff, manif) contract, err := m.Update(ic, ic.VM.GetCallingScriptHash(), neff, manif)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -434,10 +437,10 @@ func (m *Management) updateWithData(ic *interop.Context, args []stackitem.Item)
// Update updates contract's script and/or manifest in the given DAO. // Update updates contract's script and/or manifest in the given DAO.
// It doesn't run _deploy method and doesn't emit notification. // It doesn't run _deploy method and doesn't emit notification.
func (m *Management) Update(d *dao.Simple, hash util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) { func (m *Management) Update(ic *interop.Context, hash util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) {
var contract state.Contract var contract state.Contract
oldcontract, err := GetContract(d, hash) oldcontract, err := GetContract(ic.DAO, hash)
if err != nil { if err != nil {
return nil, errors.New("contract doesn't exist") return nil, errors.New("contract doesn't exist")
} }
@ -461,12 +464,12 @@ func (m *Management) Update(d *dao.Simple, hash util.Uint160, neff *nef.File, ma
} }
contract.Manifest = *manif contract.Manifest = *manif
} }
err = checkScriptAndMethods(contract.NEF.Script, contract.Manifest.ABI.Methods) err = checkScriptAndMethods(ic, contract.NEF.Script, contract.Manifest.ABI.Methods)
if err != nil { if err != nil {
return nil, err return nil, err
} }
contract.UpdateCounter++ contract.UpdateCounter++
err = PutContractState(d, &contract) err = PutContractState(ic.DAO, &contract)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -713,12 +716,22 @@ func (m *Management) emitNotification(ic *interop.Context, name string, hash uti
ic.AddNotification(m.Hash, name, stackitem.NewArray([]stackitem.Item{addrToStackItem(&hash)})) ic.AddNotification(m.Hash, name, stackitem.NewArray([]stackitem.Item{addrToStackItem(&hash)}))
} }
func checkScriptAndMethods(script []byte, methods []manifest.Method) error { func checkScriptAndMethods(ic *interop.Context, script []byte, methods []manifest.Method) error {
l := len(script) l := len(script)
offsets := bitfield.New(l)
for i := range methods { for i := range methods {
if methods[i].Offset >= l { if methods[i].Offset >= l {
return fmt.Errorf("method %s/%d: offset is out of the script range", methods[i].Name, len(methods[i].Parameters)) return fmt.Errorf("method %s/%d: offset is out of the script range", methods[i].Name, len(methods[i].Parameters))
} }
offsets.Set(methods[i].Offset)
} }
if !ic.IsHardforkEnabled(config.HFBasilisk) {
return nil
}
err := vm.IsScriptCorrect(script, offsets)
if err != nil {
return fmt.Errorf("invalid contract script: %w", err)
}
return nil return nil
} }

View file

@ -6,9 +6,13 @@ import (
"github.com/nspcc-dev/neo-go/internal/basicchain" "github.com/nspcc-dev/neo-go/internal/basicchain"
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -32,3 +36,41 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
e.NativeHash(t, nativenames.Gas), e.ContractHash(t, 1)}, bc.GetNEP17Contracts()) e.NativeHash(t, nativenames.Gas), e.ContractHash(t, 1)}, bc.GetNEP17Contracts())
}) })
} }
func TestManagement_DeployUpdateHardfork(t *testing.T) {
bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) {
c.Hardforks = map[string]uint32{
config.HFBasilisk.String(): 2,
}
})
e := neotest.NewExecutor(t, bc, acc, acc)
ne, err := nef.NewFile([]byte{byte(opcode.JMP), 0x05})
require.NoError(t, err)
m := &manifest.Manifest{
Name: "ctr",
ABI: manifest.ABI{
Methods: []manifest.Method{
{
Name: "main",
Offset: 0,
},
},
},
}
ctr := &neotest.Contract{
Hash: state.CreateContractHash(e.Validator.ScriptHash(), ne.Checksum, m.Name),
NEF: ne,
Manifest: m,
}
// Block 1: no script check on deploy.
e.DeployContract(t, ctr, nil)
e.AddNewBlock(t)
// Block 3: script check on deploy.
ctr.Manifest.Name = "other name"
e.DeployContractCheckFAULT(t, ctr, nil, "invalid contract script: invalid offset 5 ip at 0")
}

View file

@ -19,7 +19,8 @@ func TestDeployGetUpdateDestroyContract(t *testing.T) {
mgmt := newManagement() mgmt := newManagement()
mgmt.Policy = newPolicy() mgmt.Policy = newPolicy()
d := dao.NewSimple(storage.NewMemoryStore(), false, false) d := dao.NewSimple(storage.NewMemoryStore(), false, false)
err := mgmt.Initialize(&interop.Context{DAO: d}) ic := &interop.Context{DAO: d}
err := mgmt.Initialize(ic)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, mgmt.Policy.Initialize(&interop.Context{DAO: d})) require.NoError(t, mgmt.Policy.Initialize(&interop.Context{DAO: d}))
script := []byte{byte(opcode.RET)} script := []byte{byte(opcode.RET)}
@ -35,7 +36,7 @@ func TestDeployGetUpdateDestroyContract(t *testing.T) {
h := state.CreateContractHash(sender, ne.Checksum, manif.Name) h := state.CreateContractHash(sender, ne.Checksum, manif.Name)
contract, err := mgmt.Deploy(d, sender, ne, manif) contract, err := mgmt.Deploy(ic, sender, ne, manif)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int32(1), contract.ID) require.Equal(t, int32(1), contract.ID)
require.Equal(t, uint16(0), contract.UpdateCounter) require.Equal(t, uint16(0), contract.UpdateCounter)
@ -44,12 +45,12 @@ func TestDeployGetUpdateDestroyContract(t *testing.T) {
require.Equal(t, *manif, contract.Manifest) require.Equal(t, *manif, contract.Manifest)
// Double deploy. // Double deploy.
_, err = mgmt.Deploy(d, sender, ne, manif) _, err = mgmt.Deploy(ic, sender, ne, manif)
require.Error(t, err) require.Error(t, err)
// Different sender. // Different sender.
sender2 := util.Uint160{3, 2, 1} sender2 := util.Uint160{3, 2, 1}
contract2, err := mgmt.Deploy(d, sender2, ne, manif) contract2, err := mgmt.Deploy(ic, sender2, ne, manif)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int32(2), contract2.ID) require.Equal(t, int32(2), contract2.ID)
require.Equal(t, uint16(0), contract2.UpdateCounter) require.Equal(t, uint16(0), contract2.UpdateCounter)
@ -65,7 +66,7 @@ func TestDeployGetUpdateDestroyContract(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, contract, refContract) require.Equal(t, contract, refContract)
upContract, err := mgmt.Update(d, h, ne, manif) upContract, err := mgmt.Update(ic, h, ne, manif)
refContract.UpdateCounter++ refContract.UpdateCounter++
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, refContract, upContract) require.Equal(t, refContract, upContract)
@ -106,6 +107,7 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
require.Empty(t, mgmt.GetNEP17Contracts(d)) require.Empty(t, mgmt.GetNEP17Contracts(d))
private := d.GetPrivate() private := d.GetPrivate()
ic := &interop.Context{DAO: private}
// Deploy NEP-17 contract // Deploy NEP-17 contract
script := []byte{byte(opcode.RET)} script := []byte{byte(opcode.RET)}
@ -119,7 +121,7 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
Parameters: []manifest.Parameter{}, Parameters: []manifest.Parameter{},
}) })
manif.SupportedStandards = []string{manifest.NEP17StandardName} manif.SupportedStandards = []string{manifest.NEP17StandardName}
c1, err := mgmt.Deploy(private, sender, ne, manif) c1, err := mgmt.Deploy(ic, sender, ne, manif)
require.NoError(t, err) require.NoError(t, err)
// c1 contract hash should be returned, as private DAO already contains changed cache. // c1 contract hash should be returned, as private DAO already contains changed cache.
@ -140,7 +142,7 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
ReturnType: smartcontract.VoidType, ReturnType: smartcontract.VoidType,
Parameters: []manifest.Parameter{}, Parameters: []manifest.Parameter{},
}) })
c1Updated, err := mgmt.Update(private, c1.Hash, ne, manif) c1Updated, err := mgmt.Update(&interop.Context{DAO: private}, c1.Hash, ne, manif)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, c1.Hash, c1Updated.Hash) require.Equal(t, c1.Hash, c1Updated.Hash)

View file

@ -143,6 +143,17 @@ func TestManagement_ContractDeploy(t *testing.T) {
managementInvoker.InvokeFail(t, "method add/2: offset is out of the script range", "deploy", nefBytes, manifB) managementInvoker.InvokeFail(t, "method add/2: offset is out of the script range", "deploy", nefBytes, manifB)
}) })
t.Run("bad methods in manifest 2", func(t *testing.T) {
var badManifest = cs1.Manifest
badManifest.ABI.Methods = make([]manifest.Method, len(cs1.Manifest.ABI.Methods))
copy(badManifest.ABI.Methods, cs1.Manifest.ABI.Methods)
badManifest.ABI.Methods[0].Offset = len(cs1.NEF.Script) - 2 // Ends with `CALLT(X,X);RET`.
manifB, err := json.Marshal(badManifest)
require.NoError(t, err)
managementInvoker.InvokeFail(t, "some methods point to wrong offsets (not to instruction boundary)", "deploy", nefBytes, manifB)
})
t.Run("duplicated methods in manifest 1", func(t *testing.T) { t.Run("duplicated methods in manifest 1", func(t *testing.T) {
badManifest := cs1.Manifest badManifest := cs1.Manifest
badManifest.ABI.Methods = make([]manifest.Method, len(cs1.Manifest.ABI.Methods)) badManifest.ABI.Methods = make([]manifest.Method, len(cs1.Manifest.ABI.Methods))