Skip to content

Latest commit

 

History

History
429 lines (321 loc) · 18.6 KB

consensus.md

File metadata and controls

429 lines (321 loc) · 18.6 KB

Consensus algorithm

Nodes of Berith agree block based on the PoS consensus algorithm. The authority to create a block is granted to the user who deposited the token. This based on the idea that the user who deposited the money will not attempt to attack while at risk of token price fluctuation.

Managing users who stake token

To prove the validity of the block, every node needs to manage a list of users who staked tokens.

Stake Transaction Track

Staking works through transactions. Berith divides the balance of the account into two to implement the staking function.

type Account struct {
    Nonce          uint64
    Balance        *big.Int
    Root           common.Hash // merkle root of the storage trie
    CodeHash       []byte
    StakeBalance   *big.Int //brt staking balance
    StakeUpdated   *big.Int //Block number when the stake balance was updated
    Point          *big.Int //selection Point
    BehindBalance  []Behind //behind balance
    Penalty        uint64
    PenlatyUpdated *big.Int //Block Number when the penalty was updated
}

The code above is an account information structure stored in the Merkle Patricia Tree. It shows that the account of Berith is divided into two fields: the Balance field, which stores the general balance MainBalance, and the StakeBalance, field which stores the balance of staked tokens.

Therefore, the transaction should indicate which balance of the account to use. For this purpose, we use an address that indicates the wallet type of account.

type JobWallet uint8

const (
    Main = 1 + iota
    Stake
    end
)

The above code is a declaration of type JobWallet. The JobWallet is an integer type, where ‘1’ indicates MainBalance and ‘2’ indicates StakeBalance.

Berith uses JobWallet to identify the type of wallet a user will use to send and receive tokens through transactions.

type txdata struct {
    AccountNonce uint64          `json:"nonce"    gencodec:"required"`
    Price        *big.Int        `json:"gasPrice" gencodec:"required"`
    GasLimit     uint64          `json:"gas"      gencodec:"required"`
    Recipient    *common.Address `json:"to"       rlp:"nil"` // nil means contract creation
    Amount       *big.Int        `json:"value"    gencodec:"required"`
    Payload      []byte          `json:"input"    gencodec:"required"`
    Base         JobWallet       `json:"base"  gencodec:"required"`   //[Berith] 작업 주체  ex) 스테이킹시 : Main
    Target       JobWallet       `json:"target"  gencodec:"required"` //[Berith] 작업타겟 ex) 스테이킹시 : Stake

    // Signature values
    V *big.Int `json:"v" gencodec:"required"`
    R *big.Int `json:"r" gencodec:"required"`
    S *big.Int `json:"s" gencodec:"required"`

    // This is only used when marshaling to JSON.
    Hash *common.Hash `json:"hash" rlp:"-"`
}

The above code is the transaction structure. Unlike the transaction of Ethereum, you can see that the Base and Target fields added. Base indicates what kind of balance the From account contains the tokens to send through the transaction. The Target indicates the type of balance the To account will contain tokens sent via the transaction.

Thus, if Base and Target specify a StakeBalance, you can see that the transaction is not typical but related to staking. In Berith, the transaction related to staking has a condition that From and To must be the same. And the transaction divided into two types.

Stake: A transaction in which Base is MainBalance and Target is StakeBalance.

Unstake: A transaction in which Base is StakeBalance and Target is MainBalance. Used when users want to move the staked tokens back to MainBalance. All balance of StakeBalance will be Unstaked, regardless of the setting of Value to Unstake.

Berith tracks stake transactions by checking the Base and Target fields of the transactions to be included in the block within the function that validates the block body.

Manage the list of users who staked tokens by block

Berith stores a list of accounts that are staking tokens at that block time for every block. Using the stake transaction information tracked in the function that validates the block body mentioned above, add a new staked account to Stakers of the previous block, and create and save new Stakers by deleting the account that canceled the stake.

if chain.Config().IsBIP1(number) {
            if msg.Base() == types.Main && msg.Target() == types.Stake {
                stkChanged[msg.From()] = true
            } else if msg.Base() == types.Stake && msg.Target() == types.Main {
                stkChanged[msg.From()] = false
            } else {
                continue
            }
        } else {
            if msg.Base() == types.Main && msg.Target() == types.Stake {
                stkChanged[msg.From()] = true
            } else {
                continue
            }
        }
}

for addr, isAdd := range stkChanged {
	
		...
			
		if isAdd {
			stks.Put(addr)
		} else {
			stks.Remove(addr)
		}

	}

The above code is part of the code to create new Stakers, and you can see adding or removing accounts from existing Stakers.

The created Stakers are only stored in level DB for blocks with one smaller number than the current block number. In level DB, the hash value of a block is stored as a key, and the encoded Stakers value is stored as a value. There are two reasons for not saving the current block.

First, the block corresponding to the current block number is often changed.

Second, the hash value of the block is different from the actual value because the block does not contain a signature when the block is first inspected as a function to validate the block body.

Therefore, the time when the Stakers corresponding to the block are stored is when the block with the current block as a parent is received.

Block creator draw system

In Berith, any user who stakes tokens can create and propagate blocks. However, the result of the draw determines the priority of the block and the delay of propagation.

select

The figure above shows the process of drawing a block creator. It is described below.

Block creator draw system - Stakers, Epoch

Block creator draws are based on the Stakers of a specific block. A specific block refers to a block that dates back the parent block of the current block as much as Epoch. Epoch is an integer specified in the node's ChainConfig.

MainnetChainConfig = &ChainConfig{

        ...

        Bsrr: &BSRRConfig{
            Period:       5,
            Epoch:        360,
            Rewards:      common.StringToBig("360"),
            StakeMinimum: common.StringToBig("100000000000000000000000"),
            SlashRound:   0,
            ForkFactor:   1.0,
        },
    }

The code above is a code that declares ChainConfig that is applied by default when running a node without a configuration file. You can see that Epoch is set to 360 by default in Berith.

epoch

The figure above shows what is the previous block as much as Epoch. As can be seen from the figure, the draw for Block 361 is calculated based on Block 1.

There are two reasons why Block Creator draw uses blocks that are as old as Epoch instead of the current block.

  1. To prevent the draw results from changing according to transactions in the block
  2. To prevent a staked user from immediately creating a block

Block creator draw system – Random number

The Block Creator draw is the same as arranging the contents of the specified Stakers in random order. However, to apply it in the blockchain, two conditions must be satisfied.

  1. The condition that random values ​​must be the same on all nodes
  2. The condition that a particular user should not be able to determine random values

Berith generates a random number by seeding the block number to satisfy these two conditions. The number of blocks satisfies two conditions because all nodes that receive the same block have the same value and are not changed by the block contents.

Block creator drawing system – Point

Point is a value used in the draw and is calculated using the number of tokens staked and the blocks that staked the tokens. The probability of an account is drawn from the draw is (Points / Total Points) * 100%.

func CalcPointBigint(pStake, addStake, now_block, stake_block *big.Int, period uint64) *big.Int {
    b := now_block //블록넘버
    p := pStake //이전스테이킹
    n := addStake //추가스테이킹
    s := stake_block //이전 스테이킹 블록넘버

    d := float64(period) / 10 //공식이 10초 단위 이기때문에 맞추기 위함 (perioid 를 제네시스로 변경하면 자동으로 변경되기 위함)

    bb := int64(BLOCK_YEAR / d) //기준 블록

    //ratio := (b * 100)  / (bb + s) //100은 소수점 처리
    ratio := new(big.Int).Mul(b, big.NewInt(100))
    ratio.Div(ratio, new(big.Int).Add(big.NewInt(bb), s))

    /*
    if ratio > 100 {
        ratio = 100
    }
     */
    if ratio.Cmp(big.NewInt(100)) == 1 {
        ratio = big.NewInt(100)
    }

    //adv := p * ((p / (p + n)) * ratio) / 100
    temp1 := new(big.Int).Div(p, new(big.Int).Add(p, n))
    temp2 := new(big.Int).Mul(p, temp1)
    temp3 := new(big.Int).Mul(temp2, ratio)
    adv := new(big.Int).Div(temp3, big.NewInt(100))

    //result := p + adv + n
    r1 := new(big.Int).Add(p, adv)
    r2 := new(big.Int).Add(r1, n)

    return r2
}

The above code is a function to calculate the point.

Process of block creator draw

Get the previous block as old as Epoch from the current block for the draw.

// [BERITH] getStakeTargetBlock 주어진 parent header에 대하여 miner를 결정 할 target block을 반환한다.
// 1) [0, epoch-1] : target == 블록 넘버 0(즉, genesis block) 인 블록
// 2) [epoch, 2epoch] : target == 블록 넘버 epoch 인 블록
// 3) [2epoch +1, ~) : target == 블록 넘버 - epoch 인 블록
func (c *BSRR) getStakeTargetBlock(chain consensus.ChainReader, parent *types.Header) (*types.Header, bool) {
    if parent == nil {
        return &types.Header{}, false
    }

    var targetNumber uint64
    blockNumber := parent.Number.Uint64()
    d := blockNumber / c.config.Epoch

    if d > 1 {
        return c.getAncestor(chain, int64(c.config.Epoch), parent)
    }

    switch d {
    case 0:
        targetNumber = 0
    case 1:
        targetNumber = c.config.Epoch
    }

    target := chain.GetHeaderByNumber(targetNumber)
    if target != nil {
        return target, chain.HasBlockAndState(target.Hash(), targetNumber)
    }
    return target, false
}

The above code is the content of a function that gets the previous block as much as Epoch.

Get Stakers from Level DB using the hash value of the obtained block. After that, the obtained Stakers are sorted based on the hash value of the account. And then, the list of sorted accounts is traversed to produce Candidates, which are drawing tables. The drawing table consists of the sum of the Points in the account and the Points in the accounts in the previous index.

If accounts A, B, and C each have 10, 20, and 30 Points, the drawing table consists of:

