Coinbase

Here's a new transaction creation process - take a Coinbase transaction as an example.

1. Define Transaction Payload

The transaction payload needs to implement Payload interface, in which the Data method is used to obtain the byte array needed to verify the transaction signature.

// Payload define the func for loading the payload data
// base on payload type which have different structure
type Payload interface {

	Data(version byte) []byte

	Serialize(w io.Writer, version byte) error

	Deserialize(r io.Reader, version byte) error

}

Corresponding to the above interface, the implementation of Coinbase transaction payload is as follows:

type CoinBase struct {
	Content []byte
}

func (a *CoinBase) Data(version byte) []byte {
	return a.Content
}

func (a *CoinBase) Serialize(w io.Writer, version byte) error {
	return common.WriteVarBytes(w, a.Content)
}

func (a *CoinBase) Deserialize(r io.Reader, version byte) error {
	temp, err := common.ReadVarBytes(r, MaxPayloadDataSize,
		"payload coinbase data")
	a.Content = temp
	return err
}

2. Define the Transaction Structure

Every different transaction needs to create a new transaction file in the core/transaction directory, and realize the transaction interface. The methods in some interfaces are as follows:

type Transaction interface {
	...

	// get data
	Version() common2.TransactionVersion
	TxType() common2.TxType
	PayloadVersion() byte
	Payload() Payload
	Attributes() []*common2.Attribute
	Inputs() []*common2.Input
	Outputs() []*common2.Output
	LockTime() uint32
	Programs() []*pg.Program
	Fee() common.Fixed64
	FeePerKB() common.Fixed64

	// set data
	SetVersion(version common2.TransactionVersion)
	SetTxType(txType common2.TxType)
	SetFee(fee common.Fixed64)
	SetFeePerKB(feePerKB common.Fixed64)
	SetAttributes(attributes []*common2.Attribute)
	SetPayloadVersion(payloadVersion byte)
	SetPayload(payload Payload)
	SetInputs(inputs []*common2.Input)
	SetOutputs(outputs []*common2.Output)
	SetPrograms(programs []*pg.Program)
	SetLockTime(lockTime uint32)

	String() string
	Serialize(w io.Writer) error
	SerializeUnsigned(w io.Writer) error
	SerializeSizeStripped() int
	Deserialize(r io.Reader) error
	DeserializeUnsigned(r io.Reader) error
	GetSize() int
	Hash() common.Uint256
  
	...
}

Because there are many functions to be implemented in the transaction interface, it's difficult for each function developer to implement them one-by-one. Therefore, BaseTransaction is provided on the chain to implement the transaction interface by default, and users can define a new transaction structure by combining BaseTransaction:

type CoinBaseTransaction struct {
	BaseTransaction
}

3. Implement the Transaction Verification Interface

The transaction structure needs to implement the inteface required for transaction verification, including:

type TransactionChecker interface {
   BaseTransactionChecker

   SanityCheck(p Parameters) elaerr.ELAError

   ContextCheck(p Parameters) (map[*common2.Input]common2.Output, elaerr.ELAError)
}

type BaseTransactionChecker interface {

   // check height version
   HeightVersionCheck() error

   /// SANITY CHECK
   // rewrite this function to check the transaction size, otherwise the
   // transaction size if compare with default value: MaxBlockContextSize
   CheckTransactionSize() error
   // check transaction inputs
   CheckTransactionInput() error
   // check transaction outputs
   CheckTransactionOutput() error
   // check transaction payload type
   CheckTransactionPayload() error
   // check transaction attributes and programs
   CheckAttributeProgram() error

   /// CONTEXT CHECK
   // if the transaction should create in POW need to return true
   IsAllowedInPOWConsensus() bool
   // the special context check of transaction, such as check the transaction payload
   SpecialContextCheck() (error elaerr.ELAError, end bool)
}

When the transaction is implemented, if there's special verification, a step in the transaction check interface needs to be re-implemented. If a step doesn't need to be adjusted, the default implementation of DefaultChecker can be automatically adopted without rewriting. The default implementation of DefaultChecker can refer to the DefaultChecker structure in github.com/elastos/elastos.ela/core/transaction/transactionchecker.

