From 8c304aec4390d6107fefa5bce212b2b0189c8281 Mon Sep 17 00:00:00 2001 From: azuchi Date: Mon, 19 Feb 2024 16:36:20 +0900 Subject: [PATCH] Add RTS feature --- README.md | 41 ++++++- lib/frost.rb | 1 + lib/frost/polynomial.rb | 18 +++- lib/frost/repairable.rb | 56 ++++++++++ spec/fixtures/p256/repair-share.json | 15 +++ spec/fixtures/secp256k1/repair-share.json | 15 +++ spec/frost/repairable_spec.rb | 125 ++++++++++++++++++++++ 7 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 lib/frost/repairable.rb create mode 100644 spec/fixtures/p256/repair-share.json create mode 100644 spec/fixtures/secp256k1/repair-share.json create mode 100644 spec/frost/repairable_spec.rb diff --git a/README.md b/README.md index 78823a3..80bdc12 100644 --- a/README.md +++ b/README.md @@ -134,4 +134,43 @@ end group_pubkey = FROST::DKG.compute_group_pubkey(secrets[1], received_package[1]) # The subsequent signing phase is the same as above with signing_shares as the secret. -``` \ No newline at end of file +``` + +### Share repair + +Using `FROST::Repairable` module, you can repair existing (or new) participant's share with the cooperation of T participants. + +```ruby +# Dealer generate shares. +FROST::SigningKey.generate(ECDSA::Group::Secp256k1) +polynomial = dealer.gen_poly(min_signers - 1) +shares = 1.upto(max_signers).map {|identifier| polynomial.gen_share(identifier) } + +# Signer 2 will lose their share +# Signers (helpers) 1, 4 and 5 will help signer 2 (participant) to recover their share +helper1 = shares[0] +helper4 = shares[3] +helper5 = shares[4] +helper_shares = [helper1, helper4, helper5] +helpers = helper_shares.map(&:identifier) +participant_share = shares[1] + +# Each helper computes delta values. +received_values = {} +helper_shares.each do |helper_share| + delta_values = FROST::Repairable.step1(helpers, participant_share.identifier, helper_share) + delta_values.each do |target_id, value| + received_values[target_id] ||= [] + received_values[target_id] << value + end +end + +# Each helper send sum value to participant. +participant_received_values = [] +received_values.each do |_, values| + participant_received_values << FROST::Repairable.step2(values, ECDSA::Group::Secp256k1) +end + +# Participant can obtain his share. +repair_share = FROST::Repairable.step3(2, participant_received_values, ECDSA::Group::Secp256k1) +``` diff --git a/lib/frost.rb b/lib/frost.rb index 58019cb..b869d32 100644 --- a/lib/frost.rb +++ b/lib/frost.rb @@ -17,6 +17,7 @@ class Error < StandardError; end autoload :Polynomial, "frost/polynomial" autoload :SigningKey, "frost/signing_key" autoload :DKG, "frost/dkg" + autoload :Repairable, "frost/repairable" module_function diff --git a/lib/frost/polynomial.rb b/lib/frost/polynomial.rb index 8fa0ea6..6f5a70e 100644 --- a/lib/frost/polynomial.rb +++ b/lib/frost/polynomial.rb @@ -75,22 +75,34 @@ def verification_point end # Generates the lagrange coefficient for the i'th participant. + # The Lagrange polynomial for a set of points (xj, yj) for 0 <= j <= k is + # ∑_{i=0}^k yi.ℓi(x), where ℓi(x) is the Lagrange basis polynomial: + # ℓi(x) = ∏_{0≤j≤k; j≠i} (x - xj) / (xi - xj). + # This computes ℓj(x) for the set of points `xs` and for the j corresponding to the given xj. # @param [Array] x_coordinates The list of x-coordinates. # @param [Integer] xi an x-coordinate contained in x_coordinates. # @param [ECDSA::Group] group Elliptic curve group. + # @param [Integer] x (Optional) if x is nil, it uses 0 for it (since Identifiers can't be 0). # @return [Integer] The lagrange coefficient. - def self.derive_interpolating_value(x_coordinates, xi, group) + def self.derive_interpolating_value(x_coordinates, xi, group, x: nil) raise ArgumentError, "xi is not included in x_coordinates." unless x_coordinates.include?(xi) raise ArgumentError, "Duplicate values in x_coordinates." if (x_coordinates.length - x_coordinates.uniq.length) > 0 raise ArgumentError, "group must be ECDSA::Group." unless group.is_a?(ECDSA::Group) + raise ArgumentError, "x must be Integer." if x && !x.is_a?(Integer) field = ECDSA::PrimeField.new(group.order) numerator = 1 denominator = 1 + x_coordinates.each do |xj| next if xi == xj - numerator *= xj - denominator *= (xj - xi) + if x + numerator *= (x - xj) + denominator *= (xi - xj) + else + numerator *= xj + denominator *= (xj - xi) + end end field.mod(numerator * field.inverse(denominator)) diff --git a/lib/frost/repairable.rb b/lib/frost/repairable.rb new file mode 100644 index 0000000..b3128d6 --- /dev/null +++ b/lib/frost/repairable.rb @@ -0,0 +1,56 @@ +module FROST + # Implements the Repairable Threshold Scheme (RTS) from + module Repairable + module_function + + # Step 1 for RTS. + # Each helper computes delta_i,j for other helpers. + # @param [Array] helpers Array of helper's identifier. + # @param [Integer] participant Identifier of the participant whose shares you want to restore. + # @param [FROST::SecretShare] share Share of participant running this process. + # @return [Hash] Hash with helper ID as key and value as delta value. + def step1(helpers, participant, share) + raise ArgumentError, "helpers must be greater than 1." if helpers.length < 2 + raise ArgumentError, "participant must be greater than 1." if participant < 1 + raise ArgumentError, "helpers has duplicate identifier." unless helpers.uniq.length == helpers.length + raise ArgumentError, "helpers contains same identifier with participant." if helpers.include?(participant) + + field = ECDSA::PrimeField.new(share.group.order) + random_values = (helpers.length - 1).times.map { SecureRandom.random_number(share.group.order - 1) } + + # compute last random value + ## Calculate Lagrange Coefficient for helper_i + zeta_i = Polynomial.derive_interpolating_value(helpers, share.identifier, share.group, x: participant) + lhs = field.mod(zeta_i * share.share) + # last random value + last = field.mod(lhs - random_values.sum) + random_values << last + + helpers.zip(random_values).to_h + end + + # Step 2 for RTS. + # Each helper sum received delta values from other helpers. + # @param [Array] step1_values Array of delta values. + # @param [ECDSA::Group] group + # @return [Integer] Sum of delta values. + def step2(step1_values, group) + raise ArgumentError, "group must be ECDSA::Group" unless group.is_a?(ECDSA::Group) + + field = ECDSA::PrimeField.new(group.order) + field.mod(step1_values.sum) + end + + # Participant compute own share with received sum of delta value. + # @param [Integer] identifier Identifier of the participant whose shares you want to restore. + # @param [Array] step2_results Array of Step 2 results received from other helpers. + # @param [ECDSA::Group] group + # @return + def step3(identifier, step2_results, group) + raise ArgumentError, "group must be ECDSA::Group" unless group.is_a?(ECDSA::Group) + + field = ECDSA::PrimeField.new(group.order) + FROST::SecretShare.new(identifier, field.mod(step2_results.sum), group) + end + end +end diff --git a/spec/fixtures/p256/repair-share.json b/spec/fixtures/p256/repair-share.json new file mode 100644 index 0000000..a1b8f09 --- /dev/null +++ b/spec/fixtures/p256/repair-share.json @@ -0,0 +1,15 @@ +{ + "scalar_generation": { + "random_scalar_1": "72deefbbf904c44fe6bb41ad8fcd23e6de2bd950aa2bdd03a8d84fcf29a86e21", + "random_scalar_2": "63d3c1867568bcf26c2c5525cbe2af1f689adfb0f7c8c2e769c1f8bca37e0e8f", + "random_scalar_3": "90a1e9902329edc21bd8c15ceafa901ac028a543b4d06011667a29dfc377e5dd", + "random_scalar_sum": "67549ad391976f036ec0583046aa63214a086397afad6177855aa7a8943b3d3c" + }, + "sigma_generation": { + "sigma_1": "6234563e6c285de9e50569948c4648ef8205294a18f994a8f61e8c9f92c1b194", + "sigma_2": "0c9f57342d4f20f05b3c61ac049230cad852ab541ef46394778aff6e7ff55700", + "sigma_3": "1b50eb6a1b27bfbed21076913ca2892bc35a8be1c9f61e2cfa2e9c708869c049", + "sigma_4": "44260f9f457d96bd0dcdcd9b83c45231bca28ecc5ab52dee9cf59f6b361c520c", + "sigma_sum": "ce4aa87bfa1cd55620200f6d513f5517da54ef4c5c99445904cdc7e9d13d1ae9" + } +} diff --git a/spec/fixtures/secp256k1/repair-share.json b/spec/fixtures/secp256k1/repair-share.json new file mode 100644 index 0000000..f4db9bc --- /dev/null +++ b/spec/fixtures/secp256k1/repair-share.json @@ -0,0 +1,15 @@ +{ + "scalar_generation": { + "random_scalar_1": "1847f6c4a85096e5dbc9e200c9691c5164f8e276d32d4a54ebaf4275474a1403", + "random_scalar_2": "eac5595269d108812eaa865bf62c703a2c128a61fa3bd4dc837b9314bc515204", + "random_scalar_3": "5b3b6084e41c273a39a8d9bbbd87fbcd626c07030142bf78c6c91247bf175700", + "random_scalar_sum": "5e48b09bf63dc6a1441d42187d1d885a38c896f51f633e6e76218944f27c7bc6" + }, + "sigma_generation": { + "sigma_1": "ec3aa83140065181d75b746bfd6bbbbaf212bdfbb3a91670f924d1ca899cbc0c", + "sigma_2": "5dd288d659e0a2dd3ef7523a9cc4f80f4a7f919e9980005c7fbec0961d3fb500", + "sigma_3": "3e62e7461db9ca1ed2f1549a8114bbc87fa9242ce0012ed3f9ac9dcf23f4c30a", + "sigma_4": "684c44e7aba416a1982a8db8ec2a3095f5cc6a3f958a4716b69ae76524dd7200", + "sigma_sum": "f0bc5d356344d51f816ea8fa076fa029f7590120136bec7c6958b9081f7864d5" + } +} diff --git a/spec/frost/repairable_spec.rb b/spec/frost/repairable_spec.rb new file mode 100644 index 0000000..3738be4 --- /dev/null +++ b/spec/frost/repairable_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +RSpec.describe FROST::Repairable do + + let(:group) { ECDSA::Group::Secp256k1 } + let(:max_signers) { 5 } + let(:min_signers) { 3 } + let(:dealer) { FROST::SigningKey.generate(group) } + + shared_examples "Reparable Test" do + it do + # Dealer generate shares. + polynomial = dealer.gen_poly(min_signers - 1) + shares = 1.upto(max_signers).map {|identifier| polynomial.gen_share(identifier) } + + # Signer 2 will lose their share + # Signers (helpers) 1, 4 and 5 will help signer 2 (participant) to recover their share + helper1 = shares[0] + helper4 = shares[3] + helper5 = shares[4] + helper_shares = [helper1, helper4, helper5] + helpers = helper_shares.map(&:identifier) + participant_share = shares[1] + + # Each helper computes delta values. + received_values = {} + helper_shares.each do |helper_share| + delta_values = FROST::Repairable.step1(helpers, participant_share.identifier, helper_share) + delta_values.each do |target_id, value| + received_values[target_id] ||= [] + received_values[target_id] << value + end + end + + # Each helper send sum value to participant. + participant_received_values = [] + received_values.each do |_, values| + participant_received_values << FROST::Repairable.step2(values, group) + end + + repair_share = FROST::Repairable.step3(2, participant_received_values, group) + expect(repair_share.share).to eq(participant_share.share) + end + end + + describe "repair" do + context "secp256k1" do + let(:vector) { load_fixture("secp256k1/repair-share.json") } + end + it_behaves_like "Reparable Test", "secp256k1" + end + + describe "step1" do + it do + # Dealer generate shares. + polynomial = dealer.gen_poly(min_signers - 1) + shares = 1.upto(max_signers).map {|identifier| polynomial.gen_share(identifier) } + helper1 = shares[0] + helper4 = shares[3] + helper5 = shares[4] + helper_shares = [helper1, helper4, helper5] + helpers = helper_shares.map(&:identifier) + participant = shares[1] + + # Generate deltas for helper 4 + deltas = described_class.step1(helpers, participant.identifier, helper4) + + lagrange_coefficient = FROST::Polynomial.derive_interpolating_value(helpers, helper4.identifier, group, x: participant.identifier) + + field = ECDSA::PrimeField.new(group.order) + rhs = field.mod(deltas.values.sum) + lhs = field.mod(helper4.share * lagrange_coefficient) + expect(rhs).to eq(lhs) + end + end + + shared_examples "repair share step2" do + it do + values = vectors['scalar_generation'] + value1 = values['random_scalar_1'] + value2 = values['random_scalar_2'] + value3 = values['random_scalar_3'] + expected = described_class.step2([value1, value2, value3].map(&:hex), group) + expect(expected).to eq(values['random_scalar_sum'].hex) + end + end + + describe "#step2" do + context "secp256k1" do + let(:vectors) { load_fixture("secp256k1/repair-share.json") } + it_behaves_like "repair share step2", "secp256k1" + end + context "P256" do + let(:group) { ECDSA::Group::Secp256r1 } + let(:vectors) { load_fixture("p256/repair-share.json") } + it_behaves_like "repair share step2", "P256" + end + end + + shared_examples "repair share step3" do + it do + sigmas = vectors['sigma_generation'] + sigma1 = sigmas['sigma_1'] + sigma2 = sigmas['sigma_2'] + sigma3 = sigmas['sigma_3'] + sigma4 = sigmas['sigma_4'] + + expected = described_class.step3(2, [sigma1, sigma2, sigma3, sigma4].map(&:hex), group) + expect(expected.share).to eq(sigmas['sigma_sum'].hex) + expect(expected.identifier).to eq(2) + end + end + + describe "#step2" do + context "secp256k1" do + let(:vectors) { load_fixture("secp256k1/repair-share.json") } + it_behaves_like "repair share step3", "secp256k1" + end + context "P256" do + let(:group) { ECDSA::Group::Secp256r1 } + let(:vectors) { load_fixture("p256/repair-share.json") } + it_behaves_like "repair share step3", "P256" + end + end +end \ No newline at end of file