diff --git a/cli/vm/cli.go b/cli/vm/cli.go index f2f34e2706..2b46d21369 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -1160,7 +1160,7 @@ func handleRun(c *cli.Context) error { breaks := v.Context().BreakPoints() // We ensure that there's a context loaded. ic.ReuseVM(v) v.SetGasLimit(gasLimit) - v.LoadNEFMethod(&cs.NEF, &cs.Manifest, util.Uint160{}, cs.Hash, callflag.All, hasRet, offset, initOff, nil) + v.LoadNEFMethod(&cs.NEF, &cs.Manifest, util.Uint160{}, cs.Hash, callflag.All, hasRet, offset, initOff, nil, false) for _, bp := range breaks { v.AddBreakPoint(bp) } diff --git a/docs/node-configuration.md b/docs/node-configuration.md index 48ae5b618f..c94fd2434b 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -583,7 +583,7 @@ in development and can change in an incompatible way. | `Cockatrice` | Introduces the ability to update native contracts. Includes a couple of new native smart contract APIs: `keccak256` of native CryptoLib contract and `getCommitteeAddress` of native NeoToken contract. | https://github.com/nspcc-dev/neo-go/pull/3402
https://github.com/neo-project/neo/pull/2942
https://github.com/nspcc-dev/neo-go/pull/3301
https://github.com/neo-project/neo/pull/2925
https://github.com/nspcc-dev/neo-go/pull/3362
https://github.com/neo-project/neo/pull/3154 | | `Domovoi` | Makes node use executing contract state for the contract call permissions check instead of the state stored in the native Management contract. In C# also makes System.Runtime.GetNotifications interop properly count stack references of notification parameters which prevents users from creating objects that exceed MaxStackSize constraint, but NeoGo has never had this bug, thus proper behaviour is preserved even before HFDomovoi. It results in the fact that some T5 testnet transactions have different ApplicationLogs compared to the C# node, but the node states match. | https://github.com/nspcc-dev/neo-go/pull/3476
https://github.com/neo-project/neo/pull/3290
https://github.com/nspcc-dev/neo-go/pull/3473
https://github.com/neo-project/neo/pull/3290
https://github.com/neo-project/neo/pull/3301
https://github.com/nspcc-dev/neo-go/pull/3485 | | `Echidna` | Introduces `Designation` event extension with `Old` and `New` roles data to native RoleManagement contract. Adds support for `base64UrlEncode` and `base64UrlDecode` methods to native StdLib contract. Extends the list of required call flags for `registerCandidate`, `unregisterCandidate`and `vote` methods of native NeoToken contract with AllowNotify flag. Enables `onNEP17Payment` method of NEO contract for candidate registration. Introduces constraint for maximum number of execution notifications. Adds support for `recoverSecp256K1` method of native CryptoLib contract. Introduces `setMillisecondsPerBlock` and `getMillisecondsPerBlock` methods of native Policy contract. Introduces support for NotaryAssisted transaction attribute and native Notary contract. | https://github.com/nspcc-dev/neo-go/pull/3554
https://github.com/nspcc-dev/neo-go/pull/3761
https://github.com/nspcc-dev/neo-go/pull/3554
https://github.com/neo-project/neo/pull/3597
https://github.com/nspcc-dev/neo-go/pull/3700
https://github.com/nspcc-dev/neo-go/pull/3640
https://github.com/neo-project/neo/pull/3548
https://github.com/nspcc-dev/neo-go/pull/3863
https://github.com/neo-project/neo/pull/3696
https://github.com/neo-project/neo/pull/3895
https://github.com/nspcc-dev/neo-go/pull/3835
https://github.com/nspcc-dev/neo-go/pull/3854
https://github.com/neo-project/neo/pull/3175
https://github.com/nspcc-dev/neo-go/pull/3478
https://github.com/neo-project/neo/pull/3178 | -| `Faun` | Adds `getBlockedAccounts` method to native Policy contract. Adds `hexEncode` and `hexDecode` methods to native StdLib contract. Adds `getExecPicoFeeFactor` method to native Policy contract and enables 4 decimals for execution fee factor. | https://github.com/nspcc-dev/neo-go/pull/3932
https://github.com/nspcc-dev/neo-go/pull/4004
https://github.com/neo-project/neo/pull/4147
https://github.com/neo-project/neo/pull/4150
https://github.com/nspcc-dev/neo-go/pull/4057
https://github.com/neo-project/neo/pull/4278| +| `Faun` | Adds `getBlockedAccounts` method to native Policy contract. Adds `hexEncode` and `hexDecode` methods to native StdLib contract. Adds `getExecPicoFeeFactor` method to native Policy contract and enables 4 decimals for execution fee factor. Adds whitelist fee contract management to native Policy contract: `setWhitelistFeeContract`, `removeWhitelistFeeContract`, `getWhitelistFeeContracts` methods and `WhitelistFeeChanged` event. | https://github.com/nspcc-dev/neo-go/pull/3932
https://github.com/nspcc-dev/neo-go/pull/4004
https://github.com/neo-project/neo/pull/4147
https://github.com/neo-project/neo/pull/4150
https://github.com/nspcc-dev/neo-go/pull/4057
https://github.com/neo-project/neo/pull/4278
https://github.com/nspcc-dev/neo-go/pull/4052
https://github.com/neo-project/neo/pull/4201 | ## DB compatibility diff --git a/examples/engine/go.mod b/examples/engine/go.mod index 423150c644..cbdf41b71e 100644 --- a/examples/engine/go.mod +++ b/examples/engine/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/engine go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/engine/go.sum b/examples/engine/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/engine/go.sum +++ b/examples/engine/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/events/go.mod b/examples/events/go.mod index 3d8d4f27f0..27a4c604d9 100644 --- a/examples/events/go.mod +++ b/examples/events/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/events go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/events/go.sum b/examples/events/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/events/go.sum +++ b/examples/events/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/iterator/go.mod b/examples/iterator/go.mod index 2721ea5562..3075f7ad3b 100644 --- a/examples/iterator/go.mod +++ b/examples/iterator/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/iterator go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/iterator/go.sum b/examples/iterator/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/iterator/go.sum +++ b/examples/iterator/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/neofs/go.mod b/examples/neofs/go.mod index 37c7569772..7a9a40cd24 100644 --- a/examples/neofs/go.mod +++ b/examples/neofs/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/neofs go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/neofs/go.sum b/examples/neofs/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/neofs/go.sum +++ b/examples/neofs/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/nft-d/go.mod b/examples/nft-d/go.mod index 2b3ea50198..5f649df70f 100644 --- a/examples/nft-d/go.mod +++ b/examples/nft-d/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/nft go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/nft-d/go.sum b/examples/nft-d/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/nft-d/go.sum +++ b/examples/nft-d/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/nft-nd-nns/go.mod b/examples/nft-nd-nns/go.mod index e3c6cbfc78..e3b9142758 100644 --- a/examples/nft-nd-nns/go.mod +++ b/examples/nft-nd-nns/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/nspcc-dev/neo-go v0.113.1-0.20251010141927-ac58bbb39350 - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 + github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c github.com/stretchr/testify v1.11.1 ) diff --git a/examples/nft-nd-nns/go.sum b/examples/nft-nd-nns/go.sum index a2d69f41af..ef49f61189 100644 --- a/examples/nft-nd-nns/go.sum +++ b/examples/nft-nd-nns/go.sum @@ -130,8 +130,8 @@ github.com/nspcc-dev/hrw/v2 v2.0.4 h1:o3Zh/2aF+IgGpvt414f46Ya20WG9u9vWxVd16ErFI8 github.com/nspcc-dev/hrw/v2 v2.0.4/go.mod h1:dUjOx27zTTvoPmT5EG25vSSWL2tKS7ndAa2TPTiZwFo= github.com/nspcc-dev/neo-go v0.113.1-0.20251010141927-ac58bbb39350 h1:v3kn1A+IzUd+tSPutByivpVRP/R+vz4pYH9Se72pHMc= github.com/nspcc-dev/neo-go v0.113.1-0.20251010141927-ac58bbb39350/go.mod h1:h63dEk1KmppwxXGi453woIhpvPf0lBmqLfWzd5TEUrs= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.15 h1:KM1uenYD6BES9pW69ZNglCKTLu5WzQCziaD361YfuW4= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.15/go.mod h1:Vukuf6qDOQESOWAx5yOjYtVC5wdsQp3hiZrxbJIa2fs= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= diff --git a/examples/nft-nd/go.mod b/examples/nft-nd/go.mod index b1c4b929ad..3c738a152c 100644 --- a/examples/nft-nd/go.mod +++ b/examples/nft-nd/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/nft-nd go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/nft-nd/go.sum b/examples/nft-nd/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/nft-nd/go.sum +++ b/examples/nft-nd/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/oracle/go.mod b/examples/oracle/go.mod index 47dd992b97..9fbe49da3f 100644 --- a/examples/oracle/go.mod +++ b/examples/oracle/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/oracle go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/oracle/go.sum b/examples/oracle/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/oracle/go.sum +++ b/examples/oracle/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/runtime/go.mod b/examples/runtime/go.mod index c893e89f25..791532e33a 100644 --- a/examples/runtime/go.mod +++ b/examples/runtime/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/runtime go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/runtime/go.sum b/examples/runtime/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/runtime/go.sum +++ b/examples/runtime/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/storage/go.mod b/examples/storage/go.mod index 0000ae5a84..255b5d70f9 100644 --- a/examples/storage/go.mod +++ b/examples/storage/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/storage go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/storage/go.sum b/examples/storage/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/storage/go.sum +++ b/examples/storage/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/timer/go.mod b/examples/timer/go.mod index cb769cf8a1..c4d30dd749 100644 --- a/examples/timer/go.mod +++ b/examples/timer/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/timer go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/timer/go.sum b/examples/timer/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/timer/go.sum +++ b/examples/timer/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/token/go.mod b/examples/token/go.mod index 46250f1bf8..5025a1d837 100644 --- a/examples/token/go.mod +++ b/examples/token/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/token go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/token/go.sum b/examples/token/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/token/go.sum +++ b/examples/token/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/examples/zkp/cubic_circuit/go.mod b/examples/zkp/cubic_circuit/go.mod index 0a7431bd23..925682ea19 100644 --- a/examples/zkp/cubic_circuit/go.mod +++ b/examples/zkp/cubic_circuit/go.mod @@ -29,7 +29,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 // indirect - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 // indirect + github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c // indirect github.com/nspcc-dev/rfc6979 v0.2.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/examples/zkp/cubic_circuit/go.sum b/examples/zkp/cubic_circuit/go.sum index a25d1ddf24..9d9d3649de 100644 --- a/examples/zkp/cubic_circuit/go.sum +++ b/examples/zkp/cubic_circuit/go.sum @@ -83,8 +83,8 @@ github.com/nspcc-dev/hrw/v2 v2.0.1 h1:CxYUkBeJvNfMEn2lHhrV6FjY8pZPceSxXUtMVq0BUO github.com/nspcc-dev/hrw/v2 v2.0.1/go.mod h1:iZAs5hT2q47EGq6AZ0FjaUI6ggntOi7vrY4utfzk5VA= github.com/nspcc-dev/neo-go v0.106.3 h1:HEyhgkjQY+HfBzotMJ12xx2VuOUphkngZ4kEkjvXDtE= github.com/nspcc-dev/neo-go v0.106.3/go.mod h1:3vEwJ2ld12N7HRGCaH/l/7EwopplC/+8XdIdPDNmD/M= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240305074711-35bc78d84dc4 h1:arN0Ypn+jawZpu1BND7TGRn44InAVIqKygndsx0y2no= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240305074711-35bc78d84dc4/go.mod h1:7Tm1NKEoUVVIUlkVwFrPh7GG5+Lmta2m7EGr4oVpBd8= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.12 h1:mdxtlSU2I4oVZ/7AXTLKyz8uUPbDWikZw4DM8gvrddA= diff --git a/examples/zkp/xor_compat/go.mod b/examples/zkp/xor_compat/go.mod index 4f23d6de3c..63e6f43e45 100644 --- a/examples/zkp/xor_compat/go.mod +++ b/examples/zkp/xor_compat/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/examples/zkp/xor go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/examples/zkp/xor_compat/go.sum b/examples/zkp/xor_compat/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/examples/zkp/xor_compat/go.sum +++ b/examples/zkp/xor_compat/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/go.mod b/go.mod index 57218ecac2..ff0fe4820e 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/nspcc-dev/bbolt v0.0.0-20250911202005-807225ebb0c8 github.com/nspcc-dev/dbft v0.4.0 github.com/nspcc-dev/go-ordered-json v0.0.0-20250911084817-6fb4472993d1 - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 + github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16 github.com/nspcc-dev/rfc6979 v0.2.4 github.com/pierrec/lz4 v2.6.1+incompatible diff --git a/go.sum b/go.sum index 80bdd548e8..a0df9bbd3e 100644 --- a/go.sum +++ b/go.sum @@ -155,8 +155,8 @@ github.com/nspcc-dev/go-ordered-json v0.0.0-20250911084817-6fb4472993d1 h1:U3wvY github.com/nspcc-dev/go-ordered-json v0.0.0-20250911084817-6fb4472993d1/go.mod h1:CHwf1nwquA6ecSfxmNF0YuemOPHAnRGoLuZUv/WPjeY= github.com/nspcc-dev/hrw/v2 v2.0.4 h1:o3Zh/2aF+IgGpvt414f46Ya20WG9u9vWxVd16ErFI8w= github.com/nspcc-dev/hrw/v2 v2.0.4/go.mod h1:dUjOx27zTTvoPmT5EG25vSSWL2tKS7ndAa2TPTiZwFo= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16 h1:0zOMTcstqKn/j/frqOHign9rtaoIPdxhJF069LRlCJM= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16/go.mod h1:u3XqGGbGQiPBAonFBJ6Dm9KbbDTvcGjEiRhndppzgHw= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= diff --git a/internal/contracts/oracle_contract/go.mod b/internal/contracts/oracle_contract/go.mod index 9f1eaf11bb..4d1460d9e6 100644 --- a/internal/contracts/oracle_contract/go.mod +++ b/internal/contracts/oracle_contract/go.mod @@ -2,4 +2,4 @@ module github.com/nspcc-dev/neo-go/internal/examples/oracle go 1.24 -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c diff --git a/internal/contracts/oracle_contract/go.sum b/internal/contracts/oracle_contract/go.sum index 26f78b6019..fafc25d8c1 100644 --- a/internal/contracts/oracle_contract/go.sum +++ b/internal/contracts/oracle_contract/go.sum @@ -1,2 +1,2 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9 h1:rLj5z7+BkRZQWqq08Zutr1+WY48vQ+8gktGivisYK2Y= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251203103451-fcbc971adee9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c h1:2zFTP6UP4gdM7mCHouvoSJxyIW/Tr8EvJ17iQSUgYt0= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251211114251-fcdc14479f2c/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index 544df53911..a07faadc55 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -204,6 +204,9 @@ func TestNativeHelpersCompile(t *testing.T) { {"getMillisecondsPerBlock", nil}, {"setMillisecondsPerBlock", []string{"10"}}, {"getBlockedAccounts", nil}, + {"setWhitelistFeeContract", []string{u160, `"method"`, "1", "2"}}, + {"removeWhitelistFeeContract", []string{u160, `"method"`, "1"}}, + {"getWhitelistFeeContracts", nil}, }) runNativeTestCases(t, *cs.ByName(nativenames.Ledger).Metadata(), "ledger", []nativeTestCase{ {"currentHash", nil}, diff --git a/pkg/config/hardfork.go b/pkg/config/hardfork.go index daf53ac26c..3b1fc98c22 100644 --- a/pkg/config/hardfork.go +++ b/pkg/config/hardfork.go @@ -53,7 +53,8 @@ const ( // HFFaun represents hard-fork introduced in #3931, #4004 (ported from // https://github.com/neo-project/neo/pull/4147, // https://github.com/neo-project/neo/pull/4150), #4057 (ported from - // https://github.com/neo-project/neo/pull/4278). + // https://github.com/neo-project/neo/pull/4278), #4052 (ported from + // https://github.com/neo-project/neo/pull/4201). HFFaun // Faun // hfLast denotes the end of hardforks enum. Consider adding new hardforks // before hfLast. diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index c32969e4db..ebe3141f07 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -3353,7 +3353,7 @@ func (bc *Blockchain) InitVerificationContext(ic *interop.Context, hash util.Uin } ic.Invocations[cs.Hash]++ ic.VM.LoadNEFMethod(&cs.NEF, &cs.Manifest, util.Uint160{}, hash, callflag.ReadOnly, - true, verifyOffset, initOffset, nil) + true, verifyOffset, initOffset, nil, false) } if len(witness.InvocationScript) != 0 { err := scparser.IsScriptCorrect(witness.InvocationScript, nil) diff --git a/pkg/core/custom_native_test.go b/pkg/core/custom_native_test.go index 2dc47c0e3f..faf5e94173 100644 --- a/pkg/core/custom_native_test.go +++ b/pkg/core/custom_native_test.go @@ -202,6 +202,8 @@ func (p *policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error func (p *policy) GetFeePerByteInternal(d *dao.Simple) int64 { return 1 } func (p *policy) BlockAccountInternal(d *dao.Simple, hash util.Uint160) bool { return false } func (p *policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool { return false } +func (p *policy) WhitelistedFee(d *dao.Simple, hash util.Uint160, offset int) int64 { return -1 } +func (p *policy) CleanWhitelist(ic *interop.Context, cs *state.Contract) error { return nil } func (p *policy) GetMaxValidUntilBlockIncrementInternal(ic *interop.Context) uint32 { return 2 } func (p *policy) getTimePerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item { return stackitem.NewBigInteger(big.NewInt(1000)) diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index be43330602..c11239a9f3 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "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/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -49,15 +50,26 @@ type Ledger interface { NativeManagementID() int32 } +// PolicyChecker is an interface required for checking native Policy constraints. +type PolicyChecker interface { + // IsBlocked returns whether the provided account is blocked by network policy. + IsBlocked(d *dao.Simple, hash util.Uint160) bool + // WhitelistedFee returns whether the specified contract method is whitelisted. + // Non-negative return value defines the execution fee of a whitelisted method + // call in picoGAS units. + WhitelistedFee(d *dao.Simple, hash util.Uint160, offset int) int64 +} + // Context represents context in which interops are executed. type Context struct { - Chain Ledger - Container hash.Hashable - Network uint32 - Hardforks map[string]uint32 - Natives []Contract - Trigger trigger.Type - Block *block.Block + Chain Ledger + Container hash.Hashable + Network uint32 + Hardforks map[string]uint32 + Natives []Contract + PolicyChecker PolicyChecker + Trigger trigger.Type + Block *block.Block // IsBlockPersisted denotes whether current Block was persisted by native // Ledger contract via PostPersist method. IsBlockPersisted bool @@ -89,13 +101,22 @@ func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, bas getContract func(*dao.Simple, util.Uint160) (*state.Contract, error), natives []Contract, loadTokenFunc func(ic *Context, id int32) error, block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context { - dao := d.GetPrivate() - cfg := bc.GetConfig() + var ( + dao = d.GetPrivate() + cfg = bc.GetConfig() + pch PolicyChecker + ) + for _, c := range natives { + if c.Metadata().Name == nativenames.Policy { + pch = c.(PolicyChecker) + } + } return &Context{ Chain: bc, Network: uint32(cfg.Magic), Hardforks: cfg.Hardforks, Natives: natives, + PolicyChecker: pch, Trigger: trigger, Block: block, Tx: tx, diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 283e89da7c..8932a6e6c1 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -8,9 +8,8 @@ import ( "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/interop" - "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" @@ -20,10 +19,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) -type policyChecker interface { - IsBlocked(*dao.Simple, util.Uint160) bool -} - // LoadToken calls method specified by the token id. func LoadToken(ic *interop.Context, id int32) error { ctx := ic.VM.Context() @@ -110,19 +105,25 @@ func callInternal(ic *interop.Context, cs *state.Contract, name string, f callfl // callExFromNative calls a contract with flags using the provided calling hash. func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract, name string, args []stackitem.Item, f callflag.CallFlag, hasReturn bool, isDynamic bool, callFromNative bool) error { - for _, nc := range ic.Natives { - if nc.Metadata().Name == nativenames.Policy { - var pch = nc.(policyChecker) - if pch.IsBlocked(ic.DAO, cs.Hash) { - return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE()) - } - break - } - } md := cs.Manifest.ABI.GetMethod(name, len(args)) if md == nil { return fmt.Errorf("method '%s' not found", name) } + var whitelisted bool + if ic.PolicyChecker != nil { + if ic.PolicyChecker.IsBlocked(ic.DAO, cs.Hash) { + return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE()) + } + if ic.IsHardforkEnabled(config.HFFaun) { + fee := ic.PolicyChecker.WhitelistedFee(ic.DAO, cs.Hash, md.Offset) + if fee >= 0 { + if !ic.VM.AddPicoGas(fee) { + return fmt.Errorf("%w during whitelisted contract execution", storage.ErrGasLimitExceeded) + } + whitelisted = true + } + } + } if len(args) != len(md.Parameters) { return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters)) @@ -165,7 +166,7 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra return nil } ic.VM.LoadNEFMethod(&cs.NEF, &cs.Manifest, caller, cs.Hash, f, - hasReturn, methodOff, initOff, onUnload) + hasReturn, methodOff, initOff, onUnload, whitelisted) for e, i := ic.VM.Estack(), len(args)-1; i >= 0; i-- { e.PushItem(args[i]) diff --git a/pkg/core/interop/runtime/ext_test.go b/pkg/core/interop/runtime/ext_test.go index fc39fcd61b..e6bc1503d0 100644 --- a/pkg/core/interop/runtime/ext_test.go +++ b/pkg/core/interop/runtime/ext_test.go @@ -637,7 +637,7 @@ func TestNotify(t *testing.T) { _, _, bc, cs := getDeployedInternal(t) ic, err := bc.GetTestVM(trigger.Application, nil, nil) require.NoError(t, err) - ic.VM.LoadNEFMethod(&cs.NEF, &cs.Manifest, caller, cs.Hash, callflag.NoneFlag, true, 0, -1, nil) + ic.VM.LoadNEFMethod(&cs.NEF, &cs.Manifest, caller, cs.Hash, callflag.NoneFlag, true, 0, -1, nil, false) ic.VM.Estack().PushVal(args) ic.VM.Estack().PushVal(name) return ic diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 4b1a2d3e7e..252728c4af 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -74,8 +74,9 @@ type ( // Methods required for proper cross-native communication. BlockAccountInternal(d *dao.Simple, hash util.Uint160) bool - IsBlocked(dao *dao.Simple, hash util.Uint160) bool GetMaxValidUntilBlockIncrementInternal(ic *interop.Context) uint32 + CleanWhitelist(ic *interop.Context, cs *state.Contract) error + interop.PolicyChecker } // IOracle is an interface required from native OracleContract contract for diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index cb059614e4..dd99c79bed 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -64,10 +64,13 @@ func Call(ic *interop.Context) error { return fmt.Errorf("missing call flags for native %d `%s` operation call: %05b vs %05b", version, m.MD.Name, ic.VM.Context().GetCallFlags(), reqFlags) } - invokeFee := m.CPUFee*ic.BaseExecFee() + - m.StorageFee*ic.BaseStorageFee() - if !ic.VM.AddPicoGas(invokeFee) { - return errors.New("gas limit exceeded") + // Filter out whitelisted contracts since the fee was already charged by the System.Contract.Call handler. + if !ic.IsHardforkEnabled(config.HFFaun) || + ic.PolicyChecker == nil || ic.PolicyChecker.WhitelistedFee(ic.DAO, curr, m.MD.Offset) == -1 { + invokeFee := m.CPUFee*ic.BaseExecFee() + m.StorageFee*ic.BaseStorageFee() + if !ic.VM.AddPicoGas(invokeFee) { + return errors.New("gas limit exceeded") + } } ctx := ic.VM.Context() args := make([]stackitem.Item, len(m.MD.Parameters)) diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 038915fc3c..1a8fc6eb7f 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -470,6 +470,11 @@ func (m *Management) Update(ic *interop.Context, hash util.Uint160, neff *nef.Fi return nil, errors.New("the contract reached the maximum number of updates") } + err = m.Policy.CleanWhitelist(ic, oldcontract) + if err != nil { + return nil, fmt.Errorf("failed to clean whitelist for %s: %w", oldcontract.Hash.StringLE(), err) + } + contract = *oldcontract // Make a copy, don't ruin (potentially) cached contract. // if NEF was provided, update the contract script if neff != nil { @@ -498,14 +503,18 @@ func (m *Management) Update(ic *interop.Context, hash util.Uint160, neff *nef.Fi return &contract, nil } -// destroy is an implementation of destroy update method, it's run under +// destroy is an implementation of the public destroy method, it's run under // VM protections, so it's OK for it to panic instead of returning errors. func (m *Management) destroy(ic *interop.Context, sis []stackitem.Item) stackitem.Item { hash := ic.VM.GetCallingScriptHash() - err := m.Destroy(ic.DAO, hash) + cs, err := m.Destroy(ic.DAO, hash) if err != nil { panic(err) } + err = m.Policy.CleanWhitelist(ic, cs) + if err != nil { + panic(fmt.Errorf("failed to clean whitelist for %s: %w", cs.Hash.StringLE(), err)) + } err = m.emitNotification(ic, contractDestroyNotificationName, hash) if err != nil { panic(err) @@ -514,10 +523,10 @@ func (m *Management) destroy(ic *interop.Context, sis []stackitem.Item) stackite } // Destroy drops the given contract from DAO along with its storage. It doesn't emit notification. -func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) error { +func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) (*state.Contract, error) { contract, err := GetContract(d, m.ID, hash) if err != nil { - return err + return nil, err } key := MakeContractKey(hash) d.DeleteStorageItem(m.ID, key) @@ -530,7 +539,7 @@ func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) error { }) m.Policy.BlockAccountInternal(d, hash) markUpdated(d, m.ID, hash, nil) - return nil + return contract, nil } func (m *Management) getMinimumDeploymentFee(ic *interop.Context, args []stackitem.Item) stackitem.Item { diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go index cdf33ceaf8..e05f6bac46 100644 --- a/pkg/core/native/management_test.go +++ b/pkg/core/native/management_test.go @@ -72,7 +72,7 @@ func TestDeployGetUpdateDestroyContract(t *testing.T) { require.NoError(t, err) require.Equal(t, refContract, upContract) - err = mgmt.Destroy(d, h) + _, err = mgmt.Destroy(d, h) require.NoError(t, err) _, err = GetContract(d, mgmt.ID, h) require.Error(t, err) diff --git a/pkg/core/native/native_test/management_test.go b/pkg/core/native/native_test/management_test.go index c05077bf5c..9a39dd1727 100644 --- a/pkg/core/native/native_test/management_test.go +++ b/pkg/core/native/native_test/management_test.go @@ -5,10 +5,12 @@ import ( "encoding/json" "fmt" "slices" + "strings" "testing" ojson "github.com/nspcc-dev/go-ordered-json" "github.com/nspcc-dev/neo-go/internal/contracts" + "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" @@ -71,7 +73,7 @@ var ( // under assumption that hardforks from Aspidochelone to Faun (included) are enabled. faunCSS = map[string]string{ nativenames.StdLib: `{"id":-2,"hash":"0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":2426471238},"manifest":{"name":"StdLib","abi":{"methods":[{"name":"atoi","offset":0,"parameters":[{"name":"value","type":"String"}],"returntype":"Integer","safe":true},{"name":"atoi","offset":7,"parameters":[{"name":"value","type":"String"},{"name":"base","type":"Integer"}],"returntype":"Integer","safe":true},{"name":"base58CheckDecode","offset":14,"parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","safe":true},{"name":"base58CheckEncode","offset":21,"parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"base58Decode","offset":28,"parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","safe":true},{"name":"base58Encode","offset":35,"parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"base64Decode","offset":42,"parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","safe":true},{"name":"base64Encode","offset":49,"parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"base64UrlDecode","offset":56,"parameters":[{"name":"s","type":"String"}],"returntype":"String","safe":true},{"name":"base64UrlEncode","offset":63,"parameters":[{"name":"data","type":"String"}],"returntype":"String","safe":true},{"name":"deserialize","offset":70,"parameters":[{"name":"data","type":"ByteArray"}],"returntype":"Any","safe":true},{"name":"hexDecode","offset":77,"parameters":[{"name":"str","type":"String"}],"returntype":"ByteArray","safe":true},{"name":"hexEncode","offset":84,"parameters":[{"name":"bytes","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"itoa","offset":91,"parameters":[{"name":"value","type":"Integer"}],"returntype":"String","safe":true},{"name":"itoa","offset":98,"parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","safe":true},{"name":"jsonDeserialize","offset":105,"parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","safe":true},{"name":"jsonSerialize","offset":112,"parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","safe":true},{"name":"memoryCompare","offset":119,"parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","safe":true},{"name":"memorySearch","offset":126,"parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","safe":true},{"name":"memorySearch","offset":133,"parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","safe":true},{"name":"memorySearch","offset":140,"parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","safe":true},{"name":"serialize","offset":147,"parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","safe":true},{"name":"strLen","offset":154,"parameters":[{"name":"str","type":"String"}],"returntype":"Integer","safe":true},{"name":"stringSplit","offset":161,"parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","safe":true},{"name":"stringSplit","offset":168,"parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"},{"name":"removeEmptyEntries","type":"Boolean"}],"returntype":"Array","safe":true}],"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":65467259},"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":"getBlockedAccounts","offset":14,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"getExecFeeFactor","offset":21,"parameters":[],"returntype":"Integer","safe":true},{"name":"getExecPicoFeeFactor","offset":28,"parameters":[],"returntype":"Integer","safe":true},{"name":"getFeePerByte","offset":35,"parameters":[],"returntype":"Integer","safe":true},{"name":"getMaxTraceableBlocks","offset":42,"parameters":[],"returntype":"Integer","safe":true},{"name":"getMaxValidUntilBlockIncrement","offset":49,"parameters":[],"returntype":"Integer","safe":true},{"name":"getMillisecondsPerBlock","offset":56,"parameters":[],"returntype":"Integer","safe":true},{"name":"getStoragePrice","offset":63,"parameters":[],"returntype":"Integer","safe":true},{"name":"isBlocked","offset":70,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","safe":true},{"name":"setAttributeFee","offset":77,"parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setExecFeeFactor","offset":84,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setFeePerByte","offset":91,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setMaxTraceableBlocks","offset":98,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setMaxValidUntilBlockIncrement","offset":105,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setMillisecondsPerBlock","offset":112,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setStoragePrice","offset":119,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"unblockAccount","offset":126,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]}]},"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":751055395},"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":"getBlockedAccounts","offset":14,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"getExecFeeFactor","offset":21,"parameters":[],"returntype":"Integer","safe":true},{"name":"getExecPicoFeeFactor","offset":28,"parameters":[],"returntype":"Integer","safe":true},{"name":"getFeePerByte","offset":35,"parameters":[],"returntype":"Integer","safe":true},{"name":"getMaxTraceableBlocks","offset":42,"parameters":[],"returntype":"Integer","safe":true},{"name":"getMaxValidUntilBlockIncrement","offset":49,"parameters":[],"returntype":"Integer","safe":true},{"name":"getMillisecondsPerBlock","offset":56,"parameters":[],"returntype":"Integer","safe":true},{"name":"getStoragePrice","offset":63,"parameters":[],"returntype":"Integer","safe":true},{"name":"getWhitelistFeeContracts","offset":70,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"isBlocked","offset":77,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","safe":true},{"name":"removeWhitelistFeeContract","offset":84,"parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setAttributeFee","offset":91,"parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setExecFeeFactor","offset":98,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setFeePerByte","offset":105,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setMaxTraceableBlocks","offset":112,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setMaxValidUntilBlockIncrement","offset":119,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setMillisecondsPerBlock","offset":126,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setStoragePrice","offset":133,"parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setWhitelistFeeContract","offset":140,"parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fixedFee","type":"Integer"}],"returntype":"Void","safe":false},{"name":"unblockAccount","offset":147,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]},{"name":"WhitelistFeeChanged","parameters":[{"name":"contract","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fee","type":"Any"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, } ) @@ -1108,3 +1110,96 @@ func TestManagement_ContractDestroy(t *testing.T) { managementInvoker.InvokeFail(t, fmt.Sprintf("the contract %s has been blocked", cs1.Hash.StringLE()), "deploy", nefBytes, manifestBytes) }) } + +func TestManagement_WhitelistedUpdate(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(cfg *config.Blockchain) { + cfg.Hardforks = map[string]uint32{ + config.HFFaun.String(): 0, + } + }) + e := neotest.NewExecutor(t, bc, acc, acc) + m := e.CommitteeInvoker(nativehashes.ContractManagement) + p := e.CommitteeInvoker(nativehashes.PolicyContract) + src := `package free + import "github.com/nspcc-dev/neo-go/pkg/interop/native/management" + func FreeInc(i int) int { + i++ + return i + } + func Update(m []byte) { + management.Update(nil, m) + } + func Destroy() { + management.Destroy() + }` + + ctr := neotest.CompileSource(t, e.Validator.ScriptHash(), strings.NewReader(src), &compiler.Options{ + Name: "free contract", + Permissions: []manifest.Permission{ + { + Methods: manifest.WildStrings{ + Value: []string{"update", "destroy"}, + }, + }, + }, + }) + m.DeployContract(t, ctr, nil) + + // Whitelist some method and check execution price and notifications. + ctrInvoker := e.NewInvoker(ctr.Hash, e.Committee) + h1 := ctrInvoker.Invoke(t, stackitem.Make(2), "freeInc", 1) // reference invocation. + txWhitelist := p.Invoke(t, stackitem.Null{}, "setWhitelistFeeContract", ctr.Hash, "freeInc", 1, 0) + h2 := ctrInvoker.Invoke(t, stackitem.Make(2), "freeInc", 1) // free invocation. + res1 := e.GetTxExecResult(t, h1) + res2 := e.GetTxExecResult(t, h2) + require.Less(t, res2.GasConsumed, res1.GasConsumed) + e.CheckTxNotificationEvent(t, txWhitelist, 0, state.NotificationEvent{ + ScriptHash: nativehashes.PolicyContract, + Name: "WhitelistFeeChanged", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(ctr.Hash.BytesBE()), + stackitem.Make("freeInc"), + stackitem.Make(1), + stackitem.Make(0), + }), + }) + + // Update this contract and check that whitelist is cleared. + ctr.Manifest.Permissions = []manifest.Permission{ + { + Methods: manifest.WildStrings{ + Value: []string{"update", "destroy", "someNewMethod"}, + }, + }, + } + rawManifest, err := json.Marshal(ctr.Manifest) + require.NoError(t, err) + txUpdate := ctrInvoker.Invoke(t, stackitem.Null{}, "update", stackitem.Make(rawManifest)) // update invalidates all whitelisted methods of the contract. + h3 := ctrInvoker.Invoke(t, stackitem.Make(2), "freeInc", 1) // non-free invocation. + res3 := e.GetTxExecResult(t, h3) + require.Equal(t, res1.GasConsumed, res3.GasConsumed) + e.CheckTxNotificationEvent(t, txUpdate, 0, state.NotificationEvent{ + ScriptHash: nativehashes.PolicyContract, + Name: "WhitelistFeeChanged", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(ctr.Hash.BytesBE()), + stackitem.Make("freeInc"), + stackitem.Make(1), + stackitem.Null{}, + }), + }) + + // Set the whitelist one more time and destroy the contract, check that whitelist is cleared. + p.Invoke(t, stackitem.Null{}, "setWhitelistFeeContract", ctr.Hash, "freeInc", 1, 0) + txDestroy := ctrInvoker.Invoke(t, stackitem.Null{}, "destroy") // destroy invalidates all whitelisted methods of the contract. + e.CheckTxNotificationEvent(t, txDestroy, 0, state.NotificationEvent{ + ScriptHash: nativehashes.PolicyContract, + Name: "WhitelistFeeChanged", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(ctr.Hash.BytesBE()), + stackitem.Make("freeInc"), + stackitem.Make(1), + stackitem.Null{}, + }), + }) +} diff --git a/pkg/core/native/native_test/policy_test.go b/pkg/core/native/native_test/policy_test.go index 3913177983..1c134e8d83 100644 --- a/pkg/core/native/native_test/policy_test.go +++ b/pkg/core/native/native_test/policy_test.go @@ -15,11 +15,13 @@ import ( "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/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -514,3 +516,163 @@ func TestPolicy_ExecPicoFeeFactor_InteropAPI(t *testing.T) { ctrInvoker.Invoke(t, stackitem.Make(0), "getFactor") ctrInvoker.Invoke(t, stackitem.Make(5), "getPicoFactor") } + +func TestPolicy_WhitelistContracts(t *testing.T) { + const faunHeight = 4 + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) { + c.Hardforks = map[string]uint32{ + config.HFFaun.String(): faunHeight, + } + }) + e := neotest.NewExecutor(t, bc, acc, acc) + p := e.CommitteeInvoker(nativehashes.PolicyContract) + + // Invoke before Faun should fail. + p.InvokeFail(t, "System.Contract.Call failed: method not found: getWhitelistFeeContracts/1", "getWhitelistFeeContracts", nativehashes.StdLib) + p.InvokeFail(t, "System.Contract.Call failed: method not found: setWhitelistFeeContract/4", "setWhitelistFeeContract", nativehashes.StdLib, "hexEncode", 1, 0) + p.InvokeFail(t, "System.Contract.Call failed: method not found: removeWhitelistFeeContract/1", "removeWhitelistFeeContract", nativehashes.StdLib) + + for e.Chain.BlockHeight() < faunHeight { + e.AddNewBlock(t) + } + + // Invoke at/after Faun should succeed. + h := p.Invoke(t, stackitem.Null{}, "setWhitelistFeeContract", nativehashes.StdLib, "hexEncode", 1, 0) + e.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: nativehashes.PolicyContract, + Name: "WhitelistFeeChanged", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(nativehashes.StdLib), + stackitem.Make("hexEncode"), + stackitem.Make(1), + stackitem.Make(0), + }), + }) + p.Invoke(t, stackitem.Null{}, "setWhitelistFeeContract", nativehashes.StdLib, "hexDecode", 1, 1) + + checkGetWhitelisted := func(t *testing.T, expected []stackitem.Item) { + for i := range len(expected) + 1 { + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, p.Hash, "getWhitelistFeeContracts", callflag.All) + for range i + 1 { + emit.Opcodes(w.BinWriter, opcode.DUP) + emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext) + emit.Opcodes(w.BinWriter, opcode.DROP) // drop the value returned from Next. + } + emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue) + require.NoError(t, w.Err) + h := p.InvokeScript(t, w.Bytes(), p.Signers) + if i < len(expected) { + e.CheckHalt(t, h, expected[i]) + } else { + e.CheckFault(t, h, "iterator index out of range") // ensure there are no extra elements. + } + w.Reset() + } + } + checkGetWhitelisted(t, []stackitem.Item{ + stackitem.Make(append(nativehashes.StdLib.BytesBE(), 0, 0, 0, 77)), + stackitem.Make(append(nativehashes.StdLib.BytesBE(), 0, 0, 0, 84)), + }) + + // Set: negative fee. + p.InvokeFail(t, "fee should be positive", "setWhitelistFeeContract", nativehashes.StdLib, "base64Encode", 1, -1) + + // Set: not signed by committee. + p1 := e.NewInvoker(nativehashes.PolicyContract, e.NewAccount(t)) + p1.InvokeFail(t, "invalid committee signature", "setWhitelistFeeContract", nativehashes.StdLib, "base64Encode", 1, 0) + + // Set: unknown contract. + p.InvokeFail(t, "not found: key not found", "setWhitelistFeeContract", util.Uint160{1, 2, 3}, "base64Encode", 1, 0) + + // Set: unknown method. + p.InvokeFail(t, "method not found: base64Encode/8", "setWhitelistFeeContract", nativehashes.StdLib, "base64Encode", 8, 0) + + // Remove: not signed by committee. + p1.InvokeFail(t, "invalid committee signature", "removeWhitelistFeeContract", nativehashes.StdLib, "hexEncode", 1) + + // Remove: non-whitelisted. + p.InvokeFail(t, fmt.Sprintf("whitelist for %s/49 not found", nativehashes.StdLib.StringLE()), "removeWhitelistFeeContract", nativehashes.StdLib, "base64Encode", 1) + + // Remove: good. + h = p.Invoke(t, stackitem.Null{}, "removeWhitelistFeeContract", nativehashes.StdLib, "hexDecode", 1) + checkGetWhitelisted(t, []stackitem.Item{ + stackitem.Make(append(nativehashes.StdLib.BytesBE(), 0, 0, 0, 84)), + }) + e.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: nativehashes.PolicyContract, + Name: "WhitelistFeeChanged", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make(nativehashes.StdLib), + stackitem.Make("hexDecode"), + stackitem.Make(1), + stackitem.Null{}, + }), + }) +} + +func TestPolicy_WhitelistContractsInteropAPI(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) { + c.Hardforks = map[string]uint32{ + config.HFFaun.String(): 0, + } + }) + e := neotest.NewExecutor(t, bc, acc, acc) + + src := `package policywrapper + import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/native/policy" + "github.com/nspcc-dev/neo-go/pkg/interop/iterator" + ) + func SetWhitelistFeeContract(contract interop.Hash160, method string, argCnt int, fee int) { + policy.SetWhitelistFeeContract(contract, method, argCnt, fee) + } + func RemoveWhitelistFeeContract(contract interop.Hash160, method string, argCnt int) { + policy.RemoveWhitelistFeeContract(contract, method, argCnt) + } + func GetWhitelistedContracts() [][]byte { + i := policy.GetWhitelistFeeContracts() + var res [][]byte + for iterator.Next(i) { + res = append(res, iterator.Value(i).([]byte)) + } + return res + }` + + ctr := neotest.CompileSource(t, e.Validator.ScriptHash(), strings.NewReader(src), &compiler.Options{ + Name: "whitelisted wrapper", + Permissions: []manifest.Permission{ + { + Methods: manifest.WildStrings{ + Value: []string{"setWhitelistFeeContract", "removeWhitelistFeeContract", "getWhitelistedContracts"}, + }, + }, + }, + }) + e.DeployContract(t, ctr, nil) + + var stdM *manifest.Manifest + for _, cs := range e.Chain.GetNatives() { + if cs.Manifest.Name == nativenames.StdLib { + stdM = &cs.Manifest + break + } + } + m1 := stdM.ABI.GetMethod("hexDecode", 1) + m2 := stdM.ABI.GetMethod("hexEncode", 1) + require.NotNil(t, m1) + require.NotNil(t, m2) + + ctrInvoker := e.NewInvoker(ctr.Hash, e.Committee) + ctrInvoker.Invoke(t, stackitem.Null{}, "setWhitelistFeeContract", nativehashes.StdLib, "hexEncode", 1, 0) + ctrInvoker.Invoke(t, stackitem.Null{}, "setWhitelistFeeContract", nativehashes.StdLib, "hexDecode", 1, 0) + ctrInvoker.Invoke(t, stackitem.Make([]stackitem.Item{ + stackitem.NewBuffer(append(nativehashes.StdLib.BytesBE(), 0, 0, 0, byte(m1.Offset))), + stackitem.NewBuffer(append(nativehashes.StdLib.BytesBE(), 0, 0, 0, byte(m2.Offset))), + }), "getWhitelistedContracts") + ctrInvoker.Invoke(t, stackitem.Null{}, "removeWhitelistFeeContract", nativehashes.StdLib, "hexEncode", 1) + ctrInvoker.Invoke(t, stackitem.Make([]stackitem.Item{ + stackitem.NewBuffer(append(nativehashes.StdLib.BytesBE(), 0, 0, 0, byte(m1.Offset))), + }), "getWhitelistedContracts") +} diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index f9e2ce8c6a..c9c2b082c5 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -1,6 +1,8 @@ package native import ( + "cmp" + "encoding/binary" "encoding/hex" "fmt" "maps" @@ -53,6 +55,8 @@ const ( // blockedAccountPrefix is a prefix used to store blocked account. blockedAccountPrefix = 15 + // whitelistedFeeContractPrefix is a prefix used to store whitelisted contract. + whitelistedFeeContractPrefix = 16 // attributeFeePrefix is a prefix used to store attribute fee. attributeFeePrefix = 20 ) @@ -80,16 +84,48 @@ type Policy struct { } type PolicyCache struct { - execFeeFactor uint32 - faunInitialized bool - feePerByte int64 - maxVerificationGas int64 - storagePrice uint32 - msPerBlock uint32 - maxVUBIncrement uint32 - maxTraceableBlocks uint32 - attributeFee map[transaction.AttrType]uint32 - blockedAccounts []util.Uint160 + execFeeFactor uint32 + faunInitialized bool + feePerByte int64 + maxVerificationGas int64 + storagePrice uint32 + msPerBlock uint32 + maxVUBIncrement uint32 + maxTraceableBlocks uint32 + attributeFee map[transaction.AttrType]uint32 + blockedAccounts []util.Uint160 + whitelistedContracts []whitelistedContract +} + +// whitelistedContract is a structure representing a whitelisted contract with +// the pre-defined execution price. +type whitelistedContract struct { + hash util.Uint160 + offset uint32 + fee int64 +} + +var _ = (stackitem.Convertible)(&whitelistedContract{}) + +// Compare compares two whitelistedContract structures in the order that matches +// a whitelisted contract key serialization format. +func (c whitelistedContract) Compare(other whitelistedContract) int { + return cmp.Or( + c.hash.Compare(other.hash), + cmp.Compare(c.offset, other.offset), // offset is stored in BE. + ) +} + +// ToStackItem implements [stackitem.Convertible] interface. +func (c whitelistedContract) ToStackItem() (stackitem.Item, error) { + return stackitem.NewByteArray(makeWhitelistedKey(c.hash, c.offset)[1:]), // strip prefix. + nil +} + +// FromStackItem implements [stackitem.Convertible] interface. Not really needed +// since whitelistedContract conversion is one-directional only. +func (c *whitelistedContract) FromStackItem(item stackitem.Item) error { + panic("not supported") } var ( @@ -108,6 +144,7 @@ func copyPolicyCache(src, dst *PolicyCache) { *dst = *src dst.attributeFee = maps.Clone(src.attributeFee) dst.blockedAccounts = slices.Clone(src.blockedAccounts) + dst.whitelistedContracts = slices.Clone(src.whitelistedContracts) } // newPolicy returns Policy native contract. @@ -221,6 +258,34 @@ func newPolicy() *Policy { md = NewMethodAndPrice(p.getExecPicoFeeFactor, 1<<15, callflag.ReadStates, config.HFFaun) p.AddMethod(md, desc) + desc = NewDescriptor("setWhitelistFeeContract", smartcontract.VoidType, + manifest.NewParameter("contractHash", smartcontract.Hash160Type), + manifest.NewParameter("method", smartcontract.StringType), + manifest.NewParameter("argCount", smartcontract.IntegerType), + manifest.NewParameter("fixedFee", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.setWhitelistFeeContract, 1<<15, callflag.States|callflag.AllowNotify, config.HFFaun) + p.AddMethod(md, desc) + + desc = NewDescriptor("removeWhitelistFeeContract", smartcontract.VoidType, + manifest.NewParameter("contractHash", smartcontract.Hash160Type), + manifest.NewParameter("method", smartcontract.StringType), + manifest.NewParameter("argCount", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.removeWhitelistFeeContract, 1<<15, callflag.States|callflag.AllowNotify, config.HFFaun) + p.AddMethod(md, desc) + + desc = NewDescriptor("getWhitelistFeeContracts", smartcontract.InteropInterfaceType) + md = NewMethodAndPrice(p.getWhitelistFeeContracts, 1<<15, callflag.ReadStates, config.HFFaun) + p.AddMethod(md, desc) + + eDesc = NewEventDescriptor("WhitelistFeeChanged", + manifest.NewParameter("contract", smartcontract.Hash160Type), + manifest.NewParameter("method", smartcontract.StringType), + manifest.NewParameter("argCount", smartcontract.IntegerType), + manifest.NewParameter("fee", smartcontract.AnyType), + ) + eMD = NewEvent(eDesc, config.HFFaun) + p.AddEvent(eMD) + return p } @@ -237,12 +302,13 @@ func (p *Policy) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *int 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), + execFeeFactor: defaultExecFeeFactor, + feePerByte: defaultFeePerByte, + maxVerificationGas: defaultMaxVerificationGas, + storagePrice: DefaultStoragePrice, + attributeFee: map[transaction.AttrType]uint32{}, + blockedAccounts: make([]util.Uint160, 0), + whitelistedContracts: make([]whitelistedContract, 0), } ic.DAO.SetCache(p.ID, cache) } @@ -333,9 +399,36 @@ func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple, isHardforkE cache.maxTraceableBlocks = uint32(getIntWithKey(p.ID, d, MaxTraceableBlocksKey)) } + cache.whitelistedContracts = make([]whitelistedContract, 0) var faun = config.HFFaun if isHardforkEnabled(&faun, blockHeight) { cache.faunInitialized = true + d.Seek(p.ID, storage.SeekRange{Prefix: []byte{whitelistedFeeContractPrefix}}, func(k, v []byte) bool { + if len(k) != util.Uint160Size+4 { + fErr = fmt.Errorf("unexpected whitelisted contract key length %d vs %d", len(k), util.Uint160Size+4) + return false + } + h, err := util.Uint160DecodeBytesBE(k[:util.Uint160Size]) + if err != nil { + fErr = fmt.Errorf("failed to decode whitelisted contract hash: %w", err) + return false + } + offset := binary.BigEndian.Uint32(k[util.Uint160Size:]) + value := bigint.FromBytes(v) + if value == nil { + fErr = fmt.Errorf("unexpected whitelisted contract fee format: key=%s, value=%s", hex.EncodeToString(k), hex.EncodeToString(v)) + return false + } + cache.whitelistedContracts = append(cache.whitelistedContracts, whitelistedContract{ + hash: h, + offset: offset, + fee: value.Int64(), + }) + return true + }) + if fErr != nil { + return fmt.Errorf("failed to initialize whitelisted contracts: %w", fErr) + } } return nil @@ -486,7 +579,7 @@ func (p *Policy) getAttributeFeeGeneric(ic *interop.Context, args []stackitem.It func (p *Policy) getBlockedAccounts(ic *interop.Context, _ []stackitem.Item) stackitem.Item { cache := ic.DAO.GetROCache(p.ID).(*PolicyCache) cloned := slices.Clone(cache.blockedAccounts) - return stackitem.NewInterop(&iterator{keys: cloned}) + return stackitem.NewInterop(&iterator[util.Uint160]{keys: cloned}) } // GetAttributeFeeInternal returns required transaction's attribute fee. @@ -713,15 +806,170 @@ func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error { return nil } -// iterator provides an iterator over a slice of keys. -type iterator struct { - keys []util.Uint160 +// WhitelistedFee checks whether the specified contract method is whitelisted and +// returns a non-negative execution fee in picoGAS units if so. It always uses +// native cache, hence cache is expected to be initialized by this moment. +func (p *Policy) WhitelistedFee(dao *dao.Simple, hash util.Uint160, offset int) int64 { + cache := dao.GetROCache(p.ID).(*PolicyCache) + i, ok := slices.BinarySearchFunc(cache.whitelistedContracts, whitelistedContract{ + hash: hash, + offset: uint32(offset), + }, whitelistedContract.Compare) + if !ok { + return -1 + } + return cache.whitelistedContracts[i].fee * vm.ExecFeeFactorMultiplier +} + +// CleanWhitelist removes a contract with the specified hash from the list of +// whitelisted contracts. +func (p *Policy) CleanWhitelist(ic *interop.Context, cs *state.Contract) error { + var ( + cache = ic.DAO.GetRWCache(p.ID).(*PolicyCache) + err error + ) + cache.whitelistedContracts = slices.DeleteFunc(cache.whitelistedContracts, func(c whitelistedContract) bool { + if err != nil { + return false + } + if !c.hash.Equals(cs.Hash) { + return false + } + k := makeWhitelistedKey(c.hash, c.offset) + ic.DAO.DeleteStorageItem(p.ID, k) + m := cs.Manifest.ABI.GetMethodByOffset(int(c.offset)) + if m == nil { + err = fmt.Errorf("method with offset %d not found in contract %s", c.offset, cs.Hash.StringLE()) + return false + } + err = ic.AddNotification(p.Hash, "WhitelistFeeChanged", stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(cs.Hash.BytesBE()), + stackitem.NewByteArray([]byte(m.Name)), + stackitem.NewBigInteger(big.NewInt(int64(len(m.Parameters)))), + stackitem.Null{}, + })) + + return err == nil + }) + + return err +} + +func (p *Policy) removeWhitelistFeeContract(ic *interop.Context, args []stackitem.Item) stackitem.Item { + if !p.NEO.CheckCommittee(ic) { + panic("invalid committee signature") + } + h := toUint160(args[0]) + method := toString(args[1]) + argCnt := int(toInt64(args[2])) + + cs, err := ic.GetContract(h) + if err != nil { + panic(fmt.Errorf("failed to get contract %s: %w", h.StringLE(), err)) + } + md := cs.Manifest.ABI.GetMethod(method, argCnt) + if md == nil { + panic(fmt.Errorf("method not found: %s/%d", method, argCnt)) + } + + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) + k := makeWhitelistedKey(h, uint32(md.Offset)) + i, ok := slices.BinarySearchFunc(cache.whitelistedContracts, whitelistedContract{ + hash: h, + offset: uint32(md.Offset), + }, whitelistedContract.Compare) + if !ok { + panic(fmt.Errorf("whitelist for %s/%d not found", h.StringLE(), md.Offset)) + } + ic.DAO.DeleteStorageItem(p.ID, k) + cache.whitelistedContracts = slices.Delete(cache.whitelistedContracts, i, i+1) + + err = ic.AddNotification(p.Hash, "WhitelistFeeChanged", stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(h.BytesBE()), + stackitem.NewByteArray([]byte(method)), + stackitem.NewBigInteger(big.NewInt(int64(argCnt))), + stackitem.Null{}, + })) + if err != nil { + panic(err) + } + + return stackitem.Null{} +} + +func (p *Policy) setWhitelistFeeContract(ic *interop.Context, args []stackitem.Item) stackitem.Item { + h := toUint160(args[0]) + method := toString(args[1]) + argCnt := int(toInt64(args[2])) + fee := toInt64(args[3]) + if fee < 0 { + panic(fmt.Errorf("fee should be positive, got %d", fee)) + } + if !p.NEO.CheckCommittee(ic) { + panic("invalid committee signature") + } + + cs, err := ic.GetContract(h) + if err != nil { + panic(fmt.Errorf("contract %s not found: %w", h.StringLE(), err)) + } + md := cs.Manifest.ABI.GetMethod(method, argCnt) + if md == nil { + panic(fmt.Errorf("contract %s: method not found: %s/%d", h.StringLE(), method, argCnt)) + } + + setIntWithKey(p.ID, ic.DAO, makeWhitelistedKey(h, uint32(md.Offset)), fee) + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) + c := whitelistedContract{ + hash: h, + offset: uint32(md.Offset), + fee: fee, + } + i, ok := slices.BinarySearchFunc(cache.whitelistedContracts, c, whitelistedContract.Compare) + if !ok { + cache.whitelistedContracts = slices.Insert(cache.whitelistedContracts, i, c) + } + + err = ic.AddNotification(p.Hash, "WhitelistFeeChanged", stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(h.BytesBE()), + stackitem.NewByteArray([]byte(method)), + stackitem.NewBigInteger(big.NewInt(int64(argCnt))), + stackitem.NewBigInteger(big.NewInt(fee)), + })) + if err != nil { + panic(err) + } + + return stackitem.Null{} +} + +func (p *Policy) getWhitelistFeeContracts(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + cache := ic.DAO.GetROCache(p.ID).(*PolicyCache) + res := make([]stackitem.Item, len(cache.whitelistedContracts)) + for i, c := range cache.whitelistedContracts { + res[i], _ = c.ToStackItem() // never returns an error. + } + return stackitem.NewInterop(&iterator[stackitem.Item]{keys: res}) +} + +func makeWhitelistedKey(h util.Uint160, offset uint32) []byte { + k := make([]byte, 1+util.Uint160Size+4) + k[0] = whitelistedFeeContractPrefix + copy(k[1:], h.BytesBE()) + binary.BigEndian.PutUint32(k[1+util.Uint160Size:], uint32(offset)) + return k +} + +// iterator provides an iterator over a slice of T. T must be convertable to +// [stackitem.Item] via [stackitem.Make]. +type iterator[T any] struct { + keys []T next bool } // Next advances the iterator and returns true if Value can be called at the // current position. -func (i *iterator) Next() bool { +func (i *iterator[T]) Next() bool { if i.next { i.keys = i.keys[1:] } @@ -730,7 +978,7 @@ func (i *iterator) Next() bool { } // Value returns current iterators value. -func (i *iterator) Value() stackitem.Item { +func (i *iterator[T]) Value() stackitem.Item { if !i.next { panic("iterator index out of range") } diff --git a/pkg/interop/native/policy/policy.go b/pkg/interop/native/policy/policy.go index 32414187f4..edf5137aea 100644 --- a/pkg/interop/native/policy/policy.go +++ b/pkg/interop/native/policy/policy.go @@ -108,3 +108,23 @@ func SetMillisecondsPerBlock(value int) { func GetBlockedAccounts() iterator.Iterator { return neogointernal.CallWithToken(Hash, "getBlockedAccounts", int(contract.ReadStates)).(iterator.Iterator) } + +// SetWhitelistFeeContract represents the `setWhitelistFeeContract` method of Policy native contract. +// Note that this method is available starting from [config.HFFaun] hardfork. +func SetWhitelistFeeContract(hash interop.Hash160, method string, argCnt int, fixedFee int) { + neogointernal.CallWithTokenNoRet(Hash, "setWhitelistFeeContract", int(contract.States|contract.AllowNotify), + hash, method, argCnt, fixedFee) +} + +// RemoveWhitelistFeeContract represents the `removeWhitelistFeeContract` method of Policy native contract. +// Note that this method is available starting from [config.HFFaun] hardfork. +func RemoveWhitelistFeeContract(hash interop.Hash160, method string, argCnt int) { + neogointernal.CallWithTokenNoRet(Hash, "removeWhitelistFeeContract", int(contract.States|contract.AllowNotify), + hash, method, argCnt) +} + +// GetWhitelistFeeContracts represents the `getWhitelistFeeContracts` method of Policy native contract. +// Note that this method is available starting from [config.HFFaun] hardfork. +func GetWhitelistFeeContracts() iterator.Iterator { + return neogointernal.CallWithToken(Hash, "getWhitelistFeeContracts", int(contract.ReadStates)).(iterator.Iterator) +} diff --git a/pkg/rpcclient/policy/policy.go b/pkg/rpcclient/policy/policy.go index 145c6cc8e5..ff87c7e3c9 100644 --- a/pkg/rpcclient/policy/policy.go +++ b/pkg/rpcclient/policy/policy.go @@ -7,6 +7,8 @@ various methods to perform PolicyContract state-changing calls. package policy import ( + "encoding/binary" + "errors" "fmt" "github.com/google/uuid" @@ -72,6 +74,30 @@ type BlockedAccountsIterator struct { iterator result.Iterator } +// WhitelistedContractIterator is used for iterating over GetWhitelistFeeContracts results. +type WhitelistedContractIterator struct { + client Invoker + session uuid.UUID + iterator result.Iterator +} + +// WhitelistedContract represents a whitelisted contract method with the fixed +// execution cost. +type WhitelistedContract struct { + Hash util.Uint160 + Offset uint32 +} + +// WhitelistFeeChangedEvent represents a WhitelistFeeChanged Policy event. +type WhitelistFeeChangedEvent struct { + Hash util.Uint160 + Method string + ArgCnt int + // Fee is the fixed execution cost of the method in Datoshi units (it's not + // set if the method is removed from the whitelist). + Fee *int64 +} + // NewReader creates an instance of ContractReader that can be used to read // data from the contract. func NewReader(invoker Invoker) *ContractReader { @@ -392,3 +418,184 @@ func unblockScript(account util.Uint160) []byte { script, _ := smartcontract.CreateCallWithAssertScript(Hash, "unblockAccount", account) return script } + +// SetWhitelistFeeContract creates and sends a transaction that adds a specified +// contract method to the native Policy whitelist with the fixed execution fee. +// It uses the `setWhitelistFeeContract` method which is active starting from +// [config.HFFaun] hardfork. The returned values are transaction hash, its +// ValidUntilBlock value and an error if any. +func (c *Contract) SetWhitelistFeeContract(h util.Uint160, method string, argCnt int, fee int) (util.Uint256, uint32, error) { + return c.actor.SendRun(setWhitelistFeeContractScript(h, method, argCnt, fee)) +} + +// SetWhitelistFeeContractTransaction creates a transaction that adds a +// specified contract method to the native Policy whitelist with the fixed +// execution fee. It uses the `setWhitelistFeeContract` method which is active +// starting from [config.HFFaun] hardfork. This transaction is signed but not +// sent to the network, instead it's returned to the caller. +func (c *Contract) SetWhitelistFeeContractTransaction(h util.Uint160, method string, argCnt int, fee int) (*transaction.Transaction, error) { + return c.actor.MakeRun(setWhitelistFeeContractScript(h, method, argCnt, fee)) +} + +// SetWhitelistFeeContractUnsigned creates a transaction that adds a specified +// contract method to the native Policy whitelist with the fixed execution fee. +// It uses the `setWhitelistFeeContract` method which is active starting from +// [config.HFFaun] hardfork. This transaction is not signed and just returned to +// the caller. +func (c *Contract) SetWhitelistFeeContractUnsigned(h util.Uint160, method string, argCnt int, fee int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedRun(setWhitelistFeeContractScript(h, method, argCnt, fee), nil) +} + +func setWhitelistFeeContractScript(h util.Uint160, method string, argCnt int, fee int) []byte { + // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. + script, _ := smartcontract.CreateCallScript(Hash, "setWhitelistFeeContract", h, method, argCnt, fee) + return script +} + +// RemoveWhitelistFeeContract creates and sends a transaction that removes a +// specified contract method from the native Policy whitelist with the fixed +// execution fee. It uses the `removeWhitelistFeeContract` method which is +// active starting from [config.HFFaun] hardfork. The returned values are +// transaction hash, its ValidUntilBlock value and an error if any. +func (c *Contract) RemoveWhitelistFeeContract(h util.Uint160, method string, argCnt int) (util.Uint256, uint32, error) { + return c.actor.SendRun(removeWhitelistFeeContractScript(h, method, argCnt)) +} + +// RemoveWhitelistFeeContractTransaction creates a transaction that removes a +// specified contract method from the native Policy whitelist with the fixed +// execution fee. It uses the `removeWhitelistFeeContract` method which is +// active starting from [config.HFFaun] hardfork. This transaction is signed but +// not sent to the network, instead it's returned to the caller. +func (c *Contract) RemoveWhitelistFeeContractTransaction(h util.Uint160, method string, argCnt int) (*transaction.Transaction, error) { + return c.actor.MakeRun(removeWhitelistFeeContractScript(h, method, argCnt)) +} + +// RemoveWhitelistFeeContractUnsigned creates a transaction that removes a +// specified contract method from the native Policy whitelist with the fixed +// execution fee. It uses the `removeWhitelistFeeContract` method which is +// active starting from [config.HFFaun] hardfork. This transaction is not signed +// and just returned to the caller. +func (c *Contract) RemoveWhitelistFeeContractUnsigned(h util.Uint160, method string, argCnt int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedRun(removeWhitelistFeeContractScript(h, method, argCnt), nil) +} + +func removeWhitelistFeeContractScript(h util.Uint160, method string, argCnt int) []byte { + // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. + script, _ := smartcontract.CreateCallScript(Hash, "removeWhitelistFeeContract", h, method, argCnt) + return script +} + +// GetWhitelistFeeContracts returns an iterator that allows to retrieve all +// whitelisted contract methods from it. It depends on the server to provide +// a proper session-based iterator but can also work with an expanded one. +func (c *ContractReader) GetWhitelistFeeContracts() (*WhitelistedContractIterator, error) { + sess, iter, err := unwrap.SessionIterator(c.invoker.Call(Hash, "getWhitelistFeeContracts")) + if err != nil { + return nil, err + } + + return &WhitelistedContractIterator{ + client: c.invoker, + iterator: iter, + session: sess, + }, nil +} + +// GetWhitelistFeeContractsExpanded is similar to GetWhitelistFeeContracts (uses +// the same NEO method), but can be useful if the server used doesn't support +// sessions and doesn't expand iterators. It creates a script that will get num +// of result items from the iterator right in the VM and return them to you. +// It's only limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) GetWhitelistFeeContractsExpanded(num int) ([]WhitelistedContract, error) { + arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "getWhitelistFeeContracts", num)) + if err != nil { + return nil, err + } + return itemsToWhitelistedContracts(arr) +} + +// Next returns the next set of elements from the iterator (up to num of them). +// It can return less than num elements in case an iterator doesn't have that +// many or zero elements if the iterator has no more elements or the session is +// expired. +func (v *WhitelistedContractIterator) Next(num int) ([]WhitelistedContract, error) { + items, err := v.client.TraverseIterator(v.session, &v.iterator, num) + if err != nil { + return nil, err + } + return itemsToWhitelistedContracts(items) +} + +// Terminate closes the iterator session used by ValidatorIterator (if it's +// session-based). +func (v *WhitelistedContractIterator) Terminate() error { + if v.iterator.ID == nil { + return nil + } + return v.client.TerminateSession(v.session) +} + +func itemsToWhitelistedContracts(arr []stackitem.Item) ([]WhitelistedContract, error) { + res := make([]WhitelistedContract, len(arr)) + for i, itm := range arr { + str, err := itm.TryBytes() + if err != nil { + return nil, fmt.Errorf("item #%d is not a ByteString: %w", i, err) + } + h, err := util.Uint160DecodeBytesBE(str[:util.Uint160Size]) + if err != nil { + return nil, fmt.Errorf("item #%d: invalid hash: %w", i, err) + } + res[i].Hash = h + res[i].Offset = binary.BigEndian.Uint32(str[util.Uint160Size:]) + } + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to WhitelistFeeChangedEvent or returns an +// error if it's not possible to do so. +func (e *WhitelistFeeChangedEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 4 { + return fmt.Errorf("wrong number of event parameters: %d", len(arr)) + } + + h, err := arr[0].TryBytes() + if err != nil { + return fmt.Errorf("invalid hash: %w", err) + } + e.Hash, err = util.Uint160DecodeBytesBE(h) + if err != nil { + return fmt.Errorf("failed to unwrap hash: %w", err) + } + + m, err := arr[1].TryBytes() + if err != nil { + return fmt.Errorf("invalid method: %w", err) + } + e.Method = string(m) + + argCnt, err := arr[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid arg count: %w", err) + } + e.ArgCnt = int(argCnt.Int64()) + + if !arr[3].Equals(stackitem.Null{}) { + fee, err := arr[3].TryInteger() + if err != nil { + return fmt.Errorf("invalid fee: %w", err) + } + v := fee.Int64() + e.Fee = &v + } + + return nil +} diff --git a/pkg/rpcclient/policy/policy_test.go b/pkg/rpcclient/policy/policy_test.go index d9a239bba9..fba2db8a31 100644 --- a/pkg/rpcclient/policy/policy_test.go +++ b/pkg/rpcclient/policy/policy_test.go @@ -73,6 +73,8 @@ func TestReader(t *testing.T) { require.Error(t, err) _, err = pc.GetAttributeFee(transaction.ConflictsT) require.Error(t, err) + _, err = pc.GetWhitelistFeeContracts() + require.Error(t, err) ta.err = nil ta.res = &result.Invoke{ @@ -98,6 +100,26 @@ func TestReader(t *testing.T) { val, err := pc.IsBlocked(util.Uint160{1, 2, 3}) require.NoError(t, err) require.True(t, val) + iid := uuid.New() + ta.res = &result.Invoke{ + Session: uuid.New(), + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + ID: &iid, + }), + }, + } + iter, err := pc.GetWhitelistFeeContracts() + require.NoError(t, err) + ta.res = &result.Invoke{ + Stack: []stackitem.Item{ + stackitem.Make(append(util.Uint160{1, 2, 3}.BytesBE(), []byte{0, 0, 0, 1}...)), + }, + } + whitelisted, err := iter.Next(10) + require.NoError(t, err) + require.Equal(t, []WhitelistedContract{{Hash: util.Uint160{1, 2, 3}, Offset: 1}}, whitelisted) } func TestGetBlockedAccounts(t *testing.T) { @@ -246,6 +268,29 @@ func TestUint160Setters(t *testing.T) { } } +func TestWhitelistedSetters(t *testing.T) { + ta := new(testAct) + pc := New(ta) + + ta.err = errors.New("") + _, _, err := pc.SetWhitelistFeeContract(util.Uint160{}, "someMethod", 1, 0) + require.Error(t, err) + _, _, err = pc.RemoveWhitelistFeeContract(util.Uint160{}, "someMethod", 1) + require.Error(t, err) + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + h, vub, err := pc.SetWhitelistFeeContract(util.Uint160{}, "someMethod", 1, 0) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + h, vub, err = pc.RemoveWhitelistFeeContract(util.Uint160{}, "someMethod", 1) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) +} + func TestIntTransactions(t *testing.T) { ta := new(testAct) pc := New(ta) @@ -295,3 +340,110 @@ func TestUint160Transactions(t *testing.T) { require.Equal(t, ta.tx, tx) } } + +func TestWhitelistedTransactions(t *testing.T) { + ta := new(testAct) + pc := New(ta) + + ta.err = errors.New("") + _, err := pc.SetWhitelistFeeContractTransaction(util.Uint160{}, "someMethod", 1, 0) + require.Error(t, err) + _, err = pc.SetWhitelistFeeContractUnsigned(util.Uint160{}, "someMethod", 1, 0) + require.Error(t, err) + _, err = pc.RemoveWhitelistFeeContractTransaction(util.Uint160{}, "someMethod", 1) + require.Error(t, err) + _, err = pc.RemoveWhitelistFeeContractUnsigned(util.Uint160{}, "someMethod", 1) + require.Error(t, err) + + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := pc.SetWhitelistFeeContractTransaction(util.Uint160{}, "someMethod", 1, 0) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = pc.SetWhitelistFeeContractUnsigned(util.Uint160{}, "someMethod", 1, 0) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = pc.RemoveWhitelistFeeContractTransaction(util.Uint160{}, "someMethod", 1) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = pc.RemoveWhitelistFeeContractUnsigned(util.Uint160{}, "someMethod", 1) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) +} + +func TestWhitelistFeeChangedEvent_FromStackItem(t *testing.T) { + var fee int64 = 2 + tcs := []struct { + name string + in []stackitem.Item + expected *WhitelistFeeChangedEvent + err string + }{ + { + name: "nil item", + in: nil, + err: "nil item", + }, + { + name: "wrong number of elements", + in: []stackitem.Item{}, + err: "wrong number of event parameters", + }, + { + name: "invalid hash", + in: []stackitem.Item{stackitem.NewInterop(nil), stackitem.Make(1), stackitem.Make(2), stackitem.Make(3)}, + err: "invalid hash", + }, + { + name: "failed to unwrap hash", + in: []stackitem.Item{stackitem.Make([]byte{1, 2, 3}), stackitem.Make(1), stackitem.Make(2), stackitem.Make(3)}, + err: "failed to unwrap hash", + }, + { + name: "invalid method", + in: []stackitem.Item{stackitem.Make(util.Uint160{1, 2, 3}), stackitem.NewInterop(nil), stackitem.Make(2), stackitem.Make(3)}, + err: "invalid method", + }, + { + name: "invalid arg count", + in: []stackitem.Item{stackitem.Make(util.Uint160{1, 2, 3}), stackitem.Make("someMethod"), stackitem.NewInterop(nil), stackitem.Make(3)}, + err: "invalid arg count", + }, + { + name: "nil fee", + in: []stackitem.Item{stackitem.Make(util.Uint160{1, 2, 3}), stackitem.Make("someMethod"), stackitem.Make(1), stackitem.Null{}}, + expected: &WhitelistFeeChangedEvent{ + Hash: util.Uint160{1, 2, 3}, + Method: "someMethod", + ArgCnt: 1, + }, + }, + { + name: "non-nil fee", + in: []stackitem.Item{stackitem.Make(util.Uint160{1, 2, 3}), stackitem.Make("someMethod"), stackitem.Make(1), stackitem.Make(2)}, + expected: &WhitelistFeeChangedEvent{ + Hash: util.Uint160{1, 2, 3}, + Method: "someMethod", + ArgCnt: 1, + Fee: &fee, + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + actual := new(WhitelistFeeChangedEvent) + var in *stackitem.Array + if tc.in != nil { + in = stackitem.NewArray(tc.in) + } + err := actual.FromStackItem(in) + if len(tc.err) != 0 { + require.ErrorContains(t, err, tc.err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + } + }) + } +} diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 178e8f9fb8..eb993624b0 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -236,12 +236,31 @@ func TestClientPolicyContract(t *testing.T) { require.NoError(t, err) txblock3, err := polis.BlockAccountUnsigned(blocked[2]) require.NoError(t, err) + var stdM *manifest.Manifest + for _, cs := range chain.GetNatives() { + if cs.Manifest.Name == nativenames.StdLib { + stdM = &cs.Manifest + break + } + } + m1 := stdM.ABI.GetMethod("hexDecode", 1) + m2 := stdM.ABI.GetMethod("hexEncode", 1) + require.NotNil(t, m1) + require.NotNil(t, m2) + whitelisted := []policy.WhitelistedContract{ + {Hash: nativehashes.StdLib, Offset: uint32(m1.Offset)}, + {Hash: nativehashes.StdLib, Offset: uint32(m2.Offset)}, + } + txwhitelist1, err := polis.SetWhitelistFeeContractUnsigned(whitelisted[0].Hash, "hexDecode", 1, 0) + require.NoError(t, err) + txwhitelist2, err := polis.SetWhitelistFeeContractUnsigned(whitelisted[1].Hash, "hexEncode", 1, 0) + require.NoError(t, err) - for _, tx := range []*transaction.Transaction{txattr, txblock1, txblock2, txblock3, txstorage, txnetfee, txexec} { + for _, tx := range []*transaction.Transaction{txattr, txblock1, txblock2, txblock3, txwhitelist1, txwhitelist2, txstorage, txnetfee, txexec} { tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) } - bl := testchain.NewBlock(t, chain, 1, 0, txattr, txblock1, txblock2, txblock3, txstorage, txnetfee, txexec) + bl := testchain.NewBlock(t, chain, 1, 0, txattr, txblock1, txblock2, txblock3, txwhitelist1, txwhitelist2, txstorage, txnetfee, txexec) _, err = c.SubmitBlock(*bl) require.NoError(t, err) @@ -277,6 +296,30 @@ func TestClientPolicyContract(t *testing.T) { all, err = it.Next(100) require.NoError(t, err) require.Equal(t, blocked, all) + + allWhitelisted, err := polizei.GetWhitelistFeeContractsExpanded(100) + require.NoError(t, err) + require.Equal(t, whitelisted, allWhitelisted) + + whitelistedIt, err := polizei.GetWhitelistFeeContracts() + require.NoError(t, err) + allWhitelisted, err = whitelistedIt.Next(100) + require.NoError(t, err) + require.Equal(t, whitelisted, allWhitelisted) + require.NoError(t, whitelistedIt.Terminate()) + + txwhitelist3, err := polis.RemoveWhitelistFeeContractUnsigned(whitelisted[0].Hash, "hexDecode", 1) + require.NoError(t, err) + for _, tx := range []*transaction.Transaction{txwhitelist3} { + tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) + } + bl = testchain.NewBlock(t, chain, 1, 0, txwhitelist3) + _, err = c.SubmitBlock(*bl) + require.NoError(t, err) + + allWhitelisted, err = polizei.GetWhitelistFeeContractsExpanded(100) + require.NoError(t, err) + require.Equal(t, []policy.WhitelistedContract{whitelisted[1]}, allWhitelisted) } func TestClientManagementContract(t *testing.T) { diff --git a/pkg/smartcontract/manifest/abi.go b/pkg/smartcontract/manifest/abi.go index de85abcae7..918bdbcc7d 100644 --- a/pkg/smartcontract/manifest/abi.go +++ b/pkg/smartcontract/manifest/abi.go @@ -41,6 +41,16 @@ func (a *ABI) GetMethod(name string, paramCount int) *Method { return nil } +// GetMethodByOffset returns a method by the specified offset. +func (a *ABI) GetMethodByOffset(offset int) *Method { + for i := range a.Methods { + if a.Methods[i].Offset == offset { + return &a.Methods[i] + } + } + return nil +} + // GetEvent returns the event with the specified name. func (a *ABI) GetEvent(name string) *Event { for i := range a.Events { diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 4b7db2fefe..1da7671d94 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -51,6 +51,9 @@ type scriptContext struct { // onUnload is a callback that should be called after current context unloading // if no exception occurs. onUnload ContextUnloadCallback + // whitelisted denotes whether execution fee charging should be omitted for + // the current context. + whitelisted bool } // Context represents the current execution context of the VM. diff --git a/pkg/vm/debug_test.go b/pkg/vm/debug_test.go index afcf30a96e..b3f8086d68 100644 --- a/pkg/vm/debug_test.go +++ b/pkg/vm/debug_test.go @@ -56,7 +56,7 @@ func TestContext_BreakPoints(t *testing.T) { require.Equal(t, []int{3, 5}, v.Context().BreakPoints()) // New context -> clean breakpoints. - v.loadScriptWithCallingHash(prog, nil, nil, util.Uint160{}, util.Uint160{}, callflag.All, 1, 3, nil) + v.loadScriptWithCallingHash(prog, nil, nil, util.Uint160{}, util.Uint160{}, callflag.All, 1, 3, nil, false) require.Nil(t, v.Context().BreakPoints()) v.AddBreakPoint(3) diff --git a/pkg/vm/stackitem/item.go b/pkg/vm/stackitem/item.go index c2b8e38045..faa7cc0b52 100644 --- a/pkg/vm/stackitem/item.go +++ b/pkg/vm/stackitem/item.go @@ -161,6 +161,8 @@ func TryMake(v any) (Item, error) { return TryMake(*val) case nil: return Null{}, nil + case Convertible: + return val.ToStackItem() default: i64T := reflect.TypeOf(int64(0)) if reflect.TypeOf(val).ConvertibleTo(i64T) { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index f7928c0c5c..92bd818bc4 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -211,18 +211,20 @@ func PicoGasToDatoshi(x int64) int64 { return (x + ExecFeeFactorMultiplier - 1) / ExecFeeFactorMultiplier } -// AddPicoGas consumes the specified amount of gas in picoGAS units. It returns -// true if the gas limit wasn't exceeded. +// AddPicoGas consumes the specified amount of gas in picoGAS units if the +// executing method is not whitelisted. It returns true if the gas limit wasn't +// exceeded. func (v *VM) AddPicoGas(gas int64) bool { - v.gasConsumed += gas + if ctx := v.Context(); ctx == nil || !ctx.sc.whitelisted { + v.gasConsumed += gas + } return v.gasLimit < 0 || v.gasConsumed <= v.gasLimit } // AddDatoshi consumes the specified amount of gas in Datoshi units. It returns // true if the gas limit wasn't exceeded. func (v *VM) AddDatoshi(gas int64) bool { - v.gasConsumed += gas * ExecFeeFactorMultiplier - return v.gasLimit < 0 || v.gasConsumed <= v.gasLimit + return v.AddPicoGas(gas * ExecFeeFactorMultiplier) } // Estack returns the evaluation stack, so interop hooks can utilize this. @@ -390,14 +392,14 @@ func (v *VM) LoadScript(b []byte) { // LoadScriptWithFlags loads script and sets call flag to f. func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) { - v.loadScriptWithCallingHash(b, nil, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0, nil) + v.loadScriptWithCallingHash(b, nil, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0, nil, false) } // LoadDynamicScript loads the given script with the given flags. This script is // considered to be dynamic, it can either return no value at all or return // exactly one value. func (v *VM) LoadDynamicScript(b []byte, f callflag.CallFlag) { - v.loadScriptWithCallingHash(b, nil, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0, DynamicOnUnload) + v.loadScriptWithCallingHash(b, nil, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0, DynamicOnUnload, false) } // LoadScriptWithHash is similar to the LoadScriptWithFlags method, but it also loads @@ -407,19 +409,19 @@ func (v *VM) LoadDynamicScript(b []byte, f callflag.CallFlag) { // accordingly). It's up to the user of this function to make sure the script and hash match // each other. func (v *VM) LoadScriptWithHash(b []byte, hash util.Uint160, f callflag.CallFlag) { - v.loadScriptWithCallingHash(b, nil, nil, v.GetCurrentScriptHash(), hash, f, 1, 0, nil) + v.loadScriptWithCallingHash(b, nil, nil, v.GetCurrentScriptHash(), hash, f, 1, 0, nil, false) } // LoadNEFMethod allows to create a context to execute a method from the NEF // file with the specified caller and executing hash, call flags, return value, // method and _initialize offsets. func (v *VM) LoadNEFMethod(exe *nef.File, manifest *manifest.Manifest, caller util.Uint160, hash util.Uint160, f callflag.CallFlag, - hasReturn bool, methodOff int, initOff int, onContextUnload ContextUnloadCallback) { + hasReturn bool, methodOff int, initOff int, onContextUnload ContextUnloadCallback, whitelisted bool) { var rvcount int if hasReturn { rvcount = 1 } - v.loadScriptWithCallingHash(exe.Script, exe, manifest, caller, hash, f, rvcount, methodOff, onContextUnload) + v.loadScriptWithCallingHash(exe.Script, exe, manifest, caller, hash, f, rvcount, methodOff, onContextUnload, whitelisted) if initOff >= 0 { v.Call(initOff) } @@ -428,7 +430,7 @@ func (v *VM) LoadNEFMethod(exe *nef.File, manifest *manifest.Manifest, caller ut // loadScriptWithCallingHash is similar to LoadScriptWithHash but sets calling hash explicitly. // It should be used for calling from native contracts. func (v *VM) loadScriptWithCallingHash(b []byte, exe *nef.File, manifest *manifest.Manifest, caller util.Uint160, - hash util.Uint160, f callflag.CallFlag, rvcount int, offset int, onContextUnload ContextUnloadCallback) { + hash util.Uint160, f callflag.CallFlag, rvcount int, offset int, onContextUnload ContextUnloadCallback, whitelisted bool) { v.checkInvocationStackSize() ctx := NewContextWithParams(b, rvcount, offset) parent := v.Context() @@ -446,6 +448,7 @@ func (v *VM) loadScriptWithCallingHash(b []byte, exe *nef.File, manifest *manife ctx.sc.callingScriptHash = caller ctx.sc.NEF = exe ctx.sc.Manifest = manifest + ctx.sc.whitelisted = whitelisted if v.invTree != nil { curTree := v.invTree if parent != nil { @@ -684,8 +687,9 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro } }() - if v.getPrice != nil && ctx.IP() < len(ctx.sc.prog) { - v.gasConsumed += v.getPrice(op, parameter) + if v.getPrice != nil && ctx.IP() < len(ctx.sc.prog) && !ctx.sc.whitelisted { + p := v.getPrice(op, parameter) + v.gasConsumed += p if v.gasLimit >= 0 && v.gasConsumed > v.gasLimit { panic("gas limit is exceeded") }