[consensus] HIP-18: Allowlist for external leaders (#4146)

* HIP18: support allowlist
* sort allowlist by default order
* fix and add unit test
* simplified calculation of index of leader
* restore code of NthNextHmy()
* rename MaxLimit to MaxLimitPerShard and add comments
* init allowlist of testnet
* update allowlist
* change HIP18 epoch to TBD
* set HIP18 of testnet to 75840
* rename _BLS() to BLS()
* update comment
* add version of allowlist variable
* recover the travis_rpc_checker script
* update HIP18 epoch
pull/4190/head
PeekPI 2 years ago committed by GitHub
parent 8e93ea6389
commit 8b1d7a526a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      cmd/harmony/main.go
  2. 6
      consensus/consensus_service.go
  3. 4
      consensus/construct_test.go
  4. 4
      consensus/quorum/one-node-staked-vote_test.go
  5. 58
      consensus/quorum/quorom_test.go
  6. 49
      consensus/quorum/quorum.go
  7. 11
      consensus/view_change.go
  8. 2
      consensus/view_change_test.go
  9. 13
      crypto/bls/bls.go
  10. 49
      internal/configs/sharding/allowlist.go
  11. 17
      internal/configs/sharding/instance.go
  12. 10
      internal/configs/sharding/localnet.go
  13. 36
      internal/configs/sharding/mainnet.go
  14. 4
      internal/configs/sharding/pangaea.go
  15. 4
      internal/configs/sharding/partner.go
  16. 7
      internal/configs/sharding/shardingconfig.go
  17. 6
      internal/configs/sharding/stress.go
  18. 15
      internal/configs/sharding/testnet.go
  19. 11
      internal/params/config.go
  20. 2
      node/node.go
  21. 2
      scripts/travis_rpc_checker.sh

@ -489,7 +489,7 @@ func nodeconfigSetShardSchedule(config harmonyconfig.HarmonyConfig) {
}
devnetConfig, err := shardingconfig.NewInstance(
uint32(dnConfig.NumShards), dnConfig.ShardSize, dnConfig.HmyNodeSize, dnConfig.SlotsLimit, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccounts, nil, shardingconfig.VLBPE)
uint32(dnConfig.NumShards), dnConfig.ShardSize, dnConfig.HmyNodeSize, dnConfig.SlotsLimit, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccounts, shardingconfig.Allowlist{}, nil, shardingconfig.VLBPE)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "ERROR invalid devnet sharding config: %s",
err)

@ -73,10 +73,10 @@ func (consensus *Consensus) signAndMarshalConsensusMessage(message *msg_pb.Messa
// UpdatePublicKeys updates the PublicKeys for
// quorum on current subcommittee, protected by a mutex
func (consensus *Consensus) UpdatePublicKeys(pubKeys []bls_cosi.PublicKeyWrapper) int64 {
func (consensus *Consensus) UpdatePublicKeys(pubKeys, allowlist []bls_cosi.PublicKeyWrapper) int64 {
// TODO: use mutex for updating public keys pointer. No need to lock on all these logic.
consensus.pubKeyLock.Lock()
consensus.Decider.UpdateParticipants(pubKeys)
consensus.Decider.UpdateParticipants(pubKeys, allowlist)
consensus.getLogger().Info().Msg("My Committee updated")
for i := range pubKeys {
consensus.getLogger().Info().
@ -372,7 +372,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode {
consensus.getLogger().Info().
Int("numPubKeys", len(pubKeys)).
Msg("[UpdateConsensusInformation] Successfully updated public keys")
consensus.UpdatePublicKeys(pubKeys)
consensus.UpdatePublicKeys(pubKeys, shard.Schedule.InstanceForEpoch(nextEpoch).ExternalAllowlist())
// Update voters in the committee
if _, err := consensus.Decider.SetVoters(

@ -151,7 +151,7 @@ func TestConstructPrepareMessage(test *testing.T) {
if err != nil {
test.Fatalf("Cannot create consensus: %v", err)
}
consensus.UpdatePublicKeys([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2})
consensus.UpdatePublicKeys([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2}, []bls.PublicKeyWrapper{})
consensus.SetCurBlockViewID(2)
consensus.blockHash = [32]byte{}
@ -243,7 +243,7 @@ func TestConstructCommitMessage(test *testing.T) {
if err != nil {
test.Fatalf("Cannot create consensus: %v", err)
}
consensus.UpdatePublicKeys([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2})
consensus.UpdatePublicKeys([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2}, []bls.PublicKeyWrapper{})
consensus.SetCurBlockViewID(2)
consensus.blockHash = [32]byte{}

@ -71,7 +71,7 @@ func setupBaseCase() (Decider, *TallyResult, shard.SlotList, map[string]secretKe
}
decider := NewDecider(SuperMajorityStake, shard.BeaconChainShardID)
decider.UpdateParticipants(pubKeys)
decider.UpdateParticipants(pubKeys, []bls.PublicKeyWrapper{})
tally, err := decider.SetVoters(&shard.Committee{
ShardID: shard.BeaconChainShardID, Slots: slotList,
}, big.NewInt(3))
@ -100,7 +100,7 @@ func setupEdgeCase() (Decider, *TallyResult, shard.SlotList, secretKeyMap) {
}
decider := NewDecider(SuperMajorityStake, shard.BeaconChainShardID)
decider.UpdateParticipants(pubKeys)
decider.UpdateParticipants(pubKeys, []bls.PublicKeyWrapper{})
tally, err := decider.SetVoters(&shard.Committee{
ShardID: shard.BeaconChainShardID, Slots: slotList,
}, big.NewInt(3))

@ -8,6 +8,7 @@ import (
bls_core "github.com/harmony-one/bls/ffi/go/bls"
harmony_bls "github.com/harmony-one/harmony/crypto/bls"
shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding"
"github.com/harmony-one/harmony/numeric"
"github.com/harmony-one/harmony/shard"
"github.com/stretchr/testify/assert"
@ -63,7 +64,7 @@ func TestAddingQuoromParticipants(t *testing.T) {
blsKeys = append(blsKeys, wrapper)
}
decider.UpdateParticipants(blsKeys)
decider.UpdateParticipants(blsKeys, []bls.PublicKeyWrapper{})
assert.Equal(t, keyCount, decider.ParticipantsCount())
}
@ -86,7 +87,7 @@ func TestSubmitVote(test *testing.T) {
pubKeyWrapper2 := bls.PublicKeyWrapper{Object: blsPriKey2.GetPublicKey()}
pubKeyWrapper2.Bytes.FromLibBLSPublicKey(pubKeyWrapper2.Object)
decider.UpdateParticipants([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2})
decider.UpdateParticipants([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2}, []bls.PublicKeyWrapper{})
if _, err := decider.submitVote(
Prepare,
@ -143,7 +144,7 @@ func TestSubmitVoteAggregateSig(test *testing.T) {
pubKeyWrapper3 := bls.PublicKeyWrapper{Object: blsPriKey3.GetPublicKey()}
pubKeyWrapper3.Bytes.FromLibBLSPublicKey(pubKeyWrapper3.Object)
decider.UpdateParticipants([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2})
decider.UpdateParticipants([]bls.PublicKeyWrapper{pubKeyWrapper1, pubKeyWrapper2}, []bls.PublicKeyWrapper{})
decider.submitVote(
Prepare,
@ -221,7 +222,7 @@ func TestAddNewVote(test *testing.T) {
pubKeys = append(pubKeys, wrapper)
}
decider.UpdateParticipants(pubKeys)
decider.UpdateParticipants(pubKeys, []bls.PublicKeyWrapper{})
decider.SetVoters(&shard.Committee{
ShardID: shard.BeaconChainShardID, Slots: slotList,
}, big.NewInt(3))
@ -326,7 +327,7 @@ func TestAddNewVoteAggregateSig(test *testing.T) {
// make all external keys belong to same account
slotList[3].EcdsaAddress = slotList[4].EcdsaAddress
decider.UpdateParticipants(pubKeys)
decider.UpdateParticipants(pubKeys, []bls.PublicKeyWrapper{})
decider.SetVoters(&shard.Committee{
ShardID: shard.BeaconChainShardID, Slots: slotList,
}, big.NewInt(3))
@ -410,7 +411,7 @@ func TestAddNewVoteInvalidAggregateSig(test *testing.T) {
slotList[5].EcdsaAddress = slotList[7].EcdsaAddress
slotList[6].EcdsaAddress = slotList[7].EcdsaAddress
decider.UpdateParticipants(pubKeys)
decider.UpdateParticipants(pubKeys, []bls.PublicKeyWrapper{})
decider.SetVoters(&shard.Committee{
ShardID: shard.BeaconChainShardID, Slots: slotList,
}, big.NewInt(3))
@ -547,3 +548,48 @@ func TestInvalidAggregateSig(test *testing.T) {
test.Error("Expect aggregate signature verification to succeed with correctly matched keys and sigs")
}
}
func TestNthNextHmyExt(test *testing.T) {
numHmyNodes := 10
numAllExtNodes := 10
numAllowlistExtNodes := numAllExtNodes / 2
allowlist := shardingconfig.Allowlist{MaxLimitPerShard: numAllowlistExtNodes - 1}
blsKeys := []harmony_bls.PublicKeyWrapper{}
for i := 0; i < numHmyNodes+numAllExtNodes; i++ {
blsKey := harmony_bls.RandPrivateKey()
wrapper := harmony_bls.PublicKeyWrapper{Object: blsKey.GetPublicKey()}
wrapper.Bytes.FromLibBLSPublicKey(wrapper.Object)
blsKeys = append(blsKeys, wrapper)
}
allowlistLeaders := blsKeys[len(blsKeys)-allowlist.MaxLimitPerShard:]
allLeaders := append(blsKeys[:numHmyNodes], allowlistLeaders...)
decider := NewDecider(SuperMajorityVote, shard.BeaconChainShardID)
fakeInstance := shardingconfig.MustNewInstance(2, 20, numHmyNodes, 0, numeric.OneDec(), nil, nil, allowlist, nil, 0)
decider.UpdateParticipants(blsKeys, allowlistLeaders)
for i := 0; i < len(allLeaders); i++ {
leader := allLeaders[i]
for j := 0; j < len(allLeaders)*2; j++ {
expectNextLeader := allLeaders[(i+j)%len(allLeaders)]
found, nextLeader := decider.NthNextHmyExt(fakeInstance, &leader, j)
if !found {
test.Fatal("next leader not found")
}
if expectNextLeader.Bytes != nextLeader.Bytes {
test.Fatal("next leader is not expected")
}
preJ := -j
preIndex := (i + len(allLeaders) + preJ%len(allLeaders)) % len(allLeaders)
expectPreLeader := allLeaders[preIndex]
found, preLeader := decider.NthNextHmyExt(fakeInstance, &leader, preJ)
if !found {
test.Fatal("previous leader not found")
}
if expectPreLeader.Bytes != preLeader.Bytes {
test.Fatal("previous leader is not expected")
}
}
}
}

@ -3,6 +3,7 @@ package quorum
import (
"fmt"
"math/big"
"sort"
"github.com/harmony-one/harmony/crypto/bls"
@ -75,8 +76,9 @@ type ParticipantTracker interface {
ParticipantsCount() int64
NthNext(*bls.PublicKeyWrapper, int) (bool, *bls.PublicKeyWrapper)
NthNextHmy(shardingconfig.Instance, *bls.PublicKeyWrapper, int) (bool, *bls.PublicKeyWrapper)
NthNextHmyExt(shardingconfig.Instance, *bls.PublicKeyWrapper, int) (bool, *bls.PublicKeyWrapper)
FirstParticipant(shardingconfig.Instance) *bls.PublicKeyWrapper
UpdateParticipants(pubKeys []bls.PublicKeyWrapper)
UpdateParticipants(pubKeys, allowlist []bls.PublicKeyWrapper)
}
// SignatoryTracker ..
@ -160,6 +162,8 @@ type cIdentities struct {
// Public keys of the committee including leader and validators
publicKeys []bls.PublicKeyWrapper
keyIndexMap map[bls.SerializedPublicKey]int
// every element is a index of publickKeys
allowlistIndex []int
prepare *votepower.Round
commit *votepower.Round
// viewIDSigs: every validator
@ -246,6 +250,41 @@ func (s *cIdentities) NthNextHmy(instance shardingconfig.Instance, pubKey *bls.P
return found, &s.publicKeys[idx]
}
// NthNextHmyExt return the Nth next pubkey of Harmony + allowlist nodes, next can be negative number
func (s *cIdentities) NthNextHmyExt(instance shardingconfig.Instance, pubKey *bls.PublicKeyWrapper, next int) (bool, *bls.PublicKeyWrapper) {
found := false
idx := s.IndexOf(pubKey.Bytes)
if idx != -1 {
found = true
}
numHmyNodes := instance.NumHarmonyOperatedNodesPerShard()
// sanity check to avoid out of bound access
if numHmyNodes <= 0 || numHmyNodes > len(s.publicKeys) {
numHmyNodes = len(s.publicKeys)
}
nth := idx
if idx >= numHmyNodes {
nth = sort.SearchInts(s.allowlistIndex, idx) + numHmyNodes
}
numExtNodes := instance.ExternalAllowlistLimit()
if numExtNodes > len(s.allowlistIndex) {
numExtNodes = len(s.allowlistIndex)
}
totalNodes := numHmyNodes + numExtNodes
// (totalNodes + next%totalNodes) can convert negitive 'next' to positive
nth = (nth + totalNodes + next%totalNodes) % totalNodes
if nth < numHmyNodes {
idx = nth
} else {
// find index of external slot key
idx = s.allowlistIndex[nth-numHmyNodes]
}
return found, &s.publicKeys[idx]
}
// FirstParticipant returns the first participant of the shard
func (s *cIdentities) FirstParticipant(instance shardingconfig.Instance) *bls.PublicKeyWrapper {
return &s.publicKeys[0]
@ -255,11 +294,17 @@ func (s *cIdentities) Participants() multibls.PublicKeys {
return s.publicKeys
}
func (s *cIdentities) UpdateParticipants(pubKeys []bls.PublicKeyWrapper) {
func (s *cIdentities) UpdateParticipants(pubKeys, allowlist []bls.PublicKeyWrapper) {
keyIndexMap := map[bls.SerializedPublicKey]int{}
for i := range pubKeys {
keyIndexMap[pubKeys[i].Bytes] = i
}
for _, key := range allowlist {
if i, exist := keyIndexMap[key.Bytes]; exist {
s.allowlistIndex = append(s.allowlistIndex, i)
}
}
sort.Ints(s.allowlistIndex)
s.publicKeys = pubKeys
s.keyIndexMap = keyIndexMap
}

@ -213,10 +213,19 @@ func (consensus *Consensus) getNextLeaderKey(viewID uint64) *bls.PublicKeyWrappe
Msg("[getNextLeaderKey] got leaderPubKey from coinbase")
// wasFound, next := consensus.Decider.NthNext(lastLeaderPubKey, gap)
// FIXME: rotate leader on harmony nodes only before fully externalization
wasFound, next := consensus.Decider.NthNextHmy(
var wasFound bool
var next *bls.PublicKeyWrapper
if consensus.Blockchain != nil && consensus.Blockchain.Config().IsAllowlistEpoch(epoch) {
wasFound, next = consensus.Decider.NthNextHmyExt(
shard.Schedule.InstanceForEpoch(epoch),
lastLeaderPubKey,
gap)
} else {
wasFound, next = consensus.Decider.NthNextHmy(
shard.Schedule.InstanceForEpoch(epoch),
lastLeaderPubKey,
gap)
}
if !wasFound {
consensus.getLogger().Warn().
Str("key", consensus.LeaderPubKey.Bytes.Hex()).

@ -111,7 +111,7 @@ func TestGetNextLeaderKeyShouldSucceed(t *testing.T) {
wrappedBLSKeys = append(wrappedBLSKeys, wrapped)
}
consensus.Decider.UpdateParticipants(wrappedBLSKeys)
consensus.Decider.UpdateParticipants(wrappedBLSKeys, []bls.PublicKeyWrapper{})
assert.Equal(t, keyCount, consensus.Decider.ParticipantsCount())
consensus.LeaderPubKey = &wrappedBLSKeys[0]

@ -45,6 +45,19 @@ func WrapperFromPrivateKey(pri *bls.SecretKey) PrivateKeyWrapper {
}
}
// WrapperPublicKeyFromString makes a PublicKeyWrapper from public key hex string
func WrapperPublicKeyFromString(pubkey string) (*PublicKeyWrapper, error) {
pub := &bls.PublicKey{}
if err := pub.DeserializeHexStr(pubkey); err != nil {
return nil, err
}
pubBytes := FromLibBLSPublicKeyUnsafe(pub)
return &PublicKeyWrapper{
Bytes: *pubBytes,
Object: pub,
}, nil
}
// SerializedPublicKey defines the serialized bls public key
type SerializedPublicKey [PublicKeySizeInBytes]byte

@ -0,0 +1,49 @@
package shardingconfig
import (
"fmt"
bls_cosi "github.com/harmony-one/harmony/crypto/bls"
)
type Allowlist struct {
MaxLimitPerShard int
BLSPublicKeys []bls_cosi.PublicKeyWrapper
}
func BLS(pubkeys []string) []bls_cosi.PublicKeyWrapper {
blsPubkeys := make([]bls_cosi.PublicKeyWrapper, len(pubkeys))
for i := range pubkeys {
if key, err := bls_cosi.WrapperPublicKeyFromString(pubkeys[i]); err != nil {
panic(fmt.Sprintf("invalid bls key: %d:%s error:%s", i, pubkeys[i], err.Error()))
} else {
blsPubkeys[i] = *key
}
}
return blsPubkeys
}
// each time to update the allowlist, it requires a hardfork.
// keep same version of mainnet Instance
var mainnetAllowlisV3_TBD = Allowlist{
MaxLimitPerShard: 0,
BLSPublicKeys: BLS([]string{}),
}
// keep same version of testnet Instance
var testnetAllowlistV3_3 = Allowlist{
MaxLimitPerShard: 4,
BLSPublicKeys: BLS([]string{
"7915b9cbae9d675af510cb252362b80ae6d68a3684bbea203bc30d2f5fda25ffcedfa3cf2a6c1d3051469379920a418d",
"cf8dad79f5da460462b190a4996f1701589139aa0d8a2202bdd10004ad3b0d9299165278f8002bcb49599444b3607802",
"a7b563a180629a121a3f78d2864b3d5c5b76d1672a3f9de9349fde9c7a3dad0922bf9a4e68cb38d033d6a9ddef754709",
"0a62ca435c5e48983b6124768b383d3f0b2d358326604aec692189c7833f4a52dff865c1515c32f315eb8e78eaceec11",
}),
}
var localnetAllowlist = Allowlist{
MaxLimitPerShard: 0,
BLSPublicKeys: BLS([]string{}),
}
var emptyAllowlist = Allowlist{}

@ -3,6 +3,7 @@ package shardingconfig
import (
"math/big"
"github.com/harmony-one/harmony/crypto/bls"
"github.com/harmony-one/harmony/internal/genesis"
"github.com/harmony-one/harmony/numeric"
"github.com/pkg/errors"
@ -33,6 +34,7 @@ type instance struct {
reshardingEpoch []*big.Int
blocksPerEpoch uint64
slotsLimit int // HIP-16: The absolute number of maximum effective slots per shard limit for each validator. 0 means no limit.
allowlist Allowlist
}
// NewInstance creates and validates a new sharding configuration based
@ -41,6 +43,7 @@ func NewInstance(
numShards uint32, numNodesPerShard, numHarmonyOperatedNodesPerShard, slotsLimit int, harmonyVotePercent numeric.Dec,
hmyAccounts []genesis.DeployAccount,
fnAccounts []genesis.DeployAccount,
allowlist Allowlist,
reshardingEpoch []*big.Int, blocksE uint64,
) (Instance, error) {
if numShards < 1 {
@ -84,6 +87,7 @@ func NewInstance(
externalVotePercent: numeric.OneDec().Sub(harmonyVotePercent),
hmyAccounts: hmyAccounts,
fnAccounts: fnAccounts,
allowlist: allowlist,
reshardingEpoch: reshardingEpoch,
blocksPerEpoch: blocksE,
slotsLimit: slotsLimit,
@ -99,12 +103,13 @@ func MustNewInstance(
harmonyVotePercent numeric.Dec,
hmyAccounts []genesis.DeployAccount,
fnAccounts []genesis.DeployAccount,
allowlist Allowlist,
reshardingEpoch []*big.Int, blocksPerEpoch uint64,
) Instance {
slotsLimit := int(float32(numNodesPerShard-numHarmonyOperatedNodesPerShard) * slotsLimitPercent)
sc, err := NewInstance(
numShards, numNodesPerShard, numHarmonyOperatedNodesPerShard, slotsLimit, harmonyVotePercent,
hmyAccounts, fnAccounts, reshardingEpoch, blocksPerEpoch,
hmyAccounts, fnAccounts, allowlist, reshardingEpoch, blocksPerEpoch,
)
if err != nil {
panic(err)
@ -185,3 +190,13 @@ func (sc instance) ReshardingEpoch() []*big.Int {
func (sc instance) GetNetworkID() NetworkID {
return DevNet
}
// ExternalAllowlist returns the list of external leader keys in allowlist(HIP18)
func (sc instance) ExternalAllowlist() []bls.PublicKeyWrapper {
return sc.allowlist.BLSPublicKeys
}
// ExternalAllowlistLimit returns the maximum number of external leader keys on each shard
func (sc instance) ExternalAllowlistLimit() int {
return sc.allowlist.MaxLimitPerShard
}

@ -142,9 +142,9 @@ var (
big.NewInt(0), big.NewInt(localnetV1Epoch), params.LocalnetChainConfig.StakingEpoch, params.LocalnetChainConfig.TwoSecondsEpoch,
}
// Number of shards, how many slots on each , how many slots owned by Harmony
localnetV0 = MustNewInstance(2, 7, 5, 0, numeric.OneDec(), genesis.LocalHarmonyAccounts, genesis.LocalFnAccounts, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpochOld())
localnetV1 = MustNewInstance(2, 8, 5, 0, numeric.OneDec(), genesis.LocalHarmonyAccountsV1, genesis.LocalFnAccountsV1, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpochOld())
localnetV2 = MustNewInstance(2, 9, 6, 0, numeric.MustNewDecFromStr("0.68"), genesis.LocalHarmonyAccountsV2, genesis.LocalFnAccountsV2, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpochOld())
localnetV3 = MustNewInstance(2, 9, 6, 0, numeric.MustNewDecFromStr("0.68"), genesis.LocalHarmonyAccountsV2, genesis.LocalFnAccountsV2, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpoch())
localnetV3_1 = MustNewInstance(2, 9, 6, 0, numeric.MustNewDecFromStr("0.68"), genesis.LocalHarmonyAccountsV2, genesis.LocalFnAccountsV2, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpoch())
localnetV0 = MustNewInstance(2, 7, 5, 0, numeric.OneDec(), genesis.LocalHarmonyAccounts, genesis.LocalFnAccounts, emptyAllowlist, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpochOld())
localnetV1 = MustNewInstance(2, 8, 5, 0, numeric.OneDec(), genesis.LocalHarmonyAccountsV1, genesis.LocalFnAccountsV1, emptyAllowlist, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpochOld())
localnetV2 = MustNewInstance(2, 9, 6, 0, numeric.MustNewDecFromStr("0.68"), genesis.LocalHarmonyAccountsV2, genesis.LocalFnAccountsV2, emptyAllowlist, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpochOld())
localnetV3 = MustNewInstance(2, 9, 6, 0, numeric.MustNewDecFromStr("0.68"), genesis.LocalHarmonyAccountsV2, genesis.LocalFnAccountsV2, emptyAllowlist, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpoch())
localnetV3_1 = MustNewInstance(2, 9, 6, 0, numeric.MustNewDecFromStr("0.68"), genesis.LocalHarmonyAccountsV2, genesis.LocalFnAccountsV2, emptyAllowlist, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpoch())
)

@ -204,22 +204,22 @@ func (ms mainnetSchedule) IsSkippedEpoch(shardID uint32, epoch *big.Int) bool {
var mainnetReshardingEpoch = []*big.Int{big.NewInt(0), big.NewInt(mainnetV0_1Epoch), big.NewInt(mainnetV0_2Epoch), big.NewInt(mainnetV0_3Epoch), big.NewInt(mainnetV0_4Epoch), big.NewInt(mainnetV1Epoch), big.NewInt(mainnetV1_1Epoch), big.NewInt(mainnetV1_2Epoch), big.NewInt(mainnetV1_3Epoch), big.NewInt(mainnetV1_4Epoch), big.NewInt(mainnetV1_5Epoch), big.NewInt(mainnetV2_0Epoch), big.NewInt(mainnetV2_1Epoch), big.NewInt(mainnetV2_2Epoch), params.MainnetChainConfig.TwoSecondsEpoch, params.MainnetChainConfig.SixtyPercentEpoch, params.MainnetChainConfig.HIP6And8Epoch}
var (
mainnetV0 = MustNewInstance(4, 150, 112, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccounts, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_1 = MustNewInstance(4, 152, 112, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_1, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_2 = MustNewInstance(4, 200, 148, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_2, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_3 = MustNewInstance(4, 210, 148, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_3, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_4 = MustNewInstance(4, 216, 148, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_4, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_1 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_1, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_2 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_2, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_3 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_3, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_4 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_4, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_5 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV2_0 = MustNewInstance(4, 250, 170, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV2_1 = MustNewInstance(4, 250, 130, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV2_2 = MustNewInstance(4, 250, 90, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV3 = MustNewInstance(4, 250, 90, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
mainnetV3_1 = MustNewInstance(4, 250, 50, 0, numeric.MustNewDecFromStr("0.60"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
mainnetV3_2 = MustNewInstance(4, 250, 25, 0, numeric.MustNewDecFromStr("0.49"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
mainnetV3_3 = MustNewInstance(4, 250, 25, 0.06, numeric.MustNewDecFromStr("0.49"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
mainnetV0 = MustNewInstance(4, 150, 112, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccounts, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_1 = MustNewInstance(4, 152, 112, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_1, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_2 = MustNewInstance(4, 200, 148, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_2, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_3 = MustNewInstance(4, 210, 148, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_3, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV0_4 = MustNewInstance(4, 216, 148, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV0_4, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_1 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_1, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_2 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_2, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_3 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_3, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_4 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_4, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV1_5 = MustNewInstance(4, 250, 170, 0, numeric.OneDec(), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV2_0 = MustNewInstance(4, 250, 170, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV2_1 = MustNewInstance(4, 250, 130, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV2_2 = MustNewInstance(4, 250, 90, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpochOld())
mainnetV3 = MustNewInstance(4, 250, 90, 0, numeric.MustNewDecFromStr("0.68"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
mainnetV3_1 = MustNewInstance(4, 250, 50, 0, numeric.MustNewDecFromStr("0.60"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
mainnetV3_2 = MustNewInstance(4, 250, 25, 0, numeric.MustNewDecFromStr("0.49"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
mainnetV3_3 = MustNewInstance(4, 250, 25, 0.06, numeric.MustNewDecFromStr("0.49"), genesis.HarmonyAccounts, genesis.FoundationalNodeAccountsV1_5, emptyAllowlist, mainnetReshardingEpoch, MainnetSchedule.BlocksPerEpoch())
)

@ -75,5 +75,5 @@ var pangaeaReshardingEpoch = []*big.Int{
params.PangaeaChainConfig.StakingEpoch,
}
var pangaeaV0 = MustNewInstance(4, 30, 30, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, pangaeaReshardingEpoch, PangaeaSchedule.BlocksPerEpoch())
var pangaeaV1 = MustNewInstance(4, 110, 30, 0, numeric.MustNewDecFromStr("0.68"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, pangaeaReshardingEpoch, PangaeaSchedule.BlocksPerEpoch())
var pangaeaV0 = MustNewInstance(4, 30, 30, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, pangaeaReshardingEpoch, PangaeaSchedule.BlocksPerEpoch())
var pangaeaV1 = MustNewInstance(4, 110, 30, 0, numeric.MustNewDecFromStr("0.68"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, pangaeaReshardingEpoch, PangaeaSchedule.BlocksPerEpoch())

@ -76,5 +76,5 @@ var partnerReshardingEpoch = []*big.Int{
params.PartnerChainConfig.StakingEpoch,
}
var partnerV0 = MustNewInstance(2, 5, 5, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, partnerReshardingEpoch, PartnerSchedule.BlocksPerEpoch())
var partnerV1 = MustNewInstance(2, 5, 4, 0, numeric.MustNewDecFromStr("0.9"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, partnerReshardingEpoch, PartnerSchedule.BlocksPerEpoch())
var partnerV0 = MustNewInstance(2, 5, 5, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, partnerReshardingEpoch, PartnerSchedule.BlocksPerEpoch())
var partnerV1 = MustNewInstance(2, 5, 4, 0, numeric.MustNewDecFromStr("0.9"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, partnerReshardingEpoch, PartnerSchedule.BlocksPerEpoch())

@ -6,6 +6,7 @@ import (
"fmt"
"math/big"
"github.com/harmony-one/harmony/crypto/bls"
"github.com/harmony-one/harmony/numeric"
"github.com/harmony-one/harmony/internal/genesis"
@ -74,6 +75,12 @@ type Instance interface {
BlocksPerEpoch() uint64
// HIP-16: The absolute number of maximum effective slots per shard limit for each validator. 0 means no limit.
SlotsLimit() int
// ExternalAllowlist returns the list of external leader keys in allowlist(HIP18)
ExternalAllowlist() []bls.PublicKeyWrapper
// ExternalAllowlistLimit returns the maximum number of external leader keys on each shard(HIP18)
ExternalAllowlistLimit() int
}
// genShardingStructure return sharding structure, given shard number and its patterns.

@ -78,6 +78,6 @@ var stressnetReshardingEpoch = []*big.Int{
params.StressnetChainConfig.StakingEpoch,
}
var stressnetV0 = MustNewInstance(2, 10, 10, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, stressnetReshardingEpoch, StressNetSchedule.BlocksPerEpoch())
var stressnetV1 = MustNewInstance(2, 30, 10, 0, numeric.MustNewDecFromStr("0.9"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, stressnetReshardingEpoch, StressNetSchedule.BlocksPerEpoch())
var stressnetV2 = MustNewInstance(2, 30, 10, 0, numeric.MustNewDecFromStr("0.6"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, stressnetReshardingEpoch, StressNetSchedule.BlocksPerEpoch())
var stressnetV0 = MustNewInstance(2, 10, 10, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, stressnetReshardingEpoch, StressNetSchedule.BlocksPerEpoch())
var stressnetV1 = MustNewInstance(2, 30, 10, 0, numeric.MustNewDecFromStr("0.9"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, stressnetReshardingEpoch, StressNetSchedule.BlocksPerEpoch())
var stressnetV2 = MustNewInstance(2, 30, 10, 0, numeric.MustNewDecFromStr("0.6"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, stressnetReshardingEpoch, StressNetSchedule.BlocksPerEpoch())

@ -33,6 +33,8 @@ const (
func (ts testnetSchedule) InstanceForEpoch(epoch *big.Int) Instance {
switch {
case params.TestnetChainConfig.IsAllowlistEpoch(epoch):
return testnetV3_3
case params.TestnetChainConfig.IsSlotsLimited(epoch):
return testnetV3_2
case params.TestnetChainConfig.IsSixtyPercent(epoch):
@ -116,9 +118,10 @@ var testnetReshardingEpoch = []*big.Int{
params.TestnetChainConfig.TwoSecondsEpoch,
}
var testnetV0 = MustNewInstance(4, 16, 15, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpochOld())
var testnetV1 = MustNewInstance(4, 20, 15, 0, numeric.MustNewDecFromStr("0.90"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpochOld())
var testnetV2 = MustNewInstance(4, 30, 8, 0, numeric.MustNewDecFromStr("0.90"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpochOld())
var testnetV3 = MustNewInstance(4, 30, 8, 0, numeric.MustNewDecFromStr("0.90"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch())
var testnetV3_1 = MustNewInstance(4, 30, 8, 0, numeric.MustNewDecFromStr("0.60"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch())
var testnetV3_2 = MustNewInstance(4, 30, 8, 0.15, numeric.MustNewDecFromStr("0.60"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch())
var testnetV0 = MustNewInstance(4, 16, 15, 0, numeric.OneDec(), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpochOld())
var testnetV1 = MustNewInstance(4, 20, 15, 0, numeric.MustNewDecFromStr("0.90"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpochOld())
var testnetV2 = MustNewInstance(4, 30, 8, 0, numeric.MustNewDecFromStr("0.90"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpochOld())
var testnetV3 = MustNewInstance(4, 30, 8, 0, numeric.MustNewDecFromStr("0.90"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch())
var testnetV3_1 = MustNewInstance(4, 30, 8, 0, numeric.MustNewDecFromStr("0.60"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch())
var testnetV3_2 = MustNewInstance(4, 30, 8, 0.15, numeric.MustNewDecFromStr("0.60"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, emptyAllowlist, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch())
var testnetV3_3 = MustNewInstance(4, 30, 8, 0.15, numeric.MustNewDecFromStr("0.60"), genesis.TNHarmonyAccounts, genesis.TNFoundationalAccounts, testnetAllowlistV3_3, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch())

@ -67,6 +67,7 @@ var (
StakingPrecompileEpoch: big.NewInt(871), // Around Tue Feb 11 2022
ChainIdFixEpoch: EpochTBD,
SlotsLimitedEpoch: big.NewInt(999), // Around Fri, 27 May 2022 09:41:02 UTC with 2s block time
AllowlistEpoch: EpochTBD,
}
// TestnetChainConfig contains the chain parameters to run a node on the harmony test network.
@ -102,6 +103,7 @@ var (
StakingPrecompileEpoch: big.NewInt(75175),
ChainIdFixEpoch: EpochTBD,
SlotsLimitedEpoch: big.NewInt(75684), // epoch to enable HIP-16, around Mon, 02 May 2022 08:18:45 UTC with 2s block time
AllowlistEpoch: big.NewInt(75877), // around Fri, 10 Jun 2022 06:10:18 GMT with average block time 2.0065s
}
// PangaeaChainConfig contains the chain parameters for the Pangaea network.
@ -282,6 +284,7 @@ var (
big.NewInt(0), // StakingPrecompileEpoch
big.NewInt(0), // ChainIdFixEpoch
big.NewInt(0), // SlotsLimitedEpoch
big.NewInt(0), // AllowlistEpoch
}
// TestChainConfig ...
@ -319,6 +322,7 @@ var (
big.NewInt(0), // StakingPrecompileEpoch
big.NewInt(0), // ChainIdFixEpoch
big.NewInt(0), // SlotsLimitedEpoch
big.NewInt(0), // AllowlistEpoch
}
// TestRules ...
@ -442,6 +446,8 @@ type ChainConfig struct {
// SlotsLimitedEpoch is the first epoch to enable HIP-16.
SlotsLimitedEpoch *big.Int `json:"slots-limit-epoch,omitempty"`
// AllowlistEpoch is the first epoch to support allowlist of HIP18
AllowlistEpoch *big.Int
}
// String implements the fmt.Stringer interface.
@ -613,6 +619,11 @@ func (c *ChainConfig) IsChainIdFixEpoch(epoch *big.Int) bool {
return isForked(c.ChainIdFixEpoch, epoch)
}
// IsAllowlistEpoch determines whether IsAllowlist of HIP18 is enabled
func (c *ChainConfig) IsAllowlistEpoch(epoch *big.Int) bool {
return isForked(c.AllowlistEpoch, epoch)
}
// UpdateEthChainIDByShard update the ethChainID based on shard ID.
func UpdateEthChainIDByShard(shardID uint32) {
once.Do(func() {

@ -1170,7 +1170,7 @@ func (node *Node) InitConsensusWithValidators() (err error) {
Int("numPubKeys", len(pubKeys)).
Str("mode", node.Consensus.Mode().String()).
Msg("[InitConsensusWithValidators] Successfully updated public keys")
node.Consensus.UpdatePublicKeys(pubKeys)
node.Consensus.UpdatePublicKeys(pubKeys, shard.Schedule.InstanceForEpoch(epoch).ExternalAllowlist())
node.Consensus.SetMode(consensus.Normal)
return nil
}

@ -5,7 +5,7 @@ CACHE_DIR="docker_images"
mkdir -p $CACHE_DIR
echo "pulling cached docker img"
docker load -i $CACHE_DIR/images.tar || true
#docker pull harmonyone/localnet-test
docker pull harmonyone/localnet-test
echo "saving cached docker img"
docker save -o $CACHE_DIR/images.tar harmonyone/localnet-test
docker run -v "$DIR/../:/go/src/github.com/harmony-one/harmony" harmonyone/localnet-test -n
Loading…
Cancel
Save