This directory contains unit and integration tests for the COTI SDK Typescript library. Below is a detailed documentation of the tests related to itUint256 and ctUint256 types.
- Contract Implementation Details
- /coti-sdk-typescript type definitions
- Ciphertext and Signature Generation
- SDK Comparison
- itUint256 and ctUint256 Tests
- Test Execution Report
- Next Steps for Publishing
- Changes to
@coti-ethers - Critique & Improvement Suggestions
The operations and type definitions for itUint256 and ctUint256 are implemented in the coti-contracts repository.
Both structures are defined in contracts/utils/mpc/MpcCore.sol:
gtUint256(line 18): Garbled Text 256-bit integer, composed of twogtUint128(High and Low parts).ctUint256(line 42): Composed of twoctUint128(High and Low parts).itUint256(line 77): Contains thectUint256 ciphertextand abytes[2][2] signature.
The logic for handling these types is implemented in the MpcCore library. Below is a summary of the supported 256-bit operations for gtUint256:
- Arithmetic:
add(Addition),sub(Subtraction),mul(Multiplication).[!NOTE]
div(Division) andrem(Remainder) are NOT implemented forgtUint256. - Bitwise:
and(AND),or(OR),xor(XOR). - Shifts:
shl(Shift Left),shr(Shift Right).
- Equality:
eq(==),ne(!=). - Relational:
gt(>),ge(>=),lt(<),le(<=). - Selection:
min(Minimum),max(Maximum),mux(Multiplexer/Select).
validateCiphertext:itUint256→gtUint256(Validates and converts input).onBoard:ctUint256→gtUint256(Converts ciphertext to garbled text).offBoard:gtUint256→ctUint256(Converts garbled text back to ciphertext).offBoardToUser:gtUint256→ctUint256(Re-encrypts the result for a specific user).setPublic256:uint256→gtUint256(Converts a public value to garbled text).decrypt:gtUint256→uint256(Decrypts garbled text to public value; usually for tests/debug).
rand256: Generates a randomgtUint256.randBoundedBits256: Generates a randomgtUint256with a specific bit size.
Arithmetic and bitwise operations are not executed directly in the client-side JavaScript/TypeScript code. Instead, you use @coti-ethers to deploy and call a Smart Contract that implements these operations using MpcCore.sol.
The Process:
- Write a Solidity Contract: Create a contract that imports
MpcCore.soland exposes functions to callMpcCore.add(...),MpcCore.sub(...), etc. - Generate Input (Client-Side): Use
@coti-io/coti-sdk-typescriptto create an encrypted input (itUint256). - Call Contract (Client-Side): Use
@coti-ethersto send a transaction to your contract with that input. - Execute (On-Chain): The network executes the operation securely on the MPC nodes.
- Decrypt Result (Client-Side): Use
@coti-io/coti-sdk-typescriptto decrypt the result returned by the contract (after it's offboarded).
Conceptual Example:
import { prepareIT256, decryptUint256 } from '@coti-io/coti-sdk-typescript';
import { Contract, Wallet } from 'ethers'; // or coti-ethers
// 1. Prepare Input (Client)
// Encrypt the value 100
const { ciphertext, signature } = prepareIT256(100n, sender, contractAddress, functionSelector);
// 2. Call Contract (Using coti-ethers)
// This sends the tx to the blockchain where 'MpcCore.add' is actually run
const tx = await myContract.addEncryptedValues(
{ ciphertext, signature }, // itUint256
someOtherEncryptedValue // gtUint256 (internal) or another input
);
// 3. Decrypt Result (Client-Side)
const result = await tx.wait();
// ... logic to parse event or view function result ...
const decrypted = decryptUint256(resultCiphertext, userKey);A clear example of how to use these in a contract can be found in contracts/mocks/utils/mpc/Miscellaneous256BitTestsContract.sol.
This contract demonstrates several key operations for gtUint256:
validateCiphertextTest: Accepts an array ofitUint256inputs, validates them intogtUint256usingMpcCore.validateCiphertext, and decrypts them back touint256.setPublicTest: Converts an array of publicuint256values intogtUint256usingMpcCore.setPublic256and decrypts them.offBoardToUserTest: Demonstrates taking a public value, converting it togtUint256, and then re-encrypting it for the caller (offboarding) usingMpcCore.offBoardToUser.transferTest: Simulates a transfer operation. It takes sender balance (a), receiver balance (b), and an transfer amount, all as public integers. It converts them togtUint256, performs an encrypted transfer usingMpcCore.transfer, and decrypts the results (new balances and success status).transferWithAllowanceTest: Similar totransferTestbut includes an allowance check, employingMpcCore.transferWithAllowance.
Represents the input structure required for sending an encrypted 256-bit unsigned integer to a contract. It includes the split ciphertext and a signature for integrity.
type itUint256 = {
ciphertext: {
ciphertextHigh: bigint;
ciphertextLow: bigint
};
signature: Uint8Array;
};Represents the ciphertext of a 256-bit unsigned integer, split into two 128-bit parts (High and Low) to accommodate the underlying encryption scheme.
type ctUint256 = {
ciphertextHigh: bigint;
ciphertextLow: bigint;
};Source: createCiphertext256 (Lines 482-488) in src/crypto_utils.ts
The 256-bit integer is encrypted using a split-key AES approach, handling the value as two 128-bit blocks (High and Low).
-
Splitting: The 256-bit integer is split into two 128-bit parts:
- High Part: The most significant 128 bits. (If input <= 128 bits, this is 0).
- Low Part: The least significant 128 bits.
-
Encryption (per 128-bit block): Each part is encrypted separately using AES-128 with a unique random value
$r$ .- Generates a random 128-bit value
$r$ . - Computes
$EncryptedR = AES_{key}(r)$ . - Computes
$Ciphertext = EncryptedR \oplus Plaintext$ .
- Generates a random 128-bit value
-
Combination: The final ciphertext is a 64-byte array structured as:
[CiphertextHigh (16b), rHigh (16b), CiphertextLow (16b), rLow (16b)]
Source: signIT (Lines 426-431) in src/crypto_utils.ts
To ensure integrity and prevent replay attacks, a signature is generated over the transaction parameters.
- Packing: The following data is packed and hashed using
keccak256:senderAddress(bytes)contractAddress(bytes)functionSelector(bytes4)ciphertext(64 bytes, as generated above)
- Signing: The resulting hash is signed using the sender's ECDSA private key.
- Result: The signature is returned as a 65-byte
Uint8Array.- Format:
[...r, ...s, v] - r: 32 bytes (ECDSA signature component)
- s: 32 bytes (ECDSA signature component)
- v: 1 byte (Recovery identifier, 0 or 1)
- Why
Uint8Array?: This is the standard binary data format in JavaScript/TypeScript environments, ensuring compatibility with thebytestype in Solidity contracts and efficient raw data handling.
- Format:
flowchart LR
subgraph Inputs
SA[Sender Address]
CA[Contract Address]
FS[Function Selector]
CT[Ciphertext]
PK[Private Key]
end
subgraph Transformation
P[Pack Parameters]
H[Keccak256 Hash]
S[ECDSA Sign]
end
subgraph Output
Sig[Signature Uint8Array]
end
subgraph Components
R[r: 32 bytes]
s_val[s: 32 bytes]
V[v: 1 byte]
end
SA & CA & FS & CT --> P
P --> H
H --> S
PK --> S
S --> Sig
Sig --> R
Sig --> s_val
Sig --> V
style S fill:#f9f,stroke:#333
style Sig fill:#bbf,stroke:#333
This section details the new functions added in this local version of the SDK compared to the latest published version (@coti-io/coti-sdk-typescript@1.0.4).
The published version supports up to 128-bit integers using buildInputText. The local version extends this to support 256-bit integers with a new split-key encryption scheme and introduces new named functions.
| Feature | Published Version (1.0.4) |
Local Version (0.5.5+) |
|---|---|---|
| Input Generation | buildInputText |
buildInputText, prepareIT, prepareIT256 |
| Max Integer Size | 128-bit | 256-bit |
| Ciphertext Format | Single bigint (128-bit) |
bigint (128-bit) OR Struct { high, low } (256-bit) |
| Decryption | decryptUint |
decryptUint, decryptUint256 |
| New Types | itUint, ctUint |
itUint, ctUint, itUint256, ctUint256 |
Important
API Changes:
prepareIT: Included in the local version as an alias/alternative tobuildInputText.prepareIT256: A completely new function required for encryptinguint256values (e.g., ERC20 amounts). It produces a 2-part ciphertext that must be handled differently by the contract.decryptUint256: Added to decrypt the new split-format 256-bit ciphertexts.
These tests verify the functionality of 256-bit integer encryption (prepareIT256) and decryption (decryptUint256), as well as the structure of the resulting types.
Unit Tests: tests/unit/crypto_utils.test.ts
These tests focus on the cryptographic correctness of the prepareIT256 and decryptUint256 functions.
Source: Lines 771-774
Function Tested: prepareIT256
- Purpose: To verify that
prepareIT256correctly handles plaintexts of different bit lengths. - Method Executed:
prepareIT256( PLAINTEXT, sender, contractAddress, functionSelector )
- Parameters Used:
PLAINTEXT:(2n ** BigInt(bitSize)) - 1nforbitSize= 100, 129, 200, 255, and 256.sender:{ wallet: new Wallet(TEST_PRIVATE_KEY), userKey: TEST_USER_KEY }contractAddress:'0x0000000000000000000000000000000000000001'functionSelector:'0x11223344'
- Expected Results:
- Returns object with
ciphertext(ciphertextHigh,ciphertextLow) andsignature. ciphertextHigh> 0n,ciphertextLow> 0n.signatureis a non-emptyUint8Array.
- Returns object with
Source: Lines 776-794
Function Tested: prepareIT256
-
Purpose: To ensure the function throws a
RangeErrorfor plaintexts larger than 256 bits. -
Method Executed:
prepareIT256(PLAINTEXT, ...) -
Parameters Used:
-
PLAINTEXT:2n ** 256n(Which is$2^{256}$ , a 257-bit number).
-
-
Expected Results: Throws
RangeErrorwith message "Plaintext size must be 256 bits or smaller".
Source: Lines 796-799 Functions Tested:
-
prepareIT256(Encryption) -
decryptUint256(Decryption) -
Purpose: To verify that encryption followed by decryption yields the original value.
-
Methods Executed:
const { ciphertext } = prepareIT256(PLAINTEXT, sender, ...)const decrypted = decryptUint256(ciphertext, USER_KEY)
-
Parameters Used:
PLAINTEXTvalues:(2n ** 100n) - 12345n(100-bit)2n ** 128n + 12345n(129-bit)(2n ** 200n) - 12345n(200-bit)(2n ** 256n) - 1n(256-bit)
USER_KEY: Matching the key used insenderforprepareIT256.
-
Expected Results:
decrypted === PLAINTEXTfor all cases.
Integration Tests: tests/integration/format.compatibility.test.ts
These tests focus on the structure and format compatibility of the produced objects, ensuring they align with contract expectations (struct consistency).
-
Purpose: To validate that
prepareIT256returns an object matching theitUint256TypeScript type definition. -
Inputs:
plaintext=$2^{200}$ . -
Expected Results:
- Result object has
ciphertextandsignatureproperties. -
ciphertexthasciphertextHighandciphertextLow. - Properties have correct types (
bigint,Uint8Array).
- Result object has
- Purpose: To ensure the generated BigInts are valid positive integers.
-
Inputs:
plaintext=$2^{200}$ . -
Expected Results:
ciphertextHighandciphertextLoware greater than 0.
- Purpose: To check if the output can be serialized into a format suitable for passing to a smart contract struct (e.g., string representation of numbers).
-
Inputs:
plaintext=$2^{200}$ . -
Expected Results:
ciphertextHighandciphertextLowcan be successfully converted to strings.
- Purpose: To verify that the BigInt components can be converted to Hex strings (common for Ethereum JSON-RPC interactions).
-
Inputs:
plaintext=$2^{200}$ . -
Expected Results:
ciphertextHighandciphertextLowconvert to valid hex strings starting with0x.
The following table documents the results of running the "Round-Trip Encryption/Decryption" unit tests for prepareIT256 and decryptUint256.
| Test Case | Input Value (Dec) | Encrypted Sample (High, Low) | Decrypted | Result |
|---|---|---|---|---|
| 100-bit value | 0xffffffff...ffffcfc7 |
High: 0xcbfabb8e...Low: 0x31032483... |
0xffffffff...ffffcfc7 |
PASS |
| 129-bit value | 0x10000000...00003039 |
High: 0x2daa19a9...Low: 0xda3bb4a2... |
0x10000000...00003039 |
PASS |
| 200-bit value | 0xffffffff...ffffcfc7 |
High: 0x7dcbe32e...Low: 0xd106cdb3... |
0xffffffff...ffffcfc7 |
PASS |
| 256-bit value | 0xffffffff...ffffffff |
High: 0x57076ee1...Low: 0x48625a01... |
0xffffffff...ffffffff |
PASS |
Note: Encrypted values change on each run due to random salt/iv.
To generate these values, you can run the following script using npx ts-node.
- Create a file
generate_report.tsin the project root:
import { prepareIT256, decryptUint256 } from './src/crypto_utils';
import { Wallet } from 'ethers';
import dotenv from 'dotenv';
dotenv.config();
const TEST_CONSTANTS = {
PRIVATE_KEY: process.env.TEST_PRIVATE_KEY || '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
USER_KEY: process.env.TEST_USER_KEY || '00112233445566778899aabbccddeeff',
CONTRACT_ADDRESS: '0x0000000000000000000000000000000000000001',
FUNCTION_SELECTOR: '0x11223344'
};
const sender = { wallet: new Wallet(TEST_CONSTANTS.PRIVATE_KEY), userKey: TEST_CONSTANTS.USER_KEY };
const testValues = [
{ name: '100-bit value', val: (2n ** 100n) - 12345n },
{ name: '129-bit value', val: 2n ** 128n + 12345n },
{ name: '200-bit value', val: (2n ** 200n) - 12345n },
{ name: '256-bit value', val: (2n ** 256n) - 1n }
];
console.log('| Test Case | Input Value (Dec) | Encrypted (High, Low) | Decrypted | Result |');
console.log('|---|---|---|---|---|');
testValues.forEach(test => {
const it = prepareIT256(test.val, sender, TEST_CONSTANTS.CONTRACT_ADDRESS, TEST_CONSTANTS.FUNCTION_SELECTOR);
const decrypted = decryptUint256(it.ciphertext, TEST_CONSTANTS.USER_KEY);
const result = decrypted === test.val ? 'PASS' : 'FAIL';
// Format helper
const fmt = (v: any) => '0x' + v.toString(16);
const trunc = (s: string) => s.length > 20 ? s.slice(0, 10) + '...' + s.slice(-8) : s;
const inputHex = fmt(test.val);
const ctHighHex = fmt(it.ciphertext.ciphertextHigh);
const ctLowHex = fmt(it.ciphertext.ciphertextLow);
const decryptedHex = fmt(decrypted);
console.log(`| ${test.name} | \`${trunc(inputHex)}\` | High: \`${trunc(ctHighHex)}\`<br>Low: \`${trunc(ctLowHex)}\` | \`${trunc(decryptedHex)}\` | ${result} |`);
});- Run the script:
npx ts-node generate_report.tsTo release these changes as a new version of the @coti-io/coti-sdk-typescript package:
- Run Tests: Ensure all unit and integration tests pass.
npm test - Increment Version: Update the
versionfield inpackage.jsonaccording to Semantic Versioning (e.g.,0.6.0or1.1.0since this adds new features).npm version minor
- Build: Compile the TypeScript source code to JavaScript (outputs to
/dist).npm run build
- Publish: Push the new version to the npm registry.
npm publish --access public
To support the new 256-bit types and operations, the @coti-ethers library has been updated with new methods in both Wallet and JsonRpcSigner classes.
The library now re-exports the following types from @coti-io/coti-sdk-typescript:
itUint256ctUint256
Both Wallet (for direct private key usage) and JsonRpcSigner (for browser wallets like MetaMask) now support:
Encrypts a plaintext value (integer or bigint) into an itUint256 structure.
async encryptValue256(
plaintextValue: bigint | number,
contractAddress: string,
functionSelector: string
): Promise<itUint256>- Input: Auto-detects bit size. Throws error if value > 256 bits.
- Implementation Difference:
- Wallet: Uses
prepareIT256directly from the SDK. - JsonRpcSigner: Re-implements the logic internally to support
signMessage()(personal_sign) which is required for browser wallets, as they don't expose raw private keys.
- Wallet: Uses
Decrypts a ctUint256 ciphertext back to a bigint.
async decryptValue256(ciphertext: ctUint256): Promise<bigint>- Input: Must be a valid
ctUint256object{ ciphertextHigh, ciphertextLow }. - Prerequisite: The wallet/signer must be onboarded (have a user AES key).
JsonRpcSigner: 256-bit operations are fully tested intest/jsonRpcSigner.test.ts.Wallet: Currently lacks specific tests for 256-bit operations (only 128-bit operations are tested intest/wallet.test.ts).
This section provides an objective analysis of the current itUint256/ctUint256 implementation, highlighting areas for improvement.
The contract implementation (MpcCore.sol) explicitly does not support div and rem for gtUint256. This is a significant limitation for financial applications that require division (e.g., fee calculations, percentage splits).
Warning
Applications requiring encrypted division must find workarounds (e.g., pre-computing values off-chain or using multiplication by inverses).
Walletclass incoti-ethershas no unit tests forencryptValue256ordecryptValue256. This creates risk when usingWalletin server-side or automated scripts.- Integration tests do not cover end-to-end contract interactions—only format validation.
- Error path coverage is minimal (only overflow is tested, not invalid key formats, signature mismatches, etc.).
The SDK offers both buildInputText and prepareIT for similar purposes, but they have different limitations:
buildInputText: Strictly limited to 64-bit values (throws error for larger).prepareIT: Supports up to 128-bit values.prepareIT256: Supports up to 256-bit values. This inconsistency can lead to runtime errors if developers assumebuildInputTextworks for all "standard" inputs.
- No documented gas cost comparisons for 256-bit vs. 128-bit operations.
- Missing migration guide for upgrading from 128-bit to 256-bit types.
- The
signITfunction's role in replay attack prevention is mentioned but not thoroughly explained.
- Key rotation: No guidance on how users should handle AES key rotation.
- Signature replay: While signatures include contract address and function selector, there's no explicit nonce or timestamp to prevent cross-contract replay attacks if the same selector is used.
| Priority | Area | Suggestion |
|---|---|---|
| High | Testing | Add 256-bit tests for Wallet class in coti-ethers. |
| High | Testing | Create E2E tests that deploy Miscellaneous256BitTestsContract and call operations. |
| Medium | SDK | Deprecate buildInputText in favor of prepareIT for consistency. |
| Medium | Contracts | Document (or implement) workarounds for missing div/rem operations. |
| Medium | Docs | Add gas benchmarks for 256-bit operations vs. 128-bit. |
| Low | Docs | Add a migration guide for existing 128-bit applications. |
| Low | API | Consider adding prepareIT128 as an explicit alias to match prepareIT256 naming. |
- Batch Operations: Consider adding batch encryption/decryption for multiple values to reduce overhead.
- Streaming Decryption: For large datasets, a streaming API could be more memory-efficient than loading all ciphertexts at once.
- WebAssembly Build: Providing a WASM version of the crypto utilities could improve performance in browser environments.
- Hardware Wallet Support:
JsonRpcSignersupports browser wallets, but hardware wallet flows (Ledger/Trezor) may require additional testing.