Developers need to rewrite the method of transaction interface according to the actual verification conditions of new transactions. SanityCheck method in TransactionChecker is context-free transaction check, and ContextCheck is context-sensitive transaction check. The implementation of SanityCheck and ContextCheck is composed of several steps in BaseTransactionChecker. When adding a transaction, you only need to rewrite BaseTransactionChecker. If the verification order of the transaction needs to be adjusted, you still need to rewrite the corresponding method of TransactionChecker interface.

In the implementation of Coinbase transaction to transaction check interface, the rewritten Sanity check includes CheckTransactionInput, CheckTransactionOutput, CheckAttributeProgram and CheckTransactionPayload methods. The rewritten Context check including IsAllowedInPOWConsensus and SpecialContextCheck. For example, Coinbase transaction needs to adjust the order of ContextCheck, so the ContextCheck method is rewritten.

func (t *CoinBaseTransaction) CheckTransactionInput() error {
	if len(t.Inputs()) != 1 {
		return errors.New("coinbase must has only one input")
	}
	inputHash := t.Inputs()[0].Previous.TxID
	inputIndex := t.Inputs()[0].Previous.Index
	sequence := t.Inputs()[0].Sequence
	if !inputHash.IsEqual(common.EmptyHash) ||
		inputIndex != math.MaxUint16 || sequence != math.MaxUint32 {
		return errors.New("invalid coinbase input")
	}

	return nil
}

func (t *CoinBaseTransaction) CheckTransactionOutput() error {

	blockHeight := t.parameters.BlockHeight
	chainParams := t.parameters.Config

	if len(t.Outputs()) > math.MaxUint16 {
		return errors.New("output count should not be greater than 65535(MaxUint16)")
	}
	if len(t.Outputs()) < 2 {
		return errors.New("coinbase output is not enough, at least 2")
	}

	foundationReward := t.Outputs()[0].Value
	var totalReward = common.Fixed64(0)
	if blockHeight < chainParams.PublicDPOSHeight {
		for _, output := range t.Outputs() {
			if output.AssetID != config.ELAAssetID {
				return errors.New("asset ID in coinbase is invalid")
			}
			totalReward += output.Value
		}

		if foundationReward < common.Fixed64(float64(totalReward)*0.3) {
			return errors.New("reward to foundation in coinbase < 30%")
		}
	} else {
		// check the ratio of FoundationAddress reward with miner reward
		totalReward = t.Outputs()[0].Value + t.Outputs()[1].Value
		if len(t.Outputs()) == 2 && foundationReward <
			common.Fixed64(float64(totalReward)*0.3/0.65) {
			return errors.New("reward to foundation in coinbase < 30%")
		}
	}

	return nil
}

func (t *CoinBaseTransaction) CheckAttributeProgram() error {
	// no need to check attribute and program
	if len(t.Programs()) != 0 {
		return errors.New("transaction should have no programs")
	}
	return nil
}

func (t *CoinBaseTransaction) CheckTransactionPayload() error {
	switch t.Payload().(type) {
	case *payload.CoinBase:
		return nil
	}

	return errors.New("invalid payload type")
}

func (t *CoinBaseTransaction) IsAllowedInPOWConsensus() bool {
	return true
}

func (a *CoinBaseTransaction) SpecialContextCheck() (result elaerr.ELAError, end bool) {

	para := a.parameters
	if para.BlockHeight >= para.Config.CRCommitteeStartHeight {
		if para.BlockChain.GetState().GetConsensusAlgorithm() == 0x01 {
			if !a.outputs[0].ProgramHash.IsEqual(para.Config.DestroyELAAddress) {
				return elaerr.Simple(elaerr.ErrTxInvalidOutput,
					errors.New("first output address should be "+
						"DestroyAddress in POW consensus algorithm")), true
			}
		} else {
			if !a.outputs[0].ProgramHash.IsEqual(para.Config.CRAssetsAddress) {
				return elaerr.Simple(elaerr.ErrTxInvalidOutput,
					errors.New("first output address should be CR assets address")), true
			}
		}
	} else if !a.outputs[0].ProgramHash.IsEqual(para.Config.Foundation) {
		return elaerr.Simple(elaerr.ErrTxInvalidOutput,
			errors.New("first output address should be foundation address")), true
	}

	return nil, true
}

func (a *CoinBaseTransaction) ContextCheck(paras interfaces.Parameters) (map[*common2.Input]common2.Output, elaerr.ELAError) {

	if err := a.SetParameters(paras); err != nil {
		log.Warn("[CheckTransactionContext] set parameters failed.")
		return nil, elaerr.Simple(elaerr.ErrTxDuplicate, errors.New("invalid parameters"))
	}

	if err := a.HeightVersionCheck(); err != nil {
		log.Warn("[CheckTransactionContext] height version check failed.")
		return nil, elaerr.Simple(elaerr.ErrTxHeightVersion, nil)
	}

	// check if duplicated with transaction in ledger
	if exist := a.IsTxHashDuplicate(*a.txHash); exist {
		log.Warn("[CheckTransactionContext] duplicate transaction check failed.")
		return nil, elaerr.Simple(elaerr.ErrTxDuplicate, nil)
	}

	err, end := a.SpecialContextCheck()
	if end {
		log.Warn("[CheckTransactionContext] SpecialContextCheck failed:", err)
		return nil, err
	}

	return nil, nil
}

4. Local Adjustment of Trading Pits Duplication

In the trading pits, if you want to restrict packaging the similar transaction twice in one block, you need to add the duplication logic in trading pits, which is realized by conflictSlot:

// conflictSlot hold a set of transactions references that may conflict with
//	incoming transactions, those transactions will process with same rule to
//	generate key by which to detect the conflict.
type conflictSlot struct {
	keyType        keyType
	conflictTypes  map[common2.TxType]getKeyFunc
	stringSet      map[string]interfaces.Transaction
	hashSet        map[common.Uint256]interfaces.Transaction
	programHashSet map[common.Uint168]interfaces.Transaction
}	

Add the implementation of the corresponding transaction in the newConflictManager method in mempool/conflictmanager.go Because Coinbase transaction does not need special transaction duplication, this method isn't adjusted.

5. Database Storage

If the new transaction needs special processing and storage of part of the transaction information, it needs to change the database storage logic. The location of the code adjustment is in blockchain/chainstoreffldb.go:

func (c *ChainStoreFFLDB) SaveBlock(b *Block, node *BlockNode,
	confirm *payload.Confirm, medianTimePast time.Time) error {
  	err := c.db.Update(func(dbTx database.Tx) error {
		return dbStoreBlock(dbTx, &DposBlock{
			Block:       b,
			HaveConfirm: confirm != nil,
			Confirm:     confirm,
		})
	})
	if err != nil {
		return err
	}

	// Generate a new best state snapshot that will be used to update the
	// database and later memory if all database updates are successful.
	numTxns := uint64(len(b.Transactions))
	blockSize := uint64(b.GetSize())
	blockWeight := uint64(GetBlockWeight(b))
	state := newBestState(node, blockSize, blockWeight, numTxns, medianTimePast)

	// Atomically insert info into the database.
	err = c.db.Update(func(dbTx database.Tx) error {
				...
    		return nil
	})

	return err
}

6. Memory State Modification

If the new transaction needs to record some information in memory to provide services to the outside world, the memory state needs to be modified. Note that all states that need to exist for a long time should be modified by the state.History.Append method.

Generally, the needs related to consensus are put into the processTransactions method of dpos/state.go:

// processTransactions takes the transactions and the height when they have been
// packed into a block.  Then loop through the transactions to update producers
// state and votes according to transactions content.
func (s *State) processTransactions(txs []interfaces.Transaction, height uint32) {

	for _, tx := range txs {
		s.processTransaction(tx, height)
	}
 
  ...
  
}

Generally, in the processTransactions method of cr/state.go related to governance:

// processTransactions takes the transactions and the Height when they have been
// packed into a block.  Then loop through the transactions to update CR
// State and Votes according to transactions content.
func (c *Committee) processTransactions(txs []interfaces.Transaction, height uint32) {
	sortedTxs := make([]interfaces.Transaction, 0)
	if len(txs) < 1 {
		return
	}
	for _, tx := range txs {
		sortedTxs = append(sortedTxs, tx)
	}
	SortTransactions(sortedTxs[1:])
	for _, tx := range sortedTxs {
		c.processTransaction(tx, height)
	}
}

Last updated