commit 7fa2118480ea23696cf149dd2d356e1c6e9ce984
parent 1a6c17e308cb100d6d01eb860f06c23617ce436c
Author: Daniel D’Aquino <daniel@daquino.me>
Date: Wed, 30 Apr 2025 16:26:47 -0700
Implement Codable for NdbNote
Makes it easier to work with other Swift types
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Diffstat:
2 files changed, 131 insertions(+), 28 deletions(-)
diff --git a/damus/Nostr/Id.swift b/damus/Nostr/Id.swift
@@ -143,8 +143,16 @@ struct ReplaceableParam: TagConvertible {
var keychar: AsciiCharacter { "d" }
}
-struct Signature: Hashable, Equatable {
+struct Signature: Codable, Hashable, Equatable {
let data: Data
+
+ init(from decoder: Decoder) throws {
+ self.init(try hex_decoder(decoder, expected_len: 64))
+ }
+
+ func encode(to encoder: Encoder) throws {
+ try hex_encoder(to: encoder, data: self.data)
+ }
init(_ p: Data) {
self.data = p
diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift
@@ -42,9 +42,9 @@ enum NdbData {
}
}
-class NdbNote: Encodable, Equatable, Hashable {
+class NdbNote: Codable, Equatable, Hashable {
// we can have owned notes, but we can also have lmdb virtual-memory mapped notes so its optional
- let owned: Bool
+ private(set) var owned: Bool
let count: Int
let key: NoteKey?
let note: UnsafeMutablePointer<ndb_note>
@@ -72,7 +72,6 @@ class NdbNote: Encodable, Equatable, Hashable {
print("\(NdbNote.notes_created) ndb_notes, \(NdbNote.total_ndb_size) bytes")
}
#endif
-
}
func to_owned() -> NdbNote {
@@ -86,6 +85,10 @@ class NdbNote: Encodable, Equatable, Hashable {
return NdbNote(note: new_note, size: self.count, owned: true, key: self.key)
}
+
+ func mark_ownership_moved() {
+ self.owned = false
+ }
var content: String {
String(cString: content_raw, encoding: .utf8) ?? ""
@@ -161,13 +164,63 @@ class NdbNote: Encodable, Equatable, Hashable {
try container.encode(content, forKey: .content)
try container.encode(tags, forKey: .tags)
}
+
+ required init(from decoder: any Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ let content = try container.decode(String.self, forKey: .content)
+ let pubkey = try container.decode(Pubkey.self, forKey: .pubkey)
+ let kind = try container.decode(UInt32.self, forKey: .kind)
+ let tags = try container.decode([[String]].self, forKey: .tags)
+ let createdAt = try container.decode(UInt32.self, forKey: .created_at)
+ let noteId = try container.decode(NoteId.self, forKey: .id)
+ let signature = try container.decode(Signature.self, forKey: .sig)
+
+ guard let note = NdbNote.init(content: content, author: pubkey, kind: kind, tags: tags, createdAt: createdAt, id: noteId, sig: signature) else {
+ throw DecodingError.initializationFailed
+ }
+
+ self.note = note.note
+ self.owned = note.owned
+ note.mark_ownership_moved() // This is done to prevent a double-free error when both `self` and `note` get deinitialized.
+ self.count = note.count
+ self.key = note.key
+
+ }
+
+ enum DecodingError: Error {
+ case initializationFailed
+ }
#if DEBUG_NOTE_SIZE
static var total_ndb_size: Int = 0
static var notes_created: Int = 0
#endif
-
- init?(content: String, keypair: Keypair, kind: UInt32 = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970)) {
+
+ fileprivate enum NoteConstructionMaterial {
+ case keypair(Keypair)
+ case manual(Pubkey, Signature, NoteId)
+
+ var pubkey: Pubkey {
+ switch self {
+ case .keypair(let keypair):
+ return keypair.pubkey
+ case .manual(let pubkey, _, _):
+ return pubkey
+ }
+ }
+
+ var privkey: Privkey? {
+ switch self {
+ case .keypair(let kp):
+ return kp.privkey
+ case .manual(_, _, _):
+ return nil
+ }
+ }
+ }
+
+ fileprivate init?(content: String, noteConstructionMaterial: NoteConstructionMaterial, kind: UInt32 = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970)) {
var builder = ndb_builder()
let buflen = MAX_NOTE_SIZE
@@ -175,7 +228,7 @@ class NdbNote: Encodable, Equatable, Hashable {
ndb_builder_init(&builder, buf, Int32(buflen))
- var pk_raw = keypair.pubkey.bytes
+ var pk_raw = noteConstructionMaterial.pubkey.bytes
ndb_builder_set_pubkey(&builder, &pk_raw)
ndb_builder_set_kind(&builder, UInt32(kind))
@@ -203,30 +256,57 @@ class NdbNote: Encodable, Equatable, Hashable {
var n = UnsafeMutablePointer<ndb_note>?(nil)
+ var len: Int32 = 0
- var the_kp: ndb_keypair? = nil
-
- if let sec = keypair.privkey {
- var kp = ndb_keypair()
- memcpy(&kp.secret.0, sec.id.bytes, 32);
-
- if ndb_create_keypair(&kp) <= 0 {
- print("bad keypair")
+ switch noteConstructionMaterial {
+ case .keypair(let keypair):
+ var the_kp: ndb_keypair? = nil
+
+ if let sec = noteConstructionMaterial.privkey {
+ var kp = ndb_keypair()
+ memcpy(&kp.secret.0, sec.id.bytes, 32);
+
+ if ndb_create_keypair(&kp) <= 0 {
+ print("bad keypair")
+ } else {
+ the_kp = kp
+ }
+ }
+
+ if var the_kp {
+ len = ndb_builder_finalize(&builder, &n, &the_kp)
} else {
- the_kp = kp
+ len = ndb_builder_finalize(&builder, &n, nil)
+ }
+
+ if len <= 0 {
+ free(buf)
+ return nil
+ }
+ case .manual(_, let signature, _):
+ var raw_sig = signature.data.bytes
+ ndb_builder_set_sig(&builder, &raw_sig)
+
+ do {
+ // Finalize note, save length, and ensure it is higher than zero (which signals finalization has succeeded)
+ len = ndb_builder_finalize(&builder, &n, nil)
+ guard len > 0 else { throw InitError.generic }
+
+ let scratch_buf_len = MAX_NOTE_SIZE
+ let scratch_buf = malloc(scratch_buf_len)
+ defer { free(scratch_buf) } // Ensure we deallocate as soon as we leave this scope, regardless of the outcome
+
+ // Calculate the ID based on the content
+ guard ndb_calculate_id(n, scratch_buf, Int32(scratch_buf_len)) == 1 else { throw InitError.generic }
+
+ // Verify the signature against the pubkey and the computed ID, to verify the validity of the whole note
+ var ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_VERIFY))
+ guard ndb_note_verify(&ctx, ndb_note_pubkey(n), ndb_note_id(n), ndb_note_sig(n)) == 1 else { throw InitError.generic }
+ }
+ catch {
+ free(buf)
+ return nil
}
- }
-
- var len: Int32 = 0
- if var the_kp {
- len = ndb_builder_finalize(&builder, &n, &the_kp)
- } else {
- len = ndb_builder_finalize(&builder, &n, nil)
- }
-
- if len <= 0 {
- free(buf)
- return nil
}
//guard let n else { return nil }
@@ -244,6 +324,14 @@ class NdbNote: Encodable, Equatable, Hashable {
self.key = nil
}
+ convenience init?(content: String, keypair: Keypair, kind: UInt32 = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970)) {
+ self.init(content: content, noteConstructionMaterial: .keypair(keypair), kind: kind, tags: tags, createdAt: createdAt)
+ }
+
+ convenience init?(content: String, author: Pubkey, kind: UInt32 = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970), id: NoteId, sig: Signature) {
+ self.init(content: content, noteConstructionMaterial: .manual(author, sig, id), kind: kind, tags: tags, createdAt: createdAt)
+ }
+
static func owned_from_json(json: String, bufsize: Int = 2 << 18) -> NdbNote? {
return json.withCString { cstr in
return NdbNote.owned_from_json_cstr(
@@ -520,3 +608,10 @@ func hex_encode(_ data: Data) -> String {
}
return str
}
+
+extension NdbNote {
+ /// A generic init error type to help make error handling code more concise
+ fileprivate enum InitError: Error {
+ case generic
+ }
+}