Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions contracts/src/interface/slasher.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const SLASHER_AGGREGATE_STATS_OP = 100;


const SLASHER_INVALID_DATA_ERROR: uint32 = 100;
const SLASHER_INVALID_SIGNATURE_ERROR: uint32 = 101;
const SLASHER_UNKNOWN_VALIDATOR_ERROR: uint32 = 102;


const SLASHER_AGGREGATED_VALIDATORS_TAG = 100;
4 changes: 4 additions & 0 deletions contracts/src/lib/util.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
fun maxInt(): int
asm "256 PUSHPOW2DEC"

@pure
fun getPreviousMcBlocks(): tuple
asm "PREVMCBLOCKS"

@inline_ref
fun cell?.hashEq(self, expectedVsetHash: int): bool {
if (self == null) {
Expand Down
150 changes: 150 additions & 0 deletions contracts/src/slasher-base.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import "@stdlib/common"
import "@stdlib/lisp-lists"
import "@stdlib/tvm-dicts"
import "interface/slasher"
import "lib/config-params"
import "lib/util"

tolk 1.0

const BLOCKS_TO_TRACK : uint8 = 100

struct Slasher {
votes: dict
punishedValidators: Cell<PunishedValidators>?
}

struct PunishedValidators {
blockSeqno: uint32
validators: dict
}

struct SignedValidatorInfo {
validatorId: uint16
startBlockSeqno: uint64
validationMask: slice
validatorSignaturesInfo: dict
}

fun Slasher.load(): Slasher {
return Slasher.fromCell(contract.getData());
}

fun Slasher.save(self) {
contract.setData(self.toCell());
}

fun Slasher.getCurrentMcBLock(): uint32 {
val prevMcBlocks = getPreviousMcBlocks();
val prevBlocks = prevMcBlocks.first() as tuple;
val seqno = prevBlocks.get<uint32>(3);
return seqno;
}

fun Slasher.processValidatorStatistics(mutate self, body: slice): int {
val validatorId = body.loadUint(16);
val startBlockSeqno = body.loadUint(64);
val validationMask = body.loadBits(BLOCKS_TO_TRACK);
var validatorSignaturesInfo = body.loadDict();

val currentBlockSeqno = Slasher.getCurrentMcBLock();

if (startBlockSeqno + BLOCKS_TO_TRACK > currentBlockSeqno) {
return SLASHER_INVALID_DATA_ERROR;
}

if (body.remainingBitsCount() != 512 || body.remainingRefsCount() != 0) {
// Invalid signature slice.
return SLASHER_INVALID_SIGNATURE_ERROR;
}

val signature = body.loadBits(512);
body.assertEnd();

val signedData = SignedValidatorInfo {
validatorId,
startBlockSeqno,
validationMask,
validatorSignaturesInfo,
}.toCell();

var validatorCs = CurrentVset.getValidatorDescription(validatorId);
if (validatorCs == null) {
return SLASHER_UNKNOWN_VALIDATOR_ERROR;
}

val description = ValidatorDescr.readFromSlice(mutate validatorCs!);

if (!isSignatureValid(signedData.hash(), signature, description.pubkey)) {
return SLASHER_INVALID_SIGNATURE_ERROR;
}

var iterNext = maxInt();
do {
var (id, cs, found) = validatorSignaturesInfo.uDictGetPrev(16, iterNext);
var weight: uint16 = 0;
if (found) {
iterNext = id!;
var valMask = validationMask;
var data = cs!.loadRef().beginParse();
repeat (BLOCKS_TO_TRACK) {
val validated = valMask.loadBool();
if (validated) {
val gotValidSignature = data.loadBool();
val gotInvalidSignature = data.loadBool();
if (gotInvalidSignature || (!gotInvalidSignature && !gotValidSignature)) {
weight += 1;
}
}
}

var (votes, found) = self.votes.uDictGet(16, validatorId);
var total = 0;
if (found) {
val prevVotes = votes!.loadUint(32);
total = prevVotes + weight;
} else {
total = weight;
}

val slice = beginCell().storeInt(weight, 32).endCell().beginParse();
self.votes.uDictSet(16, validatorId, slice);
}
} while (found);

self.save();
return 0; //TODO: code
}

fun Slasher.runVoteAggregation(mutate self): cell {
var iterNext = maxInt();

var punishedValidators = createEmptyDict();
var punishedValidatorsList = createEmptyList();

do {
var (validatorId, totalVotesCs, found) = self.votes.uDictGetPrev(16, iterNext);
if (found) {
iterNext = validatorId!;
var score = totalVotesCs!.loadUint(32);
if (score > BLOCKS_TO_TRACK / 2) {
punishedValidators.uDictSet(16, validatorId!, createEmptySlice()); //todo: fix?
listPrepend(validatorId!, punishedValidatorsList);
}
}
} while (found);

val currentBlockSeqno = Slasher.getCurrentMcBLock();

val pv = PunishedValidators { blockSeqno: currentBlockSeqno, validators: punishedValidators };

self.punishedValidators = pv.toCell();
self.save();

return self.punishedValidators;
}

fun Slasher.clearVotes(mutate self) {
self.votes = null;
self.save();
}
96 changes: 96 additions & 0 deletions contracts/src/slasher.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import "@stdlib/common"
import "@stdlib/gas-payments"
import "@stdlib/lisp-lists"
import "@stdlib/tvm-dicts"
import "slasher-base"
import "interface/slasher"
import "lib/util"

fun onInternalMessage(in: InMessage) {
var body = in.body;

val (wc, sender) = in.senderAddress.getWorkchainAndHash();
if (wc != MASTERCHAIN || body.isEmpty()) {
// Only masterchain addresses can execute any slasher logic.
return;
}

val elector = blockchain.configParam(1)!.beginParse().preloadUint(256);
if (sender != elector) {
return; // todo: maybe some other logic?
}

val op = body.loadUint(32);
val queryId = body.loadUint(32);
body.assertEnd();

match (op) {
SLASHER_AGGREGATE_STATS_OP => {
var slasher = lazy Slasher.load();
val validatorsToPunish = slasher.runVoteAggregation();
slasher.clearVotes();
sendMessageBack(
in.senderAddress,
SLASHER_AGGREGATED_VALIDATORS_TAG,
queryId,
validatorsToPunish.beginParse()
);
}
}
}

fun onExternalMessage(inMsg: slice) {
acceptExternalMessage();
var storage = lazy Slasher.load();
storage.processValidatorStatistics(inMsg);
}

/// Sends message back to the query source.
///
/// Attaches all remaining message value.
fun sendMessageBack(dest: address, answerTag: uint32, queryId: uint64, body: slice) {
val message = createMessage({
bounce: false,
value: 0,
dest,
body: (answerTag, queryId, body),
});
message.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}

/// GETTERS
get fun getPunishedValidators(): tuple {
val data = lazy Slasher.load();
if (data.punishedValidators == null) {
return createEmptyList();
}

var punishedValidators = createEmptyList();
val punished = data.punishedValidators!.load();

var iterNext = maxInt();
do {
val (validatorId, votes, found) = data.votes.uDictGetPrev(16, iterNext);
if (found) {
iterNext = validatorId!;
punishedValidators = listPrepend(validatorId!, punishedValidators);
}
} while (found);

return [punished.blockSeqno, punishedValidators] as tuple;
}

get fun getCurrentVotes(): tuple {
val data = lazy Slasher.load();

var votersList = createEmptyList();
var iterNext = maxInt();
do {
var (validatorId, votes, found) = data.votes.uDictGetPrev(16, iterNext);
if (found) {
iterNext = validatorId!;
votersList = listPrepend([validatorId!, votes!.loadUint(32)], votersList);
}
} while (found);
return votersList;
}
Loading
Loading