Write Your Own SDK
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.
This guide intentionally explains everything in simple words and with multiple examples. You'll find it easy to follow this guide as long as you're comfortable in the language you're using.
The flow is simple:
Getting a Challenge
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 policy ID. Requests to get challenges look like follows:
POST /d1/challenges/{policy_id}/issue HTTP/1.1
Host: api.duckity.com
Accept: application/jsonThe response will look like this:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
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/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 policy, will result in a response of similar body structure and with the corresponding status code.
Decoding the Challenge
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:
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.
policy_id: string; // The ID of the policy 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.
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 policy this challenge was issued for.
policy_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 {
if (challenge.match(/\./g) || []).length != 1) {
throw Error("The challenge string contained too many or not enough sections.");
}
let [base64, _signature] = challenge.split(".", 2);
let json = atob(base64.replaceAll("-", "+").replaceAll("_", "/") as string);
let data: Challenge = JSON.parse(json as string);
return data;
}Solving the Challenge
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 numbers with the n prefix, e.g. 1n.
Convert Digits to Integers
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 get_integer_from_digits(digits: number[]) -> integer:
result = 0
⠀
for digit in digits:
result = (result << 32) | digit
⠀
return resultEasy. Then we can use that function on both X and N. The following examples show actual code implementations of such function:
/**
* 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;
}Perform the Computation
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 % nY 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
function solve(nDigits: number[], xDigits: number[], t: 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;
}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.
Proving the Solution (Z)
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):
duckityas ASCII bytes,- The challenge's
Nas 512 MSF bytes padded with 0s, - The challenge's
Xas 512 MSF bytes padded with 0s, - The challenge's
Tas 512 MSF bytes padded with 0s, - Our
Yas 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 get_bigint_from_digits() to decode N and X in the
previous step. In pseudocode, it's
function get_digits_from_bigint(number: bigint, array_width: number, digit_byte_width: number) -> number[]:
digits = [0] * array_width
⠀
mask = 0;
loop digit_byte_width times:
mask = mask << 8
mask = mask | 0b1111_1111
⠀
for i in 0..array_width: # [0; array_width), from 0 to array_width - 1 inclusive.
digit = number & mask
digits[array_width - 1 - i] = digit
number = number >> (digit_byte_width * 8)
⠀
return digitsIn TypeScript and Python, that's
/**
* 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 = arrayWidth - 1; i >= 0; i--) {
let digit = Number(number & mask);
digits[i] = digit;
number = number >> (digitByteWidth * 8);
}
return digits;
}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.
/**
* 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
}Proving the Solution (L)
Getting L from Z involves interpreting our Z bytes as a big integer and getting the next prime
using the Miller-Rabin algorithm.
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.
For this, we'll have to update our previous get_bigint_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 get_integer_from_digits(digits: number[], digit_byte_width: number):
result = 0
⠀
for digit in digits:
result = (result << (digit_byte_width * 8)) | digit
⠀
return resultIn TypeScript and Python, that's
/**
* 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;
}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 = get_bigint_from_digits(z, 1)To get L, we need to find the next prime to our Z. E.g. if Z = 5, L = 7. Note how 5 is
already a prime, yet we get the next one regardless.
Getting the next prime is simple. Starting with Z + 1, we call 40 Miller-Rabin tests to check
whether it's prime. Many libraries out there already implement Miller-Rabin tests so you don't have
to implement it yourself. In case you don't want to, or cannot, use such libraries, we'll explain
how to implement it manually here.
The implementation described below is good enough for the values handled by the API. It is not
meant to be general purpose beyond this SDK's implementation, and edge cases that do not apply
to the calculations needed may be ignored. The Duckling server uses GMP's next_prime()
function instead of a manual implementation.
Our implementation will be divided into 4 functions: is_prime(number, amount_of_bases),
is_prime_for_base(number, base), get_next_prime(number, amount_of_bases), and
mod_pow(base, exponent, modulus).
mod_pow
This function will return the result of (A^B) % C without computing A^B. This way, we do not
have to store numbers too big in memory. The algorithm is simple:
function mod_pow(base: bigint, exponent: bigint, modulus: bigint) -> bigint:
result = 1
⠀
while exponent != 0:
if (exponent & 1) == 1:
result = result * base
result = result % modulus
⠀
base = base * base
base = base % modulus
⠀
exponent = exponent / 2
⠀
return resultIn TypeScript, this equals to
/**
* Computes `(base ^ exponent) % modulus` efficiently, without storing `base ^ exponent` in
* memory.
*
* @param base The base of the modular exponentiation.
* @param exponent The exponent of the modular exponentiation.
* @param modulus The modulus of the modular exponentiation.
*
* @returns The result of the modular exponentiation as a bigint.
*/
function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint {
let result = 1n;
while (exponent !== 0n) {
// (something & 1n) is faster than (something % 2n)
if ((exponent & 1n) === 1n) {
result *= base;
result %= modulus;
}
base **= 2n;
base %= modulus;
exponent /= 2n;
}
return result;
}is_prime_for_base
This function will test a number using Miller-Rabin for the specified base. By later testing with
different bases in is_prime(), we increase the likelyhood of a number being prime. The test works
by exponentially reducing the chances of a number being a prime until it is extremely unlikely it's
not a prime, without checking whether the number is divisible by other numbers one by one. The
algorithm is simple:
function is_prime_for_base(number, base) -> boolean:
number_minus_one = number - 1
odd = number_minus_one
base_times = 0
⠀
while odd % 2 == 0:
odd = odd / 2
base_times = base_times + 1
⠀
odd_power = mod_pow(base, odd, number)
⠀
if odd_power == 1 or odd_power == number_minus_one:
return true
⠀
loop base_times - 1 times:
odd_power = (odd_power * odd_power) % number
⠀
if odd_power == number_minus_one:
return true
⠀
return falseIn TypeScript, this can be implemented as
/**
* Runs a single Miller-Rabin test on a number for a specific base.
*
* This function assumes basic checks have been run on the number, like division by two and
* by five.
*
* @param number The number to check if is prime.
* @param base The base to use to check if the number is prime.
*
* @returns If the primality test yielded positive for the specified base.
*/
function isPrimeForBase(number: bigint, base: bigint): boolean {
let numberMinusOne = number - 1n;
let odd = numberMinusOne;
let baseTimes = 0;
while (odd % 2n == 0n) {
odd /= 2n;
baseTimes += 1;
}
let oddPower = modPow(base, odd, number);
if (oddPower == 1n || oddPower == numberMinusOne) {
return true;
}
for (let i = 0n; i < baseTimes - 1n; i++) {
oddPower = modPow(oddPower, 2, number);
if (oddPower == numberMinusOne) {
return true;
}
}
return false;
}is_prime
Checking whether a number is likely a prime is simple. We'll discard numbers that pass basic tests
like division by 2 and by five, then we'll call is_prime_for_base() in loop starting in 2
increasing the loop by 1 after every iteration. All numbers between 2 and number - 1 inclusive are
valid tests. In essence,
function is_prime(number, amount_of_tests) -> boolean:
if number in [2, 3, 5, 7]:
return true
⠀
if number % 2 == 0 or number % 5 == 0:
return false
⠀
base = 2
⠀
loop amount_of_tests times:
if base >= number:
break
⠀
if not is_prime_for_base(number, base): // Any test returning false means the number is certainly not a prime.
return false
⠀
return trueIn TypeScript, that is
/**
* Runs up to `tests` amount of tests to check whether a number is a prime.
*
* @param number The number to check for primality.
* @param tests The maximum amount of tests to run on the number. Setting this value to a
* higher number means better precision at the cost of speed.
*
* @returns `true` if the number is likely a prime, `false` if the number is not a prime.
*/
function isPrime(number: bigint, tests: number): boolean {
if ([2n, 3n, 5n, 7n].includes(number)) {
return true;
}
// (something & 1n) is faster than (something % 2n)
if ((number & 1n) === 0n) {
return false;
}
if (number % 5n === 0n) {
return false;
}
let base = 2n;
for (let i = 0n; i < BigInt(tests) && base + i < number; i++) {
let prime = isPrimeForBase(number, base + i);
if (!prime) {
return false;
}
}
return true;
}get_next_prime
Last but not least, let's call our is_prime in a loop starting from number + 1 until we find a
prime. We'll set the amount of tests to 40.
This algorithm is the most simple of all the L-getting flow:
loop indefinitely:
number = number + 1
⠀
if is_prime(number, 40):
return numberNote that the number = number + 1 goes before the is_prime() call. We don't want the initial
number parameter to be returned if it's prime.
In TypeScript, that's
/**
* Returns the first prime after a number.
*
* Numbers are checked for primality using Miller-Rabin tests on 40 bases each number.
*
* @param number The number to get the following prime for.
*
* @returns The number's following prime.
*/
function getNextPrime(number: bigint): bigint {
while (true) {
number += 1n;
if (isPrime(number, 40)) {
return number;
}
}
}Now that we can get our next prime, it's finally time to get L.
l = get_next_prime(z)That's it! In TypeScript and Python, that's
let l = getNextPrime(z);Proving the Solution (PI)
Once we have L, our last step towards getting everything we need to get a solution token is
getting PI. We want a parameter Q which comes from 2^T = Q * L + R. Then for PI, we use
PI = X^Q % N. Not to compute 2^T directly, we'll use a short algorithm to compute PI
progressively:
r = 1
pi = 1
⠀
loop t times:
r = r * 2
⠀
if r >= l:
r = r - l
pi = (pi * pi * x) % n
else:
pi = (pi * pi) % nThis way, we get our solution PI without filling up our memory with 2^T. In Python and
TypeScript, this is
let r = 1n;
let pi = 1n;
for (let i = 0n; i < t; i++) {
r = r * 2n;
if (r >= l) {
r = r - l;
pi = (pi * pi * x) % n;
} else {
pi = (pi * pi) % n;
}
}Once that's done, we'll have our final PI value.
Encoding the Solution
Once we have our Y and PI values, it's time to encode them into a solution token. As stated at
the beginning of this guide, challenge tokens are encoded as
<challenge-data>.<challenge-signature>We'll now encode our solution into a third segment to have a token that looks like follows:
<challenge-data>.<challenge-signature>.<solution-data>Encoding the solution data segment is the inverse of decodind the challenge data segment. We'll
convert our PI and Y values to MSF u32 digit arrays of 128 digits using our
get_digits_from_bigint(), then encode that as JSON and the JSON as URL-safe Base64 with no
padding. The encoded JSON will have the following schema:
type Solution = {
pi: number[];
y: number[];
};In TypeScript and Python, that's done as follows:
export function encode(original: string, y: bigint, pi: bigint): string {
let solution = {
y: getDigitsFromBigint(y, 128, 4),
pi: getDigitsFromBigint(pi, 128, 4),
};
let solutionJson = JSON.stringify(solution);
let solutionBase64 = btoa(solutionJson)
.replaceAll("=", "")
.replaceAll("+", "-")
.replaceAll("/", "_");
return `${original}.${solutionBase64}`;
}That's it! Once you have your solution token encoded, you're ready to send it to the server for validation.
Validating the Solution
Validating the solution is done server-side. Usually, the solution token is posted to the application's backend server via a REST API and the server validates the challenge.
Sending the token from the client to the server is application-specific and not covered by SDKs. Once the server has received the token, the first thing the server should do is to send a request to the Duckling API to check whether the token is valid.
The request looks like follows:
POST /d1/{policy_id}/validate HTTP/1.1
Host: api.duckity.com
Authorization: Bearer <application-id>
Accept: application/json
Content-Type: application/json
{
"solution": "<solution-token>",
"ip": "123.123.123.123"
}The "solution" key in the request's body will be the solution submitted by the client. The "ip"
will be the IP of the client that submitted the solution token.
In case the server for any reason does not have a way of getting the client's IP, it can use the
"ip" key in the challenge's encoded data. Note that this is NOT recommended as it will allow
clients to submit solution tokens issued for different clients. The server must use the client's
IP whenever possible.
The duckling API's response will look like follows:
HTTP/1.1 200 OK
Content-Type: application/json
{
"is_valid": true
}That's it! Your SDK has its core implementation done, now it's time to write documentation for it, error handling where due, and anything specific to the platform you have developed the SDK for.