-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <leonard@morphbits.io>
- Loading branch information
1 parent
93733ff
commit 54874e4
Showing
5 changed files
with
296 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |