Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b836937a2 | ||
|
|
c208d28a68 | ||
|
|
0fb32163b0 | ||
|
|
54dbb2ba05 | ||
|
|
522d4cd880 | ||
|
|
4525cff920 | ||
|
|
909e7b9314 | ||
|
|
54b4e58382 | ||
|
|
f616c36ebf | ||
|
|
278608a04e |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,4 +1,16 @@
|
||||
# Changelog
|
||||
## v1.2.10
|
||||
FEATURE
|
||||
* [\#1780](https://github.com/bnb-chain/bsc/pull/1780) log: reduce logs when receiving too much votes from a peer
|
||||
* [\#1788](https://github.com/bnb-chain/bsc/pull/1788) metrics: add txpool config into metrics server
|
||||
* [\#1789](https://github.com/bnb-chain/bsc/pull/1789) rpc: add GetFinalizedHeader/Block to simplify using the fast finality feature
|
||||
* [\#1791](https://github.com/bnb-chain/bsc/pull/1791) finality: add more check to ensure result of assembleVoteAttestation
|
||||
|
||||
BUGFIX
|
||||
* [\#1773](https://github.com/bnb-chain/bsc/pull/1773) discov: do not filter out bootnodes
|
||||
* [\#1778](https://github.com/bnb-chain/bsc/pull/1778) vote: backup validator sync votes from corresponding mining validator
|
||||
* [\#1784](https://github.com/bnb-chain/bsc/pull/1784) fix: exclude same votes when doing malicious voting check
|
||||
|
||||
## v1.2.9
|
||||
FEATURE
|
||||
* [\#1775](https://github.com/bnb-chain/bsc/pull/1775) upgrade: several hardfork block height on mainnet: Plato, Hertz(Berlin, London)
|
||||
|
||||
108
cmd/extradump/extradump_test.go
Normal file
108
cmd/extradump/extradump_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExtraParse(t *testing.T) {
|
||||
// case 1, |---Extra Vanity---|---Empty---|---Empty---|---Extra Seal---|
|
||||
{
|
||||
extraData := "0xd983010209846765746889676f312e31392e3131856c696e75780000a6bf97c1e99f701bb14cb7dfb68b90bd3e6d1ca656964630de71beffc7f33f7f08ec99d336ec51ad9fad0ac84ae77ca2e8ad9512acc56e0d7c93f3c2ce7de1b69149a5a400"
|
||||
_, err := parseExtra(extraData)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// case 2, |---Extra Vanity---|---Validators Number and Validators Bytes---|---Empty---|---Extra Seal---|
|
||||
{
|
||||
extraData := "0xd983010209846765746889676f312e31392e3131856c696e75780000a6bf97c1152465176c461afb316ebc773c61faee85a6515daa8a923564c6ffd37fb2fe9f118ef88092e8762c7addb526ab7eb1e772baef85181f892c731be0c1891a50e6b06262c816295e26495cef6f69dfa69911d9d8e4f3bbadb89b977cf58294f7239d515e15b24cfeb82494056cf691eaf729b165f32c9757c429dba5051155903067e56ebe3698678e912d4c407bbe49438ed859fe965b140dcf1aab71a993c1f7f6929d1fe2a17b4e14614ef9fc5bdc713d6631d675403fbeefac55611bf612700b1b65f4744861b80b0f7d6ab03f349bbafec1551819b8be1efea2fc46ca749aa184248a459464eec1a21e7fc7b71a053d9644e9bb8da4853b8f872cd7c1d6b324bf1922829830646ceadfb658d3de009a61dd481a114a2e761c554b641742c973867899d300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000069c77a677c40c7fbea129d4b171a39b7a8ddabfab2317f59d86abfaf690850223d90e9e7593d91a29331dfc2f84d5adecc75fc39ecab4632c1b4400a3dd1e1298835bcca70f657164e5b75689b64b7fd1fa275f334f28e1896a26afa1295da81418593bd12814463d9f6e45c36a0e47eb4cd3e5b6af29c41e2a3a5636430155a466e216585af3ba772b61c6014342d914470ec7ac2975be345796c2b81db0422a5fd08e40db1fc2368d2245e4b18b1d0b85c921aaaafd2e341760e29fc613edd39f71254614e2055c3287a517ae2f5b9e386cd1b50a4550696d957cb4900f03ab84f83ff2df44193496793b847f64e9d6db1b3953682bb95edd096eb1e69bbd357c200992ca78050d0cbe180cfaa018e8b6c8fd93d6f4cea42bbb345dbc6f0dfdb5bec73a8a257074e82b881cfa06ef3eb4efeca060c2531359abd0eab8af1e3edfa2025fca464ac9c3fd123f6c24a0d78869485a6f79b60359f141df90a0c745125b131caaffd12000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b218c5d6af1f979ac42bc68d98a5a0d796c6ab01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b4dd66d7c2c7e57f628210187192fb89d4b99dd4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000be807dddb074639cd9fa61b47676c064fc50d62cb1f2c71577def3144fabeb75a8a1c8cb5b51d1d1b4a05eec67988b8685008baa17459ec425dbaebc852f496dc92196cdcc8e6d00c17eb431350c6c50d8b8f05176b90b11b3a3d4feb825ae9702711566df5dbf38e82add4dd1b573b95d2466fa6501ccb81e9d26a352b96150ccbf7b697fd0a419d1d6bf74282782b0b3eb1413c901d6ecf02e8e28000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e2d3a739effcd3a99387d015e260eefac72ebea1956c470ddff48cb49300200b5f83497f3a3ccb3aeb83c5edd9818569038e61d197184f4aa6939ea5e9911e3e98ac6d21e9ae3261a475a27bb1028f140bc2a7c843318afd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ea0a6e3c511bbd10f4519ece37dc24887e11b55db2d4c6283c44a1c7bd503aaba7666e9f0c830e0ff016c1c750a5e48757a713d0836b1cabfd5c281b1de3b77d1c192183ee226379db83cffc681495730c11fdde79ba4c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ef0274e31810c9df02f98fafde0f841f4e66a1cd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e99f701bb14cb7dfb68b90bd3e6d1ca656964630de71beffc7f33f7f08ec99d336ec51ad9fad0ac84ae77ca2e8ad9512acc56e0d7c93f3c2ce7de1b69149a5a400"
|
||||
extra, err := parseExtra(extraData)
|
||||
assert.NoError(t, err)
|
||||
{
|
||||
var have = extra.ValidatorSize
|
||||
var want = uint8(21)
|
||||
if have != want {
|
||||
t.Fatalf("extra.ValidatorSize mismatch, have %d, want %d", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
var have = common.Bytes2Hex(extra.Validators[14].Address[:])
|
||||
var want = "cc8e6d00c17eb431350c6c50d8b8f05176b90b11"
|
||||
if have != want {
|
||||
t.Fatalf("extra.Validators[14].Address mismatch, have %s, want %s", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
var have = common.Bytes2Hex(extra.Validators[18].BLSPublicKey[:])
|
||||
var want = "b2d4c6283c44a1c7bd503aaba7666e9f0c830e0ff016c1c750a5e48757a713d0836b1cabfd5c281b1de3b77d1c192183"
|
||||
if have != want {
|
||||
t.Fatalf("extra.Validators[18].BLSPublicKey mismatch, have %s, want %s", have, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// case 3, |---Extra Vanity---|---Empty---|---Vote Attestation---|---Extra Seal---|
|
||||
{
|
||||
extraData := "0xd883010205846765746888676f312e32302e35856c696e75780000002995c52af8b5830563efb86089cf168dcf4c5d3cb057926628ad1bf0f03ea67eef1458485578a4f8489afa8a853ecc7af45e2d145c21b70641c4b29f0febd2dd2c61fa1ba174be3fd47f1f5fa2ab9b5c318563d8b70ca58d0d51e79ee32b2fb721649e2cb9d36538361fba11f84c8401d14bb7a0fa67ddb3ba654d6006bf788710032247aa4d1be0707273e696b422b3ff72e9798401d14bbaa01225f505f5a0e1aefadcd2913b7aac9009fe4fb3d1bf57399e0b9dce5947f94280fe6d3647276c4127f437af59eb7c7985b2ae1ebe432619860695cb6106b80cc66c735bc1709afd11f233a2c97409d38ebaf7178aa53e895aea2fe0a229f71ec601"
|
||||
extra, err := parseExtra(extraData)
|
||||
assert.NoError(t, err)
|
||||
{
|
||||
var have = common.Bytes2Hex(extra.Data.TargetHash[:])
|
||||
var want = "1225f505f5a0e1aefadcd2913b7aac9009fe4fb3d1bf57399e0b9dce5947f942"
|
||||
if have != want {
|
||||
t.Fatalf("extra.Data.TargetHash mismatch, have %s, want %s", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
var have = extra.Data.TargetNumber
|
||||
var want = uint64(30493626)
|
||||
if have != want {
|
||||
t.Fatalf("extra.Data.TargetNumber mismatch, have %d, want %d", have, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// case 4, |---Extra Vanity---|---Validators Number and Validators Bytes---|---Vote Attestation---|---Extra Seal---|
|
||||
{
|
||||
extraData := "0xd883010209846765746888676f312e31392e38856c696e7578000000dc55905c071284214b9b9c85549ab3d2b972df0deef66ac2c98e82934ca974fdcd97f3309de967d3c9c43fa711a8d673af5d75465844bf8969c8d1948d903748ac7b8b1720fa64e50c35552c16704d214347f29fa77f77da6d75d7c752b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f247788386d0ed6c748e03a53160b4b30ed3748cc5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000980a75ecd1309ea12fa2ed87a8744fbfc9b863d589037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bb71b214cb885500844365e95cd9942c7276e7fd8a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff474cf03cceff28abc65c9cbae594f725c80e12d96c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878f8b27bb8608c11016739b3f8a19e54ab8c7abacd936cfeba200f3645a98b65adb0dd3692b69ce0b3ae10e7176b9a4b0d83f04065b1042b4bcb646a34b75c550f92fc34b8b2b1db0fa0d3172db23ba92727c80bcd306320d0ff411bf858525fde13bc8e0370f84c8401e9c2e6a0820dc11d63176a0eb1b828bc5376867b275579112b7013358da40317e7bab6e98401e9c2e7a00edc71ce80105a3220a87bea2792fa340d66c59002f02b0a09349ed1ed284070808b972fac2b9077a4dcb6fc37093799a652858016c99142b227500c844fa97ec22e3f9d3b1e982f14bcd999a7453e89ce5ef5c55f1c7f8f74ba904186cd67828200"
|
||||
extra, err := parseExtra(extraData)
|
||||
assert.NoError(t, err)
|
||||
{
|
||||
var have = common.Bytes2Hex(extra.Validators[0].Address[:])
|
||||
var want = "1284214b9b9c85549ab3d2b972df0deef66ac2c9"
|
||||
if have != want {
|
||||
t.Fatalf("extra.Validators[0].Address mismatch, have %s, want %s", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
var have = common.Bytes2Hex(extra.Validators[0].BLSPublicKey[:])
|
||||
var want = "8e82934ca974fdcd97f3309de967d3c9c43fa711a8d673af5d75465844bf8969c8d1948d903748ac7b8b1720fa64e50c"
|
||||
if have != want {
|
||||
t.Fatalf("extra.Validators[0].BLSPublicKey mismatch, have %s, want %s", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
var have = extra.Validators[0].VoteIncluded
|
||||
var want = true
|
||||
if have != want {
|
||||
t.Fatalf("extra.Validators[0].VoteIncluded mismatch, have %t, want %t", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
var have = common.Bytes2Hex(extra.Data.TargetHash[:])
|
||||
var want = "0edc71ce80105a3220a87bea2792fa340d66c59002f02b0a09349ed1ed284070"
|
||||
if have != want {
|
||||
t.Fatalf("extra.Data.TargetHash mismatch, have %s, want %s", have, want)
|
||||
}
|
||||
}
|
||||
{
|
||||
var have = extra.Data.TargetNumber
|
||||
var want = uint64(32096999)
|
||||
if have != want {
|
||||
t.Fatalf("extra.Data.TargetNumber mismatch, have %d, want %d", have, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
cmd/extradump/main.go
Normal file
162
cmd/extradump/main.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2023 The bsc Authors
|
||||
// This file is part of bsc.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/willf/bitset"
|
||||
)
|
||||
|
||||
// follow define in parlia
|
||||
const (
|
||||
AddressLength = 20
|
||||
BLSPublicKeyLength = 48
|
||||
|
||||
// follow order in extra field
|
||||
// |---Extra Vanity---|---Validators Number and Validators Bytes (or Empty)---|---Vote Attestation (or Empty)---|---Extra Seal---|
|
||||
extraVanityLength = 32 // Fixed number of extra-data prefix bytes reserved for signer vanity
|
||||
validatorNumberSize = 1 // Fixed number of extra prefix bytes reserved for validator number after Luban
|
||||
validatorBytesLength = common.AddressLength + types.BLSPublicKeyLength
|
||||
extraSealLength = 65 // Fixed number of extra-data suffix bytes reserved for signer seal
|
||||
)
|
||||
|
||||
type Extra struct {
|
||||
ExtraVanity string
|
||||
ValidatorSize uint8
|
||||
Validators validatorsAscending
|
||||
*types.VoteAttestation
|
||||
ExtraSeal []byte
|
||||
}
|
||||
|
||||
type ValidatorInfo struct {
|
||||
common.Address
|
||||
types.BLSPublicKey
|
||||
VoteIncluded bool
|
||||
}
|
||||
|
||||
// validatorsAscending implements the sort interface to allow sorting a list of ValidatorInfo
|
||||
type validatorsAscending []ValidatorInfo
|
||||
|
||||
func (s validatorsAscending) Len() int { return len(s) }
|
||||
func (s validatorsAscending) Less(i, j int) bool {
|
||||
return bytes.Compare(s[i].Address[:], s[j].Address[:]) < 0
|
||||
}
|
||||
func (s validatorsAscending) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
func init() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "[extraHexData]")
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintln(os.Stderr, `
|
||||
Dumps extra info from the given hex data, only support extra after luban upgrade.`)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
extraHexData := os.Args[1]
|
||||
if extra, err := parseExtra(extraHexData); err == nil {
|
||||
fmt.Println("extra parsed successly")
|
||||
prettyExtra(*extra)
|
||||
} else {
|
||||
fmt.Println("extra parsed failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parseExtra parse hex data into type Extra
|
||||
func parseExtra(hexData string) (*Extra, error) {
|
||||
// decode hex into bytes
|
||||
data, err := hex.DecodeString(strings.TrimPrefix(hexData, "0x"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hex data")
|
||||
}
|
||||
|
||||
// parse ExtraVanity and ExtraSeal
|
||||
dataLength := len(data)
|
||||
var extra Extra
|
||||
if dataLength < extraVanityLength+extraSealLength {
|
||||
fmt.Println("length less than min required")
|
||||
}
|
||||
extra.ExtraVanity = string(data[:extraVanityLength])
|
||||
extra.ExtraSeal = data[dataLength-extraSealLength:]
|
||||
data = data[extraVanityLength : dataLength-extraSealLength]
|
||||
dataLength = len(data)
|
||||
|
||||
// parse Validators and Vote Attestation
|
||||
if dataLength > 0 {
|
||||
// parse Validators
|
||||
if data[0] != '\xf8' { // rlp format of attestation begin with 'f8'
|
||||
validatorNum := int(data[0])
|
||||
validatorBytesTotalLength := validatorNumberSize + validatorNum*validatorBytesLength
|
||||
if dataLength < validatorBytesTotalLength {
|
||||
return nil, fmt.Errorf("parse validators failed")
|
||||
}
|
||||
extra.ValidatorSize = uint8(validatorNum)
|
||||
data = data[validatorNumberSize:]
|
||||
for i := 0; i < validatorNum; i++ {
|
||||
var validatorInfo ValidatorInfo
|
||||
validatorInfo.Address = common.BytesToAddress(data[i*validatorBytesLength : i*validatorBytesLength+common.AddressLength])
|
||||
copy(validatorInfo.BLSPublicKey[:], data[i*validatorBytesLength+common.AddressLength:(i+1)*validatorBytesLength])
|
||||
extra.Validators = append(extra.Validators, validatorInfo)
|
||||
}
|
||||
sort.Sort(extra.Validators)
|
||||
data = data[validatorBytesTotalLength-validatorNumberSize:]
|
||||
dataLength = len(data)
|
||||
}
|
||||
|
||||
// parse Vote Attestation
|
||||
if dataLength > 0 {
|
||||
if err := rlp.Decode(bytes.NewReader(data), &extra.VoteAttestation); err != nil {
|
||||
return nil, fmt.Errorf("parse voteAttestation failed")
|
||||
}
|
||||
if extra.ValidatorSize > 0 {
|
||||
validatorsBitSet := bitset.From([]uint64{uint64(extra.VoteAddressSet)})
|
||||
for i := 0; i < int(extra.ValidatorSize); i++ {
|
||||
if validatorsBitSet.Test(uint(i)) {
|
||||
extra.Validators[i].VoteIncluded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &extra, nil
|
||||
}
|
||||
|
||||
// prettyExtra print Extra with a pretty format
|
||||
func prettyExtra(extra Extra) {
|
||||
fmt.Printf("ExtraVanity : %s\n", extra.ExtraVanity)
|
||||
|
||||
if extra.ValidatorSize > 0 {
|
||||
fmt.Printf("ValidatorSize : %d\n", extra.ValidatorSize)
|
||||
for i := 0; i < int(extra.ValidatorSize); i++ {
|
||||
fmt.Printf("Validator %d\n", i+1)
|
||||
fmt.Printf("\tAddress : %s\n", common.Bytes2Hex(extra.Validators[i].Address[:]))
|
||||
fmt.Printf("\tVoteKey : %s\n", common.Bytes2Hex(extra.Validators[i].BLSPublicKey[:]))
|
||||
fmt.Printf("\tVoteIncluded : %t\n", extra.Validators[i].VoteIncluded)
|
||||
}
|
||||
}
|
||||
|
||||
if extra.VoteAttestation != nil {
|
||||
fmt.Printf("Attestation :\n")
|
||||
fmt.Printf("\tVoteAddressSet : %b, %d\n", extra.VoteAddressSet, bitset.From([]uint64{uint64(extra.VoteAddressSet)}).Count())
|
||||
fmt.Printf("\tAggSignature : %s\n", common.Bytes2Hex(extra.AggSignature[:]))
|
||||
fmt.Printf("\tVoteData :\n")
|
||||
fmt.Printf("\t\tSourceNumber : %d\n", extra.Data.SourceNumber)
|
||||
fmt.Printf("\t\tSourceHash : %s\n", common.Bytes2Hex(extra.Data.SourceHash[:]))
|
||||
fmt.Printf("\t\tTargetNumber : %d\n", extra.Data.TargetNumber)
|
||||
fmt.Printf("\t\tTargetHash : %s\n", common.Bytes2Hex(extra.Data.TargetHash[:]))
|
||||
}
|
||||
|
||||
fmt.Printf("ExtraSeal : %s\n", common.Bytes2Hex(extra.ExtraSeal))
|
||||
}
|
||||
@@ -178,6 +178,7 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
|
||||
utils.SetupMetrics(ctx,
|
||||
utils.EnableBuildInfo(gitCommit, gitDate),
|
||||
utils.EnableMinerInfo(ctx, cfg.Eth.Miner),
|
||||
utils.EnableNodeInfo(cfg.Eth.TxPool),
|
||||
)
|
||||
return stack, backend
|
||||
}
|
||||
|
||||
@@ -1972,6 +1972,21 @@ func EnableMinerInfo(ctx *cli.Context, minerConfig miner.Config) SetupMetricsOpt
|
||||
}
|
||||
}
|
||||
|
||||
func EnableNodeInfo(poolConfig core.TxPoolConfig) SetupMetricsOption {
|
||||
return func() {
|
||||
// register node info into metrics
|
||||
metrics.NewRegisteredLabel("node-info", nil).Mark(map[string]interface{}{
|
||||
"PriceLimit": poolConfig.PriceLimit,
|
||||
"PriceBump": poolConfig.PriceBump,
|
||||
"AccountSlots": poolConfig.AccountSlots,
|
||||
"GlobalSlots": poolConfig.GlobalSlots,
|
||||
"AccountQueue": poolConfig.AccountQueue,
|
||||
"GlobalQueue": poolConfig.GlobalQueue,
|
||||
"Lifetime": poolConfig.Lifetime,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func SetupMetrics(ctx *cli.Context, options ...SetupMetricsOption) {
|
||||
if metrics.Enabled {
|
||||
log.Info("Enabling metrics collection")
|
||||
|
||||
@@ -882,6 +882,11 @@ func (p *Parlia) assembleVoteAttestation(chain consensus.ChainHeaderReader, head
|
||||
attestation.VoteAddressSet |= 1 << (valInfo.Index - 1) //Index is offset by 1
|
||||
}
|
||||
}
|
||||
validatorsBitSet := bitset.From([]uint64{uint64(attestation.VoteAddressSet)})
|
||||
if validatorsBitSet.Count() < uint(len(signatures)) {
|
||||
log.Warn(fmt.Sprintf("assembleVoteAttestation, check VoteAddress Set failed, expected:%d, real:%d", len(signatures), validatorsBitSet.Count()))
|
||||
return fmt.Errorf("invalid attestation, check VoteAddress Set failed")
|
||||
}
|
||||
|
||||
// Append attestation to header extra field.
|
||||
buf := new(bytes.Buffer)
|
||||
@@ -1758,10 +1763,11 @@ func (p *Parlia) GetFinalizedHeader(chain consensus.ChainHeaderReader, header *t
|
||||
return nil
|
||||
}
|
||||
|
||||
if snap.Attestation != nil {
|
||||
return chain.GetHeader(snap.Attestation.SourceHash, snap.Attestation.SourceNumber)
|
||||
if snap.Attestation == nil {
|
||||
return chain.GetHeaderByNumber(0) // keep consistent with GetJustifiedNumberAndHash
|
||||
}
|
||||
return nil
|
||||
|
||||
return chain.GetHeader(snap.Attestation.SourceHash, snap.Attestation.SourceNumber)
|
||||
}
|
||||
|
||||
// =========================== utility function ==========================
|
||||
|
||||
@@ -58,6 +58,7 @@ func (m *MaliciousVoteMonitor) ConflictDetect(newVote *types.VoteEnvelope, pendi
|
||||
if !(blockNumber+maliciousVoteSlashScope > pendingBlockNumber) {
|
||||
blockNumber = pendingBlockNumber - maliciousVoteSlashScope + 1
|
||||
}
|
||||
newVoteHash := newVote.Data.Hash()
|
||||
for ; blockNumber <= pendingBlockNumber+upperLimitOfVoteBlockNumber; blockNumber++ {
|
||||
if voteDataBuffer.Contains(blockNumber) {
|
||||
voteEnvelope, ok := voteDataBuffer.Get(blockNumber)
|
||||
@@ -66,7 +67,7 @@ func (m *MaliciousVoteMonitor) ConflictDetect(newVote *types.VoteEnvelope, pendi
|
||||
continue
|
||||
}
|
||||
maliciousVote := false
|
||||
if blockNumber == targetNumber {
|
||||
if blockNumber == targetNumber && voteEnvelope.(*types.VoteEnvelope).Data.Hash() != newVoteHash {
|
||||
violateRule1Counter.Inc(1)
|
||||
maliciousVote = true
|
||||
} else if (blockNumber < targetNumber && voteEnvelope.(*types.VoteEnvelope).Data.SourceNumber > sourceNumber) ||
|
||||
|
||||
@@ -22,9 +22,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: uint64(0),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(("01"))),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
|
||||
@@ -34,9 +34,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: uint64(0),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("02")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
|
||||
@@ -54,9 +54,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: uint64(0),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("01")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
|
||||
@@ -65,9 +65,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: uint64(0),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("02")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
|
||||
@@ -85,9 +85,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: uint64(0),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("01")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
|
||||
@@ -96,9 +96,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: uint64(0),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("02")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, true, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
|
||||
@@ -116,9 +116,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: pendingBlockNumber - 4,
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("01")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
|
||||
@@ -127,9 +127,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: pendingBlockNumber - 2,
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 3,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("02")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, true, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
|
||||
@@ -147,9 +147,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: pendingBlockNumber - 2,
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 3,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("01")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
|
||||
@@ -158,9 +158,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: pendingBlockNumber - 4,
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("02")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, true, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
|
||||
@@ -178,9 +178,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: pendingBlockNumber - 4,
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 3,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("01")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
|
||||
@@ -189,9 +189,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: pendingBlockNumber - 3,
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 2,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("02")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
|
||||
@@ -200,9 +200,9 @@ func TestMaliciousVoteMonitor(t *testing.T) {
|
||||
Signature: types.BLSSignature{},
|
||||
Data: &types.VoteData{
|
||||
SourceNumber: pendingBlockNumber - 2,
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
|
||||
SourceHash: common.BytesToHash(common.Hex2Bytes("00")),
|
||||
TargetNumber: pendingBlockNumber - 1,
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
|
||||
TargetHash: common.BytesToHash(common.Hex2Bytes("02")),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote3, pendingBlockNumber))
|
||||
|
||||
@@ -32,6 +32,10 @@ type VoteManager struct {
|
||||
chainHeadCh chan core.ChainHeadEvent
|
||||
chainHeadSub event.Subscription
|
||||
|
||||
// used for backup validators to sync votes from corresponding mining validator
|
||||
syncVoteCh chan core.NewVoteEvent
|
||||
syncVoteSub event.Subscription
|
||||
|
||||
pool *VotePool
|
||||
signer *VoteSigner
|
||||
journal *VoteJournal
|
||||
@@ -46,9 +50,9 @@ func NewVoteManager(eth Backend, chainconfig *params.ChainConfig, chain *core.Bl
|
||||
chain: chain,
|
||||
chainconfig: chainconfig,
|
||||
chainHeadCh: make(chan core.ChainHeadEvent, chainHeadChanSize),
|
||||
|
||||
pool: pool,
|
||||
engine: engine,
|
||||
syncVoteCh: make(chan core.NewVoteEvent, voteBufferForPut),
|
||||
pool: pool,
|
||||
engine: engine,
|
||||
}
|
||||
|
||||
// Create voteSigner.
|
||||
@@ -69,6 +73,7 @@ func NewVoteManager(eth Backend, chainconfig *params.ChainConfig, chain *core.Bl
|
||||
|
||||
// Subscribe to chain head event.
|
||||
voteManager.chainHeadSub = voteManager.chain.SubscribeChainHeadEvent(voteManager.chainHeadCh)
|
||||
voteManager.syncVoteSub = voteManager.pool.SubscribeNewVoteEvent(voteManager.syncVoteCh)
|
||||
|
||||
go voteManager.loop()
|
||||
|
||||
@@ -77,6 +82,9 @@ func NewVoteManager(eth Backend, chainconfig *params.ChainConfig, chain *core.Bl
|
||||
|
||||
func (voteManager *VoteManager) loop() {
|
||||
log.Debug("vote manager routine loop started")
|
||||
defer voteManager.chainHeadSub.Unsubscribe()
|
||||
defer voteManager.syncVoteSub.Unsubscribe()
|
||||
|
||||
events := voteManager.eth.EventMux().Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{})
|
||||
defer func() {
|
||||
log.Debug("vote manager loop defer func occur")
|
||||
@@ -164,6 +172,21 @@ func (voteManager *VoteManager) loop() {
|
||||
voteManager.pool.PutVote(voteMessage)
|
||||
votesManagerCounter.Inc(1)
|
||||
}
|
||||
case event := <-voteManager.syncVoteCh:
|
||||
voteMessage := event.Vote
|
||||
if voteManager.eth.IsMining() || !voteManager.signer.UsingKey(&voteMessage.VoteAddress) {
|
||||
continue
|
||||
}
|
||||
if err := voteManager.journal.WriteVote(voteMessage); err != nil {
|
||||
log.Error("Failed to write vote into journal", "err", err)
|
||||
voteJournalErrorCounter.Inc(1)
|
||||
continue
|
||||
}
|
||||
log.Debug("vote manager synced vote", "votedBlockNumber", voteMessage.Data.TargetNumber, "votedBlockHash", voteMessage.Data.TargetHash, "voteMessageHash", voteMessage.Hash())
|
||||
votesManagerCounter.Inc(1)
|
||||
case <-voteManager.syncVoteSub.Err():
|
||||
log.Debug("voteManager subscribed votes failed")
|
||||
return
|
||||
case <-voteManager.chainHeadSub.Err():
|
||||
log.Debug("voteManager subscribed chainHead failed")
|
||||
return
|
||||
|
||||
@@ -92,6 +92,8 @@ func NewVotePool(chainconfig *params.ChainConfig, chain *core.BlockChain, engine
|
||||
|
||||
// loop is the vote pool's main even loop, waiting for and reacting to outside blockchain events and votes channel event.
|
||||
func (pool *VotePool) loop() {
|
||||
defer pool.chainHeadSub.Unsubscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
// Handle ChainHeadEvent.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -104,3 +105,7 @@ func (signer *VoteSigner) SignVote(vote *types.VoteEnvelope) error {
|
||||
copy(vote.Signature[:], signature.Marshal()[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (signer *VoteSigner) UsingKey(bLSPublicKey *types.BLSPublicKey) bool {
|
||||
return bytes.Equal(signer.pubKey[:], bLSPublicKey[:])
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ func (h *bscHandler) Handle(peer *bsc.Peer, packet bsc.Packet) error {
|
||||
// votes broadcast for the local node to process.
|
||||
func (h *bscHandler) handleVotesBroadcast(peer *bsc.Peer, votes []*types.VoteEnvelope) error {
|
||||
if peer.IsOverLimitAfterReceiving() {
|
||||
peer.Log().Warn("peer sending votes too much, votes dropped; it may be a ddos attack, please check!")
|
||||
return nil
|
||||
}
|
||||
// Here we only put the first vote, to avoid ddos attack by sending a large batch of votes.
|
||||
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
receiveRateLimitPerSecond = 10
|
||||
|
||||
// the time span of one period
|
||||
secondsPerPeriod = float64(10)
|
||||
secondsPerPeriod = float64(30)
|
||||
)
|
||||
|
||||
// max is a helper function which returns the larger of the two given integers.
|
||||
@@ -133,6 +133,9 @@ func (p *Peer) AsyncSendVotes(votes []*types.VoteEnvelope) {
|
||||
// Otherwise, check whether the number of received votes extra (secondsPerPeriod * receiveRateLimitPerSecond)
|
||||
func (p *Peer) IsOverLimitAfterReceiving() bool {
|
||||
if timeInterval := time.Since(p.periodBegin).Seconds(); timeInterval >= secondsPerPeriod {
|
||||
if p.periodCounter > uint(secondsPerPeriod*receiveRateLimitPerSecond) {
|
||||
p.Log().Debug("sending votes too much", "secondsPerPeriod", secondsPerPeriod, "count ", p.periodCounter)
|
||||
}
|
||||
p.periodBegin = time.Now()
|
||||
p.periodCounter = 0
|
||||
return false
|
||||
|
||||
@@ -54,6 +54,14 @@ import (
|
||||
|
||||
const UnHealthyTimeout = 5 * time.Second
|
||||
|
||||
// max is a helper function which returns the larger of the two given integers.
|
||||
func max(a, b int64) int64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// PublicEthereumAPI provides an API to access Ethereum related information.
|
||||
// It offers only methods that operate on public data that is freely available to anyone.
|
||||
type PublicEthereumAPI struct {
|
||||
@@ -773,6 +781,52 @@ func (s *PublicBlockChainAPI) Health() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetFinalizedHeader returns the requested finalized block header.
|
||||
// - probabilisticFinalized should be in range [2,21],
|
||||
// then the block header with number `max(fastFinalized, latest-probabilisticFinalized)` is returned
|
||||
func (s *PublicBlockChainAPI) GetFinalizedHeader(ctx context.Context, probabilisticFinalized int64) (map[string]interface{}, error) {
|
||||
if probabilisticFinalized < 2 || probabilisticFinalized > 21 {
|
||||
return nil, fmt.Errorf("%d out of range [2,21]", probabilisticFinalized)
|
||||
}
|
||||
|
||||
var err error
|
||||
fastFinalizedHeader, err := s.b.HeaderByNumber(ctx, rpc.FinalizedBlockNumber)
|
||||
if err != nil { // impossible
|
||||
return nil, err
|
||||
}
|
||||
latestHeader, err := s.b.HeaderByNumber(ctx, rpc.LatestBlockNumber)
|
||||
if err != nil { // impossible
|
||||
return nil, err
|
||||
}
|
||||
finalizedBlockNumber := max(fastFinalizedHeader.Number.Int64(), latestHeader.Number.Int64()-probabilisticFinalized)
|
||||
|
||||
return s.GetHeaderByNumber(ctx, rpc.BlockNumber(finalizedBlockNumber))
|
||||
}
|
||||
|
||||
// GetFinalizedBlock returns the requested finalized block.
|
||||
// - probabilisticFinalized should be in range [2,21],
|
||||
// then the block with number `max(fastFinalized, latest-probabilisticFinalized)` is returned
|
||||
// - When fullTx is true all transactions in the block are returned, otherwise
|
||||
// only the transaction hash is returned.
|
||||
func (s *PublicBlockChainAPI) GetFinalizedBlock(ctx context.Context, probabilisticFinalized int64, fullTx bool) (map[string]interface{}, error) {
|
||||
if probabilisticFinalized < 2 || probabilisticFinalized > 21 {
|
||||
return nil, fmt.Errorf("%d out of range [2,21]", probabilisticFinalized)
|
||||
}
|
||||
|
||||
var err error
|
||||
fastFinalizedHeader, err := s.b.HeaderByNumber(ctx, rpc.FinalizedBlockNumber)
|
||||
if err != nil { // impossible
|
||||
return nil, err
|
||||
}
|
||||
latestHeader, err := s.b.HeaderByNumber(ctx, rpc.LatestBlockNumber)
|
||||
if err != nil { // impossible
|
||||
return nil, err
|
||||
}
|
||||
finalizedBlockNumber := max(fastFinalizedHeader.Number.Int64(), latestHeader.Number.Int64()-probabilisticFinalized)
|
||||
|
||||
return s.GetBlockByNumber(ctx, rpc.BlockNumber(finalizedBlockNumber), fullTx)
|
||||
}
|
||||
|
||||
// GetUncleByBlockNumberAndIndex returns the uncle block for the given block hash and index.
|
||||
func (s *PublicBlockChainAPI) GetUncleByBlockNumberAndIndex(ctx context.Context, blockNr rpc.BlockNumber, index hexutil.Uint) (map[string]interface{}, error) {
|
||||
block, err := s.b.BlockByNumber(ctx, blockNr)
|
||||
|
||||
@@ -611,7 +611,7 @@ func (srv *Server) setupDiscovery() error {
|
||||
Tail []rlp.RawValue `rlp:"tail"`
|
||||
}
|
||||
if r.Load(enr.WithEntry("eth", ð)) != nil {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
return srv.forkFilter(eth.ForkID) == nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
const (
|
||||
VersionMajor = 1 // Major version component of the current release
|
||||
VersionMinor = 2 // Minor version component of the current release
|
||||
VersionPatch = 9 // Patch version component of the current release
|
||||
VersionPatch = 10 // Patch version component of the current release
|
||||
VersionMeta = "" // Version metadata to append to the version string
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user