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.
To prove the validity of the block, every node needs to manage a list of users who staked tokens.
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.
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.
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.
The figure above shows the process of drawing a block creator. It is described below.
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.
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.
- To prevent the draw results from changing according to transactions in the block
- To prevent a staked user from immediately creating a block
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.
- The condition that random values must be the same on all nodes
- 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.
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
.
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 ofCandidates
-
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 indexn-1
,n
inCandidates
.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 ton
as key andVoteResult
as value. -
diff -= diff_r, rank++
-
Remove the
accounts
corresponding ton
fromCandidates
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 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.
Berith rewards the account that created the block. Block rewards are designed to reward 5 billion coins over 100 years.
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.