Skip to content

Commit

Permalink
Merge pull request #185 from piotr-roslaniec/aggregate-from-subset
Browse files Browse the repository at this point in the history
Create an aggregate from subset of messages
  • Loading branch information
cygnusv authored Mar 19, 2024
2 parents aa69b36 + 975dae0 commit 299a471
Show file tree
Hide file tree
Showing 18 changed files with 829 additions and 581 deletions.
15 changes: 12 additions & 3 deletions ferveo-python/examples/server_api_precomputed.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def gen_eth_addr(i: int) -> str:
)
messages.append(ValidatorMessage(sender, dkg.generate_transcript()))

# We only need `shares_num` messages to aggregate the transcript
messages = messages[:shares_num]

# Every validator can aggregate the transcripts
dkg = Dkg(
tau=tau,
Expand All @@ -61,9 +64,13 @@ def gen_eth_addr(i: int) -> str:
aad = "my-aad".encode()
ciphertext = encrypt(msg, aad, client_aggregate.public_key)

# In precomputed variant, the client selects a subset of validators to use for decryption
selected_validators = validators[:security_threshold]
selected_keypairs = validator_keypairs[:security_threshold]

# Having aggregated the transcripts, the validators can now create decryption shares
decryption_shares = []
for validator, validator_keypair in zip(validators, validator_keypairs):
for validator, validator_keypair in zip(selected_validators, selected_keypairs):
dkg = Dkg(
tau=tau,
shares_num=shares_num,
Expand All @@ -80,13 +87,15 @@ def gen_eth_addr(i: int) -> str:

# Create a decryption share for the ciphertext
decryption_share = aggregate.create_decryption_share_precomputed(
dkg, ciphertext.header, aad, validator_keypair
dkg, ciphertext.header, aad, validator_keypair, selected_validators
)
decryption_shares.append(decryption_share)

# We need at most `security_threshold` decryption shares
decryption_shares = decryption_shares[:security_threshold]

# Now, the decryption share can be used to decrypt the ciphertext
# This part is in the client API

shared_secret = combine_decryption_shares_precomputed(decryption_shares)

# The client should have access to the public parameters of the DKG
Expand Down
6 changes: 6 additions & 0 deletions ferveo-python/examples/server_api_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def gen_eth_addr(i: int) -> str:
)
messages.append(ValidatorMessage(sender, dkg.generate_transcript()))

# We only need `shares_num` messages to aggregate the transcript
messages = messages[:shares_num]

# Now that every validator holds a dkg instance and a transcript for every other validator,
# every validator can aggregate the transcripts
me = validators[0]
Expand Down Expand Up @@ -90,6 +93,9 @@ def gen_eth_addr(i: int) -> str:
)
decryption_shares.append(decryption_share)

# We only need `threshold` decryption shares in simple variant
decryption_shares = decryption_shares[:security_threshold]

# Now, the decryption share can be used to decrypt the ciphertext
# This part is in the client API

Expand Down
1 change: 1 addition & 0 deletions ferveo-python/ferveo/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class AggregatedTranscript:
ciphertext_header: CiphertextHeader,
aad: bytes,
validator_keypair: Keypair,
selected_validators: Sequence[Validator],
) -> DecryptionSharePrecomputed: ...
@staticmethod
def from_bytes(data: bytes) -> AggregatedTranscript: ...
Expand Down
93 changes: 51 additions & 42 deletions ferveo-python/test/test_ferveo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@
def gen_eth_addr(i: int) -> str:
return f"0x{i:040x}"


def decryption_share_for_variant(v: FerveoVariant, agg_transcript):
if v == FerveoVariant.Simple:
return agg_transcript.create_decryption_share_simple
elif v == FerveoVariant.Precomputed:
return agg_transcript.create_decryption_share_precomputed
else:
raise ValueError("Unknown variant")


def combine_shares_for_variant(v: FerveoVariant, decryption_shares):
if v == FerveoVariant.Simple:
return combine_decryption_shares_simple(decryption_shares)
Expand All @@ -39,18 +29,20 @@ def combine_shares_for_variant(v: FerveoVariant, decryption_shares):


def scenario_for_variant(
variant: FerveoVariant, shares_num, validators_num, threshold, shares_to_use
variant: FerveoVariant,
shares_num,
validators_num,
threshold,
dec_shares_to_use
):
if variant not in [FerveoVariant.Simple, FerveoVariant.Precomputed]:
raise ValueError("Unknown variant: " + variant)

if validators_num < shares_num:
raise ValueError("validators_num must be >= shares_num")

if variant == FerveoVariant.Precomputed and shares_to_use != validators_num:
raise ValueError(
"In precomputed variant, shares_to_use must be equal to validators_num"
)
if shares_num < threshold:
raise ValueError("shares_num must be >= threshold")

tau = 1
validator_keypairs = [Keypair.random() for _ in range(0, validators_num)]
Expand All @@ -72,6 +64,9 @@ def scenario_for_variant(
)
messages.append(ValidatorMessage(sender, dkg.generate_transcript()))

# We only need `shares_num` messages to aggregate the transcript
messages = messages[:shares_num]

