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 structuretypePayloadinterface {Data(version byte) []byteSerialize(w io.Writer, version byte) errorDeserialize(r io.Reader, version byte) error}
Corresponding to the above interface, the implementation of Coinbase transaction payload is as follows:
typeCoinBasestruct { 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 = tempreturn 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:
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:
typeCoinBaseTransactionstruct {BaseTransaction}
3. Implement the Transaction Verification Interface
The transaction structure needs to implement the inteface required for transaction verification, including:
typeTransactionCheckerinterface {BaseTransactionCheckerSanityCheck(p Parameters) elaerr.ELAErrorContextCheck(p Parameters) (map[*common2.Input]common2.Output, elaerr.ELAError)}typeBaseTransactionCheckerinterface {// check height versionHeightVersionCheck() error/// SANITY CHECK// rewrite this function to check the transaction size, otherwise the// transaction size if compare with default value: MaxBlockContextSizeCheckTransactionSize() error// check transaction inputsCheckTransactionInput() error// check transaction outputsCheckTransactionOutput() error// check transaction payload typeCheckTransactionPayload() error// check transaction attributes and programsCheckAttributeProgram() error/// CONTEXT CHECK// if the transaction should create in POW need to return trueIsAllowedInPOWConsensus() bool// the special context check of transaction, such as check the transaction payloadSpecialContextCheck() (errorelaerr.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 {iflen(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].Sequenceif!inputHash.IsEqual(common.EmptyHash) || inputIndex != math.MaxUint16 || sequence != math.MaxUint32 {return errors.New("invalid coinbase input") }returnnil}func (t *CoinBaseTransaction) CheckTransactionOutput() error { blockHeight := t.parameters.BlockHeight chainParams := t.parameters.Configiflen(t.Outputs()) > math.MaxUint16 {return errors.New("output count should not be greater than 65535(MaxUint16)") }iflen(t.Outputs()) <2 {return errors.New("coinbase output is not enough, at least 2") } foundationReward := t.Outputs()[0].Valuevar 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].Valueiflen(t.Outputs()) ==2&& foundationReward < common.Fixed64(float64(totalReward)*0.3/0.65) {return errors.New("reward to foundation in coinbase < 30%") } }returnnil}func (t *CoinBaseTransaction) CheckAttributeProgram() error {// no need to check attribute and programiflen(t.Programs()) !=0 {return errors.New("transaction should have no programs") }returnnil}func (t *CoinBaseTransaction) CheckTransactionPayload() error {switch t.Payload().(type) {case*payload.CoinBase:returnnil }return errors.New("invalid payload type")}func (t *CoinBaseTransaction) IsAllowedInPOWConsensus() bool {returntrue}func (a *CoinBaseTransaction) SpecialContextCheck() (result elaerr.ELAError, end bool) { para := a.parametersif 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 } } } elseif!a.outputs[0].ProgramHash.IsEqual(para.Config.Foundation) {return elaerr.Simple(elaerr.ErrTxInvalidOutput, errors.New("first output address should be foundation address")), true }returnnil, 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.")returnnil, elaerr.Simple(elaerr.ErrTxDuplicate, errors.New("invalid parameters")) }if err := a.HeightVersionCheck(); err !=nil { log.Warn("[CheckTransactionContext] height version check failed.")returnnil, elaerr.Simple(elaerr.ErrTxHeightVersion, nil) }// check if duplicated with transaction in ledgerif exist := a.IsTxHashDuplicate(*a.txHash); exist { log.Warn("[CheckTransactionContext] duplicate transaction check failed.")returnnil, elaerr.Simple(elaerr.ErrTxDuplicate, nil) } err, end := a.SpecialContextCheck()if end { log.Warn("[CheckTransactionContext] SpecialContextCheck failed:", err)returnnil, err }returnnil, 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.typeconflictSlotstruct { 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 {returndbStoreBlock(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 {...returnnil })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)iflen(txs) <1 {return }for _, tx :=range txs { sortedTxs =append(sortedTxs, tx) }SortTransactions(sortedTxs[1:])for _, tx :=range sortedTxs { c.processTransaction(tx, height) }}