This is a part of my Bachelor's thesis. Nguyen Huu Thuan, University of Science - VNUHCM (2026).
- tss-node (current)
- tss-orchestrator
- tss-node-p2p
-
Verifiability: Any falsified/corrupted information a node receives from the Orchestrator server or other peer nodes can be independently verified. However, this comes at the cost of not guaranteeing the availability of the service if the Orchestrator server intentionally delays or provides falsified/corrupted information.
-
Byzantine fault tolerance: The network is fault-tolerant up to a threshold of
$t$ such that$n/2 < t \leq n$ . In the most extreme case where an attacker simultaneously controls both the Orchestrator server and$t - 1$ nodes, the system must still ensure cryptographic safety, meaning it is impossible to generate a valid signature pair for any state that is fraudulent or affects the state of the remaining$t$ honest nodes. -
Commitment of the signature: A transaction once signed must be atomic and committed. Even when
$\geq t$ nodes go offline simultaneously, any transaction that has been signed and confirmed in the past can still be unilaterally settled on-chain by anyone without causing a conflict or exposing a vulnerability that would freeze latest valid state by any kind of attacks.
-
Optimizing input throughput: Instead of signing directly on each individual transaction, the system signs indirectly on the cumulative state of the entire fund. This strategy allows an unlimited number of micro-transactions to be aggregated off-chain into a single settlement message, maximizing throughput and reducing transaction fees to near-zero.
-
Optimizing output throughput: Instead of performing direct transfers at each settlement, the Smart Contract adopts a proactive pull-model.
-
Optimizing data structure and blockchain costs: Instead of having Smart Contract to update the cumulative state for each user at each settlement, the system uses a Sparse Merkle Tree approach. This strategy allows the Smart Contract to perform only a fixed
$O(1)$ complexity of Merkle Root reassignment (settlement) and proof verification (fund withdrawal). Additionally, the Merkle root is optimized to just a fixed-size32-bytesvalue by precomputing the values of empty branches for all$2^{256}$ states. -
Optimizing consensus process and native multi-chain support: Instead of having on-chain threshold consensus, the system moves the consensus process to an off-chain network by utilizing mathematical properties to constrain the ability to generate threshold signatures. Additionally, the off-chain consensus process naturally enables native multi-chain support.
-
Optimizing cryptographic security: Instead of using only a single Feldman Verifiable Secret Sharing phase for the Distributed Key Generation, the system adds Pedersen Verifiable Secret Sharing to address the waiting attack vulnerability and employs Threshold Signature Scheme to ensure that the fund's private key exists only mathematically but is never reconstructed anywhere in memory.
-
Optimizing network complexity: Instead of using a fully distributed architecture with communication complexity of
$O(n^2)$ and$O(t^2)$ , the system employs a central Orchestrator server to aggregate messages, reducing complexity to linear$O(n)$ and$O(t)$ without compromising cryptographic security. -
Optimizing transaction finality: Instead of having a lock-time to handle the latest state dispute, the system relies on cumulative state and one-way transaction properties to eliminate the possibility of forking or state dispute.
This repository contains the implementation of a participant node
The system implements a Distributed Key Generation (DKG) protocol using Pedersen Verifiable Secret Sharing (PVSS), a Threshold Signature Scheme (TSS) based on the GG18 specification over the secp256k1 elliptic curve, and a Sparse Merkle Tree (SMT) to manage and verify users' cumulative balances.
Nodes communicate indirectly via an Orchestrator, which acts as an untrusted message router and matchmaker.
The DKG process ensures that no single entity possesses the full private key. It establishes the system parameters
-
Broadcast Phase (
POST /dkg/broadcast):- Each node generates a secret polynomial
$f(x)$ and a blinding polynomial$g(x)$ of degree$t-1$ . - It evaluates points
$s_{i,j}$ and$t_{i,j}$ for every peer$j$ . - It generates a 1028-bit Paillier Homomorphic Encryption keypair for future additive operations.
- It broadcasts Pedersen Commitments
$C_k = f_k G + g_k H$ to allow public verification of its shares without revealing the actual values.
- Each node generates a secret polynomial
-
Receive Phase (
POST /dkg/receive):- Receives encrypted secret and noise fragments routed via ECIES encryption. It validates the integrity of these shares against the sender's Pedersen Commitments.
- Aggregates the validated fragments to compute its master secret share
$x_i = \sum s_{j,i} \pmod q$ . - Generates and returns Feldman Commitments
$A_{i,k} = a_{i,k}G$ to prove the correctness of its derived secret share.
-
Compute Public Key Phase (
POST /dkg/compute-public-key):- Self-verifies the master share against all network Feldman commitments.
- Reconstructs and saves the globally aggregated uncompressed public key
$Y = \sum A_{i,0}$ , which is used for on-chain signature verification.
The SMT manages and verifies users' cumulative balances. It's a data structure that allows for a fixed-size 32-byte storage and
Before signing, a node independently calculates the impending system state using a local SMT.
- Retrieves the latest global state from the Orchestrator via
GET /fund/latest-stateto ensure itsnonceis up-to-date and to verify the old state's signature. - Validates the incoming Merkle Proof of the requesting user.
- Updates the local SMT off-chain by accumulating the user's balance, computes the new Merkle Root via binary
Bufferconcatenations, and constructs the deterministicmessageHash. - Saves this transition to the local
PendingTransactioncollection. - Dispatches the proposal to the Orchestrator via
POST /tss/propose. The node keeps distinctpendingTransactionrecords separated bychainIdto prevent overlapping states.
The TSS process is based on the GG18 protocol. It uses Homomorphic Encryption to perform operations on encrypted values, allowing
Once the Orchestrator matches
-
Start Phase (
POST /tss/start):- Generates ephemeral random scalars: nonce share
$k_i$ and auxiliary masking variable$\gamma_i$ . - Computes the subset-specific Lagrange interpolated share:
$w_i = x_i \lambda_{i,S} \pmod q$ . - Encrypts these values under its own Paillier public key:
$E_i(k_i)$ and$E_i(w_i)$ .
- Generates ephemeral random scalars: nonce share
-
Multiplicative-to-Additive Phase (
POST /tss/mta):- Transforms the distributed product of secrets into a sum of shares.
- To prevent vulnerability from negative values causing modulo wrap-around in Paillier encryption, the node applies Additive Blinding. It generates random scalars
$\beta'$ and$\mu'$ , stores their modulo$q$ complements ($\beta_{ij}, \mu_{ij}$ ), and uses Homomorphic Addition to mask the ciphertexts. This prevents numerical overflow exceeding the Paillier key length. - Outputs ciphertext shares
$\alpha_{ij}$ (for$k\gamma$ ) and$\nu_{ij}$ (for$x\gamma$ ).
-
Delta & Sigma (
POST /tss/delta):- Decrypts the incoming masked MtA shares.
- Computes the local additive components:
$\delta_i = k_i\gamma_i + \sum(\alpha_{ji} + \beta_{ij}) \pmod q$ and$\sigma_i = k_iw_i + \sum(\nu_{ji} + \mu_{ij}) \pmod q$ .
-
Sign (
POST /tss/sign):- Receives the global curve extraction
$r$ . - Evaluates the partial ECDSA signature equation:
$s_i = m \cdot k_i + r \cdot \sigma_i \pmod q$ . - Commits the transaction to the local database via atomic updates into the
Keydocument and deletes thePendingTransactionto finalize the process.
- Receives the global curve extraction
Stores the fundamental cryptographic identity and persistent state across networks.
x_i: The unrecoverable master secret polynomial share.Y: The globally aggregated uncompressed public key.f_poly: The generated secret polynomial coefficients.paillier: The Homomorphic Encryption keypair{ publicKey: { n, g }, privateKey: { lambda, mu } }.chains: A nested dictionary containing independent state tracking for multiple concurrent blockchains (chainIdmapped tononceandroot).
Stores verified payload fragments received from peer nodes during the PVSS routing phase.
-
i: The destination node index. -
s_ij: The verifiable secret fragment$f_j(i)$ . -
t_ij: The verifiable noise fragment$g_j(i)$ . -
paillierPublicKey: The peer's Paillier identity utilized during the MtA phase.
Ephemeral, short-lived state storage strictly dedicated to active signing sessions.
messageHash: The cryptographic identifier for the targeted transaction.k_i,gamma_i,w_i,sigma_i: Intermediate GG18 scalar variables isolated to this specific session.betas,alphas,mus,nus: Cross-routing dictionaries storing incoming and outgoing obfuscated MtA payloads.
Tracks transactions that have been proposed but are waiting for TSS consensus.
chainId: The target blockchain.newRoot: The newly calculated Sparse Merkle Tree root.newNonce: The next sequential nonce.messageHash: The globally unique hash of the proposed state transition.
-
Paillier Homomorphic Encryption
- Basic encrypt/decrypt
-
should decrypt to original plaintext(3 ms) -
should encrypt zero correctly(2 ms) -
should handle large values(2 ms) -
should produce different ciphertexts for same plaintext (randomized)(5 ms)
-
- Additive homomorphism
-
E(a) + E(b) = E(a + b)(3 ms) -
sum of multiple encryptions(6 ms)
-
- Scalar multiplication
-
E(a) × k = E(a × k)(3 ms) -
E(a) × 0 = E(0)(1 ms) -
E(a) × 1 = E(a)(3 ms)
-
- MtA simulation
-
should produce correct additive shares of a product(4 ms) -
should work with secp256k1-order-sized values(4 ms)
-
- Cross-key operations
-
cannot decrypt with different key(54 ms)
-
- Basic encrypt/decrypt
-
Pedersen Verifiable Secret Sharing
- Polynomial Operations
-
should generate polynomial of correct degree(6 ms) -
should evaluate polynomial correctly at x=0 (returns secret)(1 ms) -
should produce different values at different points(1 ms)
-
- Pedersen Commitments
-
should generate commitments matching polynomial length(344 ms) -
should verify valid shares against Pedersen commitments(572 ms) -
should reject tampered shares(343 ms)
-
- Feldman Commitments
-
should generate Feldman commitments from polynomial(144 ms)
-
- Master Share Verification
-
should verify aggregated master share against all Feldman commitments(350 ms)
-
- Polynomial Operations
-
Distributed Key Generation (n=3, t=2)
-
Phase 1: Each node generates polynomial, commitments, and Paillier keys(1976 ms) -
Phase 2: Each node receives batched shares, verifies, and returns Feldman Commitments(1987 ms) -
Phase 3: Nodes self-verify master shares and reconstruct public key Y(166 ms)
-
-
Threshold Signature Scheme (n=3, t=2)
-
Phase 0: Nodes propose transaction and generate messageHash(23 ms) -
Phase 1: Start (k_i, w_i, Paillier encryption)(78 ms) -
Phase 2: MtA round 1 & 2(9 ms) -
Phase 3: Delta + Sigma(4 ms) -
Phase 4: Distributed ECDSA signature and state commit(202 ms)
-
-
Comprehensive End-to-End (n=3, t=2)
-
Phase 1: Orchestrator initializes DKG and distributes keys(482 ms) -
Phase 2.1: Subset (1, 2) proposes initial transaction(147 ms) -
Phase 2.2: Orchestrator verifies signature and commits state for Tx1(396 ms) -
Phase 3: Nodes reject mismatched payloads(65 ms) -
Phase 4.1: Subset (2, 3) proposes sequential transaction(38 ms) -
Phase 4.2: Orchestrator verifies signature and accumulates balance for Tx2(388 ms) -
Phase 5.1: Subset (1, 3) proposes transaction for new user(37 ms) -
Phase 5.2: Orchestrator verifies signature and updates Merkle tree(426 ms) -
Phase 6: Nodes process multi-chain proposals concurrently(307 ms) -
Phase 7: Orchestrator resolves override(347 ms) -
Phase 8: Orchestrator generates exact Merkle proofs for users(10 ms) -
Phase 9: Node catch-up new state(345 ms)
-
These are end-to-end benchmark results ran locally with their corresponding numbers of ports and Mongo DBs.
Macbook M1 Pro 2021, 16GB RAM, 10-core CPU.
This is the result when running benchmark on this repository (proposed architecture).
This is the result when running benchmark on @SnowyField1906/tss-node-p2p (alternative architecture).
-
node-1.env.localNODE_ID=1 PRIVATE_KEY=... # 256-bit hex for deterministic secret a_{i,0} HOST=127.0.0.1 PORT=3001 MONGO_URI=mongodb://127.0.0.1:27017/node1 ORCHESTRATOR_URL=http://127.0.0.1:4000 NODE_1_PUBLIC_KEY= NODE_2_PUBLIC_KEY= NODE_3_PUBLIC_KEY= -
node-2.env.localNODE_ID=2 PRIVATE_KEY=... # 256-bit hex for deterministic secret a_{i,0} HOST=127.0.0.1 PORT=3002 MONGO_URI=mongodb://127.0.0.1:27017/node2 ORCHESTRATOR_URL=http://127.0.0.1:4000 NODE_1_PUBLIC_KEY= NODE_2_PUBLIC_KEY= NODE_3_PUBLIC_KEY= -
node-3.env.localNODE_ID=3 PRIVATE_KEY=... # 256-bit hex for deterministic secret a_{i,0} HOST=127.0.0.1 PORT=3003 MONGO_URI=mongodb://127.0.0.1:27017/node3 ORCHESTRATOR_URL=http://127.0.0.1:4000 NODE_1_PUBLIC_KEY= NODE_2_PUBLIC_KEY= NODE_3_PUBLIC_KEY=
yarnyarn test # phe.spec.ts pvss.spec.ts dkg.spec.ts tss.spec.tsStart 3 nodes and orchestrator in 4 separate terminals:
yarn start:dev:node1 # tss-node
yarn start:dev:node2 # tss-node
yarn start:dev:node3 # tss-node
yarn start:dev # tss-orchestratorThen run the test in tss-node directory:
yarn test e2e.spec.ts(mongod is required to be installed and started so that the local database at mongodb://127.0.0.1:27017 is available for this e2e testing)
yarn build
npx ts-node benchmark/e2e.benchmark.ts
cd benchmark && python plot.py(mongod is required to be installed and started so that the local database at mongodb://127.0.0.1:27017 is available for this benchmark. python is required to generate plots.)



