Skip to content

Commit ab92259

Browse files
authored
Merge pull request #163 from livepeer/yf/slashing-fixes
Slashing fixes with accompanying tests
2 parents 516eab8 + 6e083bc commit ab92259

10 files changed

+743
-26
lines changed

contracts/bonding/BondingManager.sol

+27-15
Original file line numberDiff line numberDiff line change
@@ -457,44 +457,48 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
457457
whenSystemNotPaused
458458
onlyJobsManager
459459
{
460-
// Transcoder must be valid
461-
require(transcoderStatus(_transcoder) == TranscoderStatus.Registered);
462-
463460
uint256 penalty = MathUtils.percOf(delegators[_transcoder].bondedAmount, _slashAmount);
464461
if (penalty > del.bondedAmount) {
465462
penalty = del.bondedAmount;
466463
}
467464

468465
Delegator storage del = delegators[_transcoder];
469466

470-
// Decrease delegate's delegated amount
471-
delegators[del.delegateAddress].delegatedAmount = delegators[del.delegateAddress].delegatedAmount.sub(penalty);
472-
// Update total bonded tokens
473-
totalBonded = totalBonded.sub(penalty);
474-
// Decrease transcoder's stake
467+
// Decrease bonded stake
475468
del.bondedAmount = del.bondedAmount.sub(penalty);
476469

470+
// If still bonded
471+
// - Decrease delegate's delegated amount
472+
// - Decrease total bonded tokens
473+
if (delegatorStatus(_transcoder) == DelegatorStatus.Bonded) {
474+
delegators[del.delegateAddress].delegatedAmount = delegators[del.delegateAddress].delegatedAmount.sub(penalty);
475+
totalBonded = totalBonded.sub(penalty);
476+
}
477+
477478
uint256 currentRound = roundsManager().currentRound();
478479

479480
if (activeTranscoderSet[currentRound].isActive[_transcoder]) {
480-
// Set transcoder as inactive
481-
activeTranscoderSet[currentRound].isActive[_transcoder] = false;
482481
// Decrease total active stake for the round
483482
activeTranscoderSet[currentRound].totalStake = activeTranscoderSet[currentRound].totalStake.sub(activeTranscoderTotalStake(_transcoder, currentRound));
483+
// Set transcoder as inactive
484+
activeTranscoderSet[currentRound].isActive[_transcoder] = false;
484485
}
485486

486-
// Remove transcoder from pools
487-
transcoderPool.remove(_transcoder);
487+
// If registered transcoder, remove from pool
488+
if (transcoderStatus(_transcoder) == TranscoderStatus.Registered) {
489+
transcoderPool.remove(_transcoder);
490+
}
488491

489-
// Award finder fee
490-
if (penalty > 0 && _finder != address(0)) {
492+
// Account for penalty
493+
if (penalty > 0) {
491494
uint256 burnAmount = penalty;
492495

496+
// Award finder fee if there is a finder address
493497
if (_finder != address(0)) {
494-
// Award finder fee
495498
uint256 finderAmount = MathUtils.percOf(penalty, _finderFee);
496499
minter().transferTokens(_finder, finderAmount);
497500

501+
// Subtract finder fee from the amount to be burned
498502
burnAmount = burnAmount.sub(finderAmount);
499503
}
500504

@@ -792,6 +796,14 @@ contract BondingManager is ManagerProxyTarget, IBondingManager {
792796
return activeTranscoderSet[_round].isActive[_transcoder];
793797
}
794798

799+
/*
800+
* @dev Return whether a transcoder is registered
801+
* @param _transcoder Transcoder address
802+
*/
803+
function isRegisteredTranscoder(address _transcoder) public view returns (bool) {
804+
return transcoderStatus(_transcoder) == TranscoderStatus.Registered;
805+
}
806+
795807
/*
796808
* @dev Remove transcoder
797809
*/

contracts/bonding/IBondingManager.sol

+1
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ contract IBondingManager {
2525
// Public functions
2626
function transcoderTotalStake(address _transcoder) public view returns (uint256);
2727
function activeTranscoderTotalStake(address _transcoder, uint256 _round) public view returns (uint256);
28+
function isRegisteredTranscoder(address _transcoder) public view returns (bool);
2829
function getTotalBonded() public view returns (uint256);
2930
}

contracts/jobs/JobsManager.sol

+4-2
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ contract JobsManager is ManagerProxyTarget, IVerifiable, IJobsManager {
319319
require(jobStatus(_jobId) != JobStatus.Inactive);
320320
// Segment range must be valid
321321
require(_segmentRange[1] >= _segmentRange[0]);
322+
// Caller must be registered transcoder
323+
require(bondingManager().isRegisteredTranscoder(msg.sender));
322324

323325
uint256 blockNum = roundsManager().blockNum();
324326

@@ -488,9 +490,9 @@ contract JobsManager is ManagerProxyTarget, IVerifiable, IJobsManager {
488490
// Protocol slashes transcoder for failing verification (no finder)
489491
bondingManager().slashTranscoder(transcoder, address(0), failedVerificationSlashAmount, 0);
490492

491-
PassedVerification(transcoder, _jobId, _claimId, _segmentNumber);
492-
} else {
493493
FailedVerification(transcoder, _jobId, _claimId, _segmentNumber);
494+
} else {
495+
PassedVerification(transcoder, _jobId, _claimId, _segmentNumber);
494496
}
495497
}
496498

contracts/rounds/AdjustableRoundsManager.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ contract AdjustableRoundsManager is RoundsManager {
2626
}
2727

2828
function blockHash(uint256 _block) public view returns (bytes32) {
29-
require(_block <= blockNum() - 256);
29+
require(_block >= blockNum() - 256);
3030

3131
return hash;
3232
}

contracts/test/BondingManagerMock.sol

+4
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,8 @@ contract BondingManagerMock is IBondingManager {
7373
function activeTranscoderTotalStake(address _transcoder, uint256 _round) public view returns (uint256) {
7474
return activeStake;
7575
}
76+
77+
function isRegisteredTranscoder(address _transcoder) public view returns (bool) {
78+
return true;
79+
}
7680
}

migrations/3_deploy_contracts.js

+1-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const BondingManager = artifacts.require("BondingManager")
77
const JobsManager = artifacts.require("JobsManager")
88
const RoundsManager = artifacts.require("RoundsManager")
99
const AdjustableRoundsManager = artifacts.require("AdjustableRoundsManager")
10-
const IdentityVerifier = artifacts.require("IdentityVerifier")
1110
const LivepeerVerifier = artifacts.require("LivepeerVerifier")
1211
const LivepeerToken = artifacts.require("LivepeerToken")
1312
const LivepeerTokenFaucet = artifacts.require("LivepeerTokenFaucet")
@@ -20,13 +19,7 @@ module.exports = function(deployer, network) {
2019
const controller = await lpDeployer.deployController()
2120
const token = await lpDeployer.deployAndRegister(LivepeerToken, "LivepeerToken")
2221
await lpDeployer.deployAndRegister(Minter, "Minter", controller.address, config.minter.inflation, config.minter.inflationChange, config.minter.targetBondingRate)
23-
24-
if (network === "development" || network === "testrpc" || network === "parityDev" || network === "gethDev") {
25-
await lpDeployer.deployAndRegister(IdentityVerifier, "Verifier", controller.address)
26-
} else {
27-
await lpDeployer.deployAndRegister(LivepeerVerifier, "Verifier", controller.address, config.verifier.solvers, config.verifier.verificationCodeHash)
28-
}
29-
22+
await lpDeployer.deployAndRegister(LivepeerVerifier, "Verifier", controller.address, config.verifier.solvers, config.verifier.verificationCodeHash)
3023
await lpDeployer.deployAndRegister(LivepeerTokenFaucet, "LivepeerTokenFaucet", token.address, config.faucet.requestAmount, config.faucet.requestWait)
3124

3225
const bondingManager = await lpDeployer.deployProxyAndRegister(BondingManager, "BondingManager", controller.address)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import {contractId} from "../../utils/helpers"
2+
import batchTranscodeReceiptHashes from "../../utils/batchTranscodeReceipts"
3+
import MerkleTree from "../../utils/merkleTree"
4+
import {createTranscodingOptions} from "../../utils/videoProfile"
5+
import Segment from "../../utils/segment"
6+
7+
const Controller = artifacts.require("Controller")
8+
const BondingManager = artifacts.require("BondingManager")
9+
const JobsManager = artifacts.require("JobsManager")
10+
const AdjustableRoundsManager = artifacts.require("AdjustableRoundsManager")
11+
const LivepeerToken = artifacts.require("LivepeerToken")
12+
const LivepeerTokenFaucet = artifacts.require("LivepeerTokenFaucet")
13+
14+
contract("DoubleClaimSegmentSlashing", accounts => {
15+
let controller
16+
let bondingManager
17+
let roundsManager
18+
let jobsManager
19+
let token
20+
21+
let transcoder
22+
let delegator1
23+
let delegator2
24+
let watcher
25+
let broadcaster
26+
27+
let roundLength
28+
29+
before(async () => {
30+
transcoder = accounts[0]
31+
delegator1 = accounts[1]
32+
delegator2 = accounts[2]
33+
watcher = accounts[3]
34+
broadcaster = accounts[3]
35+
36+
controller = await Controller.deployed()
37+
38+
const bondingManagerAddr = await controller.getContract(contractId("BondingManager"))
39+
bondingManager = await BondingManager.at(bondingManagerAddr)
40+
41+
const roundsManagerAddr = await controller.getContract(contractId("RoundsManager"))
42+
roundsManager = await AdjustableRoundsManager.at(roundsManagerAddr)
43+
44+
const jobsManagerAddr = await controller.getContract(contractId("JobsManager"))
45+
jobsManager = await JobsManager.at(jobsManagerAddr)
46+
47+
// Set verification rate to 1 out of 1 segments, so every segment is challenged
48+
await jobsManager.setVerificationRate(1)
49+
// Set double claim segment slash amount to 20%
50+
await jobsManager.setDoubleClaimSegmentSlashAmount(200000)
51+
52+
const tokenAddr = await controller.getContract(contractId("LivepeerToken"))
53+
token = await LivepeerToken.at(tokenAddr)
54+
55+
const faucetAddr = await controller.getContract(contractId("LivepeerTokenFaucet"))
56+
const faucet = await LivepeerTokenFaucet.at(faucetAddr)
57+
58+
await faucet.request({from: transcoder})
59+
await faucet.request({from: delegator1})
60+
await faucet.request({from: delegator2})
61+
62+
roundLength = await roundsManager.roundLength.call()
63+
await roundsManager.mineBlocks(roundLength.toNumber() * 1000)
64+
await roundsManager.initializeRound()
65+
66+
await token.approve(bondingManager.address, 1000, {from: transcoder})
67+
await bondingManager.bond(1000, transcoder, {from: transcoder})
68+
await bondingManager.transcoder(10, 5, 1, {from: transcoder})
69+
70+
await token.approve(bondingManager.address, 1000, {from: delegator1})
71+
await bondingManager.bond(1000, transcoder, {from: delegator1})
72+
73+
await token.approve(bondingManager.address, 1000, {from: delegator2})
74+
await bondingManager.bond(1000, transcoder, {from: delegator2})
75+
76+
// Fast forward to new round with locked in active transcoder set
77+
await roundsManager.mineBlocks(roundLength.toNumber())
78+
await roundsManager.initializeRound()
79+
})
80+
81+
it("watcher should slash a transcoder for double claiming segments", async () => {
82+
await jobsManager.deposit({from: broadcaster, value: 1000})
83+
84+
const endBlock = (await roundsManager.blockNum()).add(100)
85+
await jobsManager.job("foo", createTranscodingOptions(["foo", "bar"]), 1, endBlock, {from: broadcaster})
86+
87+
let rand = web3.eth.getBlock(web3.eth.blockNumber).hash
88+
await roundsManager.mineBlocks(1)
89+
await roundsManager.setBlockHash(rand)
90+
91+
// Segment data hashes
92+
const dataHashes = [
93+
"0x80084bf2fba02475726feb2cab2d8215eab14bc6bdd8bfb2c8151257032ecd8b",
94+
"0xb039179a8a4ce2c252aa6f2f25798251c19b75fc1508d9d511a191e0487d64a7",
95+
"0x263ab762270d3b73d3e2cddf9acc893bb6bd41110347e5d5e4bd1d3c128ea90a",
96+
"0x4ce8765e720c576f6f5a34ca380b3de5f0912e6e3cc5355542c363891e54594b"
97+
]
98+
99+
// Segments
100+
const segments = dataHashes.map((dataHash, idx) => new Segment("foo", idx, dataHash, broadcaster))
101+
102+
// Transcoded data hashes
103+
const tDataHashes = [
104+
"0x42538602949f370aa331d2c07a1ee7ff26caac9cc676288f94b82eb2188b8465",
105+
"0xa0b37b8bfae8e71330bd8e278e4a45ca916d00475dd8b85e9352533454c9fec8",
106+
"0x9f2898da52dedaca29f05bcac0c8e43e4b9f7cb5707c14cc3f35a567232cec7c",
107+
"0x5a082c81a7e4d5833ee20bd67d2f4d736f679da33e4bebd3838217cb27bec1d3"
108+
]
109+
110+
// Transcode receipts
111+
const tReceiptHashes = batchTranscodeReceiptHashes(segments, tDataHashes)
112+
113+
// Build merkle tree
114+
const merkleTree = new MerkleTree(tReceiptHashes)
115+
116+
const tokenStartSupply = await token.totalSupply.call()
117+
118+
// Transcoder claims segments 0 through 3
119+
await jobsManager.claimWork(0, [0, 3], merkleTree.getHexRoot(), {from: transcoder})
120+
// Transcoder claims segments 0 through 3 again
121+
await jobsManager.claimWork(0, [0, 3], merkleTree.getHexRoot(), {from: transcoder})
122+
// Wait for claims to be mined
123+
await roundsManager.mineBlocks(2)
124+
125+
// Watcher slashes transcoder for double claiming segments
126+
// Transcoder claimed segments 0 through 3 twice
127+
await jobsManager.doubleClaimSegmentSlash(0, 0, 1, 0, {from: watcher})
128+
129+
// Check that the transcoder is penalized
130+
const currentRound = await roundsManager.currentRound()
131+
const doubleClaimSegmentSlashAmount = await jobsManager.doubleClaimSegmentSlashAmount.call()
132+
const penalty = Math.floor((1000 * doubleClaimSegmentSlashAmount.toNumber()) / 1000000)
133+
const expTransStakeRemaining = 1000 - penalty
134+
const expDelegatedStakeRemaining = (1000 + 1000 + 1000) - penalty
135+
const expTotalBondedRemaining = expDelegatedStakeRemaining
136+
const tokenEndSupply = await token.totalSupply.call()
137+
const finderFeeAmount = await jobsManager.finderFee.call()
138+
const finderFee = Math.floor((penalty * finderFeeAmount) / 1000000)
139+
const burned = tokenStartSupply.sub(tokenEndSupply).toNumber()
140+
const trans = await bondingManager.getDelegator(transcoder)
141+
142+
assert.isNotOk(await bondingManager.isActiveTranscoder(transcoder, currentRound), "transcoder should be inactive")
143+
assert.equal(await bondingManager.transcoderStatus(transcoder), 0, "transcoder should not be registered")
144+
assert.equal(trans[0], expTransStakeRemaining, "wrong transcoder stake remaining")
145+
assert.equal(burned, penalty - finderFee, "wrong amount burned")
146+
147+
// Check that the finder was rewarded
148+
assert.equal(await token.balanceOf(watcher), finderFee, "wrong finder fee")
149+
150+
// Check that the broadcaster was refunded
151+
assert.equal((await jobsManager.getJob(0))[8], 0, "job escrow should be 0")
152+
assert.equal((await jobsManager.broadcasters.call(broadcaster))[0], 1000)
153+
154+
// Check that the total stake for the round is updated
155+
// activeTranscoderSet.call(round) only returns the active stake and not the array of transcoder addresses
156+
// because Solidity does not return nested arrays in structs
157+
assert.equal(await bondingManager.activeTranscoderSet.call(currentRound), 0, "wrong active stake remaining")
158+
159+
// Check that the total tokens bonded is updated
160+
assert.equal(await bondingManager.getTotalBonded(), expTotalBondedRemaining, "wrong total bonded amount")
161+
})
162+
})

0 commit comments

Comments
 (0)