Skip to content

Commit a66200c

Browse files
authored
feat: getPublicKeyEd25519 (#26)
New function to polyfill `getPublicKey` from `@noble/curves` ED25519. We use native C++ implementation from Botan library and just bridging it into JS. I converted tests from original `@noble/curves` for this functions and also added test for direct comparison of results with JS library. I will add them in separate PR because it's few thousands of lines. <table><tr><td> <img width="1080" height="2400" alt="Screenshot_1762345083" src="https://github.com/user-attachments/assets/e7eb5ea6-ac82-4b45-b56c-0b4e70c2327a" /></td><td> <img width="1080" height="2400" alt="Screenshot_1762345051" src="https://github.com/user-attachments/assets/f8f442a6-7619-4686-943a-d6639c135dbd" /></td></tr></table> ## Examples <!-- Are there any examples of this change being used in another repository? When considering changes to the MetaMask module template, it's strongly preferred that the change be experimented with in another repository first. This gives reviewers a better sense of how the change works, making it less likely the change will need to be reverted or adjusted later. -->
1 parent 58d4839 commit a66200c

File tree

5 files changed

+68
-1
lines changed

5 files changed

+68
-1
lines changed

cpp/HybridNativeUtils.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,30 @@ std::shared_ptr<ArrayBuffer> HybridNativeUtils::toPublicKeyFromBytes(const std::
7474
return generatePublicKeyFromBytes(seckey, isCompressed);
7575
}
7676

77+
// Common function to generate ed25519 public key from private key bytes (seed)
78+
static std::shared_ptr<ArrayBuffer> generateEd25519PublicKeyFromBytes(const uint8_t* privateKeyBytes) {
79+
auto buffer = ArrayBuffer::allocate(32);
80+
uint8_t* publicKey = static_cast<uint8_t*>(buffer->data());
81+
uint8_t secretKey[64];
82+
83+
Botan::ed25519_gen_keypair(publicKey, secretKey, privateKeyBytes);
84+
85+
return buffer;
86+
}
87+
88+
std::shared_ptr<ArrayBuffer> HybridNativeUtils::getPublicKeyEd25519(const std::string& privateKey) {
89+
uint8_t seed[32];
90+
hexToBytes(privateKey, seed, 32);
91+
92+
return generateEd25519PublicKeyFromBytes(seed);
93+
}
94+
95+
std::shared_ptr<ArrayBuffer> HybridNativeUtils::getPublicKeyEd25519FromBytes(const std::shared_ptr<ArrayBuffer>& privateKey) {
96+
const uint8_t* seed = static_cast<const uint8_t*>(privateKey->data());
97+
98+
return generateEd25519PublicKeyFromBytes(seed);
99+
}
100+
77101
static std::shared_ptr<ArrayBuffer> keccak256Hash(const uint8_t* dataBytes, size_t dataLen) {
78102
auto hasher = Botan::HashFunction::create("Keccak-1600(256)");
79103
if (!hasher) {

cpp/HybridNativeUtils.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class HybridNativeUtils : public HybridNativeUtilsSpec {
1212
double multiply(double a, double b) override;
1313
std::shared_ptr<ArrayBuffer> toPublicKey(const std::string& privateKey, bool isCompressed) override;
1414
std::shared_ptr<ArrayBuffer> toPublicKeyFromBytes(const std::shared_ptr<ArrayBuffer>& privateKey, bool isCompressed) override;
15+
std::shared_ptr<ArrayBuffer> getPublicKeyEd25519(const std::string& privateKey) override;
16+
std::shared_ptr<ArrayBuffer> getPublicKeyEd25519FromBytes(const std::shared_ptr<ArrayBuffer>& privateKey) override;
1517
std::shared_ptr<ArrayBuffer> keccak256(const std::string& data) override;
1618
std::shared_ptr<ArrayBuffer> keccak256FromBytes(const std::shared_ptr<ArrayBuffer>& data) override;
1719
std::shared_ptr<ArrayBuffer> pubToAddress(const std::shared_ptr<ArrayBuffer>& pubKey, bool sanitize = false) override;

scripts/build-botan.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ echo "Output directory: $OUTPUT_DIR"
2121
mkdir -p "$BOTAN_GENERATED_DIR"
2222

2323
# Configuration variables
24-
BOTAN_MODULES="keccak,hmac,sha2_64"
24+
BOTAN_MODULES="keccak,hmac,sha2_64,ed25519"
2525
COMMON_FLAGS="--amalgamation --minimized-build --disable-cc-tests"
2626

2727
echo "📦 Using modules: $BOTAN_MODULES"

src/NativeUtils.nitro.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface NativeUtils
88
privateKey: ArrayBuffer,
99
isCompressed: boolean,
1010
): ArrayBuffer;
11+
getPublicKeyEd25519(privateKey: string): ArrayBuffer;
12+
getPublicKeyEd25519FromBytes(privateKey: ArrayBuffer): ArrayBuffer;
1113
keccak256(data: string): ArrayBuffer;
1214
keccak256FromBytes(data: ArrayBuffer): ArrayBuffer;
1315
pubToAddress(pubKey: ArrayBuffer, sanitize: boolean): ArrayBuffer;

src/index.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,42 @@ export function hmacSha512(key: Uint8Array, data: Uint8Array): Uint8Array {
134134

135135
return arrayBufferToUint8Array(result);
136136
}
137+
138+
/**
139+
* Generate an Ed25519 public key from a private key using native implementation.
140+
* This is a fast native implementation that matches the noble/curves ed25519 API.
141+
*
142+
* @param privateKey - The 32-byte Ed25519 private key as Uint8Array or hex string
143+
* @param _compressed - Ignored parameter for API compatibility (Ed25519 keys have no compressed form)
144+
* @returns Uint8Array containing 32-byte Ed25519 public key
145+
*/
146+
export function getPublicKeyEd25519(
147+
privateKey: Uint8Array | string,
148+
_compressed?: boolean,
149+
): Uint8Array {
150+
let result: ArrayBuffer;
151+
152+
if (typeof privateKey === 'string') {
153+
// 64 characters = 32 bytes
154+
if (privateKey.length !== 64) {
155+
throw new Error(
156+
'Ed25519 private key must be 32 bytes (64 hex characters)',
157+
);
158+
}
159+
160+
result = NativeUtilsHybridObject.getPublicKeyEd25519(privateKey);
161+
} else if (privateKey instanceof Uint8Array) {
162+
if (privateKey.length !== 32) {
163+
throw new Error('Ed25519 private key must be 32 bytes');
164+
}
165+
166+
const privateKeyBuffer = uint8ArrayToArrayBuffer(privateKey);
167+
168+
result =
169+
NativeUtilsHybridObject.getPublicKeyEd25519FromBytes(privateKeyBuffer);
170+
} else {
171+
throw new Error('Ed25519 private key must be a hex string or Uint8Array');
172+
}
173+
174+
return arrayBufferToUint8Array(result);
175+
}

0 commit comments

Comments
 (0)