Compare commits

...

2 commits

Author SHA1 Message Date
Anna Shaleva
dc4d5a2dc6 interop: use currently executing contract state for permissions check
It's not correct to use an updated contract state got from Management to
check for the allowed method call. We need to use manifest from the
currently executing context for that. It may be critical for cases when
executing contract is being updated firstly, and after that calls
another contract. So we need an old (executing) contract manifest for
this check.

This change likely does not affect the mainnet's state since it's hard
to meet the trigger criteria, but I'd put it under the hardfork anyway.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2024-06-04 13:56:10 +03:00
Anna Shaleva
e0825e7665 core: introduce D hardfork
Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2024-06-04 13:54:15 +03:00
6 changed files with 136 additions and 8 deletions

View file

@ -27,6 +27,9 @@ const (
// https://github.com/neo-project/neo/pull/2925) and #3362 (ported from // https://github.com/neo-project/neo/pull/2925) and #3362 (ported from
// https://github.com/neo-project/neo/pull/3154). // https://github.com/neo-project/neo/pull/3154).
HFCockatrice // Cockatrice HFCockatrice // Cockatrice
// HFDomovoi represents hard-fork introduced in #3476 (ported from
// https://github.com/neo-project/neo/pull/3290).
HFDomovoi // Domovoi
// hfLast denotes the end of hardforks enum. Consider adding new hardforks // hfLast denotes the end of hardforks enum. Consider adding new hardforks
// before hfLast. // before hfLast.
hfLast hfLast

View file

@ -12,13 +12,15 @@ func _() {
_ = x[HFAspidochelone-1] _ = x[HFAspidochelone-1]
_ = x[HFBasilisk-2] _ = x[HFBasilisk-2]
_ = x[HFCockatrice-4] _ = x[HFCockatrice-4]
_ = x[hfLast-8] _ = x[HFDomovoi-8]
_ = x[hfLast-16]
} }
const ( const (
_Hardfork_name_0 = "DefaultAspidocheloneBasilisk" _Hardfork_name_0 = "DefaultAspidocheloneBasilisk"
_Hardfork_name_1 = "Cockatrice" _Hardfork_name_1 = "Cockatrice"
_Hardfork_name_2 = "hfLast" _Hardfork_name_2 = "Domovoi"
_Hardfork_name_3 = "hfLast"
) )
var ( var (
@ -33,6 +35,8 @@ func (i Hardfork) String() string {
return _Hardfork_name_1 return _Hardfork_name_1
case i == 8: case i == 8:
return _Hardfork_name_2 return _Hardfork_name_2
case i == 16:
return _Hardfork_name_3
default: default:
return "Hardfork(" + strconv.FormatInt(int64(i), 10) + ")" return "Hardfork(" + strconv.FormatInt(int64(i), 10) + ")"
} }

View file

@ -370,6 +370,7 @@ func TestNewBlockchain_InitHardforks(t *testing.T) {
config.HFAspidochelone.String(): 0, config.HFAspidochelone.String(): 0,
config.HFBasilisk.String(): 0, config.HFBasilisk.String(): 0,
config.HFCockatrice.String(): 0, config.HFCockatrice.String(): 0,
config.HFDomovoi.String(): 0,
}, bc.GetConfig().Hardforks) }, bc.GetConfig().Hardforks)
}) })
t.Run("empty set", func(t *testing.T) { t.Run("empty set", func(t *testing.T) {
@ -400,13 +401,14 @@ func TestNewBlockchain_InitHardforks(t *testing.T) {
}) })
t.Run("all present", func(t *testing.T) { t.Run("all present", func(t *testing.T) {
bc := newTestChainWithCustomCfg(t, func(c *config.Config) { bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
c.ProtocolConfiguration.Hardforks = map[string]uint32{config.HFAspidochelone.String(): 5, config.HFBasilisk.String(): 10, config.HFCockatrice.String(): 15} c.ProtocolConfiguration.Hardforks = map[string]uint32{config.HFAspidochelone.String(): 5, config.HFBasilisk.String(): 10, config.HFCockatrice.String(): 15, config.HFDomovoi.String(): 20}
require.NoError(t, c.ProtocolConfiguration.Validate()) require.NoError(t, c.ProtocolConfiguration.Validate())
}) })
require.Equal(t, map[string]uint32{ require.Equal(t, map[string]uint32{
config.HFAspidochelone.String(): 5, config.HFAspidochelone.String(): 5,
config.HFBasilisk.String(): 10, config.HFBasilisk.String(): 10,
config.HFCockatrice.String(): 15, config.HFCockatrice.String(): 15,
config.HFDomovoi.String(): 20,
}, bc.GetConfig().Hardforks) }, bc.GetConfig().Hardforks)
}) })
} }

View file

