Skip to content

Commit 0317131

Browse files
authored
use JIP5 secret key seed derivation (#343)
1 parent c8815ec commit 0317131

File tree

4 files changed

+134
-23
lines changed

4 files changed

+134
-23
lines changed

Blockchain/Sources/Blockchain/Validator/DevKeyStore.swift

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,20 @@ public final class DevKeyStore: KeyStore {
3232
}
3333

3434
public static func getDevKey(seed: UInt32) throws -> KeySet {
35-
var seedData = Data(repeating: 0, count: 32)
36-
let seedByte = seed.encode()
37-
for i in 0 ..< 8 {
38-
seedData[i * 4 ..< (i + 1) * 4] = seedByte
39-
}
40-
let seedData32 = Data32(seedData)!
41-
let bandersnatch = try Bandersnatch.SecretKey(from: seedData32)
42-
let ed25519 = try Ed25519.SecretKey(from: seedData32)
43-
let bls = try BLS.SecretKey(from: seedData32)
35+
let trivialSeed = JIP5SeedDerive.trivialSeed(seed)
36+
let derivedSeeds = JIP5SeedDerive.deriveKeySeeds(from: trivialSeed)
37+
38+
let bandersnatch = try Bandersnatch.SecretKey(from: derivedSeeds.bandersnatch)
39+
let ed25519 = try Ed25519.SecretKey(from: derivedSeeds.ed25519)
40+
let bls = try BLS.SecretKey(from: trivialSeed)
4441
return KeySet(bandersnatch: bandersnatch.publicKey, ed25519: ed25519.publicKey, bls: bls.publicKey)
4542
}
4643
}
4744

4845
extension KeyStore {
4946
@discardableResult
5047
public func addDevKeys(seed: UInt32) async throws -> KeySet {
51-
var seedData = Data(repeating: 0, count: 32)
52-
let seedByte = seed.encode()
53-
for i in 0 ..< 8 {
54-
seedData[i * 4 ..< (i + 1) * 4] = seedByte
55-
}
56-
let seedData32 = Data32(seedData)!
57-
let bandersnatch = try await add(Bandersnatch.self, seed: seedData32)
58-
let ed25519 = try await add(Ed25519.self, seed: seedData32)
59-
let bls = try await add(BLS.self, seed: seedData32)
60-
return KeySet(bandersnatch: bandersnatch.publicKey, ed25519: ed25519.publicKey, bls: bls.publicKey)
48+
let trivialSeed = JIP5SeedDerive.trivialSeed(seed)
49+
return try await generateKeys(from: trivialSeed)
6150
}
6251
}

Blockchain/Sources/Blockchain/Validator/KeySet.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ public struct KeySet: Codable, Sendable {
1414
}
1515

