Duckity

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:

Can I have a challenge? Sure, have a challenge string Solve the challenge received Here, I solved the challenge Is this solution token valid? Yes/no Process the rest of the request if ready, otherwise not. Response Client Duckling Server

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/json

The 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:

NameDescriptionValue TypePresent When
Access-Control-Allow-OriginThe CORS origin, set to the value of the request's Origin header if present and valid.HTTP URLOrigin is present in the request and matches one of the configured CORS origins for the application.
Access-Control-Allow-MethodsThe allowed methods for the endpoint. Only POST is listed, OPTIONS is implicit.HTTP method listOrigin is present in the request and matches one of the configured CORS origins for the application.
X-RateLimit-Next-InThe amount of time to wait until a new request is added to the client's available quota.MillisecondsAlways
X-RateLimit-Resets-InThe amount of time to wait for the requests quota to be fully reset.MillisecondsAlways
X-RateLimit-RemainingThe amount of requests remaining in the client's current quota after this request.Amount of requestsAlways
X-RateLimit-Penalty-Resets-InThe 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.MillisecondsThe 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 result

Easy. 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 % 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

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):

  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 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 digits

In 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 result

In 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 result

In 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 false

In 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 true

In 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 number

Note 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) % n

This 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.

On this page