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 0000000000..85c9a45afe Binary files /dev/null and b/contracts/00-nns.nef differ diff --git a/contracts/contracts.go b/contracts/contracts.go new file mode 100644 index 0000000000..e911eb38e2 --- /dev/null +++ b/contracts/contracts.go @@ -0,0 +1,162 @@ +/* +Package contracts embeds compiled Neo contracts and provides access to them. +*/ +package contracts + +import ( + "embed" + "encoding/json" + "errors" + "fmt" + "io/fs" + "sort" + "strconv" + "strings" + + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" +) + +// Contract groups information about Neo contract stored in the current package. +type Contract struct { + NEF nef.File + Manifest manifest.Manifest +} + +//go:embed *.nef *.manifest.json +var _fs embed.FS + +// Read reads compiled contracts stored in the package sorted numerically. +// File schema: +// - compiled executables (NEF) are named by pattern 'N-C.nef' +// - JSON-encoded manifests are named by pattern 'N-C.manifest.json' +// +// where C is the contract name (a-z) and N is the serial number of the contract +// starting from 0. Leading zeros are ignored (except zero sequence +// corresponding to N=0). +// +// If NEF file exists, corresponding manifest file must exist. If manifest +// file is presented without corresponding NEF file, the contract is ignored. +// +// Read fails if contract files has invalid name or format. +func Read() ([]Contract, error) { + return read(_fs) +} + +const nefFileSuffix = ".nef" + +var ( + errInvalidFilename = errors.New("invalid file name") + errDuplicatedContract = errors.New("duplicated contract") + errInvalidNEF = errors.New("invalid NEF") + errInvalidManifest = errors.New("invalid manifest") +) + +type numberedContract struct { + i int + c Contract +} + +type numberedContracts []numberedContract + +func (x numberedContracts) Len() int { return len(x) } +func (x numberedContracts) Less(i, j int) bool { return x[i].i < x[j].i } +func (x numberedContracts) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +// read same as Read by allows to override source fs.FS. +func read(_fs fs.FS) ([]Contract, error) { + nefFiles, err := fs.Glob(_fs, "*"+nefFileSuffix) + if err != nil { + return nil, fmt.Errorf("match files with suffix %s", nefFileSuffix) + } + + cs := make(numberedContracts, 0, len(nefFiles)) + + for i := range nefFiles { + prefix := strings.TrimSuffix(nefFiles[i], nefFileSuffix) + if prefix == "" { + return nil, fmt.Errorf("%w: missing prefix '%s'", errInvalidFilename, nefFiles[i]) + } + + hyphenInd := strings.IndexByte(prefix, '-') + if hyphenInd < 0 { + return nil, fmt.Errorf("%w: missing hyphen '%s'", errInvalidFilename, nefFiles[i]) + } + + name := prefix[hyphenInd+1:] + if len(name) == 0 { + return nil, fmt.Errorf("%w: missing name '%s'", errInvalidFilename, nefFiles[i]) + } + + for i := range name { + if name[i] < 'a' || name[i] > '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 +}