1616
extension KeyStore {
17-
public func generateKeys() async throws -> KeySet {
18-
let bandersnatch = try await generate(Bandersnatch.self)
19-
let ed25519 = try await generate(Ed25519.self)
20-
let bls = try await generate(BLS.self)
17+
public func generateKeys(from seed: Data32) async throws -> KeySet {
18+
let derivedSeeds = JIP5SeedDerive.deriveKeySeeds(from: seed)
19+
20+
let bandersnatch = try await add(Bandersnatch.self, seed: derivedSeeds.bandersnatch)
21+
let ed25519 = try await add(Ed25519.self, seed: derivedSeeds.ed25519)
22+
let bls = try await add(BLS.self, seed: seed)
23+
2124
return KeySet(bandersnatch: bandersnatch.publicKey, ed25519: ed25519.publicKey, bls: bls.publicKey)
2225
}
2326

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
3+
/// This module implements the standard method for deriving validator secret key seeds
4+
public enum JIP5SeedDerive {
5+
/// Derives Ed25519 and Bandersnatch secret key seeds from a master seed
6+
///
7+
/// - Parameter seed: The 32-byte master seed
8+
/// - Returns: A tuple containing (ed25519SecretSeed, bandersnatchSecretSeed)
9+
public static func deriveKeySeeds(from seed: Data32) -> (ed25519: Data32, bandersnatch: Data32) {
10+
let ed25519Prefix = "jam_val_key_ed25519"
11+
let bandersnatchPrefix = "jam_val_key_bandersnatch"
12+
13+
let ed25519Input = Data(ed25519Prefix.utf8) + seed.data
14+
let ed25519SecretSeed = ed25519Input.blake2b256hash()
15+
16+
let bandersnatchInput = Data(bandersnatchPrefix.utf8) + seed.data
17+
let bandersnatchSecretSeed = bandersnatchInput.blake2b256hash()
18+
19+
return (ed25519: ed25519SecretSeed, bandersnatch: bandersnatchSecretSeed)
20+
}
21+
22+
/// Creates a trivial seed from a 32-bit unsigned integer for testing purposes
23+
///
24+
/// Implements: trivial_seed(i) = repeat_8_times(encode_as_32bit_le(i))
25+
///
26+
/// - Parameter i: The 32-bit unsigned integer
27+
/// - Returns: A 32-byte seed with the integer repeated 8 times in little-endian format
28+
public static func trivialSeed(_ i: UInt32) -> Data32 {
29+
var seedData = Data(capacity: 32)
30+
let littleEndianBytes = i.littleEndian.encode()
31+
32+
for _ in 0 ..< 8 {
33+
seedData.append(contentsOf: littleEndianBytes)
34+
}
35+
36+
return Data32(seedData)!
37+
}
38+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import Foundation
2+
import Testing
3+
@testable import Utils
4+
5+
struct Vector {
6+
let index: UInt32
7+
let seedHex: String
8+
let ed25519SeedHex: String
9+
let ed25519PubHex: String
10+
let bandersnatchSeedHex: String
11+
let bandersnatchPubHex: String
12+
}
13+
14+
// test vectors from JIP-5 spec
15+
let vectors: [Vector] = [
16+
Vector(
17+
index: 0,
18+
seedHex: "0000000000000000000000000000000000000000000000000000000000000000",
19+
ed25519SeedHex: "996542becdf1e78278dc795679c825faca2e9ed2bf101bf3c4a236d3ed79cf59",
20+
ed25519PubHex: "4418fb8c85bb3985394a8c2756d3643457ce614546202a2f50b093d762499ace",
21+
bandersnatchSeedHex: "007596986419e027e65499cc87027a236bf4a78b5e8bd7f675759d73e7a9c799",
22+
bandersnatchPubHex: "ff71c6c03ff88adb5ed52c9681de1629a54e702fc14729f6b50d2f0a76f185b3"
23+
),
24+
Vector(
25+
index: 1,
26+
seedHex: "0100000001000000010000000100000001000000010000000100000001000000",
27+
ed25519SeedHex: "b81e308145d97464d2bc92d35d227a9e62241a16451af6da5053e309be4f91d7",
28+
ed25519PubHex: "ad93247bd01307550ec7acd757ce6fb805fcf73db364063265b30a949e90d933",
29+
bandersnatchSeedHex: "12ca375c9242101c99ad5fafe8997411f112ae10e0e5b7c4589e107c433700ac",
30+
bandersnatchPubHex: "dee6d555b82024f1ccf8a1e37e60fa60fd40b1958c4bb3006af78647950e1b91"
31+
),
32+
Vector(
33+
index: 2,
34+
seedHex: "0200000002000000020000000200000002000000020000000200000002000000",
35+
ed25519SeedHex: "0093c8c10a88ebbc99b35b72897a26d259313ee9bad97436a437d2e43aaafa0f",
36+
ed25519PubHex: "cab2b9ff25c2410fbe9b8a717abb298c716a03983c98ceb4def2087500b8e341",
37+
bandersnatchSeedHex: "3d71dc0ffd02d90524fda3e4a220e7ec514a258c59457d3077ce4d4f003fd98a",
38+
bandersnatchPubHex: "9326edb21e5541717fde24ec085000b28709847b8aab1ac51f84e94b37ca1b66"
39+
),
40+
Vector(
41+
index: 3,
42+
seedHex: "0300000003000000030000000300000003000000030000000300000003000000",
43+
ed25519SeedHex: "69b3a7031787e12bfbdcac1b7a737b3e5a9f9450c37e215f6d3b57730e21001a",
44+
ed25519PubHex: "f30aa5444688b3cab47697b37d5cac5707bb3289e986b19b17db437206931a8d",
45+
bandersnatchSeedHex: "107a9148b39a1099eeaee13ac0e3c6b9c256258b51c967747af0f8749398a276",
46+
bandersnatchPubHex: "0746846d17469fb2f95ef365efcab9f4e22fa1feb53111c995376be8019981cc"
47+
),
48+
Vector(
49+
index: 4,
50+
seedHex: "0400000004000000040000000400000004000000040000000400000004000000",
51+
ed25519SeedHex: "b4de9ebf8db5428930baa5a98d26679ab2a03eae7c791d582e6b75b7f018d0d4",
52+
ed25519PubHex: "8b8c5d436f92ecf605421e873a99ec528761eb52a88a2f9a057b3b3003e6f32a",
53+
bandersnatchSeedHex: "0bb36f5ba8e3ba602781bb714e67182410440ce18aa800c4cb4dd22525b70409",
54+
bandersnatchPubHex: "151e5c8fe2b9d8a606966a79edd2f9e5db47e83947ce368ccba53bf6ba20a40b"
55+
),
56+
Vector(
57+
index: 5,
58+
seedHex: "0500000005000000050000000500000005000000050000000500000005000000",
59+
ed25519SeedHex: "4a6482f8f479e3ba2b845f8cef284f4b3208ba3241ed82caa1b5ce9fc6281730",
60+
ed25519PubHex: "ab0084d01534b31c1dd87c81645fd762482a90027754041ca1b56133d0466c06",
61+
bandersnatchSeedHex: "75e73b8364bf4753c5802021c6aa6548cddb63fe668e3cacf7b48cdb6824bb09",
62+
bandersnatchPubHex: "2105650944fcd101621fd5bb3124c9fd191d114b7ad936c1d79d734f9f21392e"
63+
),
64+
]
65+
66+
struct JIP5SeedDeriveTests {
67+
@Test(arguments: vectors)
68+
func testJIP5SeedDerivationAndPublicKeys(vector: Vector) throws {
69+
let seed = JIP5SeedDerive.trivialSeed(vector.index)
70+
#expect(seed.toHexString() == vector.seedHex)
71+
72+
let derived = JIP5SeedDerive.deriveKeySeeds(from: seed)
73+
#expect(derived.ed25519.toHexString() == vector.ed25519SeedHex)
74+
#expect(derived.bandersnatch.toHexString() == vector.bandersnatchSeedHex)
75+
76+
let ed25519Pub = try Ed25519.SecretKey(from: derived.ed25519).publicKey
77+
let bandersnatchPub = try Bandersnatch.SecretKey(from: derived.bandersnatch).publicKey
78+
#expect(ed25519Pub.data.toHexString() == vector.ed25519PubHex)
79+
#expect(bandersnatchPub.data.toHexString() == vector.bandersnatchPubHex)
80+
}
81+
}

0 commit comments

Comments
 (0)