Skip to content

Commit

Permalink
Provide embedded compiled Neo contracts
Browse files Browse the repository at this point in the history
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 <leonard@morphbits.io>
  • Loading branch information
cthulhu-rider committed Jun 20, 2023
1 parent 93733ff commit 54874e4
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Changelog for NeoFS Node

## [Unreleased]

### Added
- Embedded Neo contracts in `contracts` dir (#2391)

### Fixed

### Removed
Expand Down
1 change: 1 addition & 0 deletions contracts/00-nns.manifest.json
Original file line number Diff line number Diff line change
@@ -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}
Binary file added contracts/00-nns.nef
Binary file not shown.
162 changes: 162 additions & 0 deletions contracts/contracts.go
Original file line number Diff line number Diff line change
@@ -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
}
130 changes: 130 additions & 0 deletions contracts/contracts_test.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 54874e4

Please sign in to comment.