neo-go/pkg/smartcontract/nef/nef.go

232 lines
6.6 KiB
Go

package nef
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"strconv"
"strings"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// NEO Executable Format 3 (NEF3)
// Standard: https://github.com/neo-project/proposals/pull/121/files
// Implementation: https://github.com/neo-project/neo/blob/v3.0.0-preview2/src/neo/SmartContract/NefFile.cs#L8
// +------------+-----------+------------------------------------------------------------+
// | Field | Length | Comment |
// +------------+-----------+------------------------------------------------------------+
// | Magic | 4 bytes | Magic header |
// | Compiler | 32 bytes | Compiler used |
// | Version | 16 bytes | Compiler version (Major, Minor, Build, Version) |
// | ScriptHash | 20 bytes | ScriptHash for the script (BE) |
// +------------+-----------+------------------------------------------------------------+
// | Checksum | 4 bytes | First four bytes of double SHA256 hash of the header |
// +------------+-----------+------------------------------------------------------------+
// | Script | Var bytes | Var bytes for the payload |
// +------------+-----------+------------------------------------------------------------+
const (
// Magic is a magic File header constant.
Magic uint32 = 0x3346454E
// MaxScriptLength is the maximum allowed contract script length.
MaxScriptLength = 1024 * 1024
// compilerFieldSize is the length of `Compiler` File header field in bytes.
compilerFieldSize = 32
)
// File represents compiled contract file structure according to the NEF3 standard.
type File struct {
Header Header
Checksum uint32
Script []byte
}
// Header represents File header.
type Header struct {
Magic uint32
Compiler string
Version Version
ScriptHash util.Uint160
}
// Version represents compiler version.
type Version struct {
Major int32
Minor int32
Build int32
Revision int32
}
// NewFile returns new NEF3 file with script specified.
func NewFile(script []byte) (File, error) {
file := File{
Header: Header{
Magic: Magic,
Compiler: "neo-go",
ScriptHash: hash.Hash160(script),
},
Script: script,
}
v, err := GetVersion(config.Version)
if err != nil {
return file, err
}
file.Header.Version = v
file.Checksum = file.Header.CalculateChecksum()
return file, nil
}
// GetVersion returns Version from the given string. It accepts the following formats:
// `major.minor.build-[...]`
// `major.minor.build.revision-[...]`
// where `major`, `minor`, `build` and `revision` are 32-bit integers with base=10
func GetVersion(version string) (Version, error) {
var (
result Version
)
versions := strings.SplitN(version, ".", 4)
if len(versions) < 3 {
return result, errors.New("invalid version format")
}
major, err := strconv.ParseInt(versions[0], 10, 32)
if err != nil {
return result, fmt.Errorf("failed to parse major version: %w", err)
}
result.Major = int32(major)
minor, err := strconv.ParseInt(versions[1], 10, 32)
if err != nil {
return result, fmt.Errorf("failed to parse minor version: %w", err)
}
result.Minor = int32(minor)
b := versions[2]
if len(versions) == 3 {
b = strings.SplitN(b, "-", 2)[0]
}
build, err := strconv.ParseInt(b, 10, 32)
if err != nil {
return result, fmt.Errorf("failed to parse build version: %w", err)
}
result.Build = int32(build)
if len(versions) == 4 {
r := strings.SplitN(versions[3], "-", 2)[0]
revision, err := strconv.ParseInt(r, 10, 32)
if err != nil {
return result, fmt.Errorf("failed to parse revision version: %w", err)
}
result.Revision = int32(revision)
}
return result, nil
}
// EncodeBinary implements io.Serializable interface.
func (v *Version) EncodeBinary(w *io.BinWriter) {
w.WriteU32LE(uint32(v.Major))
w.WriteU32LE(uint32(v.Minor))
w.WriteU32LE(uint32(v.Build))
w.WriteU32LE(uint32(v.Revision))
}
// DecodeBinary implements io.Serializable interface.
func (v *Version) DecodeBinary(r *io.BinReader) {
v.Major = int32(r.ReadU32LE())
v.Minor = int32(r.ReadU32LE())
v.Build = int32(r.ReadU32LE())
v.Revision = int32(r.ReadU32LE())
}
// EncodeBinary implements io.Serializable interface.
func (h *Header) EncodeBinary(w *io.BinWriter) {
w.WriteU32LE(h.Magic)
if len(h.Compiler) > compilerFieldSize {
w.Err = errors.New("invalid compiler name length")
return
}
bytes := []byte(h.Compiler)
w.WriteBytes(bytes)
if len(bytes) < compilerFieldSize {
w.WriteBytes(make([]byte, compilerFieldSize-len(bytes)))
}
h.Version.EncodeBinary(w)
h.ScriptHash.EncodeBinary(w)
}
// DecodeBinary implements io.Serializable interface.
func (h *Header) DecodeBinary(r *io.BinReader) {
h.Magic = r.ReadU32LE()
if h.Magic != Magic {
r.Err = errors.New("invalid Magic")
return
}
buf := make([]byte, compilerFieldSize)
r.ReadBytes(buf)
buf = bytes.TrimRightFunc(buf, func(r rune) bool {
return r == 0
})
h.Compiler = string(buf)
h.Version.DecodeBinary(r)
h.ScriptHash.DecodeBinary(r)
}
// CalculateChecksum returns first 4 bytes of double-SHA256(Header) converted to uint32.
func (h *Header) CalculateChecksum() uint32 {
buf := io.NewBufBinWriter()
h.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
panic(buf.Err)
}
return binary.LittleEndian.Uint32(hash.Checksum(buf.Bytes()))
}
// EncodeBinary implements io.Serializable interface.
func (n *File) EncodeBinary(w *io.BinWriter) {
n.Header.EncodeBinary(w)
w.WriteU32LE(n.Checksum)
w.WriteVarBytes(n.Script)
}
// DecodeBinary implements io.Serializable interface.
func (n *File) DecodeBinary(r *io.BinReader) {
n.Header.DecodeBinary(r)
n.Checksum = r.ReadU32LE()
checksum := n.Header.CalculateChecksum()
if checksum != n.Checksum {
r.Err = errors.New("CRC verification fail")
return
}
n.Script = r.ReadVarBytes(MaxScriptLength)
if !hash.Hash160(n.Script).Equals(n.Header.ScriptHash) {
r.Err = errors.New("script hashes mismatch")
return
}
}
// Bytes returns byte array with serialized NEF File.
func (n File) Bytes() ([]byte, error) {
buf := io.NewBufBinWriter()
n.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return nil, buf.Err
}
return buf.Bytes(), nil
}
// FileFromBytes returns NEF File deserialized from given bytes.
func FileFromBytes(source []byte) (File, error) {
result := File{}
r := io.NewBinReaderFromBuf(source)
result.DecodeBinary(r)
if r.Err != nil {
return result, r.Err
}
return result, nil
}