public override ActionResult QuitElection(Empty input) { var candidates = State.CandidatesField.Value; Assert(candidates != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidField, nameof(State.CandidatesField))); var publicKey = Context.RecoverPublicKey().ToHex(); Assert(candidates != null && candidates.PublicKeys.Contains(publicKey), ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Not announced.")); Assert(candidates != null && candidates.RemoveCandidate(Context.RecoverPublicKey()), ContractErrorCode.GetErrorMessage(ContractErrorCode.AttemptFailed, "Failed to remove this public key from candidates list.")); State.CandidatesField.Value = candidates; State.TokenContract.Unlock.Send(new UnlockInput { From = Context.Sender, To = Context.Self, Symbol = Context.Variables.NativeSymbol, LockId = State.HistoryMap[publicKey.ToStringValue()].AnnouncementTransactionId, Amount = State.LockTokenForElection.Value, Usage = "Unlock and quit election." }); return(new ActionResult { Success = true }); }
public override Empty InitialDPoSContract(InitialDPoSContractInput input) { Assert(!State.Initialized.Value, ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Already initialized.")); State.TokenContractSystemName.Value = input.TokenContractSystemName; State.DividendContractSystemName.Value = input.DividendsContractSystemName; State.LockTokenForElection.Value = input.LockTokenForElection; State.Initialized.Value = true; return(new Empty()); }
/// <summary> /// Initial miners can set blockchain age manually. /// For testing. /// </summary> /// <param name="input"></param> /// <returns></returns> public override Empty SetBlockchainAge(SInt64Value input) { Assert(TryToGetRoundInformation(1, out var firstRound), ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Failed to get first round information")); Assert(firstRound.RealTimeMinersInformation.Keys.Contains(Context.RecoverPublicKey().ToHex()), ContractErrorCode.GetErrorMessage(ContractErrorCode.NoPermission, "No permission to change blockchain age.")); // Don't use `UpdateBlockchainAge` here. Because in testing, we can set blockchain age for free. State.AgeField.Value = input.Value; LogVerbose($"{Context.RecoverPublicKey().ToHex()} set blockchain age to {input.Value}"); return(new Empty()); }
public override Empty InitialConsensus(Round firstRound) { Assert(firstRound.RoundNumber == 1, ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidField, "Invalid round number.")); Assert(firstRound.RealTimeMinersInformation.Any(), ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidField, "No miner in input data.")); InitialSettings(firstRound); Assert(TryToAddRoundInformation(firstRound), ContractErrorCode.GetErrorMessage(ContractErrorCode.AttemptFailed, "Failed to add round information.")); State.BasicContractZero.Value = Context.GetZeroSmartContractAddress(); return(new Empty()); }
public override Empty ConfigStrategy(DPoSStrategyInput input) { Assert(!State.IsStrategyConfigured.Value, ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Already configured.")); State.IsBlockchainAgeSettable.Value = input.IsBlockchainAgeSettable; State.IsTimeSlotSkippable.Value = input.IsTimeSlotSkippable; State.IsVerbose.Value = input.IsVerbose; State.IsStrategyConfigured.Value = true; LogVerbose("Consensus log level: Verbose"); LogVerbose($"Is blockchain age settable: {input.IsBlockchainAgeSettable}"); LogVerbose($"Is time slot skippable: {input.IsTimeSlotSkippable}"); return(new Empty()); }
// ReSharper disable once PossibleNullReferenceException public override ActionResult ReceiveDividendsByTransactionId(Hash input) { var txId = input; var votingRecord = State.VotingRecordsMap[txId]; Assert(votingRecord != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Voting record not found.")); Assert(votingRecord.From == Context.RecoverPublicKey().ToHex(), ContractErrorCode.GetErrorMessage(ContractErrorCode.NoPermission, "No permission to receive.")); State.DividendContract.TransferDividends.Send( Dividend.VotingRecord.Parser.ParseFrom(votingRecord.ToByteString())); return(new ActionResult { Success = true }); }
// ReSharper disable once PossibleNullReferenceException public override ActionResult ReceiveAllDividends(Empty input) { var tickets = State.TicketsMap[Context.RecoverPublicKey().ToHex().ToStringValue()]; Assert(tickets != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Tickets information not found.")); Assert(tickets.VoteToTransactions.Any(), ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Voting records not found.")); foreach (var transactionId in tickets.VoteToTransactions) { var votingRecord = State.VotingRecordsMap[transactionId]; Assert(votingRecord != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Voting record not found.")); State.DividendContract.TransferDividends.Send( Dividend.VotingRecord.Parser.ParseFrom(votingRecord.ToByteString())); } return(new ActionResult { Success = true }); }
// ReSharper disable PossibleNullReferenceException public override Tickets WithdrawAll(Empty input) { var voterPublicKey = Context.RecoverPublicKey().ToHex(); var ticketsCount = State.TicketsCountField.Value; var withdrawnAmount = 0L; var candidatesVotesDict = new Dictionary <string, long>(); var tickets = State.TicketsMap[voterPublicKey.ToStringValue()]; Assert(tickets != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Tickets information not found.")); foreach (var transactionId in tickets.VoteToTransactions) { var votingRecord = State.VotingRecordsMap[transactionId]; Assert(votingRecord != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Voting record not found.")); if (votingRecord.UnlockAge > CurrentAge) { // Just check next one, no need to assert. continue; } // Update voting record map. TryToGetBlockchainStartTimestamp(out var blockchainStartTimestamp); votingRecord.WithdrawTimestamp = blockchainStartTimestamp.ToDateTime().AddMinutes(CurrentAge).ToTimestamp(); votingRecord.IsWithdrawn = true; State.VotingRecordsMap[votingRecord.TransactionId] = votingRecord; // Prepare data for updating tickets map. withdrawnAmount += votingRecord.Count; if (candidatesVotesDict.ContainsKey(votingRecord.To)) { candidatesVotesDict[votingRecord.To] += votingRecord.Count; } else { candidatesVotesDict.Add(votingRecord.To, votingRecord.Count); } State.TokenContract.Unlock.Send(new UnlockInput { From = Context.Sender, To = Context.Self, Symbol = Context.Variables.NativeSymbol, Amount = votingRecord.Count, LockId = votingRecord.TransactionId, Usage = $"Withdraw locked token of transaction {transactionId}: {votingRecord}" }); State.DividendContract.SubWeights.Send( new Dividend.WeightsInfo() { TermNumber = State.CurrentTermNumberField.Value, Weights = votingRecord.Weight }); } ticketsCount -= withdrawnAmount; State.TicketsCountField.Value = ticketsCount; tickets.VotedTickets -= withdrawnAmount; State.TicketsMap[voterPublicKey.ToStringValue()] = tickets; foreach (var candidateVote in candidatesVotesDict) { var ticketsOfCandidate = State.TicketsMap[candidateVote.Key.ToStringValue()]; Assert(ticketsOfCandidate != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, $"Tickets information of {candidateVote.Key} not found.")); ticketsOfCandidate.ObtainedTickets -= candidateVote.Value; State.TicketsMap[candidateVote.Key.ToStringValue()] = ticketsOfCandidate; } return(State.TicketsMap[voterPublicKey.ToStringValue()]); }
// ReSharper disable once PossibleNullReferenceException public override Tickets WithdrawByTransactionId(Hash input) { var txId = input; var votingRecord = State.VotingRecordsMap[txId]; Assert(votingRecord != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "Voting record not found.")); Assert(!votingRecord.IsWithdrawn, ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "This voting record has already withdrawn.")); Assert(votingRecord.UnlockAge <= CurrentAge, ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "This voting record can't withdraw for now.")); Assert(votingRecord.From == Context.RecoverPublicKey().ToHex(), ContractErrorCode.GetErrorMessage(ContractErrorCode.NoPermission, "No permission to withdraw tickets of others.")); // Update voting record map. TryToGetBlockchainStartTimestamp(out var blockchainStartTimestamp); votingRecord.WithdrawTimestamp = blockchainStartTimestamp.ToDateTime().AddMinutes(CurrentAge).ToTimestamp(); votingRecord.IsWithdrawn = true; State.VotingRecordsMap[txId] = votingRecord; // Update total tickets count. var ticketsCount = State.TicketsCountField.Value; ticketsCount -= votingRecord.Count; State.TicketsCountField.Value = ticketsCount; // Update tickets number of this voter. var ticketsOfVoter = State.TicketsMap[votingRecord.From.ToStringValue()]; if (ticketsOfVoter != null) { ticketsOfVoter.VotedTickets -= votingRecord.Count; State.TicketsMap[votingRecord.From.ToStringValue()] = ticketsOfVoter; } // Update tickets number of related candidate. var ticketsOfCandidate = State.TicketsMap[votingRecord.To.ToStringValue()]; if (ticketsOfCandidate != null) { ticketsOfCandidate.ObtainedTickets -= votingRecord.Count; State.TicketsMap[votingRecord.To.ToStringValue()] = ticketsOfCandidate; } // Sub weight. State.DividendContract.SubWeights.Send( new Dividend.WeightsInfo() { TermNumber = State.CurrentTermNumberField.Value, Weights = votingRecord.Weight }); // Transfer token back to voter. State.TokenContract.Unlock.Send(new UnlockInput { From = Context.Sender, To = Context.Self, Symbol = Context.Variables.NativeSymbol, Amount = votingRecord.Count, LockId = votingRecord.TransactionId, Usage = $"Withdraw locked token of transaction {txId.ToHex()}: {votingRecord}" }); return(State.TicketsMap[votingRecord.From.ToStringValue()]); }
public override ActionResult AnnounceElection(Alias input) { if (State.DividendContract.Value == null) { State.DividendContract.Value = State.BasicContractZero.GetContractAddressByName.Call(State.DividendContractSystemName.Value); } if (State.TokenContract.Value == null) { State.TokenContract.Value = State.BasicContractZero.GetContractAddressByName.Call(State.TokenContractSystemName.Value); } var alias = input.Value; var publicKey = Context.RecoverPublicKey().ToHex(); // A voter cannot join the election before all his voting record expired. var tickets = State.TicketsMap[publicKey.ToStringValue()]; if (tickets != null) { foreach (var voteToTransaction in tickets.VoteToTransactions) { var votingRecord = State.VotingRecordsMap[voteToTransaction]; if (votingRecord != null) { Assert(votingRecord.IsWithdrawn, ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Voter can't announce election.")); } } } var candidates = State.CandidatesField.Value; if (candidates != null) { Assert(!candidates.PublicKeys.Contains(publicKey), ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Already announced election.")); candidates.AddCandidate(Context.RecoverPublicKey()); } else { candidates = new Candidates { PublicKeys = { publicKey }, Addresses = { Address.FromPublicKey(Context.RecoverPublicKey()) } }; } State.CandidatesField.Value = candidates; if (alias == "" || alias.Length > DPoSContractConsts.AliasLimit) { alias = publicKey.Substring(0, DPoSContractConsts.AliasLimit); } var publicKeyOfThisAlias = State.AliasesLookupMap[alias.ToStringValue()]; if (publicKeyOfThisAlias != null && publicKey == publicKeyOfThisAlias.Value) { return(new ActionResult { Success = true }); } State.AliasesLookupMap[alias.ToStringValue()] = publicKey.ToStringValue(); State.AliasesMap[publicKey.ToStringValue()] = alias.ToStringValue(); // Add this alias to history information of this candidate. var candidateHistoryInformation = State.HistoryMap[publicKey.ToStringValue()]; if (candidateHistoryInformation != null) { if (!candidateHistoryInformation.Aliases.Contains(alias)) { candidateHistoryInformation.Aliases.Add(alias); candidateHistoryInformation.CurrentAlias = alias; } candidateHistoryInformation.AnnouncementTransactionId = Context.TransactionId; State.HistoryMap[publicKey.ToStringValue()] = candidateHistoryInformation; } else { State.HistoryMap[publicKey.ToStringValue()] = new CandidateInHistory { CurrentAlias = alias, AnnouncementTransactionId = Context.TransactionId }; } State.TokenContract.Lock.Send(new LockInput() { From = Context.Sender, To = Context.Self, Symbol = Context.Variables.NativeSymbol, Amount = State.LockTokenForElection.Value, LockId = Context.TransactionId, Usage = "Lock for announcing election." }); return(new ActionResult { Success = true }); }
public override Hash Vote(VoteInput input) { var candidatePublicKey = input.CandidatePublicKey; var amount = input.Amount; var lockTime = input.LockTime; Assert(lockTime.InRange(90, 1095), ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Lock days is illegal.")); // Cannot vote to non-candidate. var candidates = State.CandidatesField.Value; Assert(candidates != null, ContractErrorCode.GetErrorMessage(ContractErrorCode.NotFound, "No candidate.")); Assert(candidates != null && candidates.PublicKeys.Contains(candidatePublicKey), ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Target didn't announce election.")); var voterPublicKey = Context.RecoverPublicKey().ToHex(); // A candidate cannot vote to anybody. Assert(candidates != null && !candidates.PublicKeys.Contains(voterPublicKey), ContractErrorCode.GetErrorMessage(ContractErrorCode.InvalidOperation, "Candidate can't vote.")); // Transfer the tokens to Consensus Contract address. State.TokenContract.Lock.Send(new LockInput { From = Context.Sender, To = Context.Self, Symbol = Context.Variables.NativeSymbol, Amount = amount, LockId = Context.TransactionId, Usage = "Lock for getting tickets." }); var currentTermNumber = State.CurrentTermNumberField.Value; var currentRoundNumber = State.CurrentRoundNumberField.Value; // To make up a VotingRecord instance. TryToGetBlockchainStartTimestamp(out var blockchainStartTimestamp); var votingRecord = new VotingRecord { Count = amount, From = voterPublicKey, To = candidatePublicKey, RoundNumber = currentRoundNumber, TransactionId = Context.TransactionId, VoteAge = CurrentAge, UnlockAge = CurrentAge + (long)lockTime, TermNumber = currentTermNumber, VoteTimestamp = blockchainStartTimestamp.ToDateTime().AddMinutes(CurrentAge).ToTimestamp(), UnlockTimestamp = blockchainStartTimestamp.ToDateTime().AddMinutes(CurrentAge + (long)lockTime) .ToTimestamp() }; votingRecord.LockDaysList.Add(lockTime); // Add the transaction id of this voting record to the tickets information of the voter. var tickets = State.TicketsMap[voterPublicKey.ToStringValue()]; if (tickets != null) { tickets.VoteToTransactions.Add(votingRecord.TransactionId); } else { tickets = new Tickets { PublicKey = voterPublicKey }; tickets.VoteToTransactions.Add(votingRecord.TransactionId); } tickets.VotedTickets += votingRecord.Count; tickets.HistoryVotedTickets += votingRecord.Count; State.TicketsMap[voterPublicKey.ToStringValue()] = tickets; // Add the transaction id of this voting record to the tickets information of the candidate. var candidateTickets = State.TicketsMap[candidatePublicKey.ToStringValue()]; if (candidateTickets != null) { candidateTickets.VoteFromTransactions.Add(votingRecord.TransactionId); } else { candidateTickets = new Tickets { PublicKey = candidatePublicKey }; candidateTickets.VoteFromTransactions.Add(votingRecord.TransactionId); } candidateTickets.ObtainedTickets += votingRecord.Count; candidateTickets.HistoryObtainedTickets += votingRecord.Count; State.TicketsMap[candidatePublicKey.ToStringValue()] = candidateTickets; // Update the amount of votes (voting records of whole system). State.VotesCountField.Value += 1; // Update the amount of tickets. State.TicketsCountField.Value += votingRecord.Count; // Add this voting record to voting records map. State.VotingRecordsMap[votingRecord.TransactionId] = votingRecord; // Tell Dividends Contract to add weights for this voting record. State.DividendContract.AddWeights.Send(new Dividend.WeightsInfo() { TermNumber = currentTermNumber + 1, Weights = votingRecord.Weight }); Context.LogDebug(() => $"Weights of vote {votingRecord.TransactionId.ToHex()}: {votingRecord.Weight}"); return(Context.TransactionId); }
public override DPoSHeaderInformation GetInformationToUpdateConsensus(DPoSTriggerInformation input) { // Some basic checks. Assert(input.PublicKey.Any(), "Invalid public key."); var publicKey = input.PublicKey; var currentBlockTime = Context.CurrentBlockTime; var behaviour = input.Behaviour; Assert(TryToGetCurrentRoundInformation(out var currentRound), ContractErrorCode.GetErrorMessage(ContractErrorCode.AttemptFailed, "Failed to get current round information.")); switch (behaviour) { case DPoSBehaviour.UpdateValueWithoutPreviousInValue: case DPoSBehaviour.UpdateValue: Assert(input.RandomHash != null, "Random hash should not be null."); var inValue = currentRound.CalculateInValue(input.RandomHash); var outValue = Hash.FromMessage(inValue); var signature = Hash.FromTwoHashes(outValue, input.RandomHash); // Just initial signature value. var previousInValue = Hash.Empty; // Just initial previous in value. if (TryToGetPreviousRoundInformation(out var previousRound) && !IsJustChangedTerm(out _)) { signature = previousRound.CalculateSignature(inValue); LogVerbose($"Previous random hash: {input.PreviousRandomHash.ToHex()}"); if (input.PreviousRandomHash != Hash.Empty) { // If PreviousRandomHash is Hash.Empty, it means the sender unable or unwilling to publish his previous in value. previousInValue = previousRound.CalculateInValue(input.PreviousRandomHash); } } var updatedRound = currentRound.ApplyNormalConsensusData(publicKey.ToHex(), previousInValue, outValue, signature, currentBlockTime); ShareAndRecoverInValue(updatedRound, previousRound, inValue, publicKey.ToHex()); // To publish Out Value. return(new DPoSHeaderInformation { SenderPublicKey = publicKey, Round = updatedRound, Behaviour = behaviour, }); case DPoSBehaviour.NextRound: Assert( GenerateNextRoundInformation(currentRound, currentBlockTime, out var nextRound), "Failed to generate next round information."); nextRound.RealTimeMinersInformation[publicKey.ToHex()].ProducedBlocks += 1; Context.LogDebug(() => $"Mined blocks: {nextRound.GetMinedBlocks()}"); nextRound.ExtraBlockProducerOfPreviousRound = publicKey.ToHex(); return(new DPoSHeaderInformation { SenderPublicKey = publicKey, Round = nextRound, Behaviour = behaviour }); case DPoSBehaviour.NextTerm: var firstRoundOfNextTerm = GenerateFirstRoundOfNextTerm(publicKey.ToHex()); Assert(firstRoundOfNextTerm.RoundId != 0, "Failed to generate new round information."); var information = new DPoSHeaderInformation { SenderPublicKey = publicKey, Round = firstRoundOfNextTerm, Behaviour = behaviour }; return(information); default: return(new DPoSHeaderInformation()); } }
public override Empty NextTerm(Round input) { // Count missed time slot of current round. CountMissedTimeSlots(); Assert(TryToGetTermNumber(out var termNumber), "Term number not found."); // Update current term number and current round number. Assert(TryToUpdateTermNumber(input.TermNumber), "Failed to update term number."); Assert(TryToUpdateRoundNumber(input.RoundNumber), "Failed to update round number."); // Reset some fields of first two rounds of next term. foreach (var minerInRound in input.RealTimeMinersInformation.Values) { minerInRound.MissedTimeSlots = 0; minerInRound.ProducedBlocks = 0; } var senderPublicKey = Context.RecoverPublicKey().ToHex(); // Update produced block number of this node. if (input.RealTimeMinersInformation.ContainsKey(senderPublicKey)) { input.RealTimeMinersInformation[senderPublicKey].ProducedBlocks += 1; } else { if (TryToGetMinerHistoryInformation(senderPublicKey, out var historyInformation)) { historyInformation.ProducedBlocks += 1; } else { historyInformation = new CandidateInHistory { PublicKey = senderPublicKey, ProducedBlocks = 1, CurrentAlias = senderPublicKey.Substring(0, DPoSContractConsts.AliasLimit) }; } AddOrUpdateMinerHistoryInformation(historyInformation); } // Update miners list. Assert(SetMiners(input.RealTimeMinersInformation.Keys.ToList().ToMiners(input.TermNumber)), ContractErrorCode.GetErrorMessage(ContractErrorCode.AttemptFailed, "Failed to update miners list.")); // Update term number lookup. (Using term number to get first round number of related term.) State.TermToFirstRoundMap[input.TermNumber.ToInt64Value()] = input.RoundNumber.ToInt64Value(); // Update blockchain age of new term. UpdateBlockchainAge(input.BlockchainAge); // Update rounds information of next two rounds. Assert(TryToAddRoundInformation(input), "Failed to add round information."); if (State.DividendContract.Value != null) { State.DividendContract.Value = State.BasicContractZero.GetContractAddressByName.Call(State.DividendContractSystemName.Value); State.DividendContract.KeepWeights.Send(new SInt64Value { Value = termNumber }); var termInfo = new TermInfo { RoundNumber = input.RoundNumber - 1, TermNumber = input.TermNumber - 1 }; Assert(SnapshotForTerm(termInfo).Success, ContractErrorCode.GetErrorMessage(ContractErrorCode.AttemptFailed, $"Failed to take snapshot of term {termInfo.TermNumber}")); Assert(SnapshotForMiners(termInfo).Success, ContractErrorCode.GetErrorMessage(ContractErrorCode.AttemptFailed, $"Failed to take snapshot of miners of term {termInfo.TermNumber}")); Assert(SendDividends(termInfo).Success, ContractErrorCode.GetErrorMessage(ContractErrorCode.AttemptFailed, $"Failed to send dividends of term {termInfo.TermNumber}")); } Context.LogDebug(() => $"Changing term number to {input.TermNumber}"); TryToFindLIB(); return(new Empty()); }