neoneo-go/pkg/core/transaction/transaction.go
2020-03-30 15:48:50 +03:00

396 lines
10 KiB
Go

package transaction
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"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 (
// MaxTransactionSize is the upper limit size in bytes that a transaction can reach. It is
// set to be 102400.
MaxTransactionSize = 102400
)
// Transaction is a process recorded in the NEO blockchain.
type Transaction struct {
// The type of the transaction.
Type TXType
// The trading version which is currently 0.
Version uint8
// Data specific to the type of the transaction.
// This is always a pointer to a <Type>Transaction.
Data TXer
// Transaction attributes.
Attributes []Attribute
// The inputs of the transaction.
Inputs []Input
// The outputs of the transaction.
Outputs []Output
// The scripts that comes with this transaction.
// Scripts exist out of the verification script
// and invocation script.
Scripts []Witness
// Hash of the transaction (double SHA256).
hash util.Uint256
// Hash of the transaction used to verify it (single SHA256).
verificationHash 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,
}
}
// 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
}
// VerificationHash returns the hash of the transaction used to verify it.
func (t *Transaction) VerificationHash() util.Uint256 {
if t.verificationHash.Equals(util.Uint256{}) {
if t.createHash() != nil {
panic("failed to compute hash!")
}
}
return t.verificationHash
}
// AddOutput adds the given output to the transaction outputs.
func (t *Transaction) AddOutput(out *Output) {
t.Outputs = append(t.Outputs, *out)
}
// AddInput adds the given input to the transaction inputs.
func (t *Transaction) AddInput(in *Input) {
t.Inputs = append(t.Inputs, *in)
}
// AddVerificationHash adds a script attribute for transaction verification.
func (t *Transaction) AddVerificationHash(addr util.Uint160) {
t.Attributes = append(t.Attributes, Attribute{
Usage: Script,
Data: addr.BytesBE(),
})
}
// DecodeBinary implements Serializable interface.
func (t *Transaction) DecodeBinary(br *io.BinReader) {
t.Type = TXType(br.ReadB())
t.Version = uint8(br.ReadB())
t.decodeData(br)
br.ReadArray(&t.Attributes)
br.ReadArray(&t.Inputs)
br.ReadArray(&t.Outputs)
for i := range t.Outputs {
if t.Outputs[i].Amount.LessThan(0) {
br.Err = errors.New("negative output")
return
}
}
br.ReadArray(&t.Scripts)
// Create the hash of the transaction at decode, so we dont need
// to do it anymore.
if br.Err == nil {
br.Err = t.createHash()
}
}
func (t *Transaction) decodeData(r *io.BinReader) {
switch t.Type {
case InvocationType:
t.Data = &InvocationTX{Version: t.Version}
t.Data.(*InvocationTX).DecodeBinary(r)
case MinerType:
t.Data = &MinerTX{}
t.Data.(*MinerTX).DecodeBinary(r)
case ClaimType:
t.Data = &ClaimTX{}
t.Data.(*ClaimTX).DecodeBinary(r)
case ContractType:
t.Data = &ContractTX{}
t.Data.(*ContractTX).DecodeBinary(r)
case RegisterType:
t.Data = &RegisterTX{}
t.Data.(*RegisterTX).DecodeBinary(r)
case IssueType:
t.Data = &IssueTX{}
t.Data.(*IssueTX).DecodeBinary(r)
case EnrollmentType:
t.Data = &EnrollmentTX{}
t.Data.(*EnrollmentTX).DecodeBinary(r)
case PublishType:
t.Data = &PublishTX{Version: t.Version}
t.Data.(*PublishTX).DecodeBinary(r)
case StateType:
t.Data = &StateTX{}
t.Data.(*StateTX).DecodeBinary(r)
default:
r.Err = fmt.Errorf("invalid TX type %x", t.Type)
}
}
// 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) {
noData := t.Type == ContractType
if t.Data == nil && !noData {
bw.Err = errors.New("transaction has no data")
return
}
bw.WriteB(byte(t.Type))
bw.WriteB(byte(t.Version))
// Underlying TXer.
if !noData {
t.Data.EncodeBinary(bw)
}
// Attributes
bw.WriteArray(t.Attributes)
// Inputs
bw.WriteArray(t.Inputs)
// Outputs
bw.WriteArray(t.Outputs)
}
// 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
}
b := buf.Bytes()
t.verificationHash = hash.Sha256(b)
t.hash = hash.Sha256(t.verificationHash.BytesBE())
return nil
}
// GroupOutputByAssetID groups all TX outputs by their assetID.
func (t Transaction) GroupOutputByAssetID() map[util.Uint256][]*Output {
m := make(map[util.Uint256][]*Output)
for i := range t.Outputs {
hash := t.Outputs[i].AssetID
m[hash] = append(m[hash], &t.Outputs[i])
}
return m
}
// GetSignedPart returns a part of the transaction which must be signed.
func (t *Transaction) GetSignedPart() []byte {
buf := io.NewBufBinWriter()
t.encodeHashableFields(buf.BinWriter)
if buf.Err != nil {
return nil
}
return buf.Bytes()
}
// 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()
}
// transactionJSON is a wrapper for Transaction and
// used for correct marhalling of transaction.Data
type transactionJSON struct {
TxID util.Uint256 `json:"txid"`
Size int `json:"size"`
Type TXType `json:"type"`
Version uint8 `json:"version"`
Attributes []Attribute `json:"attributes"`
Inputs []Input `json:"vin"`
Outputs []Output `json:"vout"`
Scripts []Witness `json:"scripts"`
Claims []Input `json:"claims,omitempty"`
PublicKey *keys.PublicKey `json:"pubkey,omitempty"`
Script string `json:"script,omitempty"`
Gas util.Fixed8 `json:"gas,omitempty"`
Nonce uint32 `json:"nonce,omitempty"`
Contract *publishedContract `json:"contract,omitempty"`
Asset *registeredAsset `json:"asset,omitempty"`
Descriptors []*StateDescriptor `json:"descriptors,omitempty"`
}
// MarshalJSON implements json.Marshaler interface.
func (t *Transaction) MarshalJSON() ([]byte, error) {
tx := transactionJSON{
TxID: t.Hash(),
Size: io.GetVarSize(t),
Type: t.Type,
Version: t.Version,
Attributes: t.Attributes,
Inputs: t.Inputs,
Outputs: t.Outputs,
Scripts: t.Scripts,
}
switch t.Type {
case MinerType:
tx.Nonce = t.Data.(*MinerTX).Nonce
case ClaimType:
tx.Claims = t.Data.(*ClaimTX).Claims
case EnrollmentType:
tx.PublicKey = &t.Data.(*EnrollmentTX).PublicKey
case InvocationType:
tx.Script = hex.EncodeToString(t.Data.(*InvocationTX).Script)
tx.Gas = t.Data.(*InvocationTX).Gas
case PublishType:
transaction := t.Data.(*PublishTX)
tx.Contract = &publishedContract{
Code: publishedCode{
Hash: hash.Hash160(transaction.Script),
Script: hex.EncodeToString(transaction.Script),
ParamList: transaction.ParamList,
ReturnType: transaction.ReturnType,
},
NeedStorage: transaction.NeedStorage,
Name: transaction.Name,
CodeVersion: transaction.CodeVersion,
Author: transaction.Author,
Email: transaction.Email,
Description: transaction.Description,
}
case RegisterType:
transaction := *t.Data.(*RegisterTX)
tx.Asset = &registeredAsset{
AssetType: transaction.AssetType,
Name: json.RawMessage(transaction.Name),
Amount: transaction.Amount,
Precision: transaction.Precision,
Owner: transaction.Owner,
Admin: address.Uint160ToString(transaction.Admin),
}
case StateType:
tx.Descriptors = t.Data.(*StateTX).Descriptors
}
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.Type = tx.Type
t.Version = tx.Version
t.Attributes = tx.Attributes
t.Inputs = tx.Inputs
t.Outputs = tx.Outputs
t.Scripts = tx.Scripts
switch tx.Type {
case MinerType:
t.Data = &MinerTX{
Nonce: tx.Nonce,
}
case ClaimType:
t.Data = &ClaimTX{
Claims: tx.Claims,
}
case EnrollmentType:
t.Data = &EnrollmentTX{
PublicKey: *tx.PublicKey,
}
case InvocationType:
bytes, err := hex.DecodeString(tx.Script)
if err != nil {
return err
}
t.Data = &InvocationTX{
Script: bytes,
Gas: tx.Gas,
Version: tx.Version,
}
case PublishType:
bytes, err := hex.DecodeString(tx.Contract.Code.Script)
if err != nil {
return err
}
t.Data = &PublishTX{
Script: bytes,
ParamList: tx.Contract.Code.ParamList,
ReturnType: tx.Contract.Code.ReturnType,
NeedStorage: tx.Contract.NeedStorage,
Name: tx.Contract.Name,
CodeVersion: tx.Contract.CodeVersion,
Author: tx.Contract.Author,
Email: tx.Contract.Email,
Description: tx.Contract.Description,
Version: tx.Version,
}
case RegisterType:
admin, err := address.StringToUint160(tx.Asset.Admin)
if err != nil {
return err
}
t.Data = &RegisterTX{
AssetType: tx.Asset.AssetType,
Name: string(tx.Asset.Name),
Amount: tx.Asset.Amount,
Precision: tx.Asset.Precision,
Owner: tx.Asset.Owner,
Admin: admin,
}
case StateType:
t.Data = &StateTX{
Descriptors: tx.Descriptors,
}
case ContractType:
t.Data = &ContractTX{}
case IssueType:
t.Data = &IssueTX{}
}
if t.Hash() != tx.TxID {
return errors.New("txid doesn't match transaction hash")
}
return nil
}