-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Function to compute optimal ecmult_multi scratch size for a number of points #638
base: master
Are you sure you want to change the base?
Changes from all commits
8040c1f
4c5ac49
8a29272
4d78e9b
2d7a903
0d574b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -347,9 +347,61 @@ static void secp256k1_ecmult(secp256k1_gej *r, const secp256k1_gej *a, const sec | |
secp256k1_ecmult_strauss_wnaf(&state, r, 1, a, na, ng); | ||
} | ||
|
||
static size_t secp256k1_ecmult_sum_array(size_t *array, size_t n) { | ||
size_t i = 0; | ||
size_t sum = 0; | ||
for (i = 0; i < n; i++) { | ||
sum += array[i]; | ||
} | ||
return sum; | ||
} | ||
|
||
/** | ||
* If the as_allocated argument is 0, this function will return the sum of the | ||
* sizes of the individual parts. Otherwise it will return the size that is | ||
* actually allocated with secp256k1_scratch_alloc which is greater or equal. | ||
*/ | ||
static size_t secp256k1_strauss_scratch_size_raw(size_t n_points, int as_allocated) { | ||
size_t sizes[STRAUSS_SCRATCH_OBJECTS]; | ||
sizes[0] = n_points * sizeof(secp256k1_gej); | ||
sizes[1] = n_points * sizeof(secp256k1_scalar); | ||
sizes[2] = n_points * ECMULT_TABLE_SIZE(WINDOW_A) * sizeof(secp256k1_fe); | ||
sizes[3] = n_points * ECMULT_TABLE_SIZE(WINDOW_A) * sizeof(secp256k1_ge); | ||
sizes[4] = n_points * sizeof(struct secp256k1_strauss_point_state); | ||
|
||
if (as_allocated) { | ||
size_t size; | ||
int ret = secp256k1_scratch_alloc_size(&size, sizes, STRAUSS_SCRATCH_OBJECTS); | ||
/* The n_points argument is not greater than | ||
* ECMULT_MAX_POINTS_PER_BATCH. As long as it is not too large, | ||
* scratch_alloc_size does not fail. */ | ||
VERIFY_CHECK(ECMULT_MAX_POINTS_PER_BATCH == 5000000 && ret); | ||
return size; | ||
} else { | ||
return secp256k1_ecmult_sum_array(sizes, STRAUSS_SCRATCH_OBJECTS); | ||
} | ||
} | ||
|
||
/* Returns the scratch size required for a given number of points (excluding | ||
* base point G) as it would be allocated on a scratch space. */ | ||
static size_t secp256k1_strauss_scratch_size(size_t n_points) { | ||
static const size_t point_size = (sizeof(secp256k1_ge) + sizeof(secp256k1_fe)) * ECMULT_TABLE_SIZE(WINDOW_A) + sizeof(struct secp256k1_strauss_point_state) + sizeof(secp256k1_gej) + sizeof(secp256k1_scalar); | ||
return n_points*point_size; | ||
return secp256k1_strauss_scratch_size_raw(n_points, 1); | ||
} | ||
|
||
static int secp256k1_ecmult_strauss_batch_allocate(const secp256k1_callback* error_callback, secp256k1_scratch *scratch, size_t n_points, secp256k1_gej **points, secp256k1_scalar **scalars, struct secp256k1_strauss_state *state) { | ||
/* We allocate STRAUSS_SCRATCH_OBJECTS objects on the scratch space. If these | ||
* allocations change, make sure to update the STRAUSS_SCRATCH_OBJECTS | ||
* constant and strauss_scratch_size accordingly. */ | ||
*points = (secp256k1_gej*)secp256k1_scratch_alloc(error_callback, scratch, n_points * sizeof(secp256k1_gej)); | ||
*scalars = (secp256k1_scalar*)secp256k1_scratch_alloc(error_callback, scratch, n_points * sizeof(secp256k1_scalar)); | ||
state->aux = (secp256k1_fe*)secp256k1_scratch_alloc(error_callback, scratch, n_points * ECMULT_TABLE_SIZE(WINDOW_A) * sizeof(secp256k1_fe)); | ||
state->pre_a = (secp256k1_ge*)secp256k1_scratch_alloc(error_callback, scratch, n_points * ECMULT_TABLE_SIZE(WINDOW_A) * sizeof(secp256k1_ge)); | ||
state->ps = (struct secp256k1_strauss_point_state*)secp256k1_scratch_alloc(error_callback, scratch, n_points * sizeof(struct secp256k1_strauss_point_state)); | ||
return points != NULL | ||
&& scalars != NULL | ||
&& state->aux != NULL | ||
&& state->pre_a != NULL | ||
&& state->ps != NULL; | ||
} | ||
|
||
static int secp256k1_ecmult_strauss_batch(const secp256k1_callback* error_callback, secp256k1_scratch *scratch, secp256k1_gej *r, const secp256k1_scalar *inp_g_sc, secp256k1_ecmult_multi_callback cb, void *cbdata, size_t n_points, size_t cb_offset) { | ||
|
@@ -363,17 +415,7 @@ static int secp256k1_ecmult_strauss_batch(const secp256k1_callback* error_callba | |
if (inp_g_sc == NULL && n_points == 0) { | ||
return 1; | ||
} | ||
|
||
/* We allocate STRAUSS_SCRATCH_OBJECTS objects on the scratch space. If these | ||
* allocations change, make sure to update the STRAUSS_SCRATCH_OBJECTS | ||
* constant and strauss_scratch_size accordingly. */ | ||
points = (secp256k1_gej*)secp256k1_scratch_alloc(error_callback, scratch, n_points * sizeof(secp256k1_gej)); | ||
scalars = (secp256k1_scalar*)secp256k1_scratch_alloc(error_callback, scratch, n_points * sizeof(secp256k1_scalar)); | ||
state.aux = (secp256k1_fe*)secp256k1_scratch_alloc(error_callback, scratch, n_points * ECMULT_TABLE_SIZE(WINDOW_A) * sizeof(secp256k1_fe)); | ||
state.pre_a = (secp256k1_ge*)secp256k1_scratch_alloc(error_callback, scratch, n_points * ECMULT_TABLE_SIZE(WINDOW_A) * sizeof(secp256k1_ge)); | ||
state.ps = (struct secp256k1_strauss_point_state*)secp256k1_scratch_alloc(error_callback, scratch, n_points * sizeof(struct secp256k1_strauss_point_state)); | ||
|
||
if (points == NULL || scalars == NULL || state.aux == NULL || state.pre_a == NULL || state.ps == NULL) { | ||
if (!secp256k1_ecmult_strauss_batch_allocate(error_callback, scratch, n_points, &points, &scalars, &state)) { | ||
secp256k1_scratch_apply_checkpoint(error_callback, scratch, scratch_checkpoint); | ||
return 0; | ||
} | ||
|
@@ -396,8 +438,27 @@ static int secp256k1_ecmult_strauss_batch_single(const secp256k1_callback* error | |
return secp256k1_ecmult_strauss_batch(error_callback, scratch, r, inp_g_sc, cb, cbdata, n, 0); | ||
} | ||
|
||
/* Returns the maximum number of points in addition to G that can be used with a | ||
* given scratch space. If a scratch space has exactly | ||
* `strauss_scratch_size(n_points)` left, then | ||
* `strauss_max_points(cb, scratch) = n_points`. */ | ||
static size_t secp256k1_strauss_max_points(const secp256k1_callback* error_callback, secp256k1_scratch *scratch) { | ||
return secp256k1_scratch_max_allocation(error_callback, scratch, STRAUSS_SCRATCH_OBJECTS) / secp256k1_strauss_scratch_size(1); | ||
/* Call max_allocation with 0 objects because otherwise it would assume | ||
* worst case padding but in this function we want to be exact. */ | ||
size_t max_alloc = secp256k1_scratch_max_allocation(error_callback, scratch, 0); | ||
size_t unpadded_single_size = secp256k1_strauss_scratch_size_raw(1, 0); | ||
size_t n_points = max_alloc / unpadded_single_size; | ||
if (n_points > 0 | ||
&& max_alloc < secp256k1_strauss_scratch_size(n_points)) { | ||
/* If there's not enough space after alignment is taken into | ||
* account, it suffices to decrease n_points by one. This is because | ||
* the maximum padding required is less than an entry. */ | ||
n_points -= 1; | ||
VERIFY_CHECK(max_alloc >= secp256k1_strauss_scratch_size(n_points)); | ||
VERIFY_CHECK(max_alloc - secp256k1_scratch_max_allocation(error_callback, scratch, STRAUSS_SCRATCH_OBJECTS) < unpadded_single_size); | ||
} | ||
|
||
return n_points; | ||
} | ||
|
||
/** Convert a number to WNAF notation. | ||
|
@@ -630,22 +691,90 @@ SECP256K1_INLINE static void secp256k1_ecmult_endo_split(secp256k1_scalar *s1, s | |
} | ||
} | ||
|
||
static size_t secp256k1_pippenger_scratch_size_constant(int bucket_window) { | ||
/* 4 objects are accounted for in pippenger_scratch_size_points */ | ||
enum { N_SIZES = PIPPENGER_SCRATCH_OBJECTS - 4 }; | ||
int ret; | ||
size_t size; | ||
size_t sizes[N_SIZES]; | ||
sizes[0] = sizeof(struct secp256k1_pippenger_state); | ||
sizes[1] = sizeof(secp256k1_gej) << bucket_window; | ||
|
||
ret = secp256k1_scratch_alloc_size(&size, sizes, N_SIZES); | ||
/* The inputs to scratch_alloc_size are constant */ | ||
VERIFY_CHECK(ret); | ||
return size; | ||
} | ||
|
||
static SECP256K1_INLINE size_t secp256k1_pippenger_entries(size_t n_points) { | ||
return 2*n_points + 2; | ||
} | ||
|
||
/** | ||
* Returns the scratch size required for a given number of points excluding | ||
* base point G and excluding the parts not dependent on the number of points. | ||
* If called with 0 n_points it'll return the size required for the base point | ||
* G. If the as_allocated argument is 0, this function will return the sum of | ||
* the sizes of the individual parts. Otherwise it will return the size that is | ||
* actually allocated with secp256k1_scratch_alloc which is greater or equal. | ||
*/ | ||
static size_t secp256k1_pippenger_scratch_size_points(size_t n_points, int bucket_window, int as_allocated) { | ||
size_t entries = secp256k1_pippenger_entries(n_points); | ||
/* 2 objects are accounted for in pippenger_scratch_size_constant */ | ||
enum { N_SIZES = PIPPENGER_SCRATCH_OBJECTS - 2 }; | ||
size_t sizes[N_SIZES]; | ||
sizes[0] = entries * sizeof(secp256k1_ge); | ||
sizes[1] = entries * sizeof(secp256k1_scalar); | ||
sizes[2] = entries * sizeof(struct secp256k1_pippenger_point_state); | ||
sizes[3] = entries * WNAF_SIZE(bucket_window+1) * sizeof(int); | ||
if (as_allocated) { | ||
size_t size; | ||
int ret = secp256k1_scratch_alloc_size(&size, sizes, N_SIZES); | ||
/* The n_points argument is not greater than | ||
* ECMULT_MAX_POINTS_PER_BATCH. As long as it is not too large, | ||
* scratch_alloc_size does not fail. */ | ||
VERIFY_CHECK(ECMULT_MAX_POINTS_PER_BATCH == 5000000 && ret); | ||
return size; | ||
} else { | ||
return secp256k1_ecmult_sum_array(sizes, N_SIZES); | ||
} | ||
} | ||
|
||
/** | ||
* Returns the scratch size required for a given number of points (excluding | ||
* base point G) without considering alignment. | ||
* base point G) as it would be allocated on a scratch space. | ||
*/ | ||
static size_t secp256k1_pippenger_scratch_size(size_t n_points, int bucket_window) { | ||
size_t entries = 2*n_points + 2; | ||
size_t entry_size = sizeof(secp256k1_ge) + sizeof(secp256k1_scalar) + sizeof(struct secp256k1_pippenger_point_state) + (WNAF_SIZE(bucket_window+1)+1)*sizeof(int); | ||
return (sizeof(secp256k1_gej) << bucket_window) + sizeof(struct secp256k1_pippenger_state) + entries * entry_size; | ||
return secp256k1_pippenger_scratch_size_constant(bucket_window) | ||
+ secp256k1_pippenger_scratch_size_points(n_points, bucket_window, 1); | ||
} | ||
|
||
static int secp256k1_ecmult_pippenger_batch_allocate(const secp256k1_callback* error_callback, secp256k1_scratch *scratch, size_t entries, int bucket_window, secp256k1_ge **points, secp256k1_scalar **scalars, secp256k1_gej **buckets, struct secp256k1_pippenger_state **state_space) { | ||
/* We allocate PIPPENGER_SCRATCH_OBJECTS objects on the scratch space. If | ||
* these allocations change, make sure to update the | ||
* PIPPENGER_SCRATCH_OBJECTS constant and pippenger_scratch_size | ||
* accordingly. */ | ||
*points = (secp256k1_ge *) secp256k1_scratch_alloc(error_callback, scratch, entries * sizeof(secp256k1_ge)); | ||
*scalars = (secp256k1_scalar *) secp256k1_scratch_alloc(error_callback, scratch, entries * sizeof(secp256k1_scalar)); | ||
*state_space = (struct secp256k1_pippenger_state *) secp256k1_scratch_alloc(error_callback, scratch, sizeof(struct secp256k1_pippenger_state)); | ||
if (*points == NULL || *scalars == NULL || *state_space == NULL) { | ||
return 0; | ||
} | ||
(*state_space)->ps = (struct secp256k1_pippenger_point_state *) secp256k1_scratch_alloc(error_callback, scratch, entries * sizeof(struct secp256k1_pippenger_point_state)); | ||
(*state_space)->wnaf_na = (int *) secp256k1_scratch_alloc(error_callback, scratch, entries * WNAF_SIZE(bucket_window+1) * sizeof(int)); | ||
*buckets = (secp256k1_gej *) secp256k1_scratch_alloc(error_callback, scratch, sizeof(secp256k1_gej) << bucket_window); | ||
if ((*state_space)->ps == NULL || (*state_space)->wnaf_na == NULL || *buckets == NULL) { | ||
return 0; | ||
} | ||
return 1; | ||
} | ||
|
||
static int secp256k1_ecmult_pippenger_batch(const secp256k1_callback* error_callback, secp256k1_scratch *scratch, secp256k1_gej *r, const secp256k1_scalar *inp_g_sc, secp256k1_ecmult_multi_callback cb, void *cbdata, size_t n_points, size_t cb_offset) { | ||
const size_t scratch_checkpoint = secp256k1_scratch_checkpoint(error_callback, scratch); | ||
/* Use 2(n+1) with the endomorphism, when calculating batch | ||
* sizes. The reason for +1 is that we add the G scalar to the list of | ||
* other scalars. */ | ||
size_t entries = 2*n_points + 2; | ||
size_t entries = secp256k1_pippenger_entries(n_points); | ||
secp256k1_ge *points; | ||
secp256k1_scalar *scalars; | ||
secp256k1_gej *buckets; | ||
|
@@ -660,22 +789,7 @@ static int secp256k1_ecmult_pippenger_batch(const secp256k1_callback* error_call | |
return 1; | ||
} | ||
bucket_window = secp256k1_pippenger_bucket_window(n_points); | ||
|
||
/* We allocate PIPPENGER_SCRATCH_OBJECTS objects on the scratch space. If | ||
* these allocations change, make sure to update the | ||
* PIPPENGER_SCRATCH_OBJECTS constant and pippenger_scratch_size | ||
* accordingly. */ | ||
points = (secp256k1_ge *) secp256k1_scratch_alloc(error_callback, scratch, entries * sizeof(*points)); | ||
scalars = (secp256k1_scalar *) secp256k1_scratch_alloc(error_callback, scratch, entries * sizeof(*scalars)); | ||
state_space = (struct secp256k1_pippenger_state *) secp256k1_scratch_alloc(error_callback, scratch, sizeof(*state_space)); | ||
if (points == NULL || scalars == NULL || state_space == NULL) { | ||
secp256k1_scratch_apply_checkpoint(error_callback, scratch, scratch_checkpoint); | ||
return 0; | ||
} | ||
state_space->ps = (struct secp256k1_pippenger_point_state *) secp256k1_scratch_alloc(error_callback, scratch, entries * sizeof(*state_space->ps)); | ||
state_space->wnaf_na = (int *) secp256k1_scratch_alloc(error_callback, scratch, entries*(WNAF_SIZE(bucket_window+1)) * sizeof(int)); | ||
buckets = (secp256k1_gej *) secp256k1_scratch_alloc(error_callback, scratch, (1<<bucket_window) * sizeof(*buckets)); | ||
if (state_space->ps == NULL || state_space->wnaf_na == NULL || buckets == NULL) { | ||
if (!secp256k1_ecmult_pippenger_batch_allocate(error_callback, scratch, entries, bucket_window, &points, &scalars, &buckets, &state_space)) { | ||
secp256k1_scratch_apply_checkpoint(error_callback, scratch, scratch_checkpoint); | ||
return 0; | ||
} | ||
|
@@ -721,31 +835,47 @@ static int secp256k1_ecmult_pippenger_batch_single(const secp256k1_callback* err | |
return secp256k1_ecmult_pippenger_batch(error_callback, scratch, r, inp_g_sc, cb, cbdata, n, 0); | ||
} | ||
|
||
/** | ||
* Returns the maximum number of points in addition to G that can be used with | ||
* a given scratch space. The function ensures that fewer points may also be | ||
* used. | ||
/* Returns the (near) maximum number of points in addition to G that can be | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Edit: Could this fail to be a maximum because the constant space used by the buckets decreases when you jump to the next bucket window size? |
||
* used with a given scratch space. It may not return the actual maximum number | ||
* of points possible. Otherwise, fewer points would not fit into the scratch | ||
* space in general. If a scratch space has exactly | ||
* `pippenger_scratch_size(n_points)` left, then | ||
* `pippenger_max_points(cb, scratch) <= n_points`. | ||
*/ | ||
static size_t secp256k1_pippenger_max_points(const secp256k1_callback* error_callback, secp256k1_scratch *scratch) { | ||
size_t max_alloc = secp256k1_scratch_max_allocation(error_callback, scratch, PIPPENGER_SCRATCH_OBJECTS); | ||
/* Call max_allocation with 0 objects because otherwise it would assume | ||
* worst case padding but in this function we want to be exact. */ | ||
size_t max_alloc = secp256k1_scratch_max_allocation(error_callback, scratch, 0); | ||
int bucket_window; | ||
size_t res = 0; | ||
|
||
for (bucket_window = 1; bucket_window <= PIPPENGER_MAX_BUCKET_WINDOW; bucket_window++) { | ||
size_t n_points; | ||
size_t max_points = secp256k1_pippenger_bucket_window_inv(bucket_window); | ||
size_t space_for_points; | ||
size_t space_overhead; | ||
size_t entry_size = sizeof(secp256k1_ge) + sizeof(secp256k1_scalar) + sizeof(struct secp256k1_pippenger_point_state) + (WNAF_SIZE(bucket_window+1)+1)*sizeof(int); | ||
size_t space_constant; | ||
/* Compute entry size without taking alignment into account */ | ||
size_t entry_size = secp256k1_pippenger_scratch_size_points(0, bucket_window, 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style nit (feel free to ignore): Should this be called |
||
|
||
entry_size = 2*entry_size; | ||
space_overhead = (sizeof(secp256k1_gej) << bucket_window) + entry_size + sizeof(struct secp256k1_pippenger_state); | ||
if (space_overhead > max_alloc) { | ||
space_constant = secp256k1_pippenger_scratch_size_constant(bucket_window); | ||
if (space_constant + entry_size > max_alloc) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style nit (feel free to ignore): Was this check previously here for avoiding underflow (rather than short-circuiting)? If so would it make sense to keep the check as |
||
break; | ||
} | ||
space_for_points = max_alloc - space_overhead; | ||
space_for_points = max_alloc - space_constant; | ||
|
||
/* Compute an upper bound for the number excluding the base point G. | ||
* It's an upper bound because alignment is not taken into account. */ | ||
n_points = space_for_points / entry_size - 1; | ||
if (n_points > 0 | ||
&& space_for_points < secp256k1_pippenger_scratch_size_points(n_points, bucket_window, 1)) { | ||
/* If there's not enough space after alignment is taken into | ||
* account, it suffices to decrease n_points by one. This is because | ||
* the maximum padding required is less than an entry. */ | ||
n_points -= 1; | ||
VERIFY_CHECK(max_alloc - secp256k1_scratch_max_allocation(error_callback, scratch, PIPPENGER_SCRATCH_OBJECTS) < entry_size); | ||
VERIFY_CHECK(space_for_points >= secp256k1_pippenger_scratch_size_points(n_points, bucket_window, 1)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After some discussion with @jonasnick, one of the things I'm not sure about is the added complexity in this function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like both hands are arguments in favor of the change (i.e. calling We need to do that for strauss anyway because otherwise
wouldn't hold. |
||
} | ||
|
||
n_points = space_for_points/entry_size; | ||
n_points = n_points > max_points ? max_points : n_points; | ||
if (n_points > res) { | ||
res = n_points; | ||
|
@@ -856,4 +986,20 @@ static int secp256k1_ecmult_multi_var(const secp256k1_callback* error_callback, | |
return 1; | ||
} | ||
|
||
/** | ||
* Returns the optimal scratch space size for a given number of points | ||
* excluding base point G. | ||
*/ | ||
static size_t secp256k1_ecmult_multi_scratch_size(size_t n_points) { | ||
if (n_points > ECMULT_MAX_POINTS_PER_BATCH) { | ||
n_points = ECMULT_MAX_POINTS_PER_BATCH; | ||
} | ||
if (n_points >= ECMULT_PIPPENGER_THRESHOLD) { | ||
int bucket_window = secp256k1_pippenger_bucket_window(n_points); | ||
return secp256k1_pippenger_scratch_size(n_points, bucket_window); | ||
} else { | ||
return secp256k1_strauss_scratch_size(n_points); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Approach ACK There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's an approach ACK? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I guess it's a concept ack. I was confused by other meanings of approach :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm actively testing bitcoin/bitcoin#16149 here edit: except that I just write "ACK". All my "ACK"s in this review mean "ACK thorough code inspection" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (No action needed) Responding to this 2 year old comment 😅 In Core, it looks like "Approach ACK" means "Concept ACK, and I agree with the approach of this change (but I haven't reviewed the code in detail)": https://github.com/bitcoin/bitcoin/blob/master/CONTRIBUTING.md#conceptual-review |
||
} | ||
|
||
#endif /* SECP256K1_ECMULT_IMPL_H */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Should the comment go above the definition of the function
secp256k1_pippenger_entries
instead?