@ -271,7 +271,7 @@ func TestBlockchain_StartFromExistingDB(t *testing.T) {
_, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache) _, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
require.Error(t, err) require.Error(t, err)
require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native %s: version mismatch for the latest hardfork Cockatrice (stored contract state differs from autogenerated one)", nativenames.CryptoLib)), err) require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native %s: version mismatch for the latest hardfork Domovoi (stored contract state differs from autogenerated one)", nativenames.CryptoLib)), err)
}) })
t.Run("good", func(t *testing.T) { t.Run("good", func(t *testing.T) {

View file

@ -6,6 +6,7 @@ import (
"math/big" "math/big"
"strings" "strings"
"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/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
@ -77,12 +78,18 @@ func callInternal(ic *interop.Context, cs *state.Contract, name string, f callfl
if md.Safe { if md.Safe {
f &^= (callflag.WriteStates | callflag.AllowNotify) f &^= (callflag.WriteStates | callflag.AllowNotify)
} else if ctx := ic.VM.Context(); ctx != nil && ctx.IsDeployed() { } else if ctx := ic.VM.Context(); ctx != nil && ctx.IsDeployed() {
curr, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) var mfst *manifest.Manifest
if err == nil { if ic.IsHardforkEnabled(config.HFDomovoi) {
if !curr.Manifest.CanCall(cs.Hash, &cs.Manifest, name) { mfst = ctx.GetManifest()
return errors.New("disallowed method call") } else {
curr, err := ic.GetContract(ic.VM.GetCurrentScriptHash())
if err == nil {
mfst = &curr.Manifest
} }
} }
if mfst != nil && !mfst.CanCall(cs.Hash, &cs.Manifest, name) {
return errors.New("disallowed method call")
}
} }
return callExFromNative(ic, ic.VM.GetCurrentScriptHash(), cs, name, args, f, hasReturn, isDynamic, false) return callExFromNative(ic, ic.VM.GetCurrentScriptHash(), cs, name, args, f, hasReturn, isDynamic, false)
} }

View file

@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/contracts" "github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"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"
@ -173,6 +174,117 @@ func TestCall(t *testing.T) {
}) })
} }
func TestSystemContractCall_Permissions(t *testing.T) {
check := func(t *testing.T, cfg func(*config.Blockchain), shouldUpdateFail bool) {
bc, acc := chain.NewSingleWithCustomConfig(t, cfg)
e := neotest.NewExecutor(t, bc, acc, acc)
// Contract A has an unsafe method.
srcA := `package contractA
func RetOne() int {
return 1
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
})
e.DeployContract(t, ctrA, nil)
var hashAStr string
for i := 0; i < util.Uint160Size; i++ {
hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
if i != util.Uint160Size-1 {
hashAStr += ", "
}
}
// Contract B has a method that calls contract's A and another update method that
// calls contract's A after B's update.
srcB := `package contractB
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
)
func CallRetOne() int {
res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
return res
}
func Update(nef []byte, manifest []byte) int {
management.Update(nef, manifest)
res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
return res
}`
ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
Methods: manifest.WildStrings{Value: []string{"retOne"}},
},
{
Methods: manifest.WildStrings{Value: []string{"update"}},
},
},
})
e.DeployContract(t, ctrB, nil)
ctrBInvoker := e.ValidatorInvoker(ctrB.Hash)
// ctrBUpdated differs from ctrB in that it has no permission to call retOne method of ctrA
ctrBUpdated := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
Methods: manifest.WildStrings{Value: []string{}},
},
{
Methods: manifest.WildStrings{Value: []string{"update"}},
},
},
})
// Call to A before B update should HALT.
ctrBInvoker.Invoke(t, stackitem.Make(1), "callRetOne")
// Call to A in the same context as B update should HALT.
n, err := ctrBUpdated.NEF.Bytes()
require.NoError(t, err)
m, err := json.Marshal(ctrBUpdated.Manifest)
require.NoError(t, err)
if shouldUpdateFail {
ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "update", n, m)
} else {
ctrBInvoker.Invoke(t, stackitem.Make(1), "update", n, m)
}
// If contract is updated, then all to A after B update should FAULT (manifest
// is updated, no permission to call retOne method).
if !shouldUpdateFail {
ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "callRetOne")
}
}
// Pre-Domovoi behaviour: an updated contract state is used for permissions check.
check(t, func(cfg *config.Blockchain) {
cfg.Hardforks = map[string]uint32{
config.HFDomovoi.String(): 100500,
}
}, true)
// Post-Domovoi behaviour: an executing contract state is used for permissions check.
check(t, func(cfg *config.Blockchain) {
cfg.Hardforks = map[string]uint32{
config.HFDomovoi.String(): 0,
}
}, false)
}
func TestLoadToken(t *testing.T) { func TestLoadToken(t *testing.T) {
bc, acc := chain.NewSingle(t) bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc) e := neotest.NewExecutor(t, bc, acc, acc)