Write Your Own SDK checkpoint
This commit is contained in:
@@ -2,3 +2,609 @@
|
|||||||
title: Write Your Own SDK
|
title: Write Your Own SDK
|
||||||
description: Learn to write your own Duckity SDK, for when an official one is not yet available.
|
description: Learn to write your own Duckity SDK, for when an official one is not yet available.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
This guide will guide you through writing your own SDK for Duckity in any language. We'll use
|
||||||
|
examples for JavaScript and Python, taken from the official SDKs, so you can use as reference to
|
||||||
|
make one in any language of your choice.
|
||||||
|
|
||||||
|
## Getting a Challenge [step]
|
||||||
|
|
||||||
|
Duckity serves challenges through the duckling API. It's separate from the management API running on
|
||||||
|
a specialized backend. The hosted endpoint of such version is at `https://api.duckity.com/d1`.
|
||||||
|
Subsequent versions of this API will be named `d2`, `d3`, and so on.
|
||||||
|
|
||||||
|
To get a challenge, you need to be able to make HTTP requests and a protection profile ID. Requests
|
||||||
|
to get challenges look like follows:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /d1/challenges/{protection_profile_id}/issue HTTP/1.1
|
||||||
|
Host: api.duckity.com
|
||||||
|
Accept: application/json
|
||||||
|
X-Duckity-CSRF: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The `X-Duckity-CSRF` header is required to enforce CORS preflight in web browsers and prevent CSRF.
|
||||||
|
Requests will be rejected with a `400 Bad Request` error if it's not set. Any value is valid for it.
|
||||||
|
|
||||||
|
The response will look like this:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
Access-Control-Allow-Origin: https://example.com
|
||||||
|
Access-Control-Allow-Methods: POST
|
||||||
|
Access-Control-Allow-Headers: X-Duckity-CSRF
|
||||||
|
X-RateLimit-Next-In: 1000
|
||||||
|
X-RateLimit-Resets-In: 1000
|
||||||
|
X-RateLimit-Remaining: 0
|
||||||
|
|
||||||
|
{
|
||||||
|
"challenge": "<challenge-string>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The headers returned are those that follow:
|
||||||
|
|
||||||
|
| Name | Description | Value Type | Present When |
|
||||||
|
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `Access-Control-Allow-Origin` | The CORS origin, set to the value of the request's `Origin` header if present and valid. | HTTP URL | `Origin` is present in the request and matches one of the configured CORS origins for the application. |
|
||||||
|
| `Access-Control-Allow-Methods` | The allowed methods for the endpoint. Only `POST` is listed, `OPTIONS` is implicit. | HTTP method list | `Origin` is present in the request and matches one of the configured CORS origins for the application. |
|
||||||
|
| `X-RateLimit-Next-In` | The amount of time to wait until a new request is added to the client's available quota. | Milliseconds | Always |
|
||||||
|
| `X-RateLimit-Resets-In` | The amount of time to wait for the requests quota to be fully reset. | Milliseconds | Always |
|
||||||
|
| `X-RateLimit-Remaining` | The amount of requests remaining in the client's current quota after this request. | Amount of requests | Always |
|
||||||
|
| `X-RateLimit-Penalty-Resets-In` | The amount of time to wait for the penalty to be lifted, if any. This does not equal the amount of time to wait to make a new request. | Milliseconds | The client is currently penalized. |
|
||||||
|
|
||||||
|
When the client has made too many requests, the response will instead be an error:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP/1.1 429 Too Many Requests
|
||||||
|
Content-Type: application/json
|
||||||
|
X-RateLimit-Next-In: 1000
|
||||||
|
X-RateLimit-Resets-In: 1000
|
||||||
|
X-RateLimit-Remaining: 0
|
||||||
|
X-RateLimit-Penalty-Resets-In: 500
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Too Many Requests",
|
||||||
|
"message": "You've made too many requests. Try again later."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Other errors, like an invalid protection profile, will result in a response of similar body
|
||||||
|
structure and with the corresponding status code.
|
||||||
|
|
||||||
|
## Decoding the Challenge [step]
|
||||||
|
|
||||||
|
Challenge strings have two sections separated by a dot. The first section is the challenge's data,
|
||||||
|
the second section is a signature to prevent tampering.
|
||||||
|
|
||||||
|
```
|
||||||
|
<challenge-data>.<challenge-signature>
|
||||||
|
```
|
||||||
|
|
||||||
|
Both sections are URL-safe Base64 strings with no padding. To read the challenge data, decode the
|
||||||
|
Base64 string and parse the JSON inside it. The decoded JSON will have the following structure:
|
||||||
|
|
||||||
|
```ts lineNumbers
|
||||||
|
type Challenge = {
|
||||||
|
challenge_id: string; // The challenge's unique ID. 21-character NanoID using the default alphabet.
|
||||||
|
ip: string; // The IP of the client the challenge was issued for. Only this client may submit a solution for validation.
|
||||||
|
timestamp: number; // The milliseconds since UNIX epoch when the challenge was issued.
|
||||||
|
protection_profile_id: string; // The ID of the protection profile this challenge was issued for. 21-character NanoID using the default alphabet.
|
||||||
|
n: number[]; // An array of 32-bit unsigned digits, most significant digit first. The Wesolowski VDF N parameter.
|
||||||
|
x: number[]; // An array of 32-bit unsigned digits, most significant digit first. The Wesolowski VDF X parameter.
|
||||||
|
t: number; // The Wesolowski VDF T parameter.
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
These code pieces implement the decoding as a reference.
|
||||||
|
|
||||||
|
<Tabs items={["TypeScript", "Python"]}>
|
||||||
|
<Tab value="TypeScript">
|
||||||
|
```ts lineNumbers
|
||||||
|
interface Challenge {
|
||||||
|
// The challenge's unique ID.
|
||||||
|
challenge_id: string;
|
||||||
|
// The client's IP.
|
||||||
|
ip: string;
|
||||||
|
// The unix timestamp, in milliseconds, in which the challenge was issued.
|
||||||
|
timestamp: number;
|
||||||
|
// The ID of the protection profile this challenge was issued for.
|
||||||
|
protection_profile_id: string;
|
||||||
|
// The unsigned 32-bit digits of the N Wesolowski VDF parameter, most significant digit first.
|
||||||
|
n: number[];
|
||||||
|
// The unsigned 32-bit digits of the X Wesolowski VDF parameter, most significant digit first.
|
||||||
|
x: number[];
|
||||||
|
// The T Wesolowski VDF parameter.
|
||||||
|
t: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the challenge's data from the string.
|
||||||
|
*
|
||||||
|
* @param challenge The raw challenge string.
|
||||||
|
* @returns The decoded challenge metadata.
|
||||||
|
*/
|
||||||
|
export function decode(challenge: string): Challenge {
|
||||||
|
// Counts all dots in the challenge string
|
||||||
|
if (![1, 2].includes((challenge.match(/\./g) || []).length)) {
|
||||||
|
throw Error("The challenge string contained too many or not enough sections.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let [base64, _signature] = challenge.split(".", 2);
|
||||||
|
let json = atob(base64 as string);
|
||||||
|
let data: Challenge = JSON.parse(json as string);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab value="Python">
|
||||||
|
```py lineNumbers
|
||||||
|
import base64, json
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Challenge:
|
||||||
|
id: str
|
||||||
|
"""The challenge's unique ID."""
|
||||||
|
ip: str
|
||||||
|
"""The IP of the client this challenge was issued for."""
|
||||||
|
timestamp: int
|
||||||
|
"""The UNIX timestamp in milliseconds at which this challenge was issued."""
|
||||||
|
protection_profile_id: str
|
||||||
|
"""The ID of the protection profile this challenge was issued for."""
|
||||||
|
n: list[int]
|
||||||
|
"""The unsigned 32-bit digits of the N Wesolowski VDF parameter, most significant digit first."""
|
||||||
|
x: list[int]
|
||||||
|
"""The unsigned 32-bit digits of the X Wesolowski VDF parameter, most significant digit first."""
|
||||||
|
t: int
|
||||||
|
"""The T Wesolowski VDF parameter."""
|
||||||
|
|
||||||
|
|
||||||
|
def decode(challenge: str) -> Challenge:
|
||||||
|
"""Decodes a challenge string into its data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
challenge (str): The raw challenge string received from the duckling API.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Challenge: The challenge's decoded data.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: The challenge string was not valid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if challenge.count(".") not in [2, 3]:
|
||||||
|
raise ValueError("The challenge string contained too many or not enough parts.")
|
||||||
|
|
||||||
|
raw = challenge.split(".", 1)
|
||||||
|
raw = raw[0]
|
||||||
|
|
||||||
|
# Readds padding for decoding
|
||||||
|
data = base64.urlsafe_b64decode(raw + "=" * (-len(raw) % 4))
|
||||||
|
data = json.loads(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Challenge(
|
||||||
|
id=data["challenge_id"],
|
||||||
|
ip=data["ip"],
|
||||||
|
timestamp=data["timestamp"],
|
||||||
|
protection_profile_id=data["protection_profile_id"],
|
||||||
|
n=data["n"],
|
||||||
|
x=data["x"],
|
||||||
|
t=data["t"]
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError("The decoded challenge data was incomplete.") from e
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Solving the Challenge [step]
|
||||||
|
|
||||||
|
Solving challenges is relatively simple. It does, however, require to implement a few utility
|
||||||
|
functions by hand. Some libraries, like GMP, provide optimized versions of these functions. The
|
||||||
|
Python examples will show how to use GMP via the `gmpy2` library. The TypeScript examples will show
|
||||||
|
how to implement all of these functions by hand using JavaScript's `bigint`. In JavaScript, bigints
|
||||||
|
are written like normal `number`s with the `n` prefix, e.g. `1n`.
|
||||||
|
|
||||||
|
### Convert Digits to Integers [step]
|
||||||
|
|
||||||
|
The N and X challenge parameters are encoded as arrays of 32-bit unsigned digits. To be able to
|
||||||
|
operate on those numbers comfortably and efficiently, we first have to parse those digits.
|
||||||
|
|
||||||
|
The algoritm is simple. Start with `result = 0`, then for every digit in the array do
|
||||||
|
`result = (result << 32) | digit`. The following pseudocode displays it simply:
|
||||||
|
|
||||||
|
```
|
||||||
|
function getIntegerFromDigits(digits: number[]):
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
for digit in digits:
|
||||||
|
result = (result << 32) | digit
|
||||||
|
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
Easy. Then we can use that function on both X and N. The following examples show actual code
|
||||||
|
implementations of such function:
|
||||||
|
|
||||||
|
<Tabs items={["TypeScript", "Python"]}>
|
||||||
|
<Tab value="TypeScript">
|
||||||
|
```ts lineNumbers
|
||||||
|
/**
|
||||||
|
* Converts an array of u32 digits to a bigint.
|
||||||
|
*
|
||||||
|
* @param digits The digits of the bigint, MSF.
|
||||||
|
* @returns The formed bigint.
|
||||||
|
*/
|
||||||
|
function getBigintFromDigits(digits: number[]): bigint {
|
||||||
|
let result = 0n;
|
||||||
|
|
||||||
|
for (const digit of digits) {
|
||||||
|
result = (result << 32n) | BigInt(digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab value="Python">
|
||||||
|
```py lineNumbers
|
||||||
|
# Your life will be brighter if you install `gmpy2-stubs` as a dev dependency, `gmpy2` does
|
||||||
|
# not have typing stubs integrated, therefore no IDE autocompletion nor help.
|
||||||
|
import gmpy2
|
||||||
|
|
||||||
|
|
||||||
|
def get_mpz_from_digits(digits: list[int]) -> gmpy2.mpz:
|
||||||
|
"""Converts a list of u32 digits to a `gmpy2.mpz`.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
digits (list[int]): The unsigned 32-bit digits.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
gmpy2.mpz: The resulting GMP number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return gmpy2.mpz.from_bytes(b"".join(i.to_bytes(4) for i in digits))
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Perform the Computation [step]
|
||||||
|
|
||||||
|
Once we have our utility function to get numbers from digit arrays, we can perform the calculation
|
||||||
|
that burns CPU cycles. The result of this step will later be used to compute a proof; Should this
|
||||||
|
step be skipped, the proof will fail to validate.
|
||||||
|
|
||||||
|
In plain English, the computation is simple: starting with `Y = X`, repeat `Y = Y^2 % N` `T` amount
|
||||||
|
of times. In pseudocode:
|
||||||
|
|
||||||
|
```
|
||||||
|
y = x
|
||||||
|
loop t times:
|
||||||
|
y = y**2 % n
|
||||||
|
```
|
||||||
|
|
||||||
|
`Y` will be our solution to the challenge.
|
||||||
|
|
||||||
|
This step is not skippable given that it's effectively `X^2^T % N`. Since `T` is large and so is X,
|
||||||
|
computing it in one operation is infeasible. For example, with `T = 1,000,000`, `2^T` has about
|
||||||
|
301,030 decimal digits (1,000,001 bits). By using the algorithm above in a loop, we can get the
|
||||||
|
result of the full computation without needing to build `2^T` nor `X^2^T` nor store it in memory.
|
||||||
|
|
||||||
|
This step is also not parallelizable. Each iteration needs to know the result of the previous one,
|
||||||
|
which leaves no advantage to GPUs. In code, this step would be
|
||||||
|
|
||||||
|
<Tabs items={["TypeScript", "Python"]}>
|
||||||
|
<Tab value="TypeScript">
|
||||||
|
```ts lineNumbers
|
||||||
|
function solve(nDigits: number[], xDigits: number[], tNumber: number): bigint {
|
||||||
|
let n = getBigintFromDigits(nDigits);
|
||||||
|
let x = getBigintFromDigits(xDigits);
|
||||||
|
let y = x;
|
||||||
|
|
||||||
|
for (let i = 0; i < t; i++) {
|
||||||
|
y = y ** 2 % n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab value="Python">
|
||||||
|
```py lineNumbers
|
||||||
|
import gmpy2
|
||||||
|
|
||||||
|
|
||||||
|
def solve(n: list[int], x: list[int], t: int) -> gmpy2.mpz:
|
||||||
|
n = get_mpz_from_digits(n)
|
||||||
|
x = get_mpz_from_digits(x)
|
||||||
|
y = x
|
||||||
|
|
||||||
|
for _ in range(t):
|
||||||
|
y = y**2 % n
|
||||||
|
|
||||||
|
return n
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Callout title="Solving in Async Contexts">
|
||||||
|
When solving a challenge in async contexts, this operation will block the runtime. To prevent
|
||||||
|
it, run the operation in a worker thread/process and wait for it to finish asynchronously.
|
||||||
|
|
||||||
|
In the JavaScript SDK, for example, solving the challenge is done in a web worker to prevent it
|
||||||
|
from blocking the UI.
|
||||||
|
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### Proving the Solution (Z) [step]
|
||||||
|
|
||||||
|
Now that we have our solution Y, let's make a proof we actually solved the problem. This will allow
|
||||||
|
the server to validate our solution without having to do all the iterations again. If our solution
|
||||||
|
happens to be invalid, the challenge will fail to validate to the server.
|
||||||
|
|
||||||
|
Making a proof is divided into two parts: getting parameter L and getting parameter PI (our proof).
|
||||||
|
PI depends on L, so lets get L first.
|
||||||
|
|
||||||
|
To get L, we'll create an intermediate parameter Z which is the result of SHA-256-hashing the
|
||||||
|
following parameters (in order):
|
||||||
|
|
||||||
|
1. `duckity` as ASCII bytes,
|
||||||
|
2. The challenge's `N` as 512 MSF bytes padded with 0s,
|
||||||
|
3. The challenge's `X` as 512 MSF bytes padded with 0s,
|
||||||
|
4. The challenge's `T` as 512 MSF bytes padded with 0s,
|
||||||
|
5. Our `Y` as 512 MSF bytes padded with 0s.
|
||||||
|
|
||||||
|
The result of hashing those parameters together will be 32 bytes, which will be our `Z` parameter.
|
||||||
|
|
||||||
|
For this, we'll need a new function to convert our bigints to digits (bytes). The algorithm is
|
||||||
|
simple, the inverse of what we used for our `getBigintFromDigits` or `get_mpz_from_digits` to decode
|
||||||
|
`N` and `X` in the previous step. In pseudocode, it's
|
||||||
|
|
||||||
|
```
|
||||||
|
function getDigitsFromBigint(number: bigint, arrayWidth: number, digitByteWidth: number):
|
||||||
|
digits = [0] * arrayWidth
|
||||||
|
|
||||||
|
mask = 0;
|
||||||
|
loop digitByteWidth times:
|
||||||
|
mask = mask << 8
|
||||||
|
mask = mask | 0b1111_1111
|
||||||
|
|
||||||
|
for i in 0..arrayWidth: # [0; arrayWidth), from 0 to arrayWidth - 1 inclusive.
|
||||||
|
digit = number & mask
|
||||||
|
digits[arrayWidth - 1 - i] = digit
|
||||||
|
number = number >> (digitByteWidth * 8)
|
||||||
|
|
||||||
|
return digits
|
||||||
|
```
|
||||||
|
|
||||||
|
In TypeScript and Python, that's
|
||||||
|
|
||||||
|
<Tabs items={["TypeScript", "Python"]}>
|
||||||
|
<Tab value="TypeScript">
|
||||||
|
```ts lineNumbers
|
||||||
|
/**
|
||||||
|
* Converts a bigint into a MSF digit array.
|
||||||
|
*
|
||||||
|
* @param number The bigint number to convert.
|
||||||
|
* @param arrayWidth The amount of digits to return, padded with 0s.
|
||||||
|
* @param digitByteWidth The amount of bytes each digit will carry.
|
||||||
|
*
|
||||||
|
* @returns The digits array.
|
||||||
|
*/
|
||||||
|
function getDigitsFromBigint(number: bigint, arrayWidth: number, digitByteWidth: number): number[] {
|
||||||
|
let digits: number[] = new Array(arrayWidth).fill(0);
|
||||||
|
|
||||||
|
let mask = 0n;
|
||||||
|
for (let i = digitByteWidth; i > 0; i--) {
|
||||||
|
mask <<= 8n;
|
||||||
|
mask |= 0b1111_1111n;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = digits - 1; i >= 0; i--) {
|
||||||
|
let digit = Number(number & mask);
|
||||||
|
digits[i] = digit;
|
||||||
|
number = number >> (digitByteWidth * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab value="Python">
|
||||||
|
```py lineNumbers
|
||||||
|
import gmpy2
|
||||||
|
|
||||||
|
|
||||||
|
def get_digits_from_mpz(number: gmpy2.mpz | int, array_width: int, digit_byte_width: int) -> list[int]:
|
||||||
|
"""Converts a MPZ integer to its MSF digits.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
number (gmpy2.mpz | int): The number to convert to digits.
|
||||||
|
array_width (int): The amount of digits to return, padded with 0s.
|
||||||
|
digit_byte_width (int): The amount of bytes each digit will carry.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: The digits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
digits = [0] * array_width
|
||||||
|
|
||||||
|
mask = 0
|
||||||
|
for i in range(digit_byte_width):
|
||||||
|
mask <<= 8
|
||||||
|
mask |= 0b1111_1111
|
||||||
|
|
||||||
|
for i in range(array_width):
|
||||||
|
digit = number & mask
|
||||||
|
digits[array_width - 1 - i] = int(digit)
|
||||||
|
number >>= digit_byte_width * 8
|
||||||
|
|
||||||
|
return digits
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
Now that we have our bigint to digit converters, we can hash our values as described above.
|
||||||
|
|
||||||
|
```
|
||||||
|
hasher = SHA256()
|
||||||
|
hasher.update("duckity")
|
||||||
|
hasher.update(getDigitsFromBigint(n, 512, 1))
|
||||||
|
hasher.update(getDigitsFromBigint(x, 512, 1))
|
||||||
|
hasher.update(getDigitsFromBigint(t, 512, 1))
|
||||||
|
hasher.update(getDigitsFromBigint(y, 512, 1))
|
||||||
|
z = hasher.finish()
|
||||||
|
```
|
||||||
|
|
||||||
|
Easy. The following examples show how to do such thing in TypeScript and Python.
|
||||||
|
|
||||||
|
<Tabs items={["TypeScript", "Python"]}>
|
||||||
|
<Tab value="TypeScript">
|
||||||
|
```ts lineNumbers
|
||||||
|
/**
|
||||||
|
* Hashes the provided parameters to get Z.
|
||||||
|
*
|
||||||
|
* @param n The challenge's N param.
|
||||||
|
* @param x The challenge's X param.
|
||||||
|
* @param t The challenge's T param.
|
||||||
|
* @param y The computed Y param.
|
||||||
|
*
|
||||||
|
* @returns Z as bytes.
|
||||||
|
*/
|
||||||
|
async function getZ(n: bigint, x: bigint, t: bigint, y: bigint): number[] {
|
||||||
|
let bytes = Array.from(new TextEncoder().encode("duckity"));
|
||||||
|
bytes = bytes.concat(...getDigitsFromBigint(n, 512, 1));
|
||||||
|
bytes = bytes.concat(...getDigitsFromBigint(x, 512, 1));
|
||||||
|
bytes = bytes.concat(...getDigitsFromBigint(t, 512, 1));
|
||||||
|
bytes = bytes.concat(...getDigitsFromBigint(y, 512, 1));
|
||||||
|
|
||||||
|
let hash = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
||||||
|
let z = Array.from(new Uint8Array(hash));
|
||||||
|
|
||||||
|
return z
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab value="Python">
|
||||||
|
```py lineNumbers
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import gmpy2
|
||||||
|
|
||||||
|
|
||||||
|
def get_z(n: gmpy2.mpz, x: gmpy2.mpz, t: int, y: gmpy2.mpz) -> list[int]:
|
||||||
|
"""Hashes the provided parameters to get Z.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
n (gmpy2.mpz): The challenge's N parameter.
|
||||||
|
x (gmpy2.mpz): The challenge's X parameter.
|
||||||
|
t (int): The challenge's T parameter.
|
||||||
|
y (gmpy2.mpz): The computed Y parameter.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: Z as bytes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
hash = hashlib.sha256()
|
||||||
|
hash.update(b"duckity")
|
||||||
|
hash.update(get_digits_from_mpz(n, 512, 1))
|
||||||
|
hash.update(get_digits_from_mpz(x, 512, 1))
|
||||||
|
hash.update(get_digits_from_mpz(t, 512, 1))
|
||||||
|
hash.update(get_digits_from_mpz(y, 512, 1))
|
||||||
|
z = hash.digest()
|
||||||
|
|
||||||
|
return z
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Proving the Solution (L) [step]
|
||||||
|
|
||||||
|
Getting `L` from `Z` involves interpreting our `Z` bytes as a big integer and getting the next prime
|
||||||
|
using the Miller-Rabin algorithm.
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
This section will explain how to do both, yet it's likely that you will find an existing library
|
||||||
|
for Miller-Rabin with an implementation you can use out of the box. If you choose to use such
|
||||||
|
library instead, make sure you test on exactly 40 bases.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
For this, we'll have to update our previous `getBigintFromDigits` or `get_mpz_from_digits` to allow
|
||||||
|
arbitrary digit sizes. That's simple, instead of hardcoding the digit size we'll pass it as an
|
||||||
|
argument to the function. Our pseudocode will then look like
|
||||||
|
|
||||||
|
```
|
||||||
|
function getIntegerFromDigits(digits: number[], digit_byte_width: number):
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
for digit in digits:
|
||||||
|
result = (result << (digit_byte_width * 8)) | digit
|
||||||
|
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
In TypeScript and Python, that's
|
||||||
|
|
||||||
|
<Tabs items={["TypeScript", "Python"]}>
|
||||||
|
<Tab value="TypeScript">
|
||||||
|
```ts lineNumbers
|
||||||
|
/**
|
||||||
|
* Converts an array of digits to a bigint.
|
||||||
|
*
|
||||||
|
* @param digits The digits of the bigint, MSF.
|
||||||
|
* @param digitByteWidth The amount of bytes each digit carries.
|
||||||
|
* @returns The formed bigint.
|
||||||
|
*/
|
||||||
|
function getBigintFromDigits(digits: number[], digitByteWidth: number): bigint {
|
||||||
|
let result = 0n;
|
||||||
|
|
||||||
|
for (const digit of digits) {
|
||||||
|
result = (result << (8n * BigInt(digitByteWidth))) | BigInt(digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab value="Python">
|
||||||
|
```py lineNumbers
|
||||||
|
import gmpy2
|
||||||
|
|
||||||
|
|
||||||
|
def get_mpz_from_digits(digits: list[int], digit_byte_width: int) -> gmpy2.mpz:
|
||||||
|
"""Converts a list digits to a `gmpy2.mpz`.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
digits (list[int]): The unsigned digits.
|
||||||
|
digit_byte_width (int): The byte width each digit carries.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
gmpy2.mpz: The resulting GMP number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return gmpy2.mpz.from_bytes(b"".join(i.to_bytes(digit_byte_width) for i in digits))
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
Then in our solving code, update the function calls to convert `N` and `X` to bigints with a byte
|
||||||
|
width set to 4 bytes.
|
||||||
|
|
||||||
|
Now let's interpret `Z` as a bigint. Since `Z` is currently the result of hashing our parameters
|
||||||
|
together, each byte will be a single-byte digit.
|
||||||
|
|
||||||
|
```
|
||||||
|
z = getBigintFromDigits(z, 1)
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user