From 54874e481efb79388002a67be8bb378d5f9b3a26 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Sun, 18 Jun 2023 14:40:18 +0400 Subject: [PATCH] Provide embedded compiled Neo contracts There is a need to embed Neo contracts' executables into NeoFS Inner Ring application. To do this, `contracts` directory is created. The dir contains compiled contracts (per-contract NEF and manifest). Create eponymous Go package that provides embedded `fs.FS` with the contracts. Add `Read` function which reads, decodes and validates all numerically-sored contracts from files and returns ready-to-go data for deployment. Refs #2195. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 3 + contracts/00-nns.manifest.json | 1 + contracts/00-nns.nef | Bin 0 -> 6114 bytes contracts/contracts.go | 162 +++++++++++++++++++++++++++++++++ contracts/contracts_test.go | 130 ++++++++++++++++++++++++++ 5 files changed, 296 insertions(+) create mode 100755 contracts/00-nns.manifest.json create mode 100755 contracts/00-nns.nef create mode 100644 contracts/contracts.go create mode 100644 contracts/contracts_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bcd98f60f..016e611c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog for NeoFS Node ## [Unreleased] +### Added +- Embedded Neo contracts in `contracts` dir (#2391) + ### Fixed ### Removed diff --git a/contracts/00-nns.manifest.json b/contracts/00-nns.manifest.json new file mode 100755 index 0000000000..b858bab519 --- /dev/null +++ b/contracts/00-nns.manifest.json @@ -0,0 +1 @@ +{"name":"NameService","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":32,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addRecord","offset":2567,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"balanceOf","offset":568,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"decimals","offset":479,"parameters":[],"returntype":"Integer","safe":true},{"name":"deleteRecords","offset":2702,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Void","safe":false},{"name":"getAllRecords","offset":2858,"parameters":[{"name":"name","type":"String"}],"returntype":"InteropInterface","safe":false},{"name":"getPrice","offset":972,"parameters":[],"returntype":"Integer","safe":true},{"name":"getRecords","offset":2659,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"isAvailable","offset":1006,"parameters":[{"name":"name","type":"String"}],"returntype":"Boolean","safe":true},{"name":"ownerOf","offset":501,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Hash160","safe":true},{"name":"properties","offset":523,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Map","safe":true},{"name":"register","offset":1267,"parameters":[{"name":"name","type":"String"},{"name":"owner","type":"Hash160"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"renew","offset":2026,"parameters":[{"name":"name","type":"String"}],"returntype":"Integer","safe":false},{"name":"resolve","offset":2836,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"roots","offset":866,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"setAdmin","offset":2237,"parameters":[{"name":"name","type":"String"},{"name":"admin","type":"Hash160"}],"returntype":"Void","safe":false},{"name":"setPrice","offset":894,"parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setRecord","offset":2371,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"id","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"symbol","offset":473,"parameters":[],"returntype":"String","safe":true},{"name":"tokens","offset":644,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"tokensOf","offset":673,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"InteropInterface","safe":true},{"name":"totalSupply","offset":485,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":735,"parameters":[{"name":"to","type":"Hash160"},{"name":"tokenID","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"update","offset":386,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"String"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"updateSOA","offset":2147,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"version","offset":481,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"}]}]},"features":{},"groups":[],"permissions":[{"contract":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","methods":["update"]},{"contract":"*","methods":["onNEP11Payment"]}],"supportedstandards":["NEP-11"],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/00-nns.nef b/contracts/00-nns.nef new file mode 100755 index 0000000000000000000000000000000000000000..85c9a45afe66df2182c698a4d0608e424811bee6 GIT binary patch literal 6114 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 'z' { + return nil, fmt.Errorf("%w: unsupported char in name %c", errInvalidFilename, name[i]) + } + } + + var ind int + + if noZerosPrefix := strings.TrimLeft(prefix[:hyphenInd], "0"); len(noZerosPrefix) > 0 { + ind, err = strconv.Atoi(noZerosPrefix) + if err != nil { + return nil, fmt.Errorf("%w: invalid prefix of file name '%s' (expected serial number)", errInvalidFilename, nefFiles[i]) + } else if ind < 0 { + return nil, fmt.Errorf("%w: negative serial number in file name '%s'", errInvalidFilename, nefFiles[i]) + } + } + + for i := range cs { + if cs[i].i == ind { + return nil, fmt.Errorf("%w: more than one file with serial number #%d", errDuplicatedContract, ind) + } + } + + c, err := readContractFromFiles(_fs, prefix) + if err != nil { + return nil, fmt.Errorf("read contract #%d: %w", ind, err) + } + + cs = append(cs, numberedContract{ + i: ind, + c: c, + }) + } + + sort.Sort(cs) + + res := make([]Contract, len(cs)) + + for i := range cs { + res[i] = cs[i].c + } + + return res, nil +} + +func readContractFromFiles(_fs fs.FS, filePrefix string) (c Contract, err error) { + fNEF, err := _fs.Open(filePrefix + nefFileSuffix) + if err != nil { + return c, fmt.Errorf("open file containing contract NEF: %w", err) + } + defer fNEF.Close() + + fManifest, err := _fs.Open(filePrefix + ".manifest.json") + if err != nil { + return c, fmt.Errorf("open file containing contract NEF: %w", err) + } + defer fManifest.Close() + + bReader := io.NewBinReaderFromIO(fNEF) + c.NEF.DecodeBinary(bReader) + if bReader.Err != nil { + return c, fmt.Errorf("%w: %v", errInvalidNEF, bReader.Err) + } + + err = json.NewDecoder(fManifest).Decode(&c.Manifest) + if err != nil { + return c, fmt.Errorf("%w: %v", errInvalidManifest, err) + } + + return +} diff --git a/contracts/contracts_test.go b/contracts/contracts_test.go new file mode 100644 index 0000000000..afadb0e71f --- /dev/null +++ b/contracts/contracts_test.go @@ -0,0 +1,130 @@ +package contracts + +import ( + "crypto/rand" + "encoding/json" + "testing" + "testing/fstest" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/stretchr/testify/require" +) + +func TestReadRepo(t *testing.T) { + _, err := Read() + require.NoError(t, err) +} + +func TestReadOrder(t *testing.T) { + _nef0, bNEF0 := anyValidNEF(t) + _manifest0, jManifest0 := anyValidManifest(t, "first") + _nef1, bNEF1 := anyValidNEF(t) + _manifest1, jManifest1 := anyValidManifest(t, "second") + _nef11, bNEF11 := anyValidNEF(t) + _manifest11, jManifest11 := anyValidManifest(t, "twelfth") + + _fs := fstest.MapFS{ + "00-hello.nef": {Data: bNEF0}, + "00-hello.manifest.json": {Data: jManifest0}, + "01-world.nef": {Data: bNEF1}, + "01-world.manifest.json": {Data: jManifest1}, + "11-bye.nef": {Data: bNEF11}, + "11-bye.manifest.json": {Data: jManifest11}, + } + + cs, err := read(_fs) + require.NoError(t, err) + require.Len(t, cs, 3) + + require.Equal(t, _nef0, cs[0].NEF) + require.Equal(t, _manifest0, cs[0].Manifest) + require.Equal(t, _nef1, cs[1].NEF) + require.Equal(t, _manifest1, cs[1].Manifest) + require.Equal(t, _nef11, cs[2].NEF) + require.Equal(t, _manifest11, cs[2].Manifest) +} + +func TestReadInvalidFilenames(t *testing.T) { + _fs := fstest.MapFS{} + + _, err := read(_fs) + require.NoError(t, err) + + for _, invalidName := range []string{ + "hello.nef", + "-.nef", + "-0.nef", + "0-.nef", + "0-_.nef", + "0-1.nef", + ".nef", + } { + _fs[invalidName] = &fstest.MapFile{} + _, err = read(_fs) + require.ErrorIs(t, err, errInvalidFilename, invalidName) + delete(_fs, invalidName) + } +} + +func TestReadDuplicatedContract(t *testing.T) { + _, bNEF := anyValidNEF(t) + _, jManifest := anyValidManifest(t, "some name") + + _fs := fstest.MapFS{ + "01-hello.nef": {Data: bNEF}, + "01-hello.manifest.json": {Data: jManifest}, + "001-hello.nef": {Data: bNEF}, + "001-hello.manifest.json": {Data: jManifest}, + } + + _, err := read(_fs) + require.ErrorIs(t, err, errDuplicatedContract) +} + +func TestReadInvalidFormat(t *testing.T) { + _fs := fstest.MapFS{} + + _, validNEF := anyValidNEF(t) + _, validManifest := anyValidManifest(t, "zero") + + _fs["00-hello.nef"] = &fstest.MapFile{Data: validNEF} + _fs["00-hello.manifest.json"] = &fstest.MapFile{Data: validManifest} + + _, err := read(_fs) + require.NoError(t, err, errInvalidNEF) + + _fs["00-hello.nef"] = &fstest.MapFile{Data: []byte("not a NEF")} + _fs["00-hello.manifest.json"] = &fstest.MapFile{Data: validManifest} + + _, err = read(_fs) + require.ErrorIs(t, err, errInvalidNEF) + + _fs["00-hello.nef"] = &fstest.MapFile{Data: validNEF} + _fs["00-hello.manifest.json"] = &fstest.MapFile{Data: []byte("not a manifest")} + + _, err = read(_fs) + require.ErrorIs(t, err, errInvalidManifest) +} + +func anyValidNEF(tb testing.TB) (nef.File, []byte) { + script := make([]byte, 32) + rand.Read(script) + + _nef, err := nef.NewFile(script) + require.NoError(tb, err) + + bNEF, err := _nef.Bytes() + require.NoError(tb, err) + + return *_nef, bNEF +} + +func anyValidManifest(tb testing.TB, name string) (manifest.Manifest, []byte) { + _manifest := manifest.NewManifest(name) + + jManifest, err := json.Marshal(_manifest) + require.NoError(tb, err) + + return *_manifest, jManifest +}