# Both client and server should be able to verify the aggregated transcript
dkg = Dkg(
tau=tau,
Expand All @@ -82,18 +77,27 @@ def scenario_for_variant(
)
server_aggregate = dkg.aggregate_transcripts(messages)
assert server_aggregate.verify(validators_num, messages)

client_aggregate = AggregatedTranscript(messages)
assert client_aggregate.verify(validators_num, messages)

# At this point, DKG is done, and we are proceeding to threshold decryption

# Client creates a ciphertext and requests decryption shares from validators
msg = "abc".encode()
aad = "my-aad".encode()
ciphertext = encrypt(msg, aad, client_aggregate.public_key)

# In precomputed variant, the client selects a subset of validators to use for decryption
if variant == FerveoVariant.Precomputed:
selected_validators = validators[:threshold]
selected_validator_keypairs = validator_keypairs[:threshold]
else:
selected_validators = validators
selected_validator_keypairs = validator_keypairs

# Having aggregated the transcripts, the validators can now create decryption shares
decryption_shares = []
for validator, validator_keypair in zip(validators, validator_keypairs):
for validator, validator_keypair in zip(selected_validators, selected_validator_keypairs):
assert validator.public_key == validator_keypair.public_key()
print("validator: ", validator.share_index)

Expand All @@ -104,26 +108,28 @@ def scenario_for_variant(
validators=validators,
me=validator,
)
pvss_aggregated = dkg.aggregate_transcripts(messages)
assert pvss_aggregated.verify(validators_num, messages)

decryption_share = decryption_share_for_variant(variant, pvss_aggregated)(
dkg, ciphertext.header, aad, validator_keypair
)
server_aggregate = dkg.aggregate_transcripts(messages)
assert server_aggregate.verify(validators_num, messages)

if variant == FerveoVariant.Simple:
decryption_share = server_aggregate.create_decryption_share_simple(
dkg, ciphertext.header, aad, validator_keypair
)
elif variant == FerveoVariant.Precomputed:
decryption_share = server_aggregate.create_decryption_share_precomputed(
dkg, ciphertext.header, aad, validator_keypair, selected_validators
)
else:
raise ValueError("Unknown variant")
decryption_shares.append(decryption_share)

# We are limiting the number of decryption shares to use for testing purposes
# decryption_shares = decryption_shares[:shares_to_use]
decryption_shares = decryption_shares[:dec_shares_to_use]

# Client combines the decryption shares and decrypts the ciphertext
shared_secret = combine_shares_for_variant(variant, decryption_shares)

if variant == FerveoVariant.Simple and len(decryption_shares) < threshold:
with pytest.raises(ThresholdEncryptionError):
decrypt_with_shared_secret(ciphertext, aad, shared_secret)
return

if variant == FerveoVariant.Precomputed and len(decryption_shares) < threshold:
if len(decryption_shares) < threshold:
with pytest.raises(ThresholdEncryptionError):
decrypt_with_shared_secret(ciphertext, aad, shared_secret)
return
Expand All @@ -133,54 +139,57 @@ def scenario_for_variant(


def test_simple_tdec_has_enough_messages():
shares_num = 4
threshold = shares_num - 1
shares_num = 8
threshold = int(shares_num * 2 / 3)
for validators_num in [shares_num, shares_num + 2]:
scenario_for_variant(
FerveoVariant.Simple,
shares_num=shares_num,
validators_num=validators_num,
threshold=threshold,
shares_to_use=threshold,
dec_shares_to_use=threshold,
)


def test_simple_tdec_doesnt_have_enough_messages():
shares_num = 4
threshold = shares_num - 1
shares_num = 8
threshold = int(shares_num * 2 / 3)
dec_shares_to_use = threshold - 1
for validators_num in [shares_num, shares_num + 2]:
scenario_for_variant(
FerveoVariant.Simple,
shares_num=shares_num,
validators_num=validators_num,
threshold=threshold,
shares_to_use=validators_num - 1,
dec_shares_to_use=dec_shares_to_use,
)


def test_precomputed_tdec_has_enough_messages():
shares_num = 4
threshold = shares_num # in precomputed variant, we need all shares
shares_num = 8
threshold = int(shares_num * 2 / 3)
dec_shares_to_use = threshold
for validators_num in [shares_num, shares_num + 2]:
scenario_for_variant(
FerveoVariant.Precomputed,
shares_num=shares_num,
validators_num=validators_num,
threshold=threshold,
shares_to_use=validators_num,
dec_shares_to_use=dec_shares_to_use,
)


def test_precomputed_tdec_doesnt_have_enough_messages():
shares_num = 4
threshold = shares_num # in precomputed variant, we need all shares
shares_num = 8
threshold = int(shares_num * 2 / 3)
dec_shares_to_use = threshold - 1
for validators_num in [shares_num, shares_num + 2]:
scenario_for_variant(
FerveoVariant.Simple,
shares_num=shares_num,
validators_num=validators_num,
threshold=threshold,
shares_to_use=threshold - 1,
dec_shares_to_use=dec_shares_to_use,
)


Expand Down
14 changes: 10 additions & 4 deletions ferveo-tdec/benches/tpke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl SetupSimple {
let aad: &[u8] = "my-aad".as_bytes();

let (pubkey, privkey, contexts) =
setup_simple::<E>(threshold, shares_num, rng);
setup_simple::<E>(shares_num, threshold, rng);

// Ciphertext.commitment is already computed to match U
let ciphertext =
Expand All @@ -124,10 +124,10 @@ impl SetupSimple {

let pub_contexts = contexts[0].clone().public_decryption_contexts;
let domain: Vec<Fr> = pub_contexts.iter().map(|c| c.domain).collect();
let lagrange = prepare_combine_simple::<E>(&domain);
let lagrange_coeffs = prepare_combine_simple::<E>(&domain);

let shared_secret =
share_combine_simple::<E>(&decryption_shares, &lagrange);
share_combine_simple::<E>(&decryption_shares, &lagrange_coeffs);

let shared = SetupShared {
threshold,
Expand All @@ -144,7 +144,7 @@ impl SetupSimple {
contexts,
pub_contexts,
decryption_shares,
lagrange_coeffs: lagrange,
lagrange_coeffs,
}
}
}
Expand Down Expand Up @@ -200,6 +200,8 @@ pub fn bench_create_decryption_share(c: &mut Criterion) {
};
let simple_precomputed = {
let setup = SetupSimple::new(shares_num, MSG_SIZE_CASES[0], rng);
let selected_participants =
(0..setup.shared.threshold).collect::<Vec<_>>();
move || {
black_box(
setup
Expand All @@ -209,6 +211,7 @@ pub fn bench_create_decryption_share(c: &mut Criterion) {
context.create_share_precomputed(
&setup.shared.ciphertext.header().unwrap(),
&setup.shared.aad,
&selected_participants,
)
})
.collect::<Vec<_>>(),
Expand Down Expand Up @@ -295,6 +298,8 @@ pub fn bench_share_combine(c: &mut Criterion) {
};
let simple_precomputed = {
let setup = SetupSimple::new(shares_num, MSG_SIZE_CASES[0], rng);
// TODO: Use threshold instead of shares_num
let selected_participants = (0..shares_num).collect::<Vec<_>>();

let decryption_shares: Vec<_> = setup
.contexts
Expand All @@ -304,6 +309,7 @@ pub fn bench_share_combine(c: &mut Criterion) {
.create_share_precomputed(
&setup.shared.ciphertext.header().unwrap(),
&setup.shared.aad,
&selected_participants,
)
.unwrap()
})
Expand Down
11 changes: 6 additions & 5 deletions ferveo-tdec/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,16 @@ impl<E: Pairing> PrivateDecryptionContextSimple<E> {
&self,
ciphertext_header: &CiphertextHeader<E>,
aad: &[u8],
selected_participants: &[usize],
) -> Result<DecryptionSharePrecomputed<E>> {
let domain = self
.public_decryption_contexts
let selected_domain_points = selected_participants
.iter()
.map(|c| c.domain)
.map(|i| self.public_decryption_contexts[*i].domain)
.collect::<Vec<_>>();
let lagrange_coeffs = prepare_combine_simple::<E>(&domain);
let lagrange_coeffs =
prepare_combine_simple::<E>(&selected_domain_points);

DecryptionSharePrecomputed::new(
DecryptionSharePrecomputed::create(
self.index,
&self.setup_params.b,
&self.private_key_share,
Expand Down
14 changes: 13 additions & 1 deletion ferveo-tdec/src/decryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ impl<E: Pairing> ValidatorShareChecksum<E> {
}
}

/// A decryption share for a simple variant of the threshold decryption scheme.
/// In this variant, the decryption share require additional computation on the
/// client side int order to be combined.
#[serde_as]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DecryptionShareSimple<E: Pairing> {
Expand Down Expand Up @@ -141,6 +144,11 @@ impl<E: Pairing> DecryptionShareSimple<E> {
}
}

/// A decryption share for a precomputed variant of the threshold decryption scheme.
/// In this variant, the decryption share is precomputed and can be combined
/// without additional computation on the client side.
/// The downside is that the threshold of decryption shares required to decrypt
/// is equal to the number of private key shares in the scheme.
#[serde_as]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DecryptionSharePrecomputed<E: Pairing> {
Expand All @@ -155,7 +163,9 @@ pub struct DecryptionSharePrecomputed<E: Pairing> {
}

impl<E: Pairing> DecryptionSharePrecomputed<E> {
pub fn new(
/// Create a decryption share from the given parameters.
/// This function checks that the ciphertext is valid.
pub fn create(
validator_index: usize,
validator_decryption_key: &E::ScalarField,
private_key_share: &PrivateKeyShare<E>,
Expand All @@ -174,6 +184,8 @@ impl<E: Pairing> DecryptionSharePrecomputed<E> {
)
}

/// Create a decryption share from the given parameters.
/// This function does not check that the ciphertext is valid.
pub fn create_unchecked(
validator_index: usize,
validator_decryption_key: &E::ScalarField,
Expand Down
Loading

0 comments on commit 299a471

Please sign in to comment.