You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1251 lines
32 KiB
1251 lines
32 KiB
package slash
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/woop-chain/woop/core/rawdb"
|
|
|
|
"github.com/woop-chain/woop/crypto/bls"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
bls_core "github.com/woop-chain/bls/ffi/go/bls"
|
|
blockfactory "github.com/woop-chain/woop/block/factory"
|
|
consensus_sig "github.com/woop-chain/woop/consensus/signature"
|
|
"github.com/woop-chain/woop/core/state"
|
|
"github.com/woop-chain/woop/core/types"
|
|
"github.com/woop-chain/woop/internal/params"
|
|
"github.com/woop-chain/woop/numeric"
|
|
"github.com/woop-chain/woop/shard"
|
|
"github.com/woop-chain/woop/staking/effective"
|
|
staking "github.com/woop-chain/woop/staking/types"
|
|
)
|
|
|
|
var (
|
|
bigOne = big.NewInt(1e18)
|
|
fiveKOnes = new(big.Int).Mul(big.NewInt(5000), bigOne)
|
|
tenKOnes = new(big.Int).Mul(big.NewInt(10000), bigOne)
|
|
twentyKOnes = new(big.Int).Mul(big.NewInt(20000), bigOne)
|
|
twentyFiveKOnes = new(big.Int).Mul(big.NewInt(25000), bigOne)
|
|
thirtyKOnes = new(big.Int).Mul(big.NewInt(30000), bigOne)
|
|
thirtyFiveKOnes = new(big.Int).Mul(big.NewInt(35000), bigOne)
|
|
fourtyKOnes = new(big.Int).Mul(big.NewInt(40000), bigOne)
|
|
fiftyKOnes = new(big.Int).Mul(big.NewInt(50000), bigOne)
|
|
hundredKOnes = new(big.Int).Mul(big.NewInt(100000), bigOne)
|
|
thousandKOnes = new(big.Int).Mul(big.NewInt(1000000), bigOne)
|
|
)
|
|
|
|
const (
|
|
// validator creation parameters
|
|
doubleSignShardID = 0
|
|
doubleSignEpoch = 4
|
|
doubleSignBlockNumber = 37
|
|
doubleSignViewID = 38
|
|
|
|
creationHeight = 33
|
|
lastEpochInComm = 5
|
|
currentEpoch = 5
|
|
)
|
|
|
|
const (
|
|
numShard = 4
|
|
numNodePerShard = 5
|
|
|
|
offenderShard = doubleSignShardID
|
|
offenderShardIndex = 0
|
|
)
|
|
|
|
var (
|
|
doubleSignBlock1 = makeBlockForTest(doubleSignEpoch, 0)
|
|
doubleSignBlock2 = makeBlockForTest(doubleSignEpoch, 1)
|
|
)
|
|
|
|
var (
|
|
keyPairs = genKeyPairs(25)
|
|
|
|
offIndex = offenderShard*numNodePerShard + offenderShardIndex
|
|
offAddr = makeTestAddress(offIndex)
|
|
offKey = keyPairs[offIndex]
|
|
offPub = offKey.Pub()
|
|
|
|
leaderAddr = makeTestAddress("leader")
|
|
reporterAddr = makeTestAddress("reporter")
|
|
)
|
|
|
|
func TestVerify(t *testing.T) {
|
|
tests := []struct {
|
|
r Record
|
|
sdb *state.DB
|
|
chain *fakeBlockChain
|
|
expErr error
|
|
}{
|
|
{
|
|
r: defaultSlashRecord(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: nil,
|
|
},
|
|
{
|
|
// not vWrapper in state
|
|
r: defaultSlashRecord(),
|
|
sdb: makeTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errors.New("address not present in state"),
|
|
},
|
|
{
|
|
// banned status
|
|
r: defaultSlashRecord(),
|
|
sdb: func() *state.DB {
|
|
sdb := makeTestStateDB()
|
|
w := defaultValidatorWrapper()
|
|
w.Status = effective.Banned
|
|
if err := sdb.UpdateValidatorWrapper(offAddr, w); err != nil {
|
|
panic(err)
|
|
}
|
|
return sdb
|
|
}(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errAlreadyBannedValidator,
|
|
},
|
|
{
|
|
// same offender and reporter
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Reporter = offAddr
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errReporterAndOffenderSame,
|
|
},
|
|
{
|
|
// same block in conflicting votes
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Evidence.SecondVote = copyVote(r.Evidence.FirstVote)
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errSlashBlockNoConflict,
|
|
},
|
|
{
|
|
// second vote signed with a different bls key
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Evidence.SecondVote = makeVoteData(keyPairs[24], doubleSignBlock2)
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errNoMatchingDoubleSignKeys,
|
|
},
|
|
{
|
|
// block is in the future
|
|
r: defaultSlashRecord(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: func() *fakeBlockChain {
|
|
bc := defaultFakeBlockChain()
|
|
bc.currentBlock = *makeBlockForTest(doubleSignEpoch-1, 0)
|
|
return bc
|
|
}(),
|
|
|
|
expErr: errSlashFromFutureEpoch,
|
|
},
|
|
{
|
|
// error from blockchain.ReadShardState (fakeChainErrEpoch)
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Evidence.Epoch = big.NewInt(currentEpoch)
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errFakeChainUnexpectEpoch,
|
|
},
|
|
{
|
|
// Invalid shardID
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Evidence.ShardID = 10
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: shard.ErrShardIDNotInSuperCommittee,
|
|
},
|
|
{
|
|
// Missing bls from committee
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Evidence.FirstVote = makeVoteData(keyPairs[24], doubleSignBlock1)
|
|
r.Evidence.SecondVote = makeVoteData(keyPairs[24], doubleSignBlock2)
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: shard.ErrValidNotInCommittee,
|
|
},
|
|
{
|
|
// offender address not match vote
|
|
r: defaultSlashRecord(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: func() *fakeBlockChain {
|
|
bc := defaultFakeBlockChain()
|
|
bc.superCommittee.Shards[offenderShard].Slots[offenderShardIndex].
|
|
EcdsaAddress = makeTestAddress("other")
|
|
return bc
|
|
}(),
|
|
|
|
expErr: errors.New("does not match the signer's address"),
|
|
},
|
|
{
|
|
// empty signature
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Evidence.FirstVote.Signature = nil
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errors.New("Empty buf"),
|
|
},
|
|
{
|
|
// false signature
|
|
r: func() Record {
|
|
r := defaultSlashRecord()
|
|
r.Evidence.FirstVote.Signature = r.Evidence.SecondVote.Signature
|
|
return r
|
|
}(),
|
|
sdb: defaultTestStateDB(),
|
|
chain: defaultFakeBlockChain(),
|
|
|
|
expErr: errors.New("could not verify bls key signature on slash"),
|
|
},
|
|
}
|
|
for i, test := range tests {
|
|
rawRecord := copyRecord(test.r)
|
|
|
|
err := Verify(test.chain, test.sdb, &test.r)
|
|
|
|
if assErr := assertError(err, test.expErr); assErr != nil {
|
|
t.Errorf("Test %v: %v", i, assErr)
|
|
}
|
|
if !reflect.DeepEqual(test.r, rawRecord) {
|
|
t.Errorf("Test %v: record has value changed", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplySlashRate(t *testing.T) {
|
|
tests := []struct {
|
|
amount *big.Int
|
|
rate numeric.Dec
|
|
exp *big.Int
|
|
}{
|
|
{twentyKOnes, numeric.NewDecWithPrec(5, 1), tenKOnes},
|
|
{bigOne, numeric.NewDecWithPrec(5, 1), big.NewInt(5e17)},
|
|
{new(big.Int).Mul(bigOne, big.NewInt(3)), numeric.NewDecWithPrec(5, 1), big.NewInt(15e17)},
|
|
{twentyKOnes, numeric.ZeroDec(), big.NewInt(0)},
|
|
}
|
|
for i, test := range tests {
|
|
res := applySlashRate(test.amount, test.rate)
|
|
if res.Cmp(test.exp) != 0 {
|
|
t.Errorf("Test %v: unexpected answer %v / %v", i, res, test.exp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetDifference(t *testing.T) {
|
|
tests := []struct {
|
|
indexes1 []int
|
|
indexes2 []int
|
|
expIndexes []int
|
|
}{
|
|
{[]int{}, []int{}, []int{}},
|
|
{[]int{1}, []int{}, []int{}},
|
|
{[]int{}, []int{1}, []int{1}},
|
|
{[]int{1, 2, 3}, []int{3, 4, 5}, []int{4, 5}},
|
|
}
|
|
for ti, test := range tests {
|
|
rs1 := makeSimpleRecords(test.indexes1)
|
|
rs2 := makeSimpleRecords(test.indexes2)
|
|
|
|
diff := rs1.SetDifference(rs2)
|
|
|
|
if len(diff) != len(test.expIndexes) {
|
|
t.Errorf("Test %v: size not expected %v, %v", ti, len(diff), len(test.expIndexes))
|
|
}
|
|
for i, r := range diff {
|
|
if r.Reporter != common.BigToAddress(big.NewInt(int64(test.expIndexes[i]))) {
|
|
t.Errorf("Test %v: Records[i] unexpected", ti)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeSimpleRecords(indexes []int) Records {
|
|
rs := make(Records, 0, len(indexes))
|
|
for _, index := range indexes {
|
|
rs = append(rs, Record{
|
|
Reporter: common.BigToAddress(big.NewInt(int64(index))),
|
|
})
|
|
}
|
|
return rs
|
|
}
|
|
|
|
func TestPayDown(t *testing.T) {
|
|
tests := []struct {
|
|
debt, amt *big.Int
|
|
diff *Application
|
|
|
|
expDebt, expAmt *big.Int
|
|
expDiff *Application
|
|
expErr error
|
|
}{
|
|
{
|
|
debt: new(big.Int).Set(twentyFiveKOnes),
|
|
amt: new(big.Int).Set(thirtyKOnes),
|
|
diff: &Application{
|
|
TotalSlashed: new(big.Int).Set(tenKOnes),
|
|
TotalBeneficiaryReward: new(big.Int).Set(tenKOnes),
|
|
},
|
|
expDebt: common.Big0,
|
|
expAmt: fiveKOnes,
|
|
expDiff: &Application{
|
|
TotalSlashed: thirtyFiveKOnes,
|
|
TotalBeneficiaryReward: tenKOnes,
|
|
},
|
|
},
|
|
{
|
|
debt: new(big.Int).Set(thirtyKOnes),
|
|
amt: new(big.Int).Set(twentyFiveKOnes),
|
|
diff: &Application{
|
|
TotalSlashed: new(big.Int).Set(tenKOnes),
|
|
TotalBeneficiaryReward: new(big.Int).Set(tenKOnes),
|
|
},
|
|
expDebt: fiveKOnes,
|
|
expAmt: common.Big0,
|
|
expDiff: &Application{
|
|
TotalSlashed: thirtyFiveKOnes,
|
|
TotalBeneficiaryReward: tenKOnes,
|
|
},
|
|
},
|
|
}
|
|
for i, test := range tests {
|
|
payDown(test.amt, test.debt, test.diff.TotalSlashed)
|
|
if test.debt.Cmp(test.expDebt) != 0 {
|
|
t.Errorf("Test %v: unexpected debt %v / %v", i, test.debt, test.expDebt)
|
|
}
|
|
if test.amt.Cmp(test.expAmt) != 0 {
|
|
t.Errorf("Test %v: unexpected amount %v / %v", i, test.amt, test.expAmt)
|
|
}
|
|
if test.diff.TotalSlashed.Cmp(test.expDiff.TotalSlashed) != 0 {
|
|
t.Errorf("Test %v: unexpected expSlashed %v / %v", i, test.diff.TotalSlashed,
|
|
test.expDiff.TotalSlashed)
|
|
}
|
|
if test.diff.TotalBeneficiaryReward.Cmp(test.expDiff.TotalBeneficiaryReward) != 0 {
|
|
t.Errorf("Test %v: unexpected total beneficiary reward %v / %v", i,
|
|
test.diff.TotalBeneficiaryReward, test.expDiff.TotalBeneficiaryReward)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplySlashingToDelegator(t *testing.T) {
|
|
tests := []applySlashingToDelegatorTestCase{
|
|
{
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
delegationIdx: 0,
|
|
debt: big.NewInt(0),
|
|
expDel: expDelegation{
|
|
expAmt: twentyKOnes,
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{tenKOnes, tenKOnes},
|
|
},
|
|
expSlashed: common.Big0,
|
|
expBeneficiaryReward: common.Big0,
|
|
},
|
|
{
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
delegationIdx: 0,
|
|
debt: twentyKOnes,
|
|
expDel: expDelegation{
|
|
expAmt: big.NewInt(0),
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{tenKOnes, tenKOnes},
|
|
},
|
|
expSlashed: twentyKOnes,
|
|
expBeneficiaryReward: tenKOnes,
|
|
},
|
|
{
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
delegationIdx: 0,
|
|
debt: twentyFiveKOnes,
|
|
expDel: expDelegation{
|
|
expAmt: big.NewInt(0),
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{tenKOnes, fiveKOnes},
|
|
},
|
|
expSlashed: twentyFiveKOnes,
|
|
expBeneficiaryReward: new(big.Int).Div(twentyFiveKOnes, common.Big2),
|
|
},
|
|
{
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
delegationIdx: 0,
|
|
debt: thirtyKOnes,
|
|
expDel: expDelegation{
|
|
expAmt: big.NewInt(0),
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{tenKOnes, big.NewInt(0)},
|
|
},
|
|
expSlashed: thirtyKOnes,
|
|
expBeneficiaryReward: new(big.Int).Div(thirtyKOnes, common.Big2),
|
|
},
|
|
{
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
delegationIdx: 0,
|
|
debt: thirtyFiveKOnes,
|
|
expDel: expDelegation{
|
|
expAmt: big.NewInt(0),
|
|
expReward: fiveKOnes,
|
|
expUndelAmt: []*big.Int{tenKOnes, big.NewInt(0)},
|
|
},
|
|
expSlashed: thirtyFiveKOnes,
|
|
expBeneficiaryReward: new(big.Int).Div(thirtyFiveKOnes, common.Big2),
|
|
},
|
|
{
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
delegationIdx: 0,
|
|
debt: fourtyKOnes,
|
|
expDel: expDelegation{
|
|
expAmt: big.NewInt(0),
|
|
expReward: big.NewInt(0),
|
|
expUndelAmt: []*big.Int{tenKOnes, big.NewInt(0)},
|
|
},
|
|
expSlashed: fourtyKOnes,
|
|
expBeneficiaryReward: twentyKOnes,
|
|
},
|
|
{
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
delegationIdx: 0,
|
|
debt: fiftyKOnes,
|
|
expDel: expDelegation{
|
|
expAmt: big.NewInt(0),
|
|
expReward: big.NewInt(0),
|
|
expUndelAmt: []*big.Int{tenKOnes, big.NewInt(0)},
|
|
},
|
|
expSlashed: fourtyKOnes,
|
|
expBeneficiaryReward: twentyKOnes,
|
|
},
|
|
}
|
|
for i, tc := range tests {
|
|
tc.makeData()
|
|
tc.apply()
|
|
|
|
if err := tc.checkResult(); err != nil {
|
|
t.Errorf("Test %v: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDelegatorSlashApply(t *testing.T) {
|
|
tests := []slashApplyTestCase{
|
|
{
|
|
snapshot: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: fourtyKOnes,
|
|
},
|
|
}),
|
|
current: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: fourtyKOnes,
|
|
},
|
|
}),
|
|
expDels: []expDelegation{
|
|
{
|
|
expAmt: twentyKOnes,
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{},
|
|
},
|
|
},
|
|
expSlashed: twentyKOnes,
|
|
expBeneficiaryReward: tenKOnes,
|
|
},
|
|
{
|
|
snapshot: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: fourtyKOnes,
|
|
},
|
|
{
|
|
address: "del1",
|
|
amount: fourtyKOnes,
|
|
},
|
|
}),
|
|
current: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: fourtyKOnes,
|
|
},
|
|
{
|
|
address: "del1",
|
|
amount: fourtyKOnes,
|
|
},
|
|
}),
|
|
expDels: []expDelegation{
|
|
{
|
|
expAmt: twentyKOnes,
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{},
|
|
},
|
|
{
|
|
expAmt: new(big.Int).Mul(big.NewInt(24000), bigOne),
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{},
|
|
},
|
|
},
|
|
expSlashed: new(big.Int).Mul(big.NewInt(36000), bigOne),
|
|
expBeneficiaryReward: new(big.Int).Mul(big.NewInt(18000), bigOne),
|
|
},
|
|
{
|
|
snapshot: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: fiftyKOnes,
|
|
},
|
|
{
|
|
address: "del1",
|
|
amount: fourtyKOnes,
|
|
},
|
|
{
|
|
address: "del2",
|
|
amount: tenKOnes,
|
|
},
|
|
}),
|
|
current: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: fiftyKOnes,
|
|
},
|
|
{
|
|
address: "del1",
|
|
amount: fourtyKOnes,
|
|
},
|
|
{
|
|
address: "del2",
|
|
amount: tenKOnes,
|
|
},
|
|
}),
|
|
expDels: []expDelegation{
|
|
{
|
|
expAmt: twentyFiveKOnes,
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{},
|
|
},
|
|
{
|
|
expAmt: new(big.Int).Mul(big.NewInt(24000), bigOne),
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{},
|
|
},
|
|
{
|
|
expAmt: new(big.Int).Mul(big.NewInt(6000), bigOne),
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{},
|
|
},
|
|
},
|
|
expSlashed: new(big.Int).Mul(big.NewInt(45000), bigOne),
|
|
expBeneficiaryReward: new(big.Int).Mul(big.NewInt(22500), bigOne),
|
|
},
|
|
{
|
|
snapshot: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: hundredKOnes,
|
|
},
|
|
{
|
|
address: "del1",
|
|
amount: twentyKOnes,
|
|
},
|
|
{
|
|
address: "del2",
|
|
amount: twentyKOnes,
|
|
},
|
|
}),
|
|
current: generateValidatorWrapper([]testDelegation{
|
|
{
|
|
address: "off",
|
|
amount: hundredKOnes,
|
|
},
|
|
{
|
|
address: "del1",
|
|
amount: common.Big0,
|
|
historyUndel: twentyKOnes,
|
|
afterSignUndel: common.Big0,
|
|
},
|
|
{
|
|
address: "del2",
|
|
amount: common.Big0,
|
|
historyUndel: common.Big0,
|
|
afterSignUndel: twentyKOnes,
|
|
},
|
|
}),
|
|
expDels: []expDelegation{
|
|
{
|
|
expAmt: fiftyKOnes,
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{},
|
|
},
|
|
{
|
|
expAmt: common.Big0,
|
|
expReward: common.Big0,
|
|
expUndelAmt: []*big.Int{twentyKOnes, common.Big0},
|
|
},
|
|
{
|
|
expAmt: common.Big0,
|
|
expReward: tenKOnes,
|
|
expUndelAmt: []*big.Int{common.Big0, common.Big0},
|
|
},
|
|
},
|
|
expSlashed: new(big.Int).Mul(big.NewInt(80000), bigOne),
|
|
expBeneficiaryReward: fourtyKOnes,
|
|
},
|
|
}
|
|
|
|
for i, tc := range tests {
|
|
tc.makeData()
|
|
tc.apply()
|
|
|
|
if err := tc.checkResult(); err != nil {
|
|
t.Errorf("Test %v: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
type applySlashingToDelegatorTestCase struct {
|
|
snapshot, current *staking.ValidatorWrapper
|
|
state *state.DB
|
|
beneficiary common.Address
|
|
slashTrack *Application
|
|
debt *big.Int
|
|
delegationIdx int
|
|
|
|
gotErr error
|
|
|
|
expDel expDelegation
|
|
expSlashed, expBeneficiaryReward *big.Int
|
|
expErr error
|
|
}
|
|
|
|
type slashApplyTestCase struct {
|
|
snapshot, current *staking.ValidatorWrapper
|
|
|
|
beneficiary common.Address
|
|
state *state.DB
|
|
slashTrack *Application
|
|
gotErr error
|
|
|
|
expDels []expDelegation
|
|
expSlashed, expBeneficiaryReward *big.Int
|
|
expErr error
|
|
}
|
|
|
|
func (tc *applySlashingToDelegatorTestCase) makeData() {
|
|
tc.beneficiary = leaderAddr
|
|
tc.state = makeTestStateDB()
|
|
tc.slashTrack = &Application{
|
|
TotalSlashed: new(big.Int).Set(common.Big0),
|
|
TotalBeneficiaryReward: new(big.Int).Set(common.Big0),
|
|
}
|
|
}
|
|
|
|
func (tc *applySlashingToDelegatorTestCase) apply() {
|
|
tc.gotErr = delegatorSlashApplyDebt(tc.snapshot, tc.current, tc.state, tc.debt, tc.beneficiary,
|
|
big.NewInt(doubleSignEpoch), tc.slashTrack)
|
|
}
|
|
|
|
func (tc *applySlashingToDelegatorTestCase) checkResult() error {
|
|
if err := assertError(tc.gotErr, tc.expErr); err != nil {
|
|
return err
|
|
}
|
|
if err := tc.expDel.checkDelegation(tc.current.Delegations[tc.delegationIdx]); err != nil {
|
|
return fmt.Errorf("delegations[%v]: %v", tc.delegationIdx, err)
|
|
}
|
|
if tc.slashTrack.TotalSlashed.Cmp(tc.expSlashed) != 0 {
|
|
return fmt.Errorf("unexpected total slash %v / %v", tc.slashTrack.TotalSlashed,
|
|
tc.expSlashed)
|
|
}
|
|
if tc.slashTrack.TotalBeneficiaryReward.Cmp(tc.expBeneficiaryReward) != 0 {
|
|
return fmt.Errorf("unexpected beneficiary reward %v / %v", tc.slashTrack.TotalBeneficiaryReward,
|
|
tc.expBeneficiaryReward)
|
|
}
|
|
if bal := tc.state.GetBalance(tc.beneficiary); bal.Cmp(tc.expBeneficiaryReward) != 0 {
|
|
return fmt.Errorf("unexpected balance for beneficiary %v / %v", bal, tc.expBeneficiaryReward)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (tc *slashApplyTestCase) makeData() {
|
|
tc.beneficiary = leaderAddr
|
|
tc.state = makeTestStateDB()
|
|
tc.slashTrack = &Application{
|
|
TotalSlashed: new(big.Int).Set(common.Big0),
|
|
TotalBeneficiaryReward: new(big.Int).Set(common.Big0),
|
|
}
|
|
}
|
|
|
|
func (tc *slashApplyTestCase) apply() {
|
|
tc.gotErr = delegatorSlashApply(tc.snapshot, tc.current, tc.state, tc.beneficiary, big.NewInt(doubleSignEpoch), tc.slashTrack)
|
|
}
|
|
|
|
func (tc *slashApplyTestCase) checkResult() error {
|
|
if err := assertError(tc.gotErr, tc.expErr); err != nil {
|
|
return err
|
|
}
|
|
if len(tc.expDels) != len(tc.current.Delegations) {
|
|
return fmt.Errorf("delegations size not expected %v / %v",
|
|
len(tc.current.Delegations), len(tc.expDels))
|
|
}
|
|
for i, expDel := range tc.expDels {
|
|
if err := expDel.checkDelegation(tc.current.Delegations[i]); err != nil {
|
|
return fmt.Errorf("delegations[%v]: %v", i, err)
|
|
}
|
|
}
|
|
if tc.slashTrack.TotalSlashed.Cmp(tc.expSlashed) != 0 {
|
|
return fmt.Errorf("unexpected total slash %v / %v", tc.slashTrack.TotalSlashed,
|
|
tc.expSlashed)
|
|
}
|
|
if tc.slashTrack.TotalBeneficiaryReward.Cmp(tc.expBeneficiaryReward) != 0 {
|
|
return fmt.Errorf("unexpected beneficiary reward %v / %v", tc.slashTrack.TotalBeneficiaryReward,
|
|
tc.expBeneficiaryReward)
|
|
}
|
|
if bal := tc.state.GetBalance(tc.beneficiary); bal.Cmp(tc.expBeneficiaryReward) != 0 {
|
|
return fmt.Errorf("unexpected balance for beneficiary %v / %v", bal, tc.expBeneficiaryReward)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type expDelegation struct {
|
|
expAmt, expReward *big.Int
|
|
expUndelAmt []*big.Int
|
|
}
|
|
|
|
type testDelegation struct {
|
|
address string
|
|
amount *big.Int
|
|
historyUndel *big.Int
|
|
afterSignUndel *big.Int
|
|
}
|
|
|
|
func (ed expDelegation) checkDelegation(d staking.Delegation) error {
|
|
if d.Amount.Cmp(ed.expAmt) != 0 {
|
|
return fmt.Errorf("unexpected amount %v / %v", d.Amount, ed.expAmt)
|
|
}
|
|
if d.Reward.Cmp(ed.expReward) != 0 {
|
|
return fmt.Errorf("unexpected reward %v / %v", d.Reward, ed.expReward)
|
|
}
|
|
if len(d.Undelegations) != len(ed.expUndelAmt) {
|
|
return fmt.Errorf("unexpected undelegation size %v / %v", len(d.Undelegations),
|
|
len(ed.expUndelAmt))
|
|
}
|
|
for i := range d.Undelegations {
|
|
if ed.expUndelAmt[i].Cmp(d.Undelegations[i].Amount) != 0 {
|
|
return fmt.Errorf("[%v]th undelegation unexpected amount %v / %v", i,
|
|
d.Undelegations[i].Amount, ed.expUndelAmt[i])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestApply(t *testing.T) {
|
|
tests := []applyTestCase{
|
|
{
|
|
// positive test case
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
current: defaultCurrentValidatorWrapper(),
|
|
slashes: Records{defaultSlashRecord()},
|
|
|
|
expSlashed: twentyKOnes,
|
|
expBeneficiaryReward: tenKOnes,
|
|
},
|
|
{
|
|
// missing snapshot in chain
|
|
current: defaultCurrentValidatorWrapper(),
|
|
slashes: Records{defaultSlashRecord()},
|
|
|
|
expErr: errors.New("could not find validator"),
|
|
},
|
|
{
|
|
// missing vWrapper in state
|
|
snapshot: defaultSnapValidatorWrapper(),
|
|
slashes: Records{defaultSlashRecord()},
|
|
|
|
expErr: errValidatorNotFoundDuringSlash,
|
|
},
|
|
}
|
|
for i, test := range tests {
|
|
test.makeData(t)
|
|
|
|
test.apply()
|
|
|
|
if err := test.checkResult(); err != nil {
|
|
t.Errorf("Test %v: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
type applyTestCase struct {
|
|
snapshot, current *staking.ValidatorWrapper
|
|
slashes Records
|
|
|
|
chain *fakeBlockChain
|
|
state, stateSnap *state.DB
|
|
gotErr error
|
|
gotDiff *Application
|
|
|
|
expSlashed *big.Int
|
|
expBeneficiaryReward *big.Int
|
|
expErr error
|
|
}
|
|
|
|
func (tc *applyTestCase) makeData(t *testing.T) {
|
|
tc.chain = defaultFakeBlockChain()
|
|
if tc.snapshot != nil {
|
|
tc.chain.snapshots[tc.snapshot.Address] = *tc.snapshot
|
|
}
|
|
|
|
tc.state = makeTestStateDB()
|
|
if tc.current != nil {
|
|
if err := tc.state.UpdateValidatorWrapper(tc.current.Address, tc.current); err != nil {
|
|
t.Error(err)
|
|
}
|
|
if _, err := tc.state.Commit(true); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
tc.stateSnap = tc.state.Copy()
|
|
}
|
|
|
|
func (tc *applyTestCase) apply() {
|
|
tc.gotDiff, tc.gotErr = Apply(tc.chain, tc.state, tc.slashes, leaderAddr)
|
|
}
|
|
|
|
func (tc *applyTestCase) checkResult() error {
|
|
if err := assertError(tc.gotErr, tc.expErr); err != nil {
|
|
return fmt.Errorf("unexpected error %v / %v", tc.gotErr, tc.expErr)
|
|
}
|
|
if (tc.gotErr != nil) || (tc.expErr != nil) {
|
|
return nil
|
|
}
|
|
if tc.gotDiff.TotalSlashed.Cmp(tc.expSlashed) != 0 {
|
|
return fmt.Errorf("unexpected total slash %v / %v", tc.gotDiff.TotalSlashed,
|
|
tc.expSlashed)
|
|
}
|
|
if tc.gotDiff.TotalBeneficiaryReward.Cmp(tc.expBeneficiaryReward) != 0 {
|
|
return fmt.Errorf("unexpected beneficiary reward %v / %v", tc.gotDiff.TotalBeneficiaryReward,
|
|
tc.expBeneficiaryReward)
|
|
}
|
|
if err := tc.checkState(); err != nil {
|
|
return fmt.Errorf("state check: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkState checks whether the state has been banned and whether the delegations
|
|
// are different from the original
|
|
func (tc *applyTestCase) checkState() error {
|
|
vw, err := tc.state.ValidatorWrapper(offAddr, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !IsBanned(vw) {
|
|
return fmt.Errorf("status not banned")
|
|
}
|
|
|
|
vwSnap, err := tc.stateSnap.ValidatorWrapper(offAddr, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if reflect.DeepEqual(vwSnap.Delegations, vw.Delegations) {
|
|
return fmt.Errorf("status still unchanged")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func defaultSlashRecord() Record {
|
|
return Record{
|
|
Evidence: Evidence{
|
|
ConflictingVotes: ConflictingVotes{
|
|
FirstVote: makeVoteData(offKey, doubleSignBlock1),
|
|
SecondVote: makeVoteData(offKey, doubleSignBlock2),
|
|
},
|
|
Moment: Moment{
|
|
Epoch: big.NewInt(doubleSignEpoch),
|
|
ShardID: doubleSignShardID,
|
|
Height: doubleSignBlockNumber,
|
|
ViewID: doubleSignViewID,
|
|
},
|
|
Offender: offAddr,
|
|
},
|
|
Reporter: reporterAddr,
|
|
}
|
|
}
|
|
|
|
func makeVoteData(kp blsKeyPair, block *types.Block) Vote {
|
|
return Vote{
|
|
SignerPubKeys: []bls.SerializedPublicKey{kp.Pub()},
|
|
BlockHeaderHash: block.Hash(),
|
|
Signature: kp.Sign(block),
|
|
}
|
|
}
|
|
|
|
func makeTestAddress(item interface{}) common.Address {
|
|
s := fmt.Sprintf("wikiwoop.com.%v", item)
|
|
return common.BytesToAddress([]byte(s))
|
|
}
|
|
|
|
func makeBlockForTest(epoch int64, index int) *types.Block {
|
|
h := blockfactory.NewTestHeader()
|
|
|
|
h.SetEpoch(big.NewInt(epoch))
|
|
h.SetNumber(big.NewInt(doubleSignBlockNumber))
|
|
h.SetViewID(big.NewInt(doubleSignViewID))
|
|
h.SetRoot(common.BigToHash(big.NewInt(int64(index))))
|
|
|
|
return types.NewBlockWithHeader(h)
|
|
}
|
|
|
|
func defaultValidatorWrapper() *staking.ValidatorWrapper {
|
|
pubKeys := []bls.SerializedPublicKey{offPub}
|
|
v := defaultTestValidator(pubKeys)
|
|
ds := defaultTestDelegations()
|
|
|
|
return &staking.ValidatorWrapper{
|
|
Validator: v,
|
|
Delegations: ds,
|
|
}
|
|
}
|
|
|
|
func defaultSnapValidatorWrapper() *staking.ValidatorWrapper {
|
|
return defaultValidatorWrapper()
|
|
}
|
|
|
|
func defaultCurrentValidatorWrapper() *staking.ValidatorWrapper {
|
|
pubKeys := []bls.SerializedPublicKey{offPub}
|
|
v := defaultTestValidator(pubKeys)
|
|
ds := defaultDelegationsWithUndelegates()
|
|
|
|
return &staking.ValidatorWrapper{
|
|
Validator: v,
|
|
Delegations: ds,
|
|
}
|
|
}
|
|
|
|
// defaultTestValidator makes a valid Validator kps structure
|
|
func defaultTestValidator(pubKeys []bls.SerializedPublicKey) staking.Validator {
|
|
comm := staking.Commission{
|
|
CommissionRates: staking.CommissionRates{
|
|
Rate: numeric.MustNewDecFromStr("0.167983520183826780"),
|
|
MaxRate: numeric.MustNewDecFromStr("0.179184469782137200"),
|
|
MaxChangeRate: numeric.MustNewDecFromStr("0.152212761523253600"),
|
|
},
|
|
UpdateHeight: big.NewInt(10),
|
|
}
|
|
|
|
desc := staking.Description{
|
|
Name: "someoneA",
|
|
Identity: "someoneB",
|
|
Website: "someoneC",
|
|
SecurityContact: "someoneD",
|
|
Details: "someoneE",
|
|
}
|
|
return staking.Validator{
|
|
Address: offAddr,
|
|
SlotPubKeys: pubKeys,
|
|
LastEpochInCommittee: big.NewInt(lastEpochInComm),
|
|
MinSelfDelegation: new(big.Int).Set(tenKOnes),
|
|
MaxTotalDelegation: new(big.Int).Set(thousandKOnes),
|
|
Status: effective.Active,
|
|
Commission: comm,
|
|
Description: desc,
|
|
CreationHeight: big.NewInt(creationHeight),
|
|
}
|
|
}
|
|
|
|
func generateValidatorWrapper(delData []testDelegation) *staking.ValidatorWrapper {
|
|
pubKeys := []bls.SerializedPublicKey{offPub}
|
|
v := defaultTestValidator(pubKeys)
|
|
ds := generateDelegations(delData)
|
|
|
|
return &staking.ValidatorWrapper{
|
|
Validator: v,
|
|
Delegations: ds,
|
|
}
|
|
}
|
|
|
|
func generateDelegations(delData []testDelegation) staking.Delegations {
|
|
delegations := make(staking.Delegations, len(delData))
|
|
for i, del := range delData {
|
|
delegations[i] = makeDelegation(makeTestAddress(del.address), new(big.Int).Set(del.amount))
|
|
|
|
if del.historyUndel != nil {
|
|
delegations[i].Undelegations = append(
|
|
delegations[i].Undelegations,
|
|
staking.Undelegation{
|
|
Amount: new(big.Int).Set(del.historyUndel),
|
|
Epoch: big.NewInt(doubleSignEpoch - 1),
|
|
},
|
|
)
|
|
}
|
|
if del.afterSignUndel != nil {
|
|
delegations[i].Undelegations = append(
|
|
delegations[i].Undelegations,
|
|
staking.Undelegation{
|
|
Amount: new(big.Int).Set(del.afterSignUndel),
|
|
Epoch: big.NewInt(doubleSignEpoch + 1),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
return delegations
|
|
}
|
|
|
|
func defaultTestDelegations() staking.Delegations {
|
|
return staking.Delegations{
|
|
makeDelegation(offAddr, new(big.Int).Set(fourtyKOnes)),
|
|
makeDelegation(makeTestAddress("del1"), new(big.Int).Set(fourtyKOnes)),
|
|
}
|
|
}
|
|
|
|
func defaultDelegationsWithUndelegates() staking.Delegations {
|
|
d1 := makeDelegation(offAddr, new(big.Int).Set(twentyKOnes))
|
|
d1.Undelegations = []staking.Undelegation{
|
|
makeHistoryUndelegation(),
|
|
makeDefaultUndelegation(),
|
|
}
|
|
d2 := makeDelegation(makeTestAddress("del2"), new(big.Int).Set(fourtyKOnes))
|
|
|
|
return staking.Delegations{d1, d2}
|
|
}
|
|
|
|
func makeDelegation(addr common.Address, amount *big.Int) staking.Delegation {
|
|
return staking.Delegation{
|
|
DelegatorAddress: addr,
|
|
Amount: amount,
|
|
Reward: new(big.Int).Set(tenKOnes),
|
|
}
|
|
}
|
|
|
|
func makeDefaultUndelegation() staking.Undelegation {
|
|
return staking.Undelegation{
|
|
Amount: new(big.Int).Set(tenKOnes),
|
|
Epoch: big.NewInt(doubleSignEpoch + 2),
|
|
}
|
|
}
|
|
|
|
func makeHistoryUndelegation() staking.Undelegation {
|
|
return staking.Undelegation{
|
|
Amount: new(big.Int).Set(tenKOnes),
|
|
Epoch: big.NewInt(doubleSignEpoch - 1),
|
|
}
|
|
}
|
|
|
|
// makeCommitteeFromKeyPairs makes a shard state for testing.
|
|
// address is generated by makeTestAddress
|
|
// bls key is get from the variable keyPairs []blsKeyPair
|
|
func makeDefaultCommittee() shard.State {
|
|
epoch := big.NewInt(doubleSignEpoch)
|
|
maker := newShardSlotMaker(keyPairs)
|
|
return makeCommitteeBySlotMaker(epoch, maker)
|
|
}
|
|
|
|
func makeCommitteeBySlotMaker(epoch *big.Int, maker shardSlotMaker) shard.State {
|
|
sstate := shard.State{
|
|
Epoch: epoch,
|
|
Shards: make([]shard.Committee, 0, int(numShard)),
|
|
}
|
|
for sid := uint32(0); sid != numNodePerShard; sid++ {
|
|
sstate.Shards = append(sstate.Shards, makeShardBySlotMaker(sid, maker))
|
|
}
|
|
return sstate
|
|
}
|
|
|
|
func makeShardBySlotMaker(shardID uint32, maker shardSlotMaker) shard.Committee {
|
|
cmt := shard.Committee{
|
|
ShardID: shardID,
|
|
Slots: make(shard.SlotList, 0, numNodePerShard),
|
|
}
|
|
for nid := 0; nid != numNodePerShard; nid++ {
|
|
cmt.Slots = append(cmt.Slots, maker.makeSlot())
|
|
}
|
|
return cmt
|
|
}
|
|
|
|
type shardSlotMaker struct {
|
|
kps []blsKeyPair
|
|
i int
|
|
}
|
|
|
|
func newShardSlotMaker(kps []blsKeyPair) shardSlotMaker {
|
|
return shardSlotMaker{kps, 0}
|
|
}
|
|
|
|
func (maker *shardSlotMaker) makeSlot() shard.Slot {
|
|
s := shard.Slot{
|
|
EcdsaAddress: makeTestAddress(maker.i),
|
|
BLSPublicKey: maker.kps[maker.i].Pub(), // Yes, will panic when not enough kps
|
|
}
|
|
maker.i++
|
|
return s
|
|
}
|
|
|
|
type blsKeyPair struct {
|
|
pri *bls_core.SecretKey
|
|
pub *bls_core.PublicKey
|
|
}
|
|
|
|
func genKeyPairs(size int) []blsKeyPair {
|
|
kps := make([]blsKeyPair, 0, size)
|
|
for i := 0; i != size; i++ {
|
|
kps = append(kps, genKeyPair())
|
|
}
|
|
return kps
|
|
}
|
|
|
|
func genKeyPair() blsKeyPair {
|
|
pri := bls.RandPrivateKey()
|
|
pub := pri.GetPublicKey()
|
|
return blsKeyPair{
|
|
pri: pri,
|
|
pub: pub,
|
|
}
|
|
}
|
|
|
|
func (kp blsKeyPair) Pub() bls.SerializedPublicKey {
|
|
var pub bls.SerializedPublicKey
|
|
copy(pub[:], kp.pub.Serialize())
|
|
return pub
|
|
}
|
|
|
|
func (kp blsKeyPair) Sign(block *types.Block) []byte {
|
|
msg := consensus_sig.ConstructCommitPayload(params.LocalnetChainConfig, block.Epoch(), block.Hash(),
|
|
block.Number().Uint64(), block.Header().ViewID().Uint64())
|
|
|
|
sig := kp.pri.SignHash(msg)
|
|
|
|
return sig.Serialize()
|
|
}
|
|
|
|
func defaultTestStateDB() *state.DB {
|
|
sdb := makeTestStateDB()
|
|
err := sdb.UpdateValidatorWrapper(offAddr, defaultCurrentValidatorWrapper())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return sdb
|
|
}
|
|
|
|
func makeTestStateDB() *state.DB {
|
|
db := state.NewDatabase(rawdb.NewMemoryDatabase())
|
|
sdb, err := state.New(common.Hash{}, db, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return sdb
|
|
}
|
|
|
|
func defaultFakeBlockChain() *fakeBlockChain {
|
|
return &fakeBlockChain{
|
|
config: *params.LocalnetChainConfig,
|
|
currentBlock: *makeBlockForTest(currentEpoch, 0),
|
|
superCommittee: makeDefaultCommittee(),
|
|
snapshots: make(map[common.Address]staking.ValidatorWrapper),
|
|
}
|
|
}
|
|
|
|
func assertError(got, exp error) error {
|
|
if (got == nil) != (exp == nil) {
|
|
return fmt.Errorf("unexpected error [%v] / [%v]", got, exp)
|
|
}
|
|
if got == nil {
|
|
return nil
|
|
}
|
|
if !strings.Contains(got.Error(), exp.Error()) {
|
|
return fmt.Errorf("unexpected error [%v] / [%v]", got, exp)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// copyRecord makes a deep copy the slash Record
|
|
func copyRecord(r Record) Record {
|
|
return Record{
|
|
Evidence: copyEvidence(r.Evidence),
|
|
Reporter: r.Reporter,
|
|
}
|
|
}
|
|
|
|
// copyEvidence makes a deep copy the slash evidence
|
|
func copyEvidence(e Evidence) Evidence {
|
|
return Evidence{
|
|
Moment: copyMoment(e.Moment),
|
|
ConflictingVotes: copyConflictingVotes(e.ConflictingVotes),
|
|
Offender: e.Offender,
|
|
}
|
|
}
|
|
|
|
// copyMoment makes a deep copy of the Moment structure
|
|
func copyMoment(m Moment) Moment {
|
|
cp := Moment{
|
|
ShardID: m.ShardID,
|
|
Height: m.Height,
|
|
ViewID: m.ViewID,
|
|
}
|
|
if m.Epoch != nil {
|
|
cp.Epoch = new(big.Int).Set(m.Epoch)
|
|
}
|
|
return cp
|
|
}
|
|
|
|
// copyConflictingVotes makes a deep copy of slash.ConflictingVotes
|
|
func copyConflictingVotes(cv ConflictingVotes) ConflictingVotes {
|
|
return ConflictingVotes{
|
|
FirstVote: copyVote(cv.FirstVote),
|
|
SecondVote: copyVote(cv.SecondVote),
|
|
}
|
|
}
|
|
|
|
// copyVote makes a deep copy of slash.Vote
|
|
func copyVote(v Vote) Vote {
|
|
cp := Vote{
|
|
SignerPubKeys: v.SignerPubKeys,
|
|
BlockHeaderHash: v.BlockHeaderHash,
|
|
}
|
|
if v.Signature != nil {
|
|
cp.Signature = make([]byte, len(v.Signature))
|
|
copy(cp.Signature, v.Signature)
|
|
}
|
|
return cp
|
|
}
|
|
|