Skip to content

Commit

Permalink
Add RTS feature
Browse files Browse the repository at this point in the history
  • Loading branch information
azuchi committed Feb 19, 2024
1 parent 9c7413d commit 8c304ae
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 4 deletions.
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```
```

### 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)
```
1 change: 1 addition & 0 deletions lib/frost.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 15 additions & 3 deletions lib/frost/polynomial.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
56 changes: 56 additions & 0 deletions lib/frost/repairable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module FROST
# Implements the Repairable Threshold Scheme (RTS) from <https://eprint.iacr.org/2017/1155>
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
15 changes: 15 additions & 0 deletions spec/fixtures/p256/repair-share.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
15 changes: 15 additions & 0 deletions spec/fixtures/secp256k1/repair-share.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
125 changes: 125 additions & 0 deletions spec/frost/repairable_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8c304ae

Please sign in to comment.