Skip to content
Snippets Groups Projects
tx.go 13.7 KiB
Newer Older
// Copyright 2017 Monax Industries Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package txs
Casey Kuhlman's avatar
Casey Kuhlman committed

import (
Ethan Buchman's avatar
Ethan Buchman committed
	"bytes"
Casey Kuhlman's avatar
Casey Kuhlman committed
	"encoding/json"
	"errors"
	"io"
androlo's avatar
.  
androlo committed

Ethan Buchman's avatar
Ethan Buchman committed
	"golang.org/x/crypto/ripemd160"
androlo's avatar
.  
androlo committed

	acm "github.com/monax/eris-db/account"
	ptypes "github.com/monax/eris-db/permission/types"
Ethan Buchman's avatar
Ethan Buchman committed
	. "github.com/tendermint/go-common"
	"github.com/tendermint/go-wire"
Ethan Buchman's avatar
Ethan Buchman committed
	"github.com/tendermint/go-crypto"
	tendermint_types "github.com/tendermint/tendermint/types" // votes for dupeout ..
Silas Davis's avatar
Silas Davis committed
	ErrTxInvalidAddress       = errors.New("Error invalid address")
	ErrTxDuplicateAddress     = errors.New("Error duplicate address")
	ErrTxInvalidAmount        = errors.New("Error invalid amount")
	ErrTxInsufficientFunds    = errors.New("Error insufficient funds")
Casey Kuhlman's avatar
Casey Kuhlman committed
	ErrTxInsufficientGasPrice = errors.New("Error insufficient gas price")
Silas Davis's avatar
Silas Davis committed
	ErrTxUnknownPubKey        = errors.New("Error unknown pubkey")
	ErrTxInvalidPubKey        = errors.New("Error invalid pubkey")
	ErrTxInvalidSignature     = errors.New("Error invalid signature")
	ErrTxPermissionDenied     = errors.New("Error permission denied")
Ethan Buchman's avatar
Ethan Buchman committed
type ErrTxInvalidString struct {
	Msg string
}

func (e ErrTxInvalidString) Error() string {
	return e.Msg
}

Casey Kuhlman's avatar
Casey Kuhlman committed
type ErrTxInvalidSequence struct {
	Got      int
	Expected int
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (e ErrTxInvalidSequence) Error() string {
	return Fmt("Error invalid sequence. Got %d, expected %d", e.Got, e.Expected)
}

/*
Tx (Transaction) is an atomic operation on the ledger state.

Account Txs:
 - SendTx         Send coins to address
 - CallTx         Send a msg to a contract that runs in the vm
 - NameTx	  Store some value under a name in the global namereg

Validation Txs:
 - BondTx         New validator posts a bond
 - UnbondTx       Validator leaves
 - DupeoutTx      Validator dupes out (equivocates)
androlo's avatar
androlo committed

Admin Txs:
 - PermissionsTx
Casey Kuhlman's avatar
Casey Kuhlman committed
*/
androlo's avatar
androlo committed

Casey Kuhlman's avatar
Casey Kuhlman committed
// Types of Tx implementations
const (
	// Account transactions
	TxTypeSend = byte(0x01)
	TxTypeCall = byte(0x02)
	TxTypeName = byte(0x03)

	// Validation transactions
Silas Davis's avatar
Silas Davis committed
	TxTypeBond    = byte(0x11)
	TxTypeUnbond  = byte(0x12)
	TxTypeRebond  = byte(0x13)
Casey Kuhlman's avatar
Casey Kuhlman committed
	TxTypeDupeout = byte(0x14)
androlo's avatar
androlo committed

	// Admin transactions
	TxTypePermissions = byte(0x20)
androlo's avatar
androlo committed
// for wire.readReflect
var _ = wire.RegisterInterface(
Silas Davis's avatar
Silas Davis committed
	struct{ Tx }{},
androlo's avatar
androlo committed
	wire.ConcreteType{&SendTx{}, TxTypeSend},
	wire.ConcreteType{&CallTx{}, TxTypeCall},
	wire.ConcreteType{&NameTx{}, TxTypeName},
	wire.ConcreteType{&BondTx{}, TxTypeBond},
	wire.ConcreteType{&UnbondTx{}, TxTypeUnbond},
	wire.ConcreteType{&RebondTx{}, TxTypeRebond},
	wire.ConcreteType{&DupeoutTx{}, TxTypeDupeout},
	wire.ConcreteType{&PermissionsTx{}, TxTypePermissions},
Casey Kuhlman's avatar
Casey Kuhlman committed
)

//-----------------------------------------------------------------------------

Silas Davis's avatar
Silas Davis committed
type (
Silas Davis's avatar
Silas Davis committed
	Tx interface {
		WriteSignBytes(chainID string, w io.Writer, n *int, err *error)
	}

	// UnconfirmedTxs
	UnconfirmedTxs struct {
		Txs []Tx `json:"txs"`
	}

	SendTx struct {
		Inputs  []*TxInput  `json:"inputs"`
		Outputs []*TxOutput `json:"outputs"`
	}

	// BroadcastTx or Transact
	Receipt struct {
		TxHash          []byte `json:"tx_hash"`
		CreatesContract uint8  `json:"creates_contract"`
		ContractAddr    []byte `json:"contract_addr"`
	}

	NameTx struct {
		Input *TxInput `json:"input"`
		Name  string   `json:"name"`
		Data  string   `json:"data"`
		Fee   int64    `json:"fee"`
	}

	CallTx struct {
		Input    *TxInput `json:"input"`
		Address  []byte   `json:"address"`
		GasLimit int64    `json:"gas_limit"`
		Fee      int64    `json:"fee"`
		Data     []byte   `json:"data"`
	}

	TxInput struct {
		Address   []byte           `json:"address"`   // Hash of the PubKey
		Amount    int64            `json:"amount"`    // Must not exceed account balance
		Sequence  int              `json:"sequence"`  // Must be 1 greater than the last committed TxInput
		Signature crypto.Signature `json:"signature"` // Depends on the PubKey type and the whole Tx
		PubKey    crypto.PubKey    `json:"pub_key"`   // Must not be nil, may be nil
	}

	TxOutput struct {
		Address []byte `json:"address"` // Hash of the PubKey
		Amount  int64  `json:"amount"`  // The sum of all outputs must not exceed the inputs.
	}
)
Casey Kuhlman's avatar
Casey Kuhlman committed

func (txIn *TxInput) ValidateBasic() error {
	if len(txIn.Address) != 20 {
		return ErrTxInvalidAddress
	}
	if txIn.Amount == 0 {
		return ErrTxInvalidAmount
	}
	return nil
}

func (txIn *TxInput) WriteSignBytes(w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"address":"%X","amount":%v,"sequence":%v}`, txIn.Address, txIn.Amount, txIn.Sequence)), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (txIn *TxInput) String() string {
	return Fmt("TxInput{%X,%v,%v,%v,%v}", txIn.Address, txIn.Amount, txIn.Sequence, txIn.Signature, txIn.PubKey)
}

//-----------------------------------------------------------------------------

func (txOut *TxOutput) ValidateBasic() error {
	if len(txOut.Address) != 20 {
		return ErrTxInvalidAddress
	}
	if txOut.Amount == 0 {
		return ErrTxInvalidAmount
	}
	return nil
}

func (txOut *TxOutput) WriteSignBytes(w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"address":"%X","amount":%v}`, txOut.Address, txOut.Amount)), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (txOut *TxOutput) String() string {
	return Fmt("TxOutput{%X,%v}", txOut.Address, txOut.Amount)
}

//-----------------------------------------------------------------------------

func (tx *SendTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"tx":[%v,{"inputs":[`, TxTypeSend)), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
	for i, in := range tx.Inputs {
		in.WriteSignBytes(w, n, err)
Silas Davis's avatar
Silas Davis committed
		if i != len(tx.Inputs)-1 {
androlo's avatar
androlo committed
			wire.WriteTo([]byte(","), w, n, err)
androlo's avatar
androlo committed
	wire.WriteTo([]byte(`],"outputs":[`), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
	for i, out := range tx.Outputs {
		out.WriteSignBytes(w, n, err)
Silas Davis's avatar
Silas Davis committed
		if i != len(tx.Outputs)-1 {
androlo's avatar
androlo committed
			wire.WriteTo([]byte(","), w, n, err)
androlo's avatar
androlo committed
	wire.WriteTo([]byte(`]}]}`), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (tx *SendTx) String() string {
	return Fmt("SendTx{%v -> %v}", tx.Inputs, tx.Outputs)
}

//-----------------------------------------------------------------------------

func (tx *CallTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"tx":[%v,{"address":"%X","data":"%X"`, TxTypeCall, tx.Address, tx.Data)), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"fee":%v,"gas_limit":%v,"input":`, tx.Fee, tx.GasLimit)), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
	tx.Input.WriteSignBytes(w, n, err)
androlo's avatar
androlo committed
	wire.WriteTo([]byte(`}]}`), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (tx *CallTx) String() string {
	return Fmt("CallTx{%v -> %x: %x}", tx.Input, tx.Address, tx.Data)
}

Ethan Buchman's avatar
Ethan Buchman committed
func NewContractAddress(caller []byte, nonce int) []byte {
Silas Davis's avatar
Silas Davis committed
	temp := make([]byte, 32+8)
Ethan Buchman's avatar
Ethan Buchman committed
	copy(temp, caller)
	PutInt64BE(temp[32:], int64(nonce))
	hasher := ripemd160.New()
	hasher.Write(temp) // does not error
	return hasher.Sum(nil)
}

Casey Kuhlman's avatar
Casey Kuhlman committed
//-----------------------------------------------------------------------------

func (tx *NameTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"tx":[%v,{"data":%s,"fee":%v`, TxTypeName, jsonEscape(tx.Data), tx.Fee)), w, n, err)
	wire.WriteTo([]byte(`,"input":`), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
	tx.Input.WriteSignBytes(w, n, err)
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`,"name":%s`, jsonEscape(tx.Name))), w, n, err)
	wire.WriteTo([]byte(`}]}`), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (tx *NameTx) ValidateStrings() error {
	if len(tx.Name) == 0 {
Ethan Buchman's avatar
Ethan Buchman committed
		return ErrTxInvalidString{"Name must not be empty"}
Casey Kuhlman's avatar
Casey Kuhlman committed
	}
	if len(tx.Name) > MaxNameLength {
Ethan Buchman's avatar
Ethan Buchman committed
		return ErrTxInvalidString{Fmt("Name is too long. Max %d bytes", MaxNameLength)}
Casey Kuhlman's avatar
Casey Kuhlman committed
	}
	if len(tx.Data) > MaxDataLength {
Ethan Buchman's avatar
Ethan Buchman committed
		return ErrTxInvalidString{Fmt("Data is too long. Max %d bytes", MaxDataLength)}
Casey Kuhlman's avatar
Casey Kuhlman committed
	}

	if !validateNameRegEntryName(tx.Name) {
Ethan Buchman's avatar
Ethan Buchman committed
		return ErrTxInvalidString{Fmt("Invalid characters found in NameTx.Name (%s). Only alphanumeric, underscores, dashes, forward slashes, and @ are allowed", tx.Name)}
Casey Kuhlman's avatar
Casey Kuhlman committed
	}

	if !validateNameRegEntryData(tx.Data) {
Ethan Buchman's avatar
Ethan Buchman committed
		return ErrTxInvalidString{Fmt("Invalid characters found in NameTx.Data (%s). Only the kind of things found in a JSON file are allowed", tx.Data)}
Casey Kuhlman's avatar
Casey Kuhlman committed
	}

	return nil
}

func (tx *NameTx) String() string {
	return Fmt("NameTx{%v -> %s: %s}", tx.Input, tx.Name, tx.Data)
}

//-----------------------------------------------------------------------------

type BondTx struct {
Ethan Buchman's avatar
Ethan Buchman committed
	PubKey    crypto.PubKeyEd25519    `json:"pub_key"` // NOTE: these don't have type byte
Ethan Buchman's avatar
Ethan Buchman committed
	Signature crypto.SignatureEd25519 `json:"signature"`
	Inputs    []*TxInput              `json:"inputs"`
	UnbondTo  []*TxOutput             `json:"unbond_to"`
func (tx *BondTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"tx":[%v,{"inputs":[`, TxTypeBond)), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
	for i, in := range tx.Inputs {
		in.WriteSignBytes(w, n, err)
Silas Davis's avatar
Silas Davis committed
		if i != len(tx.Inputs)-1 {
androlo's avatar
androlo committed
			wire.WriteTo([]byte(","), w, n, err)
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`],"pub_key":`)), w, n, err)
	wire.WriteTo(wire.JSONBytes(tx.PubKey), w, n, err)
	wire.WriteTo([]byte(`,"unbond_to":[`), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
	for i, out := range tx.UnbondTo {
		out.WriteSignBytes(w, n, err)
Silas Davis's avatar
Silas Davis committed
		if i != len(tx.UnbondTo)-1 {
androlo's avatar
androlo committed
			wire.WriteTo([]byte(","), w, n, err)
androlo's avatar
androlo committed
	wire.WriteTo([]byte(`]}]}`), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (tx *BondTx) String() string {
	return Fmt("BondTx{%v: %v -> %v}", tx.PubKey, tx.Inputs, tx.UnbondTo)
}

//-----------------------------------------------------------------------------

type UnbondTx struct {
Ethan Buchman's avatar
Ethan Buchman committed
	Address   []byte                  `json:"address"`
	Height    int                     `json:"height"`
	Signature crypto.SignatureEd25519 `json:"signature"`
func (tx *UnbondTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"tx":[%v,{"address":"%X","height":%v}]}`, TxTypeUnbond, tx.Address, tx.Height)), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (tx *UnbondTx) String() string {
	return Fmt("UnbondTx{%X,%v,%v}", tx.Address, tx.Height, tx.Signature)
}

