package nef import ( "bytes" "encoding/binary" "errors" "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" ) // 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 | 64 bytes | Compiler used and it's version | // | Source | Var bytes | Source file URL. | // +------------+-----------+------------------------------------------------------------+ // | Reserved | 1 byte | Reserved for extensions. Must be 0. | // | Tokens | Var array | List of method tokens | // | Reserved | 2-bytes | Reserved for extensions. Must be 0. | // | Script | Var bytes | Var bytes for the payload | // +------------+-----------+------------------------------------------------------------+ // | Checksum | 4 bytes | First four bytes of double SHA256 hash of the header | // +------------+-----------+------------------------------------------------------------+ const ( // Magic is a magic File header constant. Magic uint32 = 0x3346454E // MaxScriptLength is the maximum allowed contract script length. MaxScriptLength = 512 * 1024 // MaxSourceURLLength is the maximum allowed source URL length. MaxSourceURLLength = 256 // compilerFieldSize is the length of `Compiler` File header field in bytes. compilerFieldSize = 64 ) // File represents compiled contract file structure according to the NEF3 standard. type File struct { Header Source string `json:"source"` Tokens []MethodToken `json:"tokens"` Script []byte `json:"script"` Checksum uint32 `json:"checksum"` } // Header represents File header. type Header struct { Magic uint32 `json:"magic"` Compiler string `json:"compiler"` } // NewFile returns new NEF3 file with script specified. func NewFile(script []byte) (*File, error) { file := &File{ Header: Header{ Magic: Magic, Compiler: "neo-go-" + config.Version, }, Tokens: []MethodToken{}, Script: script, } if len(file.Compiler) > compilerFieldSize { return nil, errors.New("too long compiler field") } file.Checksum = file.CalculateChecksum() return file, nil } // 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 } var b = make([]byte, compilerFieldSize) copy(b, []byte(h.Compiler)) w.WriteBytes(b) } // 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) } // CalculateChecksum returns first 4 bytes of double-SHA256(Header) converted to uint32. func (n *File) CalculateChecksum() uint32 { bb, err := n.Bytes() if err != nil { panic(err) } return binary.LittleEndian.Uint32(hash.Checksum(bb[:len(bb)-4])) } // EncodeBinary implements io.Serializable interface. func (n *File) EncodeBinary(w *io.BinWriter) { n.Header.EncodeBinary(w) if len(n.Source) > MaxSourceURLLength { w.Err = errors.New("source url too long") return } w.WriteString(n.Source) w.WriteB(0) w.WriteArray(n.Tokens) w.WriteU16LE(0) w.WriteVarBytes(n.Script) w.WriteU32LE(n.Checksum) } var errInvalidReserved = errors.New("reserved bytes must be 0") // DecodeBinary implements io.Serializable interface. func (n *File) DecodeBinary(r *io.BinReader) { n.Header.DecodeBinary(r) n.Source = r.ReadString(MaxSourceURLLength) reservedB := r.ReadB() if r.Err == nil && reservedB != 0 { r.Err = errInvalidReserved return } r.ReadArray(&n.Tokens) reserved := r.ReadU16LE() if r.Err == nil && reserved != 0 { r.Err = errInvalidReserved return } n.Script = r.ReadVarBytes(MaxScriptLength) if r.Err == nil && len(n.Script) == 0 { r.Err = errors.New("empty script") return } n.Checksum = r.ReadU32LE() checksum := n.CalculateChecksum() if r.Err == nil && checksum != n.Checksum { r.Err = errors.New("checksum verification failure") 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 }