mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 18:41:05 +00:00
define context trait and initialize statement table
This commit is contained in:
Generated
+40
-1
@@ -534,6 +534,11 @@ name = "odds"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "owning_ref"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "owning_ref"
|
||||
version = "0.3.3"
|
||||
@@ -542,6 +547,16 @@ dependencies = [
|
||||
"stable_deref_trait 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"owning_ref 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parking_lot_core 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread-id 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.4.8"
|
||||
@@ -607,7 +622,8 @@ name = "polkadot-candidate-agreement"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"polkadot-primitives 0.1.0",
|
||||
"parking_lot 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -955,6 +971,16 @@ dependencies = [
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.4"
|
||||
@@ -1030,6 +1056,15 @@ dependencies = [
|
||||
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-timer"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triehash"
|
||||
version = "0.1.0"
|
||||
@@ -1178,7 +1213,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "99843c856d68d8b4313b03a17e33c4bb42ae8f6610ea81b28abe076ac721b9b0"
|
||||
"checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d"
|
||||
"checksum odds 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "c3df9b730298cea3a1c3faa90b7e2f9df3a9c400d0936d6015e6165734eefcba"
|
||||
"checksum owning_ref 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9d52571ddcb42e9c900c901a18d8d67e393df723fcd51dd59c5b1a85d0acb6cc"
|
||||
"checksum owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37"
|
||||
"checksum parking_lot 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "fa12d706797d42551663426a45e2db2e0364bd1dbf6aeada87e89c5f981f43e9"
|
||||
"checksum parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e"
|
||||
"checksum parking_lot_core 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "4f610cb9664da38e417ea3225f23051f589851999535290e077939838ab7a595"
|
||||
"checksum patricia-trie 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f1e2f638d79aba5c4a71a4f373df6e3cd702250a53b7f0ed4da1e2a7be9737ae"
|
||||
@@ -1217,6 +1254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
||||
"checksum thread-id 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2af4d6289a69a35c4d3aea737add39685f2784122c28119a7713165a63d68c9d"
|
||||
"checksum thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14"
|
||||
"checksum time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d788d3aa77bc0ef3e9621256885555368b47bd495c13dd2e7413c89f845520"
|
||||
"checksum tiny-keccak 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52d12ad79e4063e0cb0ca5efa202ed7244b6ce4d25f4d3abe410b2a66128292"
|
||||
@@ -1224,6 +1262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "514aae203178929dbf03318ad7c683126672d4d96eccb77b29603d33c9e25743"
|
||||
"checksum tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389"
|
||||
"checksum tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162"
|
||||
"checksum tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6131e780037787ff1b3f8aad9da83bca02438b72277850dd6ad0d455e0e20efc"
|
||||
"checksum triehash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9291c7f0fae44858b5e087dd462afb382354120003778f1695b44aab98c7abd7"
|
||||
"checksum uint 0.1.0 (git+https://github.com/paritytech/primitives.git)" = "<none>"
|
||||
"checksum unicase 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e01da42520092d0cd2d6ac3ae69eb21a22ad43ff195676b86f8c37f487d6b80"
|
||||
|
||||
@@ -5,4 +5,5 @@ authors = ["Parity Technologies <admin@parity.io>"]
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1"
|
||||
polkadot-primitives = { path = "../primitives" }
|
||||
parking_lot = "0.3"
|
||||
tokio-timer = "0.1.2"
|
||||
|
||||
@@ -83,13 +83,13 @@ pub trait Context {
|
||||
/// A future that resolves when a round timeout is concluded.
|
||||
type RoundTimeout: Future<Item=()>;
|
||||
/// A future that resolves when a proposal is ready.
|
||||
type Proposal: Future<Item=Self::Candidate>;
|
||||
type CreateProposal: Future<Item=Self::Candidate>;
|
||||
|
||||
/// Get the local validator ID.
|
||||
fn local_id(&self) -> Self::ValidatorId;
|
||||
|
||||
/// Get the best proposal.
|
||||
fn proposal(&self) -> Self::Proposal;
|
||||
fn proposal(&self) -> Self::CreateProposal;
|
||||
|
||||
/// Get the digest of a candidate.
|
||||
fn candidate_digest(&self, candidate: &Self::Candidate) -> Self::Digest;
|
||||
@@ -247,7 +247,7 @@ enum LocalState {
|
||||
struct Strategy<C: Context> {
|
||||
nodes: usize,
|
||||
max_faulty: usize,
|
||||
fetching_proposal: Option<C::Proposal>,
|
||||
fetching_proposal: Option<C::CreateProposal>,
|
||||
round_timeout: future::Fuse<C::RoundTimeout>,
|
||||
local_state: LocalState,
|
||||
locked: Option<Locked<C::Digest, C::Signature>>,
|
||||
@@ -330,7 +330,7 @@ impl<C: Context> Strategy<C> {
|
||||
-> Poll<Committed<C::Candidate, C::Digest, C::Signature>, E>
|
||||
where
|
||||
C::RoundTimeout: Future<Error=E>,
|
||||
C::Proposal: Future<Error=E>,
|
||||
C::CreateProposal: Future<Error=E>,
|
||||
{
|
||||
let mut last_watermark = (
|
||||
self.current_accumulator.round_number(),
|
||||
@@ -363,7 +363,7 @@ impl<C: Context> Strategy<C> {
|
||||
-> Poll<Committed<C::Candidate, C::Digest, C::Signature>, E>
|
||||
where
|
||||
C::RoundTimeout: Future<Error=E>,
|
||||
C::Proposal: Future<Error=E>,
|
||||
C::CreateProposal: Future<Error=E>,
|
||||
{
|
||||
self.propose(context, sending)?;
|
||||
self.prepare(context, sending);
|
||||
@@ -413,7 +413,7 @@ impl<C: Context> Strategy<C> {
|
||||
}
|
||||
|
||||
fn propose(&mut self, context: &C, sending: &mut Sending<ContextCommunication<C>>)
|
||||
-> Result<(), <C::Proposal as Future>::Error>
|
||||
-> Result<(), <C::CreateProposal as Future>::Error>
|
||||
{
|
||||
if let LocalState::Start = self.local_state {
|
||||
let mut propose = false;
|
||||
@@ -629,7 +629,7 @@ impl<C, I, O, E> Future for Agreement<C, I, O>
|
||||
where
|
||||
C: Context,
|
||||
C::RoundTimeout: Future<Error=E>,
|
||||
C::Proposal: Future<Error=E>,
|
||||
C::CreateProposal: Future<Error=E>,
|
||||
I: Stream<Item=ContextCommunication<C>,Error=E>,
|
||||
O: Sink<SinkItem=ContextCommunication<C>,SinkError=E>,
|
||||
E: From<InputStreamConcluded>,
|
||||
|
||||
@@ -104,13 +104,13 @@ impl Context for TestContext {
|
||||
type ValidatorId = ValidatorId;
|
||||
type Signature = Signature;
|
||||
type RoundTimeout = Box<Future<Item=(), Error=Error>>;
|
||||
type Proposal = FutureResult<Candidate, Error>;
|
||||
type CreateProposal = FutureResult<Candidate, Error>;
|
||||
|
||||
fn local_id(&self) -> ValidatorId {
|
||||
self.local_id.clone()
|
||||
}
|
||||
|
||||
fn proposal(&self) -> Self::Proposal {
|
||||
fn proposal(&self) -> Self::CreateProposal {
|
||||
let proposal = {
|
||||
let mut p = self.proposal.lock().unwrap();
|
||||
let x = *p;
|
||||
|
||||
@@ -30,7 +30,168 @@
|
||||
//! Groups themselves may be compromised by malicious validators.
|
||||
|
||||
extern crate futures;
|
||||
extern crate polkadot_primitives as primitives;
|
||||
extern crate parking_lot;
|
||||
extern crate tokio_timer;
|
||||
|
||||
pub mod bft;
|
||||
pub mod table;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
|
||||
use futures::prelude::*;
|
||||
use tokio_timer::Timer;
|
||||
|
||||
use table::Table;
|
||||
|
||||
/// Context necessary for agreement.
|
||||
pub trait Context: Send + Clone {
|
||||
/// A validator ID
|
||||
type ValidatorId: Debug + Hash + Eq + Clone;
|
||||
/// The digest (hash or other unique attribute) of a candidate.
|
||||
type Digest: Debug + Hash + Eq + Clone;
|
||||
/// The group ID type
|
||||
type GroupId: Debug + Hash + Ord + Eq + Clone;
|
||||
/// A signature type.
|
||||
type Signature: Debug + Eq + Clone;
|
||||
/// Candidate type. In practice this will be a candidate receipt.
|
||||
type ParachainCandidate: Debug + Ord + Eq + Clone;
|
||||
/// The actual block proposal type. This is what is agreed upon, and
|
||||
/// is composed of multiple candidates.
|
||||
type Proposal: Debug + Eq + Clone;
|
||||
|
||||
/// A future that resolves when a candidate is checked for validity.
|
||||
///
|
||||
/// In Polkadot, this will involve fetching the corresponding block data,
|
||||
/// producing the necessary ingress, and running the parachain validity function.
|
||||
type CheckCandidate: IntoFuture<Item=bool>;
|
||||
|
||||
/// A future that resolves when availability of a candidate's external
|
||||
/// data is checked.
|
||||
type CheckAvailability: IntoFuture<Item=bool>;
|
||||
|
||||
/// Get the digest of a candidate.
|
||||
fn candidate_digest(candidate: &Self::ParachainCandidate) -> Self::Digest;
|
||||
|
||||
/// Get the group of a candidate.
|
||||
fn candidate_group(candidate: &Self::ParachainCandidate) -> Self::GroupId;
|
||||
|
||||
/// Get the primary for a given round.
|
||||
fn round_proposer(&self, round: usize) -> Self::ValidatorId;
|
||||
|
||||
/// Check a candidate for validity.
|
||||
fn check_validity(&self, candidate: &Self::ParachainCandidate) -> Self::CheckCandidate;
|
||||
|
||||
/// Check availability of candidate data.
|
||||
fn check_availability(&self, candidate: &Self::ParachainCandidate) -> Self::CheckAvailability;
|
||||
|
||||
/// Attempt to combine a set of parachain candidates into a proposal.
|
||||
///
|
||||
/// This may arbitrarily return `None`, but the intent is for `Some`
|
||||
/// to only be returned when candidates from enough groups are known.
|
||||
///
|
||||
/// "enough" may be subjective as well.
|
||||
fn create_proposal(&self, candidates: Vec<&Self::ParachainCandidate>)
|
||||
-> Option<Self::Proposal>;
|
||||
|
||||
/// Check validity of a proposal. This may also be somewhat subjective
|
||||
/// based on a monotonic-decreasing curve.
|
||||
fn proposal_valid(&self, proposal: &Self::Proposal) -> bool;
|
||||
|
||||
/// Get the local validator ID.
|
||||
fn local_id(&self) -> Self::ValidatorId;
|
||||
|
||||
/// Sign a table validity statement with the local key.
|
||||
fn sign_table_statement(
|
||||
&self,
|
||||
statement: &table::Statement<Self::ParachainCandidate, Self::Digest>
|
||||
) -> Self::Signature;
|
||||
|
||||
/// Sign a BFT agreement message.
|
||||
fn sign_bft_message(&self, &bft::Message<Self::Proposal, Self::Digest>) -> Self::Signature;
|
||||
}
|
||||
|
||||
/// Information about a specific group.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GroupInfo<V: Hash + Eq> {
|
||||
/// Validators meant to check validity of candidates.
|
||||
pub validity_guarantors: HashSet<V>,
|
||||
/// Validators meant to check availability of candidate data.
|
||||
pub availability_guarantors: HashSet<V>,
|
||||
/// Number of votes needed for validity.
|
||||
pub needed_validity: usize,
|
||||
/// Number of votes needed for availability.
|
||||
pub needed_availability: usize,
|
||||
}
|
||||
|
||||
struct TableContext<C: Context> {
|
||||
context: C,
|
||||
groups: HashMap<C::GroupId, GroupInfo<C::ValidatorId>>,
|
||||
}
|
||||
|
||||
impl<C: Context> table::Context for TableContext<C> {
|
||||
type ValidatorId = C::ValidatorId;
|
||||
type Digest = C::Digest;
|
||||
type GroupId = C::GroupId;
|
||||
type Signature = C::Signature;
|
||||
type Candidate = C::ParachainCandidate;
|
||||
|
||||
fn candidate_digest(candidate: &Self::Candidate) -> Self::Digest {
|
||||
C::candidate_digest(candidate)
|
||||
}
|
||||
|
||||
fn candidate_group(candidate: &Self::Candidate) -> Self::GroupId {
|
||||
C::candidate_group(candidate)
|
||||
}
|
||||
|
||||
fn is_member_of(&self, validator: &Self::ValidatorId, group: &Self::GroupId) -> bool {
|
||||
self.groups.get(group).map_or(false, |g| g.validity_guarantors.contains(validator))
|
||||
}
|
||||
|
||||
fn is_availability_guarantor_of(&self, validator: &Self::ValidatorId, group: &Self::GroupId) -> bool {
|
||||
self.groups.get(group).map_or(false, |g| g.availability_guarantors.contains(validator))
|
||||
}
|
||||
|
||||
fn requisite_votes(&self, group: &Self::GroupId) -> (usize, usize) {
|
||||
self.groups.get(group).map_or(
|
||||
(usize::max_value(), usize::max_value()),
|
||||
|g| (g.needed_validity, g.needed_availability),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters necessary for agreement.
|
||||
pub struct AgreementParams<C: Context> {
|
||||
/// The context itself.
|
||||
pub context: C,
|
||||
/// For scheduling timeouts.
|
||||
pub timer: Timer,
|
||||
/// Group assignments.
|
||||
pub groups: HashMap<C::GroupId, GroupInfo<C::ValidatorId>>,
|
||||
/// The local candidate proposal.
|
||||
// TODO: replace with future.
|
||||
pub local_proposal: Option<C::ParachainCandidate>,
|
||||
}
|
||||
|
||||
pub fn agree<C: Context + Clone>(params: AgreementParams<C>) {
|
||||
let context = params.context;
|
||||
let local_id = context.local_id();
|
||||
let mut table = Table::<TableContext<C>>::default();
|
||||
|
||||
let table_context = TableContext {
|
||||
context: context.clone(),
|
||||
groups: params.groups,
|
||||
};
|
||||
|
||||
if let Some(candidate) = params.local_proposal {
|
||||
let statement = table::Statement::Candidate(candidate);
|
||||
let signed_statement = table::SignedStatement {
|
||||
signature: context.sign_table_statement(&statement),
|
||||
sender: local_id.clone(),
|
||||
statement: statement,
|
||||
};
|
||||
|
||||
table.import_statement(&table_context, signed_statement, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,21 +35,21 @@ use std::fmt::Debug;
|
||||
/// Context for the statement table.
|
||||
pub trait Context {
|
||||
/// A validator ID
|
||||
type ValidatorId: Hash + Eq + Clone + Debug;
|
||||
type ValidatorId: Debug + Hash + Eq + Clone;
|
||||
/// The digest (hash or other unique attribute) of a candidate.
|
||||
type Digest: Hash + Eq + Clone + Debug;
|
||||
/// Candidate type.
|
||||
type Candidate: Ord + Eq + Clone + Debug;
|
||||
type Digest: Debug + Hash + Eq + Clone;
|
||||
/// The group ID type
|
||||
type GroupId: Hash + Ord + Eq + Clone + Debug;
|
||||
type GroupId: Debug + Hash + Ord + Eq + Clone;
|
||||
/// A signature type.
|
||||
type Signature: Eq + Clone + Debug;
|
||||
type Signature: Debug + Eq + Clone;
|
||||
/// Candidate type. In practice this will be a candidate receipt.
|
||||
type Candidate: Debug + Ord + Eq + Clone;
|
||||
|
||||
/// get the digest of a candidate.
|
||||
fn candidate_digest(&self, candidate: &Self::Candidate) -> Self::Digest;
|
||||
fn candidate_digest(candidate: &Self::Candidate) -> Self::Digest;
|
||||
|
||||
/// get the group of a candidate.
|
||||
fn candidate_group(&self, candidate: &Self::Candidate) -> Self::GroupId;
|
||||
fn candidate_group(candidate: &Self::Candidate) -> Self::GroupId;
|
||||
|
||||
/// Whether a validator is a member of a group.
|
||||
/// Members are meant to submit candidates and vote on validity.
|
||||
@@ -259,13 +259,22 @@ pub fn create<C: Context>() -> Table<C> {
|
||||
}
|
||||
|
||||
/// Stores votes
|
||||
#[derive(Default)]
|
||||
pub struct Table<C: Context> {
|
||||
validator_data: HashMap<C::ValidatorId, ValidatorData<C>>,
|
||||
detected_misbehavior: HashMap<C::ValidatorId, <C as ResolveMisbehavior>::Misbehavior>,
|
||||
candidate_votes: HashMap<C::Digest, CandidateData<C>>,
|
||||
}
|
||||
|
||||
impl<C: Context> Default for Table<C> {
|
||||
fn default() -> Self {
|
||||
Table {
|
||||
validator_data: HashMap::new(),
|
||||
detected_misbehavior: HashMap::new(),
|
||||
candidate_votes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Context> Table<C> {
|
||||
/// Produce a set of proposed candidates.
|
||||
///
|
||||
@@ -385,7 +394,7 @@ impl<C: Context> Table<C> {
|
||||
candidate: C::Candidate,
|
||||
signature: C::Signature,
|
||||
) -> (Option<<C as ResolveMisbehavior>::Misbehavior>, Option<Summary<C::Digest, C::GroupId>>) {
|
||||
let group = context.candidate_group(&candidate);
|
||||
let group = C::candidate_group(&candidate);
|
||||
if !context.is_member_of(&from, &group) {
|
||||
return (
|
||||
Some(Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
|
||||
@@ -400,7 +409,7 @@ impl<C: Context> Table<C> {
|
||||
}
|
||||
|
||||
// check that validator hasn't already specified another candidate.
|
||||
let digest = context.candidate_digest(&candidate);
|
||||
let digest = C::candidate_digest(&candidate);
|
||||
|
||||
let new_proposal = match self.validator_data.entry(from.clone()) {
|
||||
Entry::Occupied(mut occ) => {
|
||||
@@ -608,11 +617,11 @@ mod tests {
|
||||
type GroupId = GroupId;
|
||||
type Signature = Signature;
|
||||
|
||||
fn candidate_digest(&self, candidate: &Candidate) -> Digest {
|
||||
fn candidate_digest(candidate: &Candidate) -> Digest {
|
||||
Digest(candidate.1)
|
||||
}
|
||||
|
||||
fn candidate_group(&self, candidate: &Candidate) -> GroupId {
|
||||
fn candidate_group(candidate: &Candidate) -> GroupId {
|
||||
GroupId(candidate.0)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user