//-----------------------------------------------------------------------------

type RebondTx struct {
Ethan Buchman's avatar
Ethan Buchman committed
	Address   []byte                  `json:"address"`
	Height    int                     `json:"height"`
	Signature crypto.SignatureEd25519 `json:"signature"`
func (tx *RebondTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"tx":[%v,{"address":"%X","height":%v}]}`, TxTypeRebond, tx.Address, tx.Height)), w, n, err)
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (tx *RebondTx) String() string {
	return Fmt("RebondTx{%X,%v,%v}", tx.Address, tx.Height, tx.Signature)
}

//-----------------------------------------------------------------------------

type DupeoutTx struct {
	Address []byte                `json:"address"`
	VoteA   tendermint_types.Vote `json:"vote_a"`
	VoteB   tendermint_types.Vote `json:"vote_b"`
func (tx *DupeoutTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
Ethan Buchman's avatar
Ethan Buchman committed
	//PanicSanity("DupeoutTx has no sign bytes")
	// TODO
	return
Casey Kuhlman's avatar
Casey Kuhlman committed
}

func (tx *DupeoutTx) String() string {
	return Fmt("DupeoutTx{%X,%v,%v}", tx.Address, tx.VoteA, tx.VoteB)
}

//-----------------------------------------------------------------------------

androlo's avatar
androlo committed
type PermissionsTx struct {
	Input    *TxInput        `json:"input"`
	PermArgs ptypes.PermArgs `json:"args"`
}

func (tx *PermissionsTx) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) {
androlo's avatar
androlo committed
	wire.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err)
	wire.WriteTo([]byte(Fmt(`,"tx":[%v,{"args":"`, TxTypePermissions)), w, n, err)
Ethan Buchman's avatar
Ethan Buchman committed
	wire.WriteJSON(&tx.PermArgs, w, n, err)
androlo's avatar
androlo committed
	wire.WriteTo([]byte(`","input":`), w, n, err)
	tx.Input.WriteSignBytes(w, n, err)
	wire.WriteTo([]byte(`}]}`), w, n, err)
}

func (tx *PermissionsTx) String() string {
	return Fmt("PermissionsTx{%v -> %v}", tx.Input, tx.PermArgs)
}

//-----------------------------------------------------------------------------

func TxHash(chainID string, tx Tx) []byte {
	signBytes := acm.SignBytes(chainID, tx)
	hasher := ripemd160.New()
	hasher.Write(signBytes)
	// Calling Sum(nil) just gives us the digest with nothing prefixed
	return hasher.Sum(nil)
}

Ethan Buchman's avatar
Ethan Buchman committed
//-----------------------------------------------------------------------------

func EncodeTx(tx Tx) ([]byte, error) {
	var n int
	var err error
	buf := new(bytes.Buffer)
	wire.WriteBinary(struct{ Tx }{tx}, buf, &n, &err)
	if err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
Ethan Buchman's avatar
Ethan Buchman committed
// panic on err
func DecodeTx(txBytes []byte) (Tx, error) {
Ethan Buchman's avatar
Ethan Buchman committed
	var n int
	var err error
	tx := new(Tx)
	buf := bytes.NewBuffer(txBytes)
	wire.ReadBinaryPtr(tx, buf, len(txBytes), &n, &err)
	if err != nil {
		return nil, err
	return *tx, nil
Silas Davis's avatar
Silas Davis committed
func GenerateReceipt(chainId string, tx Tx) Receipt {
	receipt := Receipt{
		TxHash:          TxHash(chainId, tx),
Silas Davis's avatar
Silas Davis committed
		CreatesContract: 0,
		ContractAddr:    nil,
	}
	if callTx, ok := tx.(*CallTx); ok {
		if len(callTx.Address) == 0 {
			receipt.CreatesContract = 1
			receipt.ContractAddr = NewContractAddress(callTx.Input.Address,
				callTx.Input.Sequence)
		}
	}
	return receipt
}

Casey Kuhlman's avatar
Casey Kuhlman committed
//--------------------------------------------------------------------------------

// Contract: This function is deterministic and completely reversible.
func jsonEscape(str string) string {
	escapedBytes, err := json.Marshal(str)
	if err != nil {
androlo's avatar
androlo committed
		PanicSanity(Fmt("Error json-escaping a string", str))
Casey Kuhlman's avatar
Casey Kuhlman committed
	}
	return string(escapedBytes)
}