From b096e6842879540df4ffe85d738b57e939e7431e Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Mon, 16 Dec 2024 18:52:57 +0300 Subject: [PATCH] native: add Base64URL support to the StdLib starting from HFEchidna Close #3550 Signed-off-by: Ekaterina Pavlova --- docs/node-configuration.md | 2 +- .../native/native_test/management_test.go | 1 + pkg/core/native/std.go | 27 +++++++++++++++++ pkg/core/native/std_test.go | 29 +++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/docs/node-configuration.md b/docs/node-configuration.md index d7d461e890..c37fefd43f 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -470,7 +470,7 @@ in development and can change in an incompatible way. | `Basilisk` | Enables strict smart contract script check against a set of JMP instructions and against method boundaries enabled on contract deploy or update. Increases `stackitem.Integer` JSON parsing precision up to the maximum value supported by the NeoVM. Enables strict check for notifications emitted by a contract to precisely match the events specified in the contract manifest. | https://github.com/nspcc-dev/neo-go/pull/3056
https://github.com/neo-project/neo/pull/2881
https://github.com/nspcc-dev/neo-go/pull/3080
https://github.com/neo-project/neo/pull/2883
https://github.com/nspcc-dev/neo-go/pull/3085
https://github.com/neo-project/neo/pull/2810 | | `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. | https://github.com/nspcc-dev/neo-go/pull/3554
https://github.com/nspcc-dev/neo-go/pull/3761 | +| `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. | https://github.com/nspcc-dev/neo-go/pull/3554
https://github.com/nspcc-dev/neo-go/pull/3761 | ## DB compatibility diff --git a/pkg/core/native/native_test/management_test.go b/pkg/core/native/native_test/management_test.go index 2f5ffd08d9..09ef1b2c12 100644 --- a/pkg/core/native/native_test/management_test.go +++ b/pkg/core/native/native_test/management_test.go @@ -57,6 +57,7 @@ var ( nativenames.Neo: `{"id":-5,"hash":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1325686241},"manifest":{"name":"NeoToken","abi":{"methods":[{"name":"balanceOf","offset":0,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"decimals","offset":7,"parameters":[],"returntype":"Integer","safe":true},{"name":"getAccountState","offset":14,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Array","safe":true},{"name":"getAllCandidates","offset":21,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"getCandidateVote","offset":28,"parameters":[{"name":"pubKey","type":"PublicKey"}],"returntype":"Integer","safe":true},{"name":"getCandidates","offset":35,"parameters":[],"returntype":"Array","safe":true},{"name":"getCommittee","offset":42,"parameters":[],"returntype":"Array","safe":true},{"name":"getCommitteeAddress","offset":49,"parameters":[],"returntype":"Hash160","safe":true},{"name":"getGasPerBlock","offset":56,"parameters":[],"returntype":"Integer","safe":true},{"name":"getNextBlockValidators","offset":63,"parameters":[],"returntype":"Array","safe":true},{"name":"getRegisterPrice","offset":70,"parameters":[],"returntype":"Integer","safe":true},{"name":"registerCandidate","offset":77,"parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","safe":false},{"name":"setGasPerBlock","offset":84,"parameters":[{"name":"gasPerBlock","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setRegisterPrice","offset":91,"parameters":[{"name":"registerPrice","type":"Integer"}],"returntype":"Void","safe":false},{"name":"symbol","offset":98,"parameters":[],"returntype":"String","safe":true},{"name":"totalSupply","offset":105,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":112,"parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"unclaimedGas","offset":119,"parameters":[{"name":"account","type":"Hash160"},{"name":"end","type":"Integer"}],"returntype":"Integer","safe":true},{"name":"unregisterCandidate","offset":126,"parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","safe":false},{"name":"vote","offset":133,"parameters":[{"name":"account","type":"Hash160"},{"name":"voteTo","type":"PublicKey"}],"returntype":"Boolean","safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"CandidateStateChanged","parameters":[{"name":"pubkey","type":"PublicKey"},{"name":"registered","type":"Boolean"},{"name":"votes","type":"Integer"}]},{"name":"Vote","parameters":[{"name":"account","type":"Hash160"},{"name":"from","type":"PublicKey"},{"name":"to","type":"PublicKey"},{"name":"amount","type":"Integer"}]},{"name":"CommitteeChanged","parameters":[{"name":"old","type":"Array"},{"name":"new","type":"Array"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":["NEP-17"],"trusts":[],"extra":null},"updatecounter":0}`, } echidnaCSS = map[string]string{ + nativenames.StdLib: `{"id":-2,"hash":"0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2681632925},"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":"ByteArray","safe":true},{"name":"base64UrlEncode","offset":63,"parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"deserialize","offset":70,"parameters":[{"name":"data","type":"ByteArray"}],"returntype":"Any","safe":true},{"name":"itoa","offset":77,"parameters":[{"name":"value","type":"Integer"}],"returntype":"String","safe":true},{"name":"itoa","offset":84,"parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","safe":true},{"name":"jsonDeserialize","offset":91,"parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","safe":true},{"name":"jsonSerialize","offset":98,"parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","safe":true},{"name":"memoryCompare","offset":105,"parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","safe":true},{"name":"memorySearch","offset":112,"parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","safe":true},{"name":"memorySearch","offset":119,"parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","safe":true},{"name":"memorySearch","offset":126,"parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","safe":true},{"name":"serialize","offset":133,"parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","safe":true},{"name":"strLen","offset":140,"parameters":[{"name":"str","type":"String"}],"returntype":"Integer","safe":true},{"name":"stringSplit","offset":147,"parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","safe":true},{"name":"stringSplit","offset":154,"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.Designation: `{"id":-8,"hash":"0x49cf4e5378ffcd4dec034fd98a174c5491e395e2","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0A=","checksum":983638438},"manifest":{"name":"RoleManagement","abi":{"methods":[{"name":"designateAsRole","offset":0,"parameters":[{"name":"role","type":"Integer"},{"name":"nodes","type":"Array"}],"returntype":"Void","safe":false},{"name":"getDesignatedByRole","offset":7,"parameters":[{"name":"role","type":"Integer"},{"name":"index","type":"Integer"}],"returntype":"Array","safe":true}],"events":[{"name":"Designation","parameters":[{"name":"Role","type":"Integer"},{"name":"BlockIndex","type":"Integer"},{"name":"Old","type":"Array"},{"name":"New","type":"Array"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, } ) diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go index 453c441ef7..4e05fc96e3 100644 --- a/pkg/core/native/std.go +++ b/pkg/core/native/std.go @@ -100,6 +100,16 @@ func newStd() *Std { md = newMethodAndPrice(s.base64Decode, 1<<5, callflag.NoneFlag) s.AddMethod(md, desc) + desc = newDescriptor("base64UrlEncode", smartcontract.StringType, + manifest.NewParameter("data", smartcontract.ByteArrayType)) + md = newMethodAndPrice(s.base64UrlEncode, 1<<5, callflag.NoneFlag, config.HFEchidna) + s.AddMethod(md, desc) + + desc = newDescriptor("base64UrlDecode", smartcontract.ByteArrayType, + manifest.NewParameter("s", smartcontract.StringType)) + md = newMethodAndPrice(s.base64UrlDecode, 1<<5, callflag.NoneFlag, config.HFEchidna) + s.AddMethod(md, desc) + desc = newDescriptor("base58Encode", smartcontract.StringType, manifest.NewParameter("data", smartcontract.ByteArrayType)) md = newMethodAndPrice(s.base58Encode, 1<<13, callflag.NoneFlag) @@ -314,6 +324,23 @@ func (s *Std) base64Decode(_ *interop.Context, args []stackitem.Item) stackitem. return stackitem.NewByteArray(result) } +func (s *Std) base64UrlEncode(_ *interop.Context, args []stackitem.Item) stackitem.Item { + src := s.toLimitedBytes(args[0]) + result := base64.URLEncoding.EncodeToString(src) + + return stackitem.NewByteArray([]byte(result)) +} + +func (s *Std) base64UrlDecode(_ *interop.Context, args []stackitem.Item) stackitem.Item { + src := s.toLimitedString(args[0]) + result, err := base64.URLEncoding.DecodeString(src) + if err != nil { + panic(err) + } + + return stackitem.NewByteArray(result) +} + func (s *Std) base58Encode(_ *interop.Context, args []stackitem.Item) stackitem.Item { src := s.toLimitedBytes(args[0]) result := base58.Encode(src) diff --git a/pkg/core/native/std_test.go b/pkg/core/native/std_test.go index 974e54b8f5..1a59dcfce7 100644 --- a/pkg/core/native/std_test.go +++ b/pkg/core/native/std_test.go @@ -170,7 +170,10 @@ func TestStdLibJSON(t *testing.T) { func TestStdLibEncodeDecode(t *testing.T) { s := newStd() original := []byte("my pretty string") + // Source C# implementation: https://github.com/neo-project/neo/blob/216c39eddd03de2cb1f6f8aa5b3c19be60606f27/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs#s#L417-L418 + originalUrl := []byte("Subject=test@example.com&Issuer=https://example.com") encoded64 := base64.StdEncoding.EncodeToString(original) + encoded64Url := "U3ViamVjdD10ZXN0QGV4YW1wbGUuY29tJklzc3Vlcj1odHRwczovL2V4YW1wbGUuY29t" encoded58 := base58.Encode(original) encoded58Check := base58neogo.CheckEncode(original) ic := &interop.Context{VM: vm.New()} @@ -188,6 +191,16 @@ func TestStdLibEncodeDecode(t *testing.T) { require.PanicsWithError(t, ErrTooBigInput.Error(), func() { s.base64Encode(ic, bigInputArgs) }) }) + t.Run("Encode64Url", func(t *testing.T) { + require.NotPanics(t, func() { + actual = s.base64UrlEncode(ic, []stackitem.Item{stackitem.Make(originalUrl)}) + }) + require.Equal(t, stackitem.Make(encoded64Url), actual) + }) + t.Run("Encode64Url/error", func(t *testing.T) { + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.base64UrlEncode(ic, bigInputArgs) }) + }) t.Run("Encode58", func(t *testing.T) { require.NotPanics(t, func() { actual = s.base58Encode(ic, []stackitem.Item{stackitem.Make(original)}) @@ -224,6 +237,22 @@ func TestStdLibEncodeDecode(t *testing.T) { require.PanicsWithError(t, ErrTooBigInput.Error(), func() { s.base64Decode(ic, bigInputArgs) }) }) + t.Run("Decode64Url/positive", func(t *testing.T) { + require.NotPanics(t, func() { + actual = s.base64UrlDecode(ic, []stackitem.Item{stackitem.Make(encoded64Url)}) + }) + require.Equal(t, stackitem.Make(originalUrl), actual) + }) + t.Run("Decode64Url/error", func(t *testing.T) { + require.Panics(t, func() { + _ = s.base64UrlDecode(ic, []stackitem.Item{stackitem.Make(encoded64Url + "%")}) + }) + require.Panics(t, func() { + _ = s.base64UrlDecode(ic, []stackitem.Item{stackitem.NewInterop(nil)}) + }) + require.PanicsWithError(t, ErrTooBigInput.Error(), + func() { s.base64UrlDecode(ic, bigInputArgs) }) + }) t.Run("Decode58/positive", func(t *testing.T) { require.NotPanics(t, func() { actual = s.base58Decode(ic, []stackitem.Item{stackitem.Make(encoded58)})