neoneo-go/pkg/smartcontract/nef/nef.go
Anna Shaleva 90705e37e9 compiler: perform NEF size check on serialization
Retun an error if the serialized NEF size exceeds stackitem.MaxSize.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-11-20 15:14:05 +03:00

198 lines
6.3 KiB
Go

package nef
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"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/vm/stackitem"
)
// 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
// 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 a 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 a File header.
type Header struct {
Magic uint32 `json:"magic"`
Compiler string `json:"compiler"`
}
// NewFile returns a new NEF3 file with the 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 the 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 the 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.
// CalculateChecksum doesn't perform the resulting serialized NEF size check, and return
// the checksum irrespectively to the size limit constraint. It's a caller's duty to check
// the resulting NEF size.
func (n *File) CalculateChecksum() uint32 {
bb, err := n.BytesLong()
if err != nil {
panic(err)
}
return binary.LittleEndian.Uint32(hash.Checksum(bb[:len(bb)-4]))
}
// EncodeBinary implements the 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 the 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(stackitem.MaxSize)
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 a byte array with a serialized NEF File. It performs the
// resulting NEF file size check and returns an error if serialized slice length
// exceeds [stackitem.MaxSize].
func (n File) Bytes() ([]byte, error) {
return n.bytes(true)
}
// BytesLong returns a byte array with a serialized NEF File. It performs no
// resulting slice check.
func (n File) BytesLong() ([]byte, error) {
return n.bytes(false)
}
// bytes returns the serialized NEF File representation and performs the resulting
// byte array size check if needed.
func (n File) bytes(checkSize bool) ([]byte, error) {
buf := io.NewBufBinWriter()
n.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return nil, buf.Err
}
res := buf.Bytes()
if checkSize && len(res) > stackitem.MaxSize {
return nil, fmt.Errorf("serialized NEF size exceeds VM stackitem limits: %d bytes is allowed at max, got %d", stackitem.MaxSize, len(res))
}
return res, nil
}
// FileFromBytes returns a NEF File deserialized from the given bytes.
func FileFromBytes(source []byte) (File, error) {
result := File{}
if len(source) > stackitem.MaxSize {
return result, fmt.Errorf("invalid NEF file size: expected %d at max, got %d", stackitem.MaxSize, len(source))
}
r := io.NewBinReaderFromBuf(source)
result.DecodeBinary(r)
if r.Err != nil {
return result, r.Err
}
return result, nil
}