field \ Index 0 1 2
account A B C
point 10 20 30
val 10 30 60
func SelectBlockCreator(config *params.ChainConfig, number uint64, hash common.Hash, stks staking.Stakers, state *state.StateDB) VoteResults {
    result := make(VoteResults)

    list := sortableList(stks.AsList())
    if len(list) == 0 {
        return result
    }

    sort.Sort(list)

    cddts := NewCandidates()

    for _, stk := range list {
        point := state.GetPoint(stk).Uint64()
        cddts.Add(Candidate{
            point:   point,
            address: stk,
        })
    }
    if config.IsBIP3(big.NewInt(int64(number))) {
        result = cddts.selectBIP3BlockCreator(config, number)
    } else {
        result = cddts.selectBlockCreator(config, number)
    }

    return result

}

The code above shows the contents of a function that sorts Stakers to creates a drawing table, Candidates.

Draw is the process of creating a map with the account as the key and the VoteResult structure as the value.

type VoteResult struct {
    Score *big.Int `json:"score"`
    Rank  int      `json:"rank"`
}

The code above is the declaration of the VoteResult structure. It can be confirmed that the structure has Score and Rank. Rank is determined drawn order, Score is by the following formula.

len : Length of the drawing table

Score : 5000000 - ((5000000 - 10000) / len * (Rank-1))

The draw repeats the process below while the length of the Candidates is greater than zero, and constructs a Map VoteResults with the account as the result and the VoteResult value as the key.

  • Generate a random number x.

  • rank : 1

  • len : Length of Candidates

  • diff : 5000000

  • diff_r : (diff - 10000) / len

  • Repeat this process until you find the desired index n in a binary search.

    • The generated random number gets Val corresponding to index n-1, n in Candidates. val(n-1), val(n) in that order.

    • Check that the random number x satisfies val (n-1) <= x <= val (n)

    • If the condition is met, the following process is executed. If not, skip it and repeat.

      • Store a structure in map with the account corresponding to n as key and VoteResult as value.

      • diff -= diff_r, rank++

      • Remove the accounts corresponding to n from Candidates and end the binary search.

func (cs *Candidates) selectBIP3BlockCreator(config *params.ChainConfig, number uint64) VoteResults {
    result := make(VoteResults)

    DIF := DIF_MAX
    DIF_R := (DIF_MAX - DIF_MIN) / int64(len(cs.selections))
    rank := 1
    rand.Seed(cs.GetSeed(config, number))

    for len(cs.selections) > 0 {

        target := uint64(rand.Int63n(int64(cs.total)))

        var chosen int
        start := 0
        end := len(cs.selections) - 1

        for {
            mid := (start + end) / 2
            a := uint64(0)
            if mid > 0 {
                a = cs.selections[mid-1].val
            }
            b := cs.selections[mid].val

            if target >= a && target <= b {
                chosen = mid
                cddt := cs.selections[mid]
                result[cddt.address] = VoteResult{
                    Rank:  rank,
                    Score: big.NewInt(DIF),
                }
                DIF -= DIF_R
                rank++
                break
            }

            if target < a {
                end = mid - 1
            }
            if target > b {
                start = mid + 1
            }
        }

        out := cs.selections[chosen]
        for i := chosen; i+1 < len(cs.selections); i++ {
            newCddt := cs.selections[i+1]
            newCddt.val -= out.point
            cs.selections[i] = newCddt
        }

        cs.selections = cs.selections[:len(cs.selections)-1]
        cs.total -= out.point
    }

    //fmt.Println(DIF)
    return result
}

The above code is the content of the function that draws the block constructor.

Block creator draw and block generation priority

Block creators can get the Score and Rank for their account from VoteResults. These values relate to prioritizing blocks. Score is used as a Difficulty of blocks. Difficulty is used when a node selects a blockchain. A node selects the blockchain with the highest sum of the Difficulty. Rank is related to the propagation delay of the block. Nodes propagate blocks as long as the timestamp of the parent block is as long as Period second of ChainConfig and it is given additional delay according to the Rank.

Therefore, the block generated from the account that was selected first in the draw has a higher priority.

Block Miner Reward

Berith rewards the account that created the block. Block rewards are designed to reward 5 billion coins over 100 years.

reward The figure above is a graph showing how the block reward changes over time. Block rewards can be seen to decrease gradually over time in Berith.

func getReward(config *params.ChainConfig, header *types.Header) *big.Int {
	number := header.Number.Uint64()
	// 특정 블록 이후로 보상을 지급
	if number < config.Bsrr.Rewards.Uint64() {
		return big.NewInt(0)
	}

	//공식이 10초 단위 이기때문
	d := float64(config.Bsrr.Period) / 10
	n := float64(number) * d

	var z float64 = 0
	if n <= 3150000 {
		z = 5
	}

	re := (26 - math.Round(n/(7370000))*0.5 + z) * d
	if re <= 0 {
		re = 0

		return big.NewInt(0)
	} else {
		temp := re * 1e+10
		return new(big.Int).Mul(big.NewInt(int64(temp)), big.NewInt(1e+8))
	}
}

The above code is the content of a function that calculates the value of the block reward.