neoneo-go/pkg/core/transaction/transaction.go
Roman Khimov d2732a71d8 transaction: don't overwrite error and witnesses length check
ReadArray() can return some error and we shouldn't overwrite it. At the same
time limiting ReadArray() to the number of Signers can make it return wrong
error if the number of witnesses actually is bigger than the number of
signers, so use MaxAttributes.
2021-08-04 23:17:50 +03:00

406 lines
11 KiB
Go

package transaction
import (
"encoding/json"
"errors"
"fmt"
"math"
"math/rand"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
)
const (
// MaxScriptLength is the limit for transaction's script length.
MaxScriptLength = math.MaxUint16
// MaxTransactionSize is the upper limit size in bytes that a transaction can reach. It is
// set to be 102400.
MaxTransactionSize = 102400
// MaxAttributes is maximum number of attributes including signers that can be contained
// within a transaction. It is set to be 16.
MaxAttributes = 16
// DummyVersion represents reserved transaction version for trimmed transactions.
DummyVersion = 255
)
// ErrInvalidWitnessNum returns when the number of witnesses does not match signers.
var ErrInvalidWitnessNum = errors.New("number of signers doesn't match witnesses")
// Transaction is a process recorded in the NEO blockchain.
type Transaction struct {
// The trading version which is currently 0.
Version uint8
// Random number to avoid hash collision.
Nonce uint32
// Fee to be burned.
SystemFee int64
// Fee to be distributed to consensus nodes.
NetworkFee int64
// Maximum blockchain height exceeding which
// transaction should fail verification.
ValidUntilBlock uint32
// Code to run in NeoVM for this transaction.
Script []byte
// Transaction attributes.
Attributes []Attribute
// Transaction signers list (starts with Sender).
Signers []Signer
// The scripts that comes with this transaction.
// Scripts exist out of the verification script
// and invocation script.
Scripts []Witness
// size is transaction's serialized size.
size int
// Hash of the transaction (double SHA256).
hash util.Uint256
// Trimmed indicates this is a transaction from trimmed
// data.
Trimmed bool
}
// NewTrimmedTX returns a trimmed transaction with only its hash
// and Trimmed to true.
func NewTrimmedTX(hash util.Uint256) *Transaction {
return &Transaction{
hash: hash,
Trimmed: true,
}
}
// New returns a new transaction to execute given script and pay given system
// fee.
func New(script []byte, gas int64) *Transaction {
return &Transaction{
Version: 0,
Nonce: rand.Uint32(),
Script: script,
SystemFee: gas,
Attributes: []Attribute{},
Signers: []Signer{},
Scripts: []Witness{},
}
}
// Hash returns the hash of the transaction.
func (t *Transaction) Hash() util.Uint256 {
if t.hash.Equals(util.Uint256{}) {
if t.createHash() != nil {
panic("failed to compute hash!")
}
}
return t.hash
}
// HasAttribute returns true iff t has an attribute of type typ.
func (t *Transaction) HasAttribute(typ AttrType) bool {
for i := range t.Attributes {
if t.Attributes[i].Type == typ {
return true
}
}
return false
}
// GetAttributes returns the list of transaction's attributes of the given type.
// Returns nil in case if attributes not found.
func (t *Transaction) GetAttributes(typ AttrType) []Attribute {
var result []Attribute
for _, attr := range t.Attributes {
if attr.Type == typ {
result = append(result, attr)
}
}
return result
}
// decodeHashableFields decodes the fields that are used for signing the
// transaction, which are all fields except the scripts.
func (t *Transaction) decodeHashableFields(br *io.BinReader) {
t.Version = uint8(br.ReadB())
t.Nonce = br.ReadU32LE()
t.SystemFee = int64(br.ReadU64LE())
t.NetworkFee = int64(br.ReadU64LE())
t.ValidUntilBlock = br.ReadU32LE()
br.ReadArray(&t.Signers, MaxAttributes)
br.ReadArray(&t.Attributes, MaxAttributes-len(t.Signers))
t.Script = br.ReadVarBytes(MaxScriptLength)
if br.Err == nil {
br.Err = t.isValid()
}
}
func (t *Transaction) decodeBinaryNoSize(br *io.BinReader) {
t.decodeHashableFields(br)
if br.Err != nil {
return
}
br.ReadArray(&t.Scripts, MaxAttributes)
if br.Err == nil && len(t.Signers) != len(t.Scripts) {
br.Err = fmt.Errorf("%w: %d vs %d", ErrInvalidWitnessNum, len(t.Signers), len(t.Scripts))
return
}
// Create the hash of the transaction at decode, so we dont need
// to do it anymore.
if br.Err == nil {
br.Err = t.createHash()
}
}
// DecodeBinary implements Serializable interface.
func (t *Transaction) DecodeBinary(br *io.BinReader) {
t.decodeBinaryNoSize(br)
if br.Err == nil {
_ = t.Size()
}
}
// EncodeBinary implements Serializable interface.
func (t *Transaction) EncodeBinary(bw *io.BinWriter) {
t.encodeHashableFields(bw)
bw.WriteArray(t.Scripts)
}
// encodeHashableFields encodes the fields that are not used for
// signing the transaction, which are all fields except the scripts.
func (t *Transaction) encodeHashableFields(bw *io.BinWriter) {
if len(t.Script) == 0 {
bw.Err = errors.New("transaction has no script")
return
}
bw.WriteB(byte(t.Version))
bw.WriteU32LE(t.Nonce)
bw.WriteU64LE(uint64(t.SystemFee))
bw.WriteU64LE(uint64(t.NetworkFee))
bw.WriteU32LE(t.ValidUntilBlock)
bw.WriteArray(t.Signers)
bw.WriteArray(t.Attributes)
bw.WriteVarBytes(t.Script)
}
// EncodeHashableFields returns serialized transaction's fields which are hashed.
func (t *Transaction) EncodeHashableFields() ([]byte, error) {
bw := io.NewBufBinWriter()
t.encodeHashableFields(bw.BinWriter)
if bw.Err != nil {
return nil, bw.Err
}
return bw.Bytes(), nil
}
// createHash creates the hash of the transaction.
func (t *Transaction) createHash() error {
buf := io.NewBufBinWriter()
t.encodeHashableFields(buf.BinWriter)
if buf.Err != nil {
return buf.Err
}
t.hash = hash.Sha256(buf.Bytes())
return nil
}
// DecodeHashableFields decodes a part of transaction which should be hashed.
func (t *Transaction) DecodeHashableFields(buf []byte) error {
r := io.NewBinReaderFromBuf(buf)
t.decodeHashableFields(r)
if r.Err != nil {
return r.Err
}
// Ensure all the data was read.
_ = r.ReadB()
if r.Err == nil {
return errors.New("additional data after the signed part")
}
t.Scripts = make([]Witness, 0)
t.hash = hash.Sha256(buf)
return nil
}
// Bytes converts the transaction to []byte.
func (t *Transaction) Bytes() []byte {
buf := io.NewBufBinWriter()
t.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return nil
}
return buf.Bytes()
}
// NewTransactionFromBytes decodes byte array into *Transaction.
func NewTransactionFromBytes(b []byte) (*Transaction, error) {
tx := &Transaction{}
r := io.NewBinReaderFromBuf(b)
tx.decodeBinaryNoSize(r)
if r.Err != nil {
return nil, r.Err
}
_ = r.ReadB()
if r.Err == nil {
return nil, errors.New("additional data after the transaction")
}
tx.size = len(b)
return tx, nil
}
// FeePerByte returns NetworkFee of the transaction divided by
// its size.
func (t *Transaction) FeePerByte() int64 {
return t.NetworkFee / int64(t.Size())
}
// Size returns size of the serialized transaction.
func (t *Transaction) Size() int {
if t.size == 0 {
t.size = io.GetVarSize(t)
}
return t.size
}
// Sender returns the sender of the transaction which is always on the first place
// in the transaction's signers list.
func (t *Transaction) Sender() util.Uint160 {
if len(t.Signers) == 0 {
panic("transaction does not have signers")
}
return t.Signers[0].Account
}
// transactionJSON is a wrapper for Transaction and
// used for correct marhalling of transaction.Data.
type transactionJSON struct {
TxID util.Uint256 `json:"hash"`
Size int `json:"size"`
Version uint8 `json:"version"`
Nonce uint32 `json:"nonce"`
Sender string `json:"sender"`
SystemFee int64 `json:"sysfee,string"`
NetworkFee int64 `json:"netfee,string"`
ValidUntilBlock uint32 `json:"validuntilblock"`
Attributes []Attribute `json:"attributes"`
Signers []Signer `json:"signers"`
Script []byte `json:"script"`
Scripts []Witness `json:"witnesses"`
}
// MarshalJSON implements json.Marshaler interface.
func (t *Transaction) MarshalJSON() ([]byte, error) {
tx := transactionJSON{
TxID: t.Hash(),
Size: t.Size(),
Version: t.Version,
Nonce: t.Nonce,
Sender: address.Uint160ToString(t.Sender()),
ValidUntilBlock: t.ValidUntilBlock,
Attributes: t.Attributes,
Signers: t.Signers,
Script: t.Script,
Scripts: t.Scripts,
SystemFee: t.SystemFee,
NetworkFee: t.NetworkFee,
}
return json.Marshal(tx)
}
// UnmarshalJSON implements json.Unmarshaler interface.
func (t *Transaction) UnmarshalJSON(data []byte) error {
tx := new(transactionJSON)
if err := json.Unmarshal(data, tx); err != nil {
return err
}
t.Version = tx.Version
t.Nonce = tx.Nonce
t.ValidUntilBlock = tx.ValidUntilBlock
t.Attributes = tx.Attributes
t.Signers = tx.Signers
t.Scripts = tx.Scripts
t.SystemFee = tx.SystemFee
t.NetworkFee = tx.NetworkFee
t.Script = tx.Script
if t.Hash() != tx.TxID {
return errors.New("txid doesn't match transaction hash")
}
if t.Size() != tx.Size {
return errors.New("'size' doesn't match transaction size")
}
return t.isValid()
}
// Various errors for transaction validation.
var (
ErrInvalidVersion = errors.New("only version 0 is supported")
ErrNegativeSystemFee = errors.New("negative system fee")
ErrNegativeNetworkFee = errors.New("negative network fee")
ErrTooBigFees = errors.New("too big fees: int64 overflow")
ErrEmptySigners = errors.New("signers array should contain sender")
ErrNonUniqueSigners = errors.New("transaction signers should be unique")
ErrInvalidAttribute = errors.New("invalid attribute")
ErrEmptyScript = errors.New("no script")
)
// isValid checks whether decoded/unmarshalled transaction has all fields valid.
func (t *Transaction) isValid() error {
if t.Version > 0 && t.Version != DummyVersion {
return ErrInvalidVersion
}
if t.SystemFee < 0 {
return ErrNegativeSystemFee
}
if t.NetworkFee < 0 {
return ErrNegativeNetworkFee
}
if t.NetworkFee+t.SystemFee < t.SystemFee {
return ErrTooBigFees
}
if len(t.Signers) == 0 {
return ErrEmptySigners
}
for i := 0; i < len(t.Signers); i++ {
for j := i + 1; j < len(t.Signers); j++ {
if t.Signers[i].Account.Equals(t.Signers[j].Account) {
return ErrNonUniqueSigners
}
}
}
attrs := map[AttrType]bool{}
for i := range t.Attributes {
typ := t.Attributes[i].Type
if !typ.allowMultiple() {
if attrs[typ] {
return fmt.Errorf("%w: multiple '%s' attributes", ErrInvalidAttribute, typ.String())
}
attrs[typ] = true
}
}
if len(t.Script) == 0 {
return ErrEmptyScript
}
return nil
}
// HasSigner returns true in case if hash is present in the list of signers.
func (t *Transaction) HasSigner(hash util.Uint160) bool {
for _, h := range t.Signers {
if h.Account.Equals(hash) {
return true
}
}
return false
}