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.
1
// Payload define the func for loading the payload data
2
// base on payload type which have different structure
3
type Payload interface {
4
5
Data(version byte) []byte
6
7
Serialize(w io.Writer, version byte) error
8
9
Deserialize(r io.Reader, version byte) error
10
11
}
Copied!
Corresponding to the above interface, the implementation of Coinbase transaction payload is as follows:
1
type CoinBase struct {
2
Content []byte
3
}
4
5
func (a *CoinBase) Data(version byte) []byte {
6
return a.Content
7
}
8
9
func (a *CoinBase) Serialize(w io.Writer, version byte) error {
10
return common.WriteVarBytes(w, a.Content)
11
}
12
13
func (a *CoinBase) Deserialize(r io.Reader, version byte) error {
14
temp, err := common.ReadVarBytes(r, MaxPayloadDataSize,
15
"payload coinbase data")
16
a.Content = temp
17
return err
18
}
Copied!

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:
1
type Transaction interface {
2
...
3
4
// get data
5
Version() common2.TransactionVersion
6
TxType() common2.TxType
7
PayloadVersion() byte
8
Payload() Payload
9
Attributes() []*common2.Attribute
10
Inputs() []*common2.Input
11
Outputs() []*common2.Output
12
LockTime() uint32
13
Programs() []*pg.Program
14
Fee() common.Fixed64
15
FeePerKB() common.Fixed64
16
17
// set data
18
SetVersion(version common2.TransactionVersion)
19
SetTxType(txType common2.TxType)
20
SetFee(fee common.Fixed64)
21
SetFeePerKB(feePerKB common.Fixed64)
22
SetAttributes(attributes []*common2.Attribute)
23
SetPayloadVersion(payloadVersion byte)
24
SetPayload(payload Payload)
25
SetInputs(inputs []*common2.Input)
26
SetOutputs(outputs []*common2.Output)
27
SetPrograms(programs []*pg.Program)
28
SetLockTime(lockTime uint32)
29
30
String() string
31
Serialize(w io.Writer) error
32
SerializeUnsigned(w io.Writer) error
33
SerializeSizeStripped() int
34
Deserialize(r io.Reader) error
35
DeserializeUnsigned(r io.Reader) error
36
GetSize() int
37
Hash() common.Uint256
38
39
...
40
}
Copied!
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:
1
type CoinBaseTransaction struct {
2
BaseTransaction
3
}
Copied!

3. Implement the Transaction Verification Interface

The transaction structure needs to implement the inteface required for transaction verification, including:
1
type TransactionChecker interface {
2
BaseTransactionChecker
3
4
SanityCheck(p Parameters) elaerr.ELAError
5
6
ContextCheck(p Parameters) (map[*common2.Input]common2.Output, elaerr.ELAError)
7
}
8
9
type BaseTransactionChecker interface {
10
11
// check height version
12
HeightVersionCheck() error
13
14
/// SANITY CHECK
15
// rewrite this function to check the transaction size, otherwise the
16
// transaction size if compare with default value: MaxBlockContextSize
17
CheckTransactionSize() error
18
// check transaction inputs
19
CheckTransactionInput() error
20
// check transaction outputs
21
CheckTransactionOutput() error
22
// check transaction payload type
23
CheckTransactionPayload() error
24
// check transaction attributes and programs
25
CheckAttributeProgram() error
26
27
/// CONTEXT CHECK
28
// if the transaction should create in POW need to return true
29
IsAllowedInPOWConsensus() bool
30
// the special context check of transaction, such as check the transaction payload
31
SpecialContextCheck() (error elaerr.ELAError, end bool)
32
}
Copied!
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.
1
func (t *CoinBaseTransaction) CheckTransactionInput() error {
2
if len(t.Inputs()) != 1 {
3
return errors.New("coinbase must has only one input")
4
}
5
inputHash := t.Inputs()[0].Previous.TxID
6
inputIndex := t.Inputs()[0].Previous.Index
7
sequence := t.Inputs()[0].Sequence
8
if !inputHash.IsEqual(common.EmptyHash) ||
9
inputIndex != math.MaxUint16 || sequence != math.MaxUint32 {
10
return errors.New("invalid coinbase input")
11
}
12
13
return nil
14
}
15
16
func (t *CoinBaseTransaction) CheckTransactionOutput() error {
17
18
blockHeight := t.parameters.BlockHeight
19
chainParams := t.parameters.Config
20
21
if len(t.Outputs()) > math.MaxUint16 {
22
return errors.New("output count should not be greater than 65535(MaxUint16)")
23
}
24
if len(t.Outputs()) < 2 {
25
return errors.New("coinbase output is not enough, at least 2")
26
}
27
28
foundationReward := t.Outputs()[0].Value
29
var totalReward = common.Fixed64(0)
30
if blockHeight < chainParams.PublicDPOSHeight {
31
for _, output := range t.Outputs() {
32
if output.AssetID != config.ELAAssetID {
33
return errors.New("asset ID in coinbase is invalid")
34
}
35
totalReward += output.Value
36
}
37
38
if foundationReward < common.Fixed64(float64(totalReward)*0.3) {
39
return errors.New("reward to foundation in coinbase < 30%")
40
}
41
} else {
42
// check the ratio of FoundationAddress reward with miner reward
43
totalReward = t.Outputs()[0].Value + t.Outputs()[1].Value
44
if len(t.Outputs()) == 2 && foundationReward <
45
common.Fixed64(float64(totalReward)*0.3/0.65) {
46
return errors.New("reward to foundation in coinbase < 30%")
47
}
48
}
49
50
return nil
51
}
52
53
func (t *CoinBaseTransaction) CheckAttributeProgram() error {
54
// no need to check attribute and program
55
if len(t.Programs()) != 0 {
56
return errors.New("transaction should have no programs")
57
}
58
return nil
59
}
60
61
func (t *CoinBaseTransaction) CheckTransactionPayload() error {
62
switch t.Payload().(type) {
63
case *payload.CoinBase:
64
return nil
65
}
66
67
return errors.New("invalid payload type")
68
}
69
70
func (t *CoinBaseTransaction) IsAllowedInPOWConsensus() bool {
71
return true
72
}
73
74
func (a *CoinBaseTransaction) SpecialContextCheck() (result elaerr.ELAError, end bool) {
75
76
para := a.parameters
77
if para.BlockHeight >= para.Config.CRCommitteeStartHeight {
78
if para.BlockChain.GetState().GetConsensusAlgorithm() == 0x01 {
79
if !a.outputs[0].ProgramHash.IsEqual(para.Config.DestroyELAAddress) {
80
return elaerr.Simple(elaerr.ErrTxInvalidOutput,
81
errors.New("first output address should be "+
82
"DestroyAddress in POW consensus algorithm")), true
83
}
84
} else {
85
if !a.outputs[0].ProgramHash.IsEqual(para.Config.CRAssetsAddress) {
86
return elaerr.Simple(elaerr.ErrTxInvalidOutput,
87
errors.New("first output address should be CR assets address")), true
88
}
89
}
90
} else if !a.outputs[0].ProgramHash.IsEqual(para.Config.Foundation) {
91
return elaerr.Simple(elaerr.ErrTxInvalidOutput,
92
errors.New("first output address should be foundation address")), true
93
}
94
95
return nil, true
96
}
97
98
func (a *CoinBaseTransaction) ContextCheck(paras interfaces.Parameters) (map[*common2.Input]common2.Output, elaerr.ELAError) {
99
100
if err := a.SetParameters(paras); err != nil {
101
log.Warn("[CheckTransactionContext] set parameters failed.")
102
return nil, elaerr.Simple(elaerr.ErrTxDuplicate, errors.New("invalid parameters"))
103
}
104
105
if err := a.HeightVersionCheck(); err != nil {
106
log.Warn("[CheckTransactionContext] height version check failed.")
107
return nil, elaerr.Simple(elaerr.ErrTxHeightVersion, nil)
108
}
109
110
// check if duplicated with transaction in ledger
111
if exist := a.IsTxHashDuplicate(*a.txHash); exist {
112
log.Warn("[CheckTransactionContext] duplicate transaction check failed.")
113
return nil, elaerr.Simple(elaerr.ErrTxDuplicate, nil)
114
}
115
116
err, end := a.SpecialContextCheck()
117
if end {
118
log.Warn("[CheckTransactionContext] SpecialContextCheck failed:", err)
119
return nil, err
120
}
121
122
return nil, nil
123
}
Copied!

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:
1
// conflictSlot hold a set of transactions references that may conflict with
2
// incoming transactions, those transactions will process with same rule to
3
// generate key by which to detect the conflict.
4
type conflictSlot struct {
5
keyType keyType
6
conflictTypes map[common2.TxType]getKeyFunc
7
stringSet map[string]interfaces.Transaction
8
hashSet map[common.Uint256]interfaces.Transaction
9
programHashSet map[common.Uint168]interfaces.Transaction
10
}
Copied!
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:
1
func (c *ChainStoreFFLDB) SaveBlock(b *Block, node *BlockNode,
2
confirm *payload.Confirm, medianTimePast time.Time) error {
3
err := c.db.Update(func(dbTx database.Tx) error {
4
return dbStoreBlock(dbTx, &DposBlock{
5
Block: b,
6
HaveConfirm: confirm != nil,
7
Confirm: confirm,
8
})
9
})
10
if err != nil {
11
return err
12
}
13
14
// Generate a new best state snapshot that will be used to update the
15
// database and later memory if all database updates are successful.
16
numTxns := uint64(len(b.Transactions))
17
blockSize := uint64(b.GetSize())
18
blockWeight := uint64(GetBlockWeight(b))
19
state := newBestState(node, blockSize, blockWeight, numTxns, medianTimePast)
20
21
// Atomically insert info into the database.
22
err = c.db.Update(func(dbTx database.Tx) error {
23
...
24
return nil
25
})
26
27
return err
28
}
Copied!

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:
1
// processTransactions takes the transactions and the height when they have been
2
// packed into a block. Then loop through the transactions to update producers
3
// state and votes according to transactions content.
4
func (s *State) processTransactions(txs []interfaces.Transaction, height uint32) {
5
6
for _, tx := range txs {
7
s.processTransaction(tx, height)
8
}
9
10
...
11
12
}
Copied!
Generally, in the processTransactions method of cr/state.go related to governance:
1
// processTransactions takes the transactions and the Height when they have been
2
// packed into a block. Then loop through the transactions to update CR
3
// State and Votes according to transactions content.
4
func (c *Committee) processTransactions(txs []interfaces.Transaction, height uint32) {
5
sortedTxs := make([]interfaces.Transaction, 0)
6
if len(txs) < 1 {
7
return
8
}
9
for _, tx := range txs {
10
sortedTxs = append(sortedTxs, tx)
11
}
12
SortTransactions(sortedTxs[1:])
13
for _, tx := range sortedTxs {
14
c.processTransaction(tx, height)
15
}
16
}
Copied!