Commit 1239ea28 authored by madengji's avatar madengji Committed by 33cn

para bind miner

parent d1046df7
...@@ -85,8 +85,14 @@ maxTxNumber = 1600 ...@@ -85,8 +85,14 @@ maxTxNumber = 1600
[mver.consensus.paracross] [mver.consensus.paracross]
#超级节点挖矿奖励
coinReward=18 coinReward=18
#发展基金奖励
coinDevFund=12 coinDevFund=12
#如果超级节点上绑定了委托账户,则奖励超级节点coinBaseReward,其余部分(coinReward-coinBaseReward)按权重分给委托账户
coinBaseReward=5
#委托账户最少解绑定时间(按小时)
unBindTime=24
[consensus.sub.para] [consensus.sub.para]
......
...@@ -300,6 +300,15 @@ func (a *action) getNodesGroup(title string) (map[string]struct{}, []string, err ...@@ -300,6 +300,15 @@ func (a *action) getNodesGroup(title string) (map[string]struct{}, []string, err
} }
func (a *action) isValidSuperNode(addr string) (bool, error) {
cfg := a.api.GetConfig()
nodes, _, err := getParacrossNodes(a.db, cfg.GetTitle())
if err != nil {
return false, errors.Wrapf(err, "getNodes for title:%s", cfg.GetTitle())
}
return validNode(addr, nodes), nil
}
//相同的BlockHash,只保留一份数据 //相同的BlockHash,只保留一份数据
func updateCommitBlockHashs(stat *pt.ParacrossHeightStatus, commit *pt.ParacrossNodeStatus) { func updateCommitBlockHashs(stat *pt.ParacrossHeightStatus, commit *pt.ParacrossNodeStatus) {
if stat.BlockDetails == nil { if stat.BlockDetails == nil {
......
...@@ -119,3 +119,9 @@ func (e *Paracross) Exec_SelfStageConfig(payload *pt.ParaStageConfig, tx *types. ...@@ -119,3 +119,9 @@ func (e *Paracross) Exec_SelfStageConfig(payload *pt.ParaStageConfig, tx *types.
a := newAction(e, tx) a := newAction(e, tx)
return a.SelfStageConfig(payload) return a.SelfStageConfig(payload)
} }
//Exec_ParaBindMiner node group config process
func (e *Paracross) Exec_ParaBindMiner(payload *pt.ParaBindMinerInfo, tx *types.Transaction, index int) (*types.Receipt, error) {
a := newAction(e, tx)
return a.bindMiner(payload)
}
...@@ -35,6 +35,9 @@ var ( ...@@ -35,6 +35,9 @@ var (
paraSelfConsensStages string paraSelfConsensStages string
paraSelfConsensStageIDPrefix string paraSelfConsensStageIDPrefix string
paraBindMinderAddr string
paraBindMinderNode string
) )
func setPrefix() { func setPrefix() {
...@@ -50,6 +53,10 @@ func setPrefix() { ...@@ -50,6 +53,10 @@ func setPrefix() {
paraSelfConsensStages = "mavl-paracross-selfconsens-stages-" paraSelfConsensStages = "mavl-paracross-selfconsens-stages-"
paraSelfConsensStageIDPrefix = "mavl-paracross-selfconsens-id-" paraSelfConsensStageIDPrefix = "mavl-paracross-selfconsens-id-"
//bind miner
paraBindMinderAddr = "mavl-paracross-bindmineraddr-"
paraBindMinderNode = "mavl-paracross-bindminernode-"
localTx = "LODB-paracross-titleHeightAddr-" localTx = "LODB-paracross-titleHeightAddr-"
localTitle = "LODB-paracross-title-" localTitle = "LODB-paracross-title-"
localTitleHeight = "LODB-paracross-titleHeight-" localTitleHeight = "LODB-paracross-titleHeight-"
...@@ -179,3 +186,16 @@ func calcLocalNodeGroupStatusPrefix(status int32) []byte { ...@@ -179,3 +186,16 @@ func calcLocalNodeGroupStatusPrefix(status int32) []byte {
func calcLocalNodeGroupAllPrefix() []byte { func calcLocalNodeGroupAllPrefix() []byte {
return []byte(fmt.Sprintf(localNodeGroupStatusTitle)) return []byte(fmt.Sprintf(localNodeGroupStatusTitle))
} }
//bind miner
func calcParaBindMinerAddr(node, bind string) []byte {
return []byte(fmt.Sprintf(paraBindMinderAddr+"%s-%s", node, bind))
}
func calcParaBindMinerNode(node string) []byte {
return []byte(paraBindMinderNode + node)
}
func calcParaBindMinerNodePrefix() []byte {
return []byte(paraBindMinderNode)
}
...@@ -2,40 +2,157 @@ package executor ...@@ -2,40 +2,157 @@ package executor
import ( import (
"bytes" "bytes"
"github.com/pkg/errors"
"github.com/33cn/chain33/types" "github.com/33cn/chain33/types"
pt "github.com/33cn/plugin/plugin/dapp/paracross/types" pt "github.com/33cn/plugin/plugin/dapp/paracross/types"
) )
// reward 挖矿奖励, 主要处理挖矿分配逻辑,先实现基本策略,后面根据需求进行重构 const (
func (a *action) reward(nodeStatus *pt.ParacrossNodeStatus, stat *pt.ParacrossHeightStatus) (*types.Receipt, error) { opBind = 1
opUnBind = 2
)
//获取挖矿相关配置,这里需注意是共识的高度,而不是交易的高度 //根据挖矿节点地址 获取委托挖矿地址
cfg := a.api.GetConfig() func (a *action) getBindAddrs(nodes []string, statusHeight int64) []*pt.ParaNodeBindList {
coinReward := cfg.MGInt("mver.consensus.paracross.coinReward", nodeStatus.Height) * types.Coin var lists []*pt.ParaNodeBindList
fundReward := cfg.MGInt("mver.consensus.paracross.coinDevFund", nodeStatus.Height) * types.Coin for _, m := range nodes {
fundAddr := cfg.MGStr("mver.consensus.fundKeyAddr", nodeStatus.Height) list, err := a.getBindNodeInfo(m)
if isNotFound(errors.Cause(err)) {
continue
}
if err != nil {
clog.Error("paracross getBindAddrs err", "height", statusHeight, "node", m)
continue
}
lists = append(lists, list)
}
minerAddrs := getMiners(stat.Details, nodeStatus.BlockHash) return lists
//分配给矿工的单位奖励
minerUnit := coinReward / int64(len(minerAddrs))
}
func isBindAddrFound(bindAddrs []*pt.ParaNodeBindList) bool {
if len(bindAddrs) <= 0 {
return false
}
for _, addr := range bindAddrs {
if len(addr.Miners) > 0 {
return true
}
}
return false
}
func (a *action) rewardSuperNode(coinReward int64, miners []string, statusHeight int64) (*types.Receipt, int64, error) {
//分配给矿工的单位奖励
minerUnit := coinReward / int64(len(miners))
var change int64
receipt := &types.Receipt{Ty: types.ExecOk} receipt := &types.Receipt{Ty: types.ExecOk}
if minerUnit > 0 { if minerUnit > 0 {
//如果不等分转到发展基金 //如果不等分转到发展基金
fundReward += coinReward % minerUnit change = coinReward % minerUnit
for _, addr := range minerAddrs { for _, addr := range miners {
rep, err := a.coinsAccount.ExecDeposit(addr, a.execaddr, minerUnit) rep, err := a.coinsAccount.ExecDeposit(addr, a.execaddr, minerUnit)
if err != nil { if err != nil {
clog.Error("paracross miner reward deposit err", "height", nodeStatus.Height, clog.Error("paracross super node reward deposit err", "height", statusHeight,
"execAddr", a.execaddr, "minerAddr", addr, "amount", minerUnit, "err", err) "execAddr", a.execaddr, "minerAddr", addr, "amount", minerUnit, "err", err)
return nil, err return nil, 0, err
}
receipt = mergeReceipt(receipt, rep)
}
}
return receipt, change, nil
}
//奖励委托挖矿账户
func (a *action) rewardBindAddr(coinReward int64, bindList []*pt.ParaNodeBindList, statusHeight int64) (*types.Receipt, int64, error) {
if coinReward <= 0 {
return nil, 0, nil
}
//有可能一个bindAddr 在多个node绑定,这里会累计上去
var bindAddrList []*pt.ParaBindMinerInfo
for _, node := range bindList {
for _, miner := range node.Miners {
info, err := a.getBindAddrInfo(node.SuperNode, miner)
if err != nil {
return nil, 0, err
}
bindAddrList = append(bindAddrList, info)
}
}
var totalCoins int64
for _, addr := range bindAddrList {
totalCoins += addr.BindCount
}
//分配给矿工的单位奖励
minerUnit := coinReward / totalCoins
var change int64
receipt := &types.Receipt{Ty: types.ExecOk}
if minerUnit > 0 {
//如果不等分转到发展基金
change = coinReward % minerUnit
for _, miner := range bindAddrList {
rep, err := a.coinsAccount.ExecDeposit(miner.Addr, a.execaddr, minerUnit*miner.BindCount)
if err != nil {
clog.Error("paracross bind miner reward deposit err", "height", statusHeight,
"execAddr", a.execaddr, "minerAddr", miner.Addr, "amount", minerUnit*miner.BindCount, "err", err)
return nil, 0, err
} }
receipt = mergeReceipt(receipt, rep) receipt = mergeReceipt(receipt, rep)
} }
} }
return receipt, change, nil
}
// reward 挖矿奖励, 主要处理挖矿分配逻辑,先实现基本策略,后面根据需求进行重构
func (a *action) reward(nodeStatus *pt.ParacrossNodeStatus, stat *pt.ParacrossHeightStatus) (*types.Receipt, error) {
//获取挖矿相关配置,这里需注意是共识的高度,而不是交易的高度
cfg := a.api.GetConfig()
coinReward := cfg.MGInt("mver.consensus.paracross.coinReward", nodeStatus.Height) * types.Coin
coinBaseReward := cfg.MGInt("mver.consensus.paracross.coinBaseReward", nodeStatus.Height) * types.Coin
fundReward := cfg.MGInt("mver.consensus.paracross.coinDevFund", nodeStatus.Height) * types.Coin
fundAddr := cfg.MGStr("mver.consensus.fundKeyAddr", nodeStatus.Height)
//防止coinBaseReward 设置出错场景, coinBaseReward 一定要比coinReward小
if coinBaseReward >= coinReward {
coinBaseReward = coinReward / 10
}
//超级节点地址
minerAddrs := getMiners(stat.Details, nodeStatus.BlockHash)
//委托地址
bindAddrs := a.getBindAddrs(minerAddrs, nodeStatus.Height)
//奖励超级节点
minderRewards := coinReward
//如果有委托挖矿地址,则超级节点分baseReward部分,否则全部
if isBindAddrFound(bindAddrs) {
minderRewards = coinBaseReward
}
receipt := &types.Receipt{Ty: types.ExecOk}
r, change, err := a.rewardSuperNode(minderRewards, minerAddrs, nodeStatus.Height)
if err != nil {
return nil, err
}
fundReward += change
mergeReceipt(receipt, r)
//奖励委托挖矿地址
r, change, err = a.rewardBindAddr(coinReward-minderRewards, bindAddrs, nodeStatus.Height)
if err != nil {
return nil, err
}
fundReward += change
mergeReceipt(receipt, r)
//奖励发展基金
if fundReward > 0 { if fundReward > 0 {
rep, err := a.coinsAccount.ExecDeposit(fundAddr, a.execaddr, fundReward) rep, err := a.coinsAccount.ExecDeposit(fundAddr, a.execaddr, fundReward)
if err != nil { if err != nil {
...@@ -70,3 +187,253 @@ func mergeReceipt(receipt1, receipt2 *types.Receipt) *types.Receipt { ...@@ -70,3 +187,253 @@ func mergeReceipt(receipt1, receipt2 *types.Receipt) *types.Receipt {
return receipt1 return receipt1
} }
func makeAddrBindReceipt(node, addr string, prev, current *pt.ParaBindMinerInfo) *types.Receipt {
key := calcParaBindMinerAddr(node, addr)
log := &pt.ReceiptParaBindMinerInfo{
Addr: addr,
Prev: prev,
Current: current,
}
var val []byte
if current != nil {
val = types.Encode(current)
}
return &types.Receipt{
Ty: types.ExecOk,
KV: []*types.KeyValue{
{Key: key, Value: val},
},
Logs: []*types.ReceiptLog{
{
Ty: pt.TyLogParaBindMinerAddr,
Log: types.Encode(log),
},
},
}
}
func makeNodeBindReceipt(addr string, prev, current *pt.ParaNodeBindList) *types.Receipt {
key := calcParaBindMinerNode(addr)
log := &pt.ReceiptParaNodeBindListUpdate{
Prev: prev,
Current: current,
}
var val []byte
if current != nil {
val = types.Encode(current)
}
return &types.Receipt{
Ty: types.ExecOk,
KV: []*types.KeyValue{
{Key: key, Value: val},
},
Logs: []*types.ReceiptLog{
{
Ty: pt.TyLogParaBindMinerNode,
Log: types.Encode(log),
},
},
}
}
//绑定到超级节点
func (a *action) bind2Node(node string) (*types.Receipt, error) {
key := calcParaBindMinerNode(node)
data, err := a.db.Get(key)
if err != nil && err != types.ErrNotFound {
return nil, errors.Wrapf(err, "unbind2Node get failed node=%s", node)
}
//found
if len(data) > 0 {
var list pt.ParaNodeBindList
err = types.Decode(data, &list)
if err != nil {
return nil, errors.Wrapf(err, "bind2Node decode failed node=%s", node)
}
listCopy := list
list.Miners = append(list.Miners, a.fromaddr)
return makeNodeBindReceipt(node, &listCopy, &list), nil
}
//unfound
var list pt.ParaNodeBindList
list.SuperNode = node
list.Miners = append(list.Miners, a.fromaddr)
return makeNodeBindReceipt(node, nil, &list), nil
}
//从超级节点解绑
func (a *action) unbind2Node(node string) (*types.Receipt, error) {
list, err := a.getBindNodeInfo(node)
if err != nil {
return nil, errors.Wrap(err, "unbind2Node")
}
newList := &pt.ParaNodeBindList{SuperNode: list.SuperNode}
for _, m := range list.Miners {
if m != a.fromaddr {
newList.Miners = append(newList.Miners, m)
}
}
return makeNodeBindReceipt(node, list, newList), nil
}
func (a *action) getBindNodeInfo(node string) (*pt.ParaNodeBindList, error) {
key := calcParaBindMinerNode(node)
data, err := a.db.Get(key)
if err != nil {
return nil, errors.Wrapf(err, "get key failed node=%s", node)
}
var list pt.ParaNodeBindList
err = types.Decode(data, &list)
if err != nil {
return nil, errors.Wrapf(err, "decode failed node=%s", node)
}
return &list, nil
}
func (a *action) getBindAddrInfo(node, addr string) (*pt.ParaBindMinerInfo, error) {
key := calcParaBindMinerAddr(node, addr)
data, err := a.db.Get(key)
if err != nil {
return nil, errors.Wrapf(err, "get key failed node=%s,addr=%s", node, addr)
}
var info pt.ParaBindMinerInfo
err = types.Decode(data, &info)
if err != nil {
return nil, errors.Wrapf(err, "decode failed node=%s,addr=%s", node, addr)
}
return &info, nil
}
func (a *action) bindOp(info *pt.ParaBindMinerInfo) (*types.Receipt, error) {
if len(info.Addr) > 0 && info.Addr != a.fromaddr {
return nil, errors.Wrapf(types.ErrInvalidParam, "bindMiner addr=%s not from addr %s", info.Addr, a.fromaddr)
}
if info.BindCount <= 0 {
return nil, errors.Wrapf(types.ErrInvalidParam, "bindMiner bindCount nil from addr %s", a.fromaddr)
}
ok, err := a.isValidSuperNode(info.TargetAddr)
if err != nil || !ok {
return nil, errors.Wrapf(err, "invalid target node=%s", info.TargetAddr)
}
key := calcParaBindMinerAddr(info.TargetAddr, a.fromaddr)
data, err := a.db.Get(key)
if err != nil && err != types.ErrNotFound {
return nil, err
}
//found, 修改当前的绑定
if len(data) > 0 {
var receipt *types.Receipt
var acct pt.ParaBindMinerInfo
err = types.Decode(data, &acct)
if err != nil {
return nil, errors.Wrapf(err, "bindOp decode for addr=%s", a.fromaddr)
}
if info.BindCount == acct.BindCount {
return nil, errors.Wrapf(types.ErrInvalidParam, "bindOp bind coins not change current=%d, modify=%d",
acct.BindCount, info.BindCount)
}
//释放一部分冻结coins
if info.BindCount < acct.BindCount {
receipt, err = a.coinsAccount.ExecActive(a.fromaddr, a.execaddr, (acct.BindCount-info.BindCount)*types.Coin)
if err != nil {
return nil, errors.Wrapf(err, "bindOp Active addr=%s,execaddr=%s,coins=%d", a.fromaddr, a.execaddr, acct.BindCount-info.BindCount)
}
}
//冻结更多
receipt, err = a.coinsAccount.ExecFrozen(a.fromaddr, a.execaddr, (info.BindCount-acct.BindCount)*types.Coin)
if err != nil {
return nil, errors.Wrapf(err, "bindOp frozen more addr=%s,execaddr=%s,coins=%d", a.fromaddr, a.execaddr, info.BindCount-acct.BindCount)
}
acctCopy := acct
acct.BindCount = info.BindCount
r := makeAddrBindReceipt(info.TargetAddr, a.fromaddr, &acctCopy, &acct)
return mergeReceipt(receipt, r), nil
}
//not found, 增加新绑定
receipt, err := a.coinsAccount.ExecFrozen(a.fromaddr, a.execaddr, info.BindCount*types.Coin)
if err != nil {
return nil, errors.Wrapf(err, "bindOp frozen addr=%s,execaddr=%s,count=%d", a.fromaddr, a.execaddr, info.BindCount)
}
//bind addr
acct := &pt.ParaBindMinerInfo{
Addr: a.fromaddr,
BindStatus: opBind,
BindCount: info.BindCount,
BlockTime: a.blocktime,
TargetAddr: info.TargetAddr,
}
rBind := makeAddrBindReceipt(info.TargetAddr, a.fromaddr, nil, acct)
mergeReceipt(receipt, rBind)
//增加到列表中
rList, err := a.bind2Node(info.TargetAddr)
if err != nil {
return nil, err
}
mergeReceipt(receipt, rList)
return receipt, nil
}
func (a *action) unBindOp(info *pt.ParaBindMinerInfo) (*types.Receipt, error) {
acct, err := a.getBindAddrInfo(info.TargetAddr, a.fromaddr)
if err != nil {
return nil, err
}
cfg := a.api.GetConfig()
unBindHours := cfg.MGInt("mver.consensus.paracross.unBindTime", a.height)
if acct.BlockTime-a.blocktime < unBindHours*60*60 {
return nil, errors.Wrapf(err, "unBindOp unbind time=%d less %d hours than bind time =%d", a.blocktime, unBindHours, acct.BlockTime)
}
//unfrozen
receipt, err := a.coinsAccount.ExecActive(a.fromaddr, a.execaddr, acct.BindCount*types.Coin)
if err != nil {
return nil, errors.Wrapf(err, "unBindOp addr=%s,execaddr=%s,count=%d", a.fromaddr, a.execaddr, acct.BindCount)
}
//删除 bind addr
rUnBind := makeAddrBindReceipt(info.TargetAddr, a.fromaddr, acct, nil)
mergeReceipt(receipt, rUnBind)
//从列表删除
rUnList, err := a.unbind2Node(info.TargetAddr)
if err != nil {
return nil, err
}
mergeReceipt(receipt, rUnList)
return receipt, nil
}
func (a *action) bindMiner(info *pt.ParaBindMinerInfo) (*types.Receipt, error) {
if len(info.TargetAddr) <= 0 {
return nil, errors.Wrapf(types.ErrInvalidParam, "bindMiner targetAddr should not be nil to addr %s", a.fromaddr)
}
if info.BindStatus != opBind && info.BindStatus != opUnBind {
return nil, errors.Wrapf(types.ErrInvalidParam, "bindMiner status=%d not correct", info.BindStatus)
}
if info.BindStatus == opBind {
return a.bindOp(info)
}
return a.unBindOp(info)
}
...@@ -158,6 +158,31 @@ message RespParacrossNodeGroups { ...@@ -158,6 +158,31 @@ message RespParacrossNodeGroups {
repeated ParaNodeGroupStatus ids = 1; repeated ParaNodeGroupStatus ids = 1;
} }
//para bind miner
message ParaBindMinerInfo{
string addr = 1; // miner addr
int32 bindStatus = 2; // 0: init, 1: bind, 2:unbind
int64 bindCount = 3; // bind coins count
int64 blockTime = 4; // status bind block time
string targetAddr = 5; // super node addr
}
message ReceiptParaBindMinerInfo{
string addr = 1; // miner addr
ParaBindMinerInfo prev = 2;
ParaBindMinerInfo current = 3;
}
message ParaNodeBindList{
string superNode = 1;
repeated string miners = 2;
}
message ReceiptParaNodeBindListUpdate{
ParaNodeBindList prev = 1;
ParaNodeBindList current = 2;
}
message ParaBlock2MainMap { message ParaBlock2MainMap {
int64 height = 1; int64 height = 1;
string blockHash = 2; string blockHash = 2;
......
...@@ -47,6 +47,8 @@ const ( ...@@ -47,6 +47,8 @@ const (
TyLogParaStageGroupUpdate = 667 TyLogParaStageGroupUpdate = 667
//TyLogParaCrossAssetTransfer 统一的跨链资产转移 //TyLogParaCrossAssetTransfer 统一的跨链资产转移
TyLogParaCrossAssetTransfer = 670 TyLogParaCrossAssetTransfer = 670
TyLogParaBindMinerAddr = 671
TyLogParaBindMinerNode = 672
) )
// action type // action type
...@@ -61,6 +63,8 @@ const ( ...@@ -61,6 +63,8 @@ const (
ParacrossActionWithdraw ParacrossActionWithdraw
// ParacrossActionTransferToExec asset transfer to exec // ParacrossActionTransferToExec asset transfer to exec
ParacrossActionTransferToExec ParacrossActionTransferToExec
// ParacrossActionParaBindMiner para chain bind super node miner
ParacrossActionParaBindMiner
) )
const ( const (
......
...@@ -109,6 +109,8 @@ func (p *ParacrossType) GetLogMap() map[int64]*types.LogInfo { ...@@ -109,6 +109,8 @@ func (p *ParacrossType) GetLogMap() map[int64]*types.LogInfo {
TyLogParaSelfConsStageConfig: {Ty: reflect.TypeOf(ReceiptSelfConsStageConfig{}), Name: "LogParaSelfConsStageConfig"}, TyLogParaSelfConsStageConfig: {Ty: reflect.TypeOf(ReceiptSelfConsStageConfig{}), Name: "LogParaSelfConsStageConfig"},
TyLogParaStageVoteDone: {Ty: reflect.TypeOf(ReceiptSelfConsStageVoteDone{}), Name: "LogParaSelfConfStageVoteDoen"}, TyLogParaStageVoteDone: {Ty: reflect.TypeOf(ReceiptSelfConsStageVoteDone{}), Name: "LogParaSelfConfStageVoteDoen"},
TyLogParaStageGroupUpdate: {Ty: reflect.TypeOf(ReceiptSelfConsStagesUpdate{}), Name: "LogParaSelfConfStagesUpdate"}, TyLogParaStageGroupUpdate: {Ty: reflect.TypeOf(ReceiptSelfConsStagesUpdate{}), Name: "LogParaSelfConfStagesUpdate"},
TyLogParaBindMinerAddr: {Ty: reflect.TypeOf(ReceiptParaBindMinerInfo{}), Name: "TyLogParaBindMinerAddrUpdate"},
TyLogParaBindMinerNode: {Ty: reflect.TypeOf(ReceiptParaNodeBindListUpdate{}), Name: "TyLogParaBindNodeListUpdate"},
} }
} }
...@@ -126,6 +128,7 @@ func (p *ParacrossType) GetTypeMap() map[string]int32 { ...@@ -126,6 +128,7 @@ func (p *ParacrossType) GetTypeMap() map[string]int32 {
"NodeConfig": ParacrossActionNodeConfig, "NodeConfig": ParacrossActionNodeConfig,
"NodeGroupConfig": ParacrossActionNodeGroupApply, "NodeGroupConfig": ParacrossActionNodeGroupApply,
"SelfStageConfig": ParacrossActionSelfStageConfig, "SelfStageConfig": ParacrossActionSelfStageConfig,
"ParaBindMiner": ParacrossActionParaBindMiner,
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment