Skip to content

nostrord/pomegranate-dealer

Repository files navigation

pomegranate-dealer

Maven Central License: Unlicense

A pure-Kotlin FROST trusted dealer for the promenade threshold signer, byte-compatible with @fiatjaf/promenade-trusted-dealer 0.4.3.

It splits a secp256k1 secret key into shards (Shamir over the scalar field, BIP340 even-y normalized) and recovers it from any threshold-many shards (Lagrange interpolation at x=0). The shard wire format matches the reference implementation byte-for-byte, verified by cross-language test vectors captured from the JS library with a pinned RNG (see FrostDealerVectorTest).

No JS/npm dependency: the elliptic-curve math is implemented over a multiplatform BigInteger (ionspin/kotlin-multiplatform-bignum), so the same code runs on every Kotlin target.

Supported targets

Target Status
JVM yes
Android (minSdk 21) yes
iOS (arm64, simulatorArm64, x64) yes
JS (browser + Node.js, IR) yes

Install

Available on Maven Central:

// build.gradle.kts
dependencies {
    implementation("io.github.nostrord:pomegranate-dealer:0.1.0")
}

Or with a version catalog:

# gradle/libs.versions.toml
[libraries]
pomegranate-dealer = { module = "io.github.nostrord:pomegranate-dealer", version = "0.1.0" }
// build.gradle.kts
dependencies {
    implementation(libs.pomegranate.dealer)
}

It's a Kotlin Multiplatform library — add it to commonMain in KMP projects and Gradle resolves the right variant per target (JVM, Android, iOS, JS).

Usage

import io.github.nostrord.pomegranatedealer.FrostDealer

// Split a secret into 5 shards, any 3 can sign.
// randomScalar must return 32 CSPRNG-quality bytes per call (one per secret coefficient).
val shards = FrostDealer.deal(
    secretKeyHex = "c90c269c4f8fcbe6880f72a721ddfbf1914268a346d2100b7eaec33b21ac1e3f",
    threshold = 3,
    count = 5,
    randomScalar = { secureRandom32Bytes() },
)
shards.forEach { println(it.shardHex) }      // 104-byte hex, ready for the operators
shards.forEach { println(it.pubShardHex) }   // 39-byte public shard

// Recover the secret from any `threshold` shard hexes.
val secretHex = FrostDealer.aggregate(shards.take(3).map { it.shardHex })

Shard wire format (104 bytes)

id (u16 LE) + vssCommitCount (u32 LE, always 0) + pubShard (33-byte compressed point) + secret (32-byte BE scalar) + accountPubkey (33-byte compressed point). The public shard is the first 39 bytes (id + count + pubShard).

Security notes

  • You supply randomness. deal takes a randomScalar: () -> ByteArray so the caller owns CSPRNG selection. Pass platform-secure bytes (e.g. SecureRandom, SecKeyGenerate, crypto.getRandomValues); never kotlin.random.Random.
  • Key material lifetime is the caller's job. The dealer works with hex strings and returns them; it does not zero buffers. Wipe secrets you no longer need.
  • Not constant-time. The secp256k1 arithmetic here is for one-shot dealing, not for signing or any operation exposed to timing attackers.
  • BIP340 even-y normalization means the recovered secret may be n - original (same x-only public key), matching the reference library.

Building

./gradlew jvmTest            # runs the byte-exact cross-language vectors on the JVM
./gradlew jsNodeTest         # same vectors on Node.js
./gradlew iosSimulatorArm64Test   # same vectors on the iOS simulator (macOS only)

License

The Unlicense — public domain.

About

Pure-Kotlin FROST trusted dealer for promenade (KMP: JVM, Android, iOS, JS)

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages