damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 2f8aa29e92f9c5077936bf61b64a12d09ec0bd30
parent e3c04465fc4e3801a95baa71d6487c4e2b8df19c
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 25 Jul 2023 08:58:06 -0700

ndb: make NostrEvents immutable

Since we can't mutate NdbNotes, let's update the existing codebase to
generate and sign ids on NostrEvent constructions. This will allow us to
match NdbNote's constructor

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 10++++++++++
Mdamus/ContentView.swift | 16+++++++++++-----
Mdamus/Models/Contacts.swift | 92+++++++++++++++++++++++++++++++++++++------------------------------------------
Mdamus/Models/HomeModel.swift | 4+---
Mdamus/Models/Mentions.swift | 7++-----
Mdamus/Models/ProfileModel.swift | 2+-
Mdamus/Models/Report.swift | 15+++------------
Mdamus/Nostr/NostrEvent.swift | 141++++++++++++++++++++++++++++++-------------------------------------------------
Mdamus/Nostr/ReferencedId.swift | 31++++++++++++++++++++++++++++++-
Mdamus/TestData.swift | 31++++++++++++++++++++++++++-----
Mdamus/Util/AccountDeletion.swift | 11+++++------
Mdamus/Util/Keys.swift | 4++++
Mdamus/Util/Lists.swift | 53+++++++++++++++++------------------------------------
Mdamus/Views/ActionBar/EventActionBar.swift | 12+++++-------
Mdamus/Views/ActionBar/RepostAction.swift | 7+++----
Mdamus/Views/ConfigView.swift | 10+++-------
Mdamus/Views/DMChatView.swift | 10+++-------
Mdamus/Views/DMView.swift | 2+-
Mdamus/Views/Events/Longform/LongformView.swift | 25++++++++++++-------------
Mdamus/Views/Events/ZapEvent.swift | 9---------
Mdamus/Views/Notifications/EventGroupView.swift | 6------
Mdamus/Views/Profile/EditMetadataView.swift | 6++++--
Mdamus/Views/Reactions/ReactionView.swift | 2+-
Mdamus/Views/Relays/RecommendedRelayView.swift | 20++++++++++----------
Mdamus/Views/Relays/RelayConfigView.swift | 16+++++-----------
Mdamus/Views/Relays/RelayDetailView.swift | 43+++++++++++++++++++++++--------------------
Mdamus/Views/Relays/RelayView.swift | 3++-
Mdamus/Views/ReportView.swift | 19++++++++++---------
Mdamus/Views/Reposts/RepostView.swift | 2+-
Mdamus/Views/SaveKeysView.swift | 4++--
Mdamus/Views/Wallet/WalletView.swift | 4+++-
MdamusTests/EventGroupViewTests.swift | 38+++++++++++++++++++++++---------------
MdamusTests/LikeTests.swift | 30++++++++++++------------------
MdamusTests/ReplyTests.swift | 10+++++-----
MdamusTests/UserSearchCacheTests.swift | 9+--------
MdamusTests/damusTests.swift | 6++----
Mnostrdb/NdbNote.swift | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mnostrdb/NdbTagElem.swift | 40++++++++++++++++++++++++++++++++++------
Mnostrdb/NdbTagsIterator.swift | 6++++++
Mnostrdb/Test/NdbTests.swift | 8++++----
Mnostrdb/nostrdb.c | 316+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mnostrdb/nostrdb.h | 40+++++++++++++++++++++-------------------
Anostrdb/random.h | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostrdb/secp256k1.h | 909+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostrdb/secp256k1_ecdh.h | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostrdb/secp256k1_extrakeys.h | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostrdb/secp256k1_schnorrsig.h | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
47 files changed, 2213 insertions(+), 464 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -644,6 +644,11 @@ 4C75EFB628049D990006080F /* RelayPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPool.swift; sourceTree = "<group>"; }; 4C75EFB82804A2740006080F /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; }; 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; }; + 4C78EFD62A7078C5007E8197 /* random.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = random.h; sourceTree = "<group>"; }; + 4C78EFD72A707C4D007E8197 /* secp256k1_schnorrsig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = secp256k1_schnorrsig.h; sourceTree = "<group>"; }; + 4C78EFD82A707C4D007E8197 /* secp256k1_ecdh.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = secp256k1_ecdh.h; sourceTree = "<group>"; }; + 4C78EFD92A707C4D007E8197 /* secp256k1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = secp256k1.h; sourceTree = "<group>"; }; + 4C78EFDA2A707C67007E8197 /* secp256k1_extrakeys.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = secp256k1_extrakeys.h; sourceTree = "<group>"; }; 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardVisible.swift; sourceTree = "<group>"; }; 4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectWalletView.swift; sourceTree = "<group>"; }; 4C7D095D2A098C5D00943473 /* WalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletView.swift; sourceTree = "<group>"; }; @@ -1327,7 +1332,12 @@ 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */, 4CE9FBB82A6B3B26007E485C /* nostrdb.c */, 4CE9FBB92A6B3B26007E485C /* nostrdb.h */, + 4C78EFD62A7078C5007E8197 /* random.h */, 4CDD1AE72A6B3611001CD4DF /* jsmn.h */, + 4C78EFD82A707C4D007E8197 /* secp256k1_ecdh.h */, + 4C78EFD72A707C4D007E8197 /* secp256k1_schnorrsig.h */, + 4C78EFDA2A707C67007E8197 /* secp256k1_extrakeys.h */, + 4C78EFD92A707C4D007E8197 /* secp256k1.h */, ); path = nostrdb; sourceTree = "<group>"; diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -202,8 +202,8 @@ struct ContentView: View { func MaybeReportView(target: ReportTarget) -> some View { Group { if let damus_state { - if let sec = damus_state.keypair.privkey { - ReportView(postbox: damus_state.postbox, target: target, privkey: sec) + if let keypair = damus_state.keypair.to_full() { + ReportView(postbox: damus_state.postbox, target: target, keypair: keypair) } else { EmptyView() } @@ -377,7 +377,9 @@ struct ContentView: View { } profile.lud16 = lud16 - let ev = make_metadata_event(keypair: keypair, metadata: profile) + guard let ev = make_metadata_event(keypair: keypair, metadata: profile) else { + return + } ds.postbox.send(ev) } .onReceive(handle_notify(.broadcast_event)) { obj in @@ -505,7 +507,9 @@ struct ContentView: View { } profile.reactions = !hide - let profile_ev = make_metadata_event(keypair: keypair, metadata: profile) + guard let profile_ev = make_metadata_event(keypair: keypair, metadata: profile) else { + return + } damus_state.postbox.send(profile_ev) } .alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: { @@ -947,7 +951,9 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev //let post = tup.0 //let to_relays = tup.1 print("post \(post.content)") - let new_ev = post_to_event(post: post, privkey: keypair.privkey, pubkey: keypair.pubkey) + guard let new_ev = post_to_event(post: post, keypair: keypair) else { + return false + } postbox.send(new_ev) for eref in new_ev.referenced_ids.prefix(3) { // also broadcast at most 3 referenced events diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift @@ -40,15 +40,15 @@ class Contacts { for d in diff { if new.contains(d) { - new_mutes.append(d) + new_mutes.append(d.string()) } else { - new_unmutes.append(d) + new_unmutes.append(d.string()) } } - + // TODO: set local mutelist here - self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id })) - + self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id.string() })) + if new_mutes.count > 0 { notify(.new_mutes, new_mutes) } @@ -72,11 +72,7 @@ class Contacts { func get_followed_hashtags() -> Set<String> { guard let ev = self.event else { return Set() } - return ev.tags.reduce(into: Set<String>(), { htags, tag in - if tag.count >= 2 && tag[0] == "t" && tag[1] != "" { - htags.insert(tag[1]) - } - }) + return Set(ev.referenced_hashtags.map({ $0.ref_id.string() })) } func add_friend_pubkey(_ pubkey: String) { @@ -85,18 +81,17 @@ class Contacts { func add_friend_contact(_ contact: NostrEvent) { friends.insert(contact.pubkey) - for tag in contact.tags { - if tag.count >= 2 && tag[0] == "p" { - friend_of_friends.insert(tag[1]) - - // Exclude themself and us. - if contact.pubkey != our_pubkey && contact.pubkey != tag[1] { - if pubkey_to_our_friends[tag[1]] == nil { - pubkey_to_our_friends[tag[1]] = Set<String>() - } - - pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey) + for tag in contact.referenced_pubkeys { + let pk = tag.id.string() + friend_of_friends.insert(pk) + + // Exclude themself and us. + if contact.pubkey != our_pubkey && contact.pubkey != pk { + if pubkey_to_our_friends[pk] == nil { + pubkey_to_our_friends[pk] = Set<String>() } + + pubkey_to_our_friends[pk]?.insert(contact.pubkey) } } } @@ -128,13 +123,10 @@ class Contacts { } func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: ReferencedId) -> NostrEvent? { - guard let ev = follow_user_event(our_contacts: our_contacts, our_pubkey: keypair.pubkey, follow: follow) else { + guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else { return nil } - ev.calculate_id() - ev.sign(privkey: keypair.privkey) - box.send(ev) return ev @@ -145,28 +137,35 @@ func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: Fu return nil } - let ev = unfollow_reference_event(our_contacts: cs, our_pubkey: keypair.pubkey, unfollow: unfollow) - ev.calculate_id() - ev.sign(privkey: keypair.privkey) + guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else { + return nil + } postbox.send(ev) return ev } -func unfollow_reference_event(our_contacts: NostrEvent, our_pubkey: String, unfollow: ReferencedId) -> NostrEvent { +func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: ReferencedId) -> NostrEvent? { let tags = our_contacts.tags.filter { tag in - if tag.count >= 2 && tag[0] == unfollow.key && tag[1] == unfollow.ref_id { + if let key = tag[safe: 0], + let ref = tag[safe: 1], + let fst = unfollow.key.first, + let afst = AsciiCharacter(fst), + key.matches_char(afst), + ref.string() == unfollow.ref_id + { return false } return true } let kind = NostrKind.contacts.rawValue - return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags) + + return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) } -func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: ReferencedId) -> NostrEvent? { +func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: ReferencedId) -> NostrEvent? { guard let cs = our_contacts else { // don't create contacts for now so we don't nuke our contact list due to connectivity issues // we should only create contacts during profile creation @@ -174,7 +173,7 @@ func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: Re return nil } - guard let ev = follow_with_existing_contacts(our_pubkey: our_pubkey, our_contacts: cs, follow: follow) else { + guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else { return nil } @@ -186,23 +185,18 @@ func decode_json_relays(_ content: String) -> [String: RelayInfo]? { return decode_json(content) } -func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], privkey: String, relay: String) -> NostrEvent? { +func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: String) -> NostrEvent?{ var relays = ensure_relay_info(relays: current_relays, content: ev.content) - relays.removeValue(forKey: relay) - print("remove_relay \(relays)") guard let content = encode_json(relays) else { return nil } - let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags) - new_ev.calculate_id() - new_ev.sign(privkey: privkey) - return new_ev + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags) } -func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? { +func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? { var relays = ensure_relay_info(relays: current_relays, content: ev.content) guard relays.index(forKey: relay) == nil else { @@ -215,10 +209,7 @@ func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor return nil } - let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags) - new_ev.calculate_id() - new_ev.sign(privkey: privkey) - return new_ev + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags) } func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: RelayInfo] { @@ -229,19 +220,22 @@ func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: R } func is_already_following(contacts: NostrEvent, follow: ReferencedId) -> Bool { - return contacts.references(id: follow.ref_id, key: follow.key) + guard let key = follow.key.first_char() else { return false } + return contacts.references(id: follow.ref_id, key: key) } -func follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? { +func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? { // don't update if we're already following if is_already_following(contacts: our_contacts, follow: follow) { return nil } let kind = NostrKind.contacts.rawValue - var tags = our_contacts.tags + + var tags = our_contacts.tags.strings() tags.append(refid_to_tag(follow)) - return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags) + + return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) } func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] { diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -821,10 +821,8 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P } } +// TODO: remove this, let nostrdb handle all validation func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) { - guard ev.id == hex_encode(calculate_event_id(ev: ev)) else { - return - } let validated = events.is_event_valid(ev.id) switch validated { diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift @@ -473,14 +473,11 @@ func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags { return PostTags(blocks: post_blocks, tags: new_tags) } -func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent { +func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? { let tags = post.references.map(refid_to_tag) + post.tags let post_blocks = parse_post_blocks(content: post.content) let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags) let content = render_blocks(blocks: post_tags.blocks) - let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags) - new_ev.calculate_id() - new_ev.sign(privkey: privkey) - return new_ev + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags) } diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -35,7 +35,7 @@ class ProfileModel: ObservableObject, Equatable { } for tag in contacts.tags { - guard tag.count >= 2 && tag[0] == "p" else { + guard tag.count >= 2 && tag[0].matches_char("p") else { continue } diff --git a/damus/Models/Report.swift b/damus/Models/Report.swift @@ -55,17 +55,8 @@ func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] { } } -func create_report_event(privkey: String, report: Report) -> NostrEvent? { - guard let pubkey = privkey_to_pubkey(privkey: privkey) else { - return nil - } - - let kind = 1984 +func create_report_event(keypair: FullKeypair, report: Report) -> NostrEvent? { + let kind: UInt32 = 1984 let tags = create_report_tags(target: report.target, type: report.type) - let ev = NostrEvent(content: report.message, pubkey: pubkey, kind: kind, tags: tags) - - ev.id = hex_encode(calculate_event_id(ev: ev)) - ev.sig = sign_event(privkey: privkey, ev: ev) - - return ev + return NostrEvent(content: report.message, keypair: keypair.to_keypair(), kind: kind, tags: tags) } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -23,6 +23,8 @@ enum ValidationResult: Decodable { //typealias NostrEvent = NdbNote typealias NostrEvent = NostrEventOld +let MAX_NOTE_SIZE: Int = 2 << 18 + class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable { // TODO: memory mapped db events /* @@ -49,10 +51,10 @@ class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, var tags: TagIterator */ - var id: String - var content: String - var sig: String - var tags: [[String]] + let id: String + let content: String + let sig: String + let tags: [[String]] //var boosted_by: String? @@ -60,7 +62,7 @@ class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, //var pow: Int? // custom flags for internal use - var flags: Int = 0 + //var flags: Int = 0 let pubkey: String let created_at: UInt32 @@ -86,16 +88,21 @@ class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, hasher.combine(id) } - init(id: String = "", content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) { - - self.id = id - self.sig = "" + init?(content: String, keypair: Keypair, kind: UInt32 = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970)) { self.content = content - self.pubkey = pubkey + self.pubkey = keypair.pubkey self.kind = kind self.tags = tags self.created_at = createdAt + + if let privkey = keypair.privkey { + self.id = hex_encode(calculate_event_id(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content)) + self.sig = sign_id(privkey: privkey, id: self.id) + } else { + self.id = "" + self.sig = "" + } } } @@ -112,10 +119,6 @@ extension NostrEventOld { return !too_big } - var is_valid_id: Bool { - return hex_encode(calculate_event_id(ev: self)) == self.id - } - func blocks(_ privkey: String?) -> Blocks { if let bs = _blocks { return bs @@ -136,7 +139,7 @@ extension NostrEventOld { } if self.content == "", let ref = self.referenced_ids.first { - return cache.lookup(ref.ref_id) + return cache.lookup(ref.ref_id.string()) } return self.inner_event @@ -240,9 +243,9 @@ extension NostrEventOld { return tag_to_refid(tags[last]) } - public func references(id: String, key: String) -> Bool { + public func references(id: String, key: AsciiCharacter) -> Bool { for tag in tags { - if tag.count >= 2 && tag[0] == key { + if tag.count >= 2 && tag[0].matches_char(key) { if tag[1] == id { return true } @@ -283,16 +286,8 @@ extension NostrEventOld { return get_referenced_ids(key: "p") } - public var is_local: Bool { - return (self.flags & 1) != 0 - } - - func calculate_id() { - self.id = hex_encode(calculate_event_id(ev: self)) - } - - func sign(privkey: String) { - self.sig = sign_event(privkey: privkey, ev: self) + public var referenced_hashtags: [ReferencedId] { + return get_referenced_ids(key: "t") } var age: TimeInterval { @@ -301,14 +296,14 @@ extension NostrEventOld { } } -func sign_event(privkey: String, ev: NostrEvent) -> String { +func sign_id(privkey: String, id: String) -> String { let priv_key_bytes = try! privkey.bytes let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes) // Extra params for custom signing var aux_rand = random_bytes(count: 64) - var digest = try! ev.id.bytes + var digest = try! id.bytes // API allows for signing variable length messages let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand) @@ -370,32 +365,28 @@ func decode_data<T: Decodable>(_ data: Data) -> T? { return nil } -func event_commitment(ev: NostrEvent, tags: String) -> String { +func event_commitment(pubkey: String, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> String { let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes - let str_data = try! encoder.encode(ev.content) + let str_data = try! encoder.encode(content) let content = String(decoding: str_data, as: UTF8.self) - let commit = "[0,\"\(ev.pubkey)\",\(ev.created_at),\(ev.kind),\(tags),\(content)]" - //print("COMMIT", commit) - return commit -} - -func calculate_event_commitment(ev: NostrEvent) -> Data { + let tags_encoder = JSONEncoder() tags_encoder.outputFormatting = .withoutEscapingSlashes - let tags_data = try! tags_encoder.encode(ev.tags) + let tags_data = try! tags_encoder.encode(tags) let tags = String(decoding: tags_data, as: UTF8.self) - let target = event_commitment(ev: ev, tags: tags) - let target_data = target.data(using: .utf8)! - return target_data + return "[0,\"\(pubkey)\",\(created_at),\(kind),\(tags),\(content)]" } -func calculate_event_id(ev: NostrEvent) -> Data { - let commitment = calculate_event_commitment(ev: ev) - let hash = sha256(commitment) +func calculate_event_commitment(pubkey: String, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data { + let target = event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content) + return target.data(using: .utf8)! +} - return hash +func calculate_event_id(pubkey: String, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data { + let commitment = calculate_event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content) + return sha256(commitment) } @@ -480,10 +471,6 @@ func get_referenced_ids(tags: [[String]], key: String) -> [ReferencedId] { } func make_first_contact_event(keypair: Keypair) -> NostrEvent? { - guard let privkey = keypair.privkey else { - return nil - } - let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey) let rw_relay_info = RelayInfo(read: true, write: true) var relays: [String: RelayInfo] = [:] @@ -498,48 +485,31 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? { ["p", damus_pubkey], ["p", keypair.pubkey] // you're a friend of yourself! ] - let ev = NostrEvent(content: relay_json, - pubkey: keypair.pubkey, - kind: NostrKind.contacts.rawValue, - tags: tags) - ev.calculate_id() - ev.sign(privkey: privkey) - return ev + return NostrEvent(content: relay_json, keypair: keypair, kind: NostrKind.contacts.rawValue, tags: tags) } -func make_metadata_event(keypair: FullKeypair, metadata: Profile) -> NostrEvent { - let metadata_json = encode_json(metadata)! - let ev = NostrEvent(content: metadata_json, - pubkey: keypair.pubkey, - kind: NostrKind.metadata.rawValue, - tags: []) +func make_metadata_event(keypair: FullKeypair, metadata: Profile) -> NostrEvent? { + guard let metadata_json = encode_json(metadata) else { + return nil + } + return NostrEvent(content: metadata_json, keypair: keypair.to_keypair(), kind: NostrKind.metadata.rawValue, tags: []) - ev.calculate_id() - ev.sign(privkey: keypair.privkey) - return ev } -func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> NostrEvent { +func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent? { var tags: [[String]] = boosted.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") } tags.append(["e", boosted.id, "", "root"]) tags.append(["p", boosted.pubkey]) - let ev = NostrEvent(content: event_to_json(ev: boosted), pubkey: pubkey, kind: 6, tags: tags) - ev.calculate_id() - ev.sign(privkey: privkey) - return ev + return NostrEvent(content: event_to_json(ev: boosted), keypair: keypair.to_keypair(), kind: 6, tags: tags) } -func make_like_event(pubkey: String, privkey: String, liked: NostrEvent, content: String = "🤙") -> NostrEvent { +func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? { var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") } tags.append(["e", liked.id]) tags.append(["p", liked.pubkey]) - let ev = NostrEvent(content: content, pubkey: pubkey, kind: 7, tags: tags) - ev.calculate_id() - ev.sign(privkey: privkey) - - return ev + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags) } func zap_target_to_tags(_ target: ZapTarget) -> [[String]] { @@ -560,11 +530,8 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, // target tags must be the same as zap request target tags let tags = zap_target_to_tags(target) - let note = NostrEvent(content: message, pubkey: identity.pubkey, kind: 9733, tags: tags) - note.id = hex_encode(calculate_event_id(ev: note)) - note.sig = sign_event(privkey: identity.privkey, ev: note) - - guard let note_json = encode_json(note), + guard let note = NostrEvent(content: message, keypair: identity.to_keypair(), kind: 9733, tags: tags), + let note_json = encode_json(note), let enc = encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32) else { return nil @@ -584,7 +551,7 @@ func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTar // check to see if the private note was from us if note == nil { - guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else{ + guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else { return nil } // use our private keypair and their pubkey to get the shared secret @@ -620,7 +587,7 @@ func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTar return note } -func generate_private_keypair(our_privkey: String, id: String, created_at: Int64) -> FullKeypair? { +func generate_private_keypair(our_privkey: String, id: String, created_at: UInt32) -> FullKeypair? { let to_hash = our_privkey + id + String(created_at) guard let dat = to_hash.data(using: .utf8) else { return nil @@ -691,9 +658,9 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela privzap_req = privreq } - let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now) - ev.id = hex_encode(calculate_event_id(ev: ev)) - ev.sig = sign_event(privkey: kp.privkey, ev: ev) + guard let ev = NostrEvent(content: message, keypair: kp.to_keypair(), kind: 9734, tags: tags, createdAt: now) else { + return nil + } let zapreq = ZapRequest(ev: ev) if let privzap_req { return .priv(zapreq, privzap_req) @@ -942,7 +909,7 @@ func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_se func validate_event(ev: NostrEvent) -> ValidationResult { - let raw_id = sha256(calculate_event_commitment(ev: ev)) + let raw_id = calculate_event_id(pubkey: ev.pubkey, created_at: ev.created_at, kind: ev.kind, tags: ev.tags, content: ev.content) let id = hex_encode(raw_id) if id != ev.id { diff --git a/damus/Nostr/ReferencedId.swift b/damus/Nostr/ReferencedId.swift @@ -19,6 +19,10 @@ struct Reference { } } +func tagref_should_be_id(_ tag: NdbTagElem) -> Bool { + return !tag.matches_char("t") +} + struct References: Sequence, IteratorProtocol { let tags: TagsSequence var tags_iter: TagsIterator @@ -26,7 +30,7 @@ struct References: Sequence, IteratorProtocol { mutating func next() -> Reference? { while let tag = tags_iter.next() { guard let key = tag[0], key.count == 1, - let id = tag[1], id.is_id + let id = tag[1], tagref_should_be_id(key) else { continue } for c in key { @@ -49,12 +53,37 @@ struct References: Sequence, IteratorProtocol { .filter() { ref in ref.key == "p" } } + static func hashtags(tags: TagsSequence) -> LazyFilterSequence<References> { + References(tags: tags).lazy + .filter() { ref in ref.key == "t" } + } + init(tags: TagsSequence) { self.tags = tags self.tags_iter = tags.makeIterator() } } +extension [[String]] { + func strings() -> [[String]] { + return self + } +} + +extension String { + func string() -> String { + return self + } + + func first_char() -> AsciiCharacter? { + self.first.flatMap { chr in AsciiCharacter(chr) } + } + + func matches_char(_ c: AsciiCharacter) -> Bool { + return self.first == c.character + } +} + struct ReferencedId: Identifiable, Hashable, Equatable { let ref_id: String let relay_id: String? diff --git a/damus/TestData.swift b/damus/TestData.swift @@ -7,23 +7,44 @@ import Foundation +let test_seckey = "8e33316b227de8215d36f4787573beaaf532229bb00398430a0ae963b658e656" +let test_pubkey = "a9952fe066ced622167acb8069a0dfd1d44d9493ef2a4c28cf93e2877248b41a" +let test_keypair = Keypair(pubkey: test_pubkey, privkey: test_seckey) +let test_keypair_full = test_keypair.to_full()! let test_event_holder = EventHolder(events: [], incoming: [test_event]) let test_event = NostrEvent( content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jpg cool", - pubkey: "pk", - createdAt: Int64(Date().timeIntervalSince1970 - 100) - ) + keypair: test_keypair, + createdAt: UInt32(Date().timeIntervalSince1970 - 100) + )! + +let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" + +let test_repost_1 = NostrEvent(content: test_encoded_post, keypair: test_keypair, kind: 6, tags: [], createdAt: 1)! +let test_repost_2 = NostrEvent(content: test_encoded_post, keypair: test_keypair, kind: 6, tags: [], createdAt: 1)! + +let test_reposts = [test_repost_1, test_repost_2] +let test_event_group = EventGroup(events: test_reposts) + +let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000) +let test_zap_request_ev = NostrEvent(content: "hi", keypair: test_keypair, kind: 9734)! +let test_zap_request = ZapRequest(ev: test_zap_request_ev) +let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: nil) + +let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: .init(ev: test_event)) + +let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) + func test_damus_state() -> DamusState { - let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let damus = DamusState.empty let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil) let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event) - damus.profiles.add(id: pubkey, profile: tsprof) + damus.profiles.add(id: test_pubkey, profile: tsprof) return damus } diff --git a/damus/Util/AccountDeletion.swift b/damus/Util/AccountDeletion.swift @@ -8,15 +8,14 @@ import Foundation -func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent { +func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent? { let profile = Profile() profile.deleted = true profile.about = "account deleted" profile.name = "nobody" - let content = encode_json(profile)! - let ev = NostrEvent(content: content, pubkey: keypair.pubkey, kind: 0) - ev.id = hex_encode(calculate_event_id(ev: ev)) - ev.sig = sign_event(privkey: keypair.privkey, ev: ev) - return ev + guard let content = encode_json(profile) else { + return nil + } + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 0) } diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift @@ -14,6 +14,10 @@ let ANON_PUBKEY = "anon" struct FullKeypair: Equatable { let pubkey: String let privkey: String + + func to_keypair() -> Keypair { + return Keypair(pubkey: pubkey, privkey: privkey) + } } struct Keypair { diff --git a/damus/Util/Lists.swift b/damus/Util/Lists.swift @@ -16,22 +16,15 @@ func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent, to_remove: Str } func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_add: String, list_name: String, list_type: String) -> NostrEvent? { - let pubkey = keypair.pubkey - - if let prev = mprev { - if let okprev = ensure_list_name(list: prev, name: list_name), prev.pubkey == keypair.pubkey { - return add_to_list_event(keypair: keypair, prev: okprev, to_add: to_add, tag_type: list_type) - } + if let prev = mprev, + prev.pubkey == keypair.pubkey, + matches_list_name(tags: prev.tags, name: list_name) + { + return add_to_list_event(keypair: keypair, prev: prev, to_add: to_add, tag_type: list_type) } let tags = [["d", list_name], [list_type, to_add]] - let ev = NostrEvent(content: "", pubkey: pubkey, kind: 30000, tags: tags) - - ev.tags = tags - ev.id = hex_encode(calculate_event_id(ev: ev)) - ev.sig = sign_event(privkey: keypair.privkey, ev: ev) - - return ev + return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 30000, tags: tags) } func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: String, tag_type: String) -> NostrEvent? { @@ -51,11 +44,7 @@ func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: S !(tag.count >= 2 && tag[0] == tag_type && tag[1] == to_remove) } - let ev = NostrEvent(content: prev.content, pubkey: keypair.pubkey, kind: 30000, tags: new_tags) - ev.id = hex_encode(calculate_event_id(ev: ev)) - ev.sig = sign_event(privkey: keypair.privkey, ev: ev) - - return ev + return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: new_tags) } func add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: String, tag_type: String) -> NostrEvent? { @@ -65,27 +54,19 @@ func add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: String, t return nil } } - - let new = NostrEvent(content: prev.content, pubkey: keypair.pubkey, kind: 30000, tags: prev.tags) - new.tags.append([tag_type, to_add]) - new.id = hex_encode(calculate_event_id(ev: new)) - new.sig = sign_event(privkey: keypair.privkey, ev: new) - - return new + + var tags = Array(prev.tags) + tags.append([tag_type, to_add]) + + return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: tags) } -func ensure_list_name(list: NostrEvent, name: String) -> NostrEvent? { - for tag in list.tags { +func matches_list_name(tags: [[String]], name: String) -> Bool { + for tag in tags { if tag.count >= 2 && tag[0] == "d" { - if tag[1] != name { - return nil - } else { - return list - } + return tag[1] == name } } - - list.tags.insert(["d", name], at: 0) - - return list + + return false } diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -140,12 +140,11 @@ struct EventActionBar: View { } func send_like() { - guard let privkey = damus_state.keypair.privkey else { + guard let keypair = damus_state.keypair.to_full(), + let like_ev = make_like_event(keypair: keypair, liked: event) else { return } - - let like_ev = make_like_event(pubkey: damus_state.pubkey, privkey: privkey, liked: event) - + self.bar.our_like = like_ev generator.impactOccurred() @@ -222,10 +221,9 @@ struct LikeButton: View { struct EventActionBar_Previews: PreviewProvider { static var previews: some View { - let pk = "pubkey" let ds = test_damus_state() - let ev = NostrEvent(content: "hi", pubkey: pk) - + let ev = NostrEvent(content: "hi", keypair: test_keypair)! + let bar = ActionBarModel.empty() let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: nil) diff --git a/damus/Views/ActionBar/RepostAction.swift b/damus/Views/ActionBar/RepostAction.swift @@ -20,12 +20,11 @@ struct RepostAction: View { Button { dismiss() - guard let privkey = self.damus_state.keypair.privkey else { + guard let keypair = self.damus_state.keypair.to_full(), + let boost = make_boost_event(keypair: keypair, boosted: self.event) else { return } - - let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event) - + damus_state.postbox.send(boost) } label: { Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost") diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -116,15 +116,11 @@ struct ConfigView: View { confirm_delete_account = false } Button(NSLocalizedString("Delete", comment: "Button for deleting the users account."), role: .destructive) { - guard let full_kp = state.keypair.to_full() else { + guard let keypair = state.keypair.to_full(), + delete_text == DELETE_KEYWORD, + let ev = created_deleted_account_profile(keypair: keypair) else { return } - - guard delete_text == DELETE_KEYWORD else { - return - } - - let ev = created_deleted_account_profile(keypair: full_kp) state.postbox.send(ev) notify(.logout, ()) } diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -177,9 +177,9 @@ struct DMChatView: View, KeyboardReadable { struct DMChatView_Previews: PreviewProvider { static var previews: some View { - let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: []) + let ev = NostrEvent(content: "hi", keypair: test_keypair, kind: 1, tags: [])! - let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey", pubkey: "the_pk") + let model = DirectMessageModel(events: [ev], our_pubkey: test_pubkey, pubkey: test_pubkey) DMChatView(damus_state: test_damus_state(), dms: model) } @@ -216,11 +216,7 @@ func create_encrypted_event(_ message: String, to_pk: String, tags: [[String]], return nil } - let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: kind, tags: tags, createdAt: created_at) - - ev.calculate_id() - ev.sign(privkey: privkey) - return ev + return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at) } func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent? diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift @@ -73,7 +73,7 @@ struct DMView: View { struct DMView_Previews: PreviewProvider { static var previews: some View { - let ev = NostrEvent(content: "Hey there *buddy*, want to grab some drinks later? 🍻", pubkey: "pubkey", kind: 1, tags: []) + let ev = NostrEvent(content: "Hey there *buddy*, want to grab some drinks later? 🍻", keypair: test_keypair, kind: 1, tags: [])! DMView(event: ev, damus_state: test_damus_state()) } } diff --git a/damus/Views/Events/Longform/LongformView.swift b/damus/Views/Events/Longform/LongformView.swift @@ -59,19 +59,18 @@ struct LongformView: View { } } -let test_longform_event = LongformEvent.parse(from: - .init(id: "longform_id", - content: longform_long_test_data, - pubkey: "pk", - kind: NostrKind.longform.rawValue, - tags: [ - ["title", "What is WASTOIDS?"], - ["summary", "WASTOIDS is an audio/visual feed, created by Sam Means..."], - ["published_at", "1685638715"], - ["t", "coffee"], - ["t", "coffeechain"], - ["image", "https://cdn.jb55.com/s/038fe8f558153b52.jpg"], - ]) +let test_longform_event = LongformEvent.parse(from: NostrEvent( + content: longform_long_test_data, + keypair: test_keypair, + kind: NostrKind.longform.rawValue, + tags: [ + ["title", "What is WASTOIDS?"], + ["summary", "WASTOIDS is an audio/visual feed, created by Sam Means..."], + ["published_at", "1685638715"], + ["t", "coffee"], + ["t", "coffeechain"], + ["image", "https://cdn.jb55.com/s/038fe8f558153b52.jpg"], + ])! ) struct LongformView_Previews: PreviewProvider { diff --git a/damus/Views/Events/ZapEvent.swift b/damus/Views/Events/ZapEvent.swift @@ -48,15 +48,6 @@ struct ZapEvent: View { } -let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000) -let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734) -let test_zap_request = ZapRequest(ev: test_zap_request_ev) -let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: nil) - -let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: .init(ev: test_event)) - -let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) - struct ZapEvent_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift @@ -258,12 +258,6 @@ struct EventGroupView: View { } } -let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" -let test_repost_1 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk1", kind: 6, tags: [], createdAt: 1) -let test_repost_2 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk2", kind: 6, tags: [], createdAt: 1) -let test_reposts = [test_repost_1, test_repost_2] -let test_event_group = EventGroup(events: test_reposts) - struct EventGroupView_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/damus/Views/Profile/EditMetadataView.swift b/damus/Views/Profile/EditMetadataView.swift @@ -62,10 +62,12 @@ struct EditMetadataView: View { func save() { let profile = to_profile() - guard let keypair = damus_state.keypair.to_full() else { + guard let keypair = damus_state.keypair.to_full(), + let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile) + else { return } - let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile) + damus_state.postbox.send(metadata_ev) } diff --git a/damus/Views/Reactions/ReactionView.swift b/damus/Views/Reactions/ReactionView.swift @@ -28,6 +28,6 @@ struct ReactionView: View { struct ReactionView_Previews: PreviewProvider { static var previews: some View { - ReactionView(damus_state: test_damus_state(), reaction: NostrEvent(id: "", content: "🤙🏼", pubkey: "")) + ReactionView(damus_state: test_damus_state(), reaction: NostrEvent(content: "🤙🏼", keypair: test_keypair)!) } } diff --git a/damus/Views/Relays/RecommendedRelayView.swift b/damus/Views/Relays/RecommendedRelayView.swift @@ -24,9 +24,9 @@ struct RecommendedRelayView: View { var body: some View { ZStack { HStack { - if let privkey = damus.keypair.privkey { + if let keypair = damus.keypair.to_full() { if showActionButtons && add_button { - AddButton(privkey: privkey, showText: false) + AddButton(keypair: keypair, showText: false) } } @@ -59,8 +59,8 @@ struct RecommendedRelayView: View { } .swipeActions { if add_button { - if let privkey = damus.keypair.privkey { - AddButton(privkey: privkey, showText: false) + if let keypair = damus.keypair.to_full() { + AddButton(keypair: keypair, showText: false) .tint(.accentColor) } } @@ -68,8 +68,8 @@ struct RecommendedRelayView: View { .contextMenu { CopyAction(relay: relay) - if let privkey = damus.keypair.privkey { - AddButton(privkey: privkey, showText: true) + if let keypair = damus.keypair.to_full() { + AddButton(keypair: keypair, showText: true) } } } @@ -82,9 +82,9 @@ struct RecommendedRelayView: View { } } - func AddButton(privkey: String, showText: Bool) -> some View { + func AddButton(keypair: FullKeypair, showText: Bool) -> some View { Button(action: { - add_action(privkey: privkey) + add_action(keypair: keypair) }) { if showText { Text(NSLocalizedString("Connect", comment: "Button to connect to recommended relay server.")) @@ -97,11 +97,11 @@ struct RecommendedRelayView: View { } } - func add_action(privkey: String) { + func add_action(keypair: FullKeypair) { guard let ev_before_add = damus.contacts.event else { return } - guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.our_descriptors, relay: relay, info: .rw) else { + guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: damus.pool.our_descriptors, relay: relay, info: .rw) else { return } process_contact_event(state: damus, ev: ev_after_add) diff --git a/damus/Views/Relays/RelayConfigView.swift b/damus/Views/Relays/RelayConfigView.swift @@ -54,7 +54,7 @@ struct RelayConfigView: View { VStack { HStack { Spacer() - if(!new_relay.isEmpty) { + if !new_relay.isEmpty { Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted relay.")) { new_relay = "" UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) @@ -76,15 +76,9 @@ struct RelayConfigView: View { new_relay.removeLast(); } - guard let url = RelayURL(new_relay) else { - return - } - - guard let ev = state.contacts.event else { - return - } - - guard let privkey = state.keypair.privkey else { + guard let url = RelayURL(new_relay), + let ev = state.contacts.event, + let keypair = state.keypair.to_full() else { return } @@ -103,7 +97,7 @@ struct RelayConfigView: View { state.pool.connect(to: [new_relay]) - guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.our_descriptors, relay: new_relay, info: info) else { + guard let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: new_relay, info: info) else { return } diff --git a/damus/Views/Relays/RelayDetailView.swift b/damus/Views/Relays/RelayDetailView.swift @@ -40,35 +40,38 @@ struct RelayDetailView: View { return Text("No data available", comment: "Text indicating that there is no data available to show for specific metadata about a relay server.") } } - + + func RemoveRelayButton(_ keypair: FullKeypair) -> some View { + Button(action: { + guard let ev = state.contacts.event else { + return + } + + let descriptors = state.pool.our_descriptors + guard let new_ev = remove_relay( ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else { + return + } + + process_contact_event(state: state, ev: new_ev) + state.postbox.send(new_ev) + dismiss() + }) { + Text("Disconnect From Relay", comment: "Button to disconnect from the relay.") + } + } + var body: some View { Group { Form { - - if let privkey = state.keypair.privkey { + if let keypair = state.keypair.to_full() { if check_connection() { - Button(action: { - guard let ev = state.contacts.event else { - return - } - - let descriptors = state.pool.our_descriptors - guard let new_ev = remove_relay( ev: ev, current_relays: descriptors, privkey: privkey, relay: relay) else { - return - } - - process_contact_event(state: state, ev: new_ev) - state.postbox.send(new_ev) - dismiss() - }) { - Text("Disconnect From Relay", comment: "Button to disconnect from the relay.") - } + RemoveRelayButton(keypair) } else { Button(action: { guard let ev_before_add = state.contacts.event else { return } - guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else { + guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else { return } process_contact_event(state: state, ev: ev_after_add) diff --git a/damus/Views/Relays/RelayView.swift b/damus/Views/Relays/RelayView.swift @@ -95,7 +95,8 @@ struct RelayView: View { } let descriptors = state.pool.our_descriptors - guard let new_ev = remove_relay( ev: ev, current_relays: descriptors, privkey: privkey, relay: relay) else { + guard let keypair = state.keypair.to_full(), + let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else { return } diff --git a/damus/Views/ReportView.swift b/damus/Views/ReportView.swift @@ -10,8 +10,8 @@ import SwiftUI struct ReportView: View { let postbox: PostBox let target: ReportTarget - let privkey: String - + let keypair: FullKeypair + @State var report_sent: Bool = false @State var report_id: String = "" @State var report_message: String = "" @@ -46,7 +46,8 @@ struct ReportView: View { } func do_send_report() { - guard let selected_report_type, let ev = send_report(privkey: privkey, postbox: postbox, target: target, type: selected_report_type, message: report_message) else { + guard let selected_report_type, + let ev = send_report(keypair: keypair, postbox: postbox, target: target, type: selected_report_type, message: report_message) else { return } @@ -116,9 +117,9 @@ struct ReportView: View { } } -func send_report(privkey: String, postbox: PostBox, target: ReportTarget, type: ReportType, message: String) -> NostrEvent? { +func send_report(keypair: FullKeypair, postbox: PostBox, target: ReportTarget, type: ReportType, message: String) -> NostrEvent? { let report = Report(type: type, target: target, message: message) - guard let ev = create_report_event(privkey: privkey, report: report) else { + guard let ev = create_report_event(keypair: keypair, report: report) else { return nil } postbox.send(ev) @@ -130,10 +131,10 @@ struct ReportView_Previews: PreviewProvider { let ds = test_damus_state() VStack { - ReportView(postbox: ds.postbox, target: ReportTarget.user(""), privkey: "") - - ReportView(postbox: ds.postbox, target: ReportTarget.user(""), privkey: "", report_sent: true, report_id: "report_id") - + ReportView(postbox: ds.postbox, target: ReportTarget.user(""), keypair: test_keypair.to_full()!) + + ReportView(postbox: ds.postbox, target: ReportTarget.user(""), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id") + } } } diff --git a/damus/Views/Reposts/RepostView.swift b/damus/Views/Reposts/RepostView.swift @@ -18,7 +18,7 @@ struct RepostView: View { struct RepostView_Previews: PreviewProvider { static var previews: some View { - RepostView(damus_state: test_damus_state(), repost: NostrEvent(id: "", content: "", pubkey: "")) + RepostView(damus_state: test_damus_state(), repost: NostrEvent(content: "", keypair: test_keypair)!) } } diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift @@ -130,8 +130,8 @@ struct SaveKeysView: View { let metadata = create_account_to_metadata(account) let contacts_ev = make_first_contact_event(keypair: account.keypair) - if let keypair = account.keypair.to_full() { - let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) + if let keypair = account.keypair.to_full(), + let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) { self.pool.send(.event(metadata_ev)) } diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift @@ -181,7 +181,9 @@ struct WalletView: View { } profile.damus_donation = settings.donation_percent - let meta = make_metadata_event(keypair: keypair, metadata: profile) + guard let meta = make_metadata_event(keypair: keypair, metadata: profile) else { + return + } let tsprofile = TimestampedProfile(profile: profile, timestamp: meta.created_at, event: meta) damus_state.profiles.add(id: damus_state.pubkey, profile: tsprofile) damus_state.postbox.send(meta) diff --git a/damusTests/EventGroupViewTests.swift b/damusTests/EventGroupViewTests.swift @@ -29,14 +29,17 @@ final class EventGroupViewTests: XCTestCase { let damusState = test_damus_state() let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" - let repost1 = NostrEvent(id: "", content: encodedPost, pubkey: "pk1", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) - let repost2 = NostrEvent(id: "", content: encodedPost, pubkey: "pk2", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) - let repost3 = NostrEvent(id: "", content: encodedPost, pubkey: "pk3", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + let pk1 = Keypair(pubkey: "pk1", privkey: nil) + let pk2 = Keypair(pubkey: "pk2", privkey: nil) + let pk3 = Keypair(pubkey: "pk3", privkey: nil) + let repost1 = NostrEvent(content: encodedPost, keypair: pk1, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)! + let repost2 = NostrEvent(content: encodedPost, keypair: pk2, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)! + let repost3 = NostrEvent(content: encodedPost, keypair: pk3, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)! XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: []))), []) - XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1]))), ["pk1"]) - XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2]))), ["pk1", "pk2"]) - XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), ["pk1", "pk2", "pk3"]) + XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1]))), [pk1.pubkey]) + XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2]))), [pk1.pubkey, pk2.pubkey]) + XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), [pk1.pubkey, pk2.pubkey, pk3.pubkey]) } func testReactingToText() throws { @@ -44,20 +47,25 @@ final class EventGroupViewTests: XCTestCase { let damusState = test_damus_state() let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" - let repost1 = NostrEvent(id: "", content: encodedPost, pubkey: "pk1", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) - let repost2 = NostrEvent(id: "", content: encodedPost, pubkey: "pk2", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) - let repost3 = NostrEvent(id: "", content: encodedPost, pubkey: "pk3", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + + let pk1 = Keypair(pubkey: "pk1", privkey: nil) + let pk2 = Keypair(pubkey: "pk2", privkey: nil) + let pk3 = Keypair(pubkey: "pk3", privkey: nil) + + let repost1 = NostrEvent(content: encodedPost, keypair: pk1, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)! + let repost2 = NostrEvent(content: encodedPost, keypair: pk2, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)! + let repost3 = NostrEvent(content: encodedPost, keypair: pk3, kind: NostrKind.boost.rawValue, tags: [], createdAt: 1)! XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, pubkeys: [], locale: enUsLocale), "??") - XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, pubkeys: ["pk1"], locale: enUsLocale), "pk1:pk1 reposted a note you were tagged in") - XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, pubkeys: ["pk1", "pk2"], locale: enUsLocale), "pk1:pk1 and pk2:pk2 reposted a note you were tagged in") - XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, pubkeys: ["pk1", "pk2", "pk3"], locale: enUsLocale), "pk1:pk1 and 2 others reposted a note you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, pubkeys: [pk1.pubkey], locale: enUsLocale), "pk1:pk1 reposted a note you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, pubkeys: [pk1.pubkey, pk2.pubkey], locale: enUsLocale), "pk1:pk1 and pk2:pk2 reposted a note you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, pubkeys: [pk1.pubkey, pk2.pubkey, pk3.pubkey], locale: enUsLocale), "pk1:pk1 and 2 others reposted a note you were tagged in") Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, pubkeys: [], locale: $0), "??") - XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, pubkeys: ["pk1"], locale: $0)) - XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, pubkeys: ["pk1", "pk2"], locale: $0)) - XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost3])), ev: test_event, pubkeys: ["pk1", "pk2", "pk3"], locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, pubkeys: [pk1.pubkey], locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, pubkeys: [pk1.pubkey, pk2.pubkey], locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost3])), ev: test_event, pubkeys: [pk1.pubkey, pk2.pubkey, pk3.pubkey], locale: $0)) } } diff --git a/damusTests/LikeTests.swift b/damusTests/LikeTests.swift @@ -19,31 +19,25 @@ class LikeTests: XCTestCase { } func testLikeHasNotification() throws { - let privkey = "0fc2092231f958f8d57d66f5e238bb45b6a2571f44c0ce024bbc6f3a9c8a15fe" - let pubkey = "30c6d1dc7f7c156794fa15055e651b758a61b99f50fcf759de59386050bf6ae2" - let liked = NostrEvent(content: "awesome #[0] post", pubkey: "orig_pk", tags: [["p", "cindy"], ["e", "bob"]]) - liked.calculate_id() + let liked = NostrEvent(content: "awesome #[0] post", keypair: test_keypair, tags: [["p", "cindy"], ["e", "bob"]])! let id = liked.id - let like_ev = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked) - - XCTAssertTrue(like_ev.references(id: "orig_pk", key: "p")) + let like_ev = make_like_event(keypair: test_keypair_full, liked: liked)! + + XCTAssertTrue(like_ev.references(id: test_keypair.pubkey, key: "p")) XCTAssertTrue(like_ev.references(id: "cindy", key: "p")) XCTAssertTrue(like_ev.references(id: "bob", key: "e")) XCTAssertEqual(like_ev.last_refid()!.ref_id, id) } func testToReactionEmoji() { - let privkey = "0fc2092231f958f8d57d66f5e238bb45b6a2571f44c0ce024bbc6f3a9c8a15fe" - let pubkey = "30c6d1dc7f7c156794fa15055e651b758a61b99f50fcf759de59386050bf6ae2" - let liked = NostrEvent(content: "awesome #[0] post", pubkey: "orig_pk", tags: [["p", "cindy"], ["e", "bob"]]) - liked.calculate_id() - - let emptyReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "") - let plusReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "+") - let minusReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "-") - let heartReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "❤️") - let thumbsUpReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "👍") - let shakaReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "🤙") + let liked = NostrEvent(content: "awesome #[0] post", keypair: test_keypair, tags: [["p", "cindy"], ["e", "bob"]])! + + let emptyReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "")! + let plusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "+")! + let minusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "-")! + let heartReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "❤️")! + let thumbsUpReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "👍")! + let shakaReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "🤙")! XCTAssertEqual(to_reaction_emoji(ev: emptyReaction), "❤️") XCTAssertEqual(to_reaction_emoji(ev: plusReaction), "❤️") diff --git a/damusTests/ReplyTests.swift b/damusTests/ReplyTests.swift @@ -331,8 +331,8 @@ class ReplyTests: XCTestCase { let reply_ref = ReferencedId(ref_id: evid, relay_id: nil, key: "e") let blocks = parse_post_blocks(content: content) let post = NostrPost(content: content, references: [reply_ref]) - let ev = post_to_event(post: post, privkey: evid, pubkey: pk) - + let ev = post_to_event(post: post, keypair: test_keypair_full)! + XCTAssertEqual(ev.tags.count, 2) XCTAssertEqual(blocks.count, 3) XCTAssertEqual(blocks[1].is_mention, .pubkey(hex_pk)) @@ -347,8 +347,8 @@ class ReplyTests: XCTestCase { let reply_ref = ReferencedId(ref_id: evid, relay_id: nil, key: "e") let blocks = parse_post_blocks(content: content) let post = NostrPost(content: content, references: [reply_ref]) - let ev = post_to_event(post: post, privkey: evid, pubkey: pk) - + let ev = post_to_event(post: post, keypair: test_keypair_full)! + XCTAssertEqual(ev.tags.count, 2) XCTAssertEqual(blocks.count, 3) XCTAssertEqual(blocks[1].is_mention, .pubkey(hex_pk)) @@ -367,7 +367,7 @@ class ReplyTests: XCTestCase { ] let post = NostrPost(content: "this is a (@\(npub)) mention", references: refs) - let ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey) + let ev = post_to_event(post: post, keypair: test_keypair_full)! XCTAssertEqual(ev.content, "this is a (nostr:\(npub)) mention") XCTAssertEqual(ev.tags[2][1], pubkey) diff --git a/damusTests/UserSearchCacheTests.swift b/damusTests/UserSearchCacheTests.swift @@ -116,7 +116,6 @@ final class UserSearchCacheTests: XCTestCase { private func createContactsEventWithPetnames(pubkeysToPetnames: [String: String]) throws -> NostrEvent { let keypair = try XCTUnwrap(keypair) - let privkey = try XCTUnwrap(keypair.privkey) let bootstrapRelays = load_bootstrap_relays(pubkey: keypair.pubkey) let relayInfo = RelayInfo(read: true, write: true) @@ -132,13 +131,7 @@ final class UserSearchCacheTests: XCTestCase { ["p", $0.element.key, "", $0.element.value] } - let ev = NostrEvent(content: relayJson, - pubkey: keypair.pubkey, - kind: NostrKind.contacts.rawValue, - tags: tags) - ev.calculate_id() - ev.sign(privkey: privkey) - return ev + return NostrEvent(content: relayJson, keypair: keypair, kind: NostrKind.contacts.rawValue, tags: tags)! } } diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift @@ -180,11 +180,9 @@ class damusTests: XCTestCase { } func testMakeHashtagPost() { - let privkey = "d05f5fcceef3e4529703f62a29222d6ee2d1b7bf1f24729b5e01df7c633cec8a" - let pubkey = "6e59d3b78b1c1490a6489c94405873b57d8ef398a830ae5e39608f4107e9a790" let post = NostrPost(content: "#damus some content #bitcoin derp", references: []) - let ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey) - + let ev = post_to_event(post: post, keypair: test_keypair_full)! + XCTAssertEqual(ev.tags.count, 2) XCTAssertEqual(ev.content, "#damus some content #bitcoin derp") XCTAssertEqual(ev.tags[0][0], "t") diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift @@ -34,7 +34,7 @@ enum NdbData { } } -class NdbNote: Equatable { +class NdbNote: Equatable, Hashable { // we can have owned notes, but we can also have lmdb virtual-memory mapped notes so its optional private let owned: Bool let count: Int @@ -66,14 +66,14 @@ class NdbNote: Equatable { ndb_note_content_length(note) } - /// These are unsafe if working on owned data and if this outlives the NdbNote - var id: Data { - Data(buffer: UnsafeBufferPointer(start: ndb_note_id(note), count: 32)) + /// NDBTODO: make this into data + var id: String { + hex_encode(Data(buffer: UnsafeBufferPointer(start: ndb_note_id(note), count: 32))) } - /// Make a copy if this outlives the note and the note has owned data! - var pubkey: Data { - Data(buffer: UnsafeBufferPointer(start: ndb_note_pubkey(note), count: 32)) + /// NDBTODO: make this into data + var pubkey: String { + hex_encode(Data(buffer: UnsafeBufferPointer(start: ndb_note_pubkey(note), count: 32))) } var created_at: UInt32 { @@ -98,6 +98,64 @@ class NdbNote: Equatable { return lhs.id == rhs.id } + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static let max_note_size: Int = 2 << 18 + + init?(content: String, keypair: Keypair, kind: Int = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970)) { + + var builder = ndb_builder() + let buflen = MAX_NOTE_SIZE + let buf = malloc(buflen) + let idbuf = malloc(buflen) + + ndb_builder_init(&builder, buf, Int32(buflen)) + + guard var pk_raw = hex_decode(keypair.pubkey) else { return nil } + + ndb_builder_set_pubkey(&builder, &pk_raw) + ndb_builder_set_kind(&builder, UInt32(kind)) + ndb_builder_set_created_at(&builder, createdAt) + + for tag in tags { + ndb_builder_new_tag(&builder); + for elem in tag { + _ = elem.withCString { eptr in + ndb_builder_push_tag_str(&builder, eptr, Int32(elem.utf8.count)) + } + } + } + + _ = content.withCString { cptr in + ndb_builder_set_content(&builder, content, Int32(content.utf8.count)); + } + + var n = UnsafeMutablePointer<ndb_note>?(nil) + + let keypair = keypair.privkey.map { sec in + var kp = ndb_keypair() + return sec.withCString { secptr in + ndb_decode_key(secptr, &kp) + return kp + } + } + + var len: Int32 = 0 + if var keypair { + len = ndb_builder_finalize(&builder, &n, &keypair) + } else { + len = ndb_builder_finalize(&builder, &n, nil) + } + + free(idbuf) + + self.owned = true + self.count = Int(len) + self.note = realloc(n, Int(len)).assumingMemoryBound(to: ndb_note.self) + } + static func owned_from_json(json: String, bufsize: Int = 2 << 18) -> NdbNote? { return json.withCString { cstr in return NdbNote.owned_from_json_cstr( @@ -135,7 +193,7 @@ extension NdbNote { } var known_kind: NostrKind? { - return NostrKind.init(rawValue: Int(kind)) + return NostrKind.init(rawValue: kind) } var too_big: Bool { @@ -180,6 +238,10 @@ extension NdbNote { References.pubkeys(tags: self.tags) } + public var referenced_hashtags: LazyFilterSequence<References> { + References.hashtags(tags: self.tags) + } + func event_refs(_ privkey: String?) -> [EventRef] { if let rs = _event_refs { return rs @@ -220,7 +282,7 @@ extension NdbNote { } // NDBTODO: don't hex encode - var pubkey = hex_encode(self.pubkey) + var pubkey = self.pubkey // This is our DM, we need to use the pubkey of the person we're talking to instead if our_pubkey == pubkey { @@ -266,7 +328,7 @@ extension NdbNote { } } - return hex_encode(self.id) + return self.id } public func last_refid() -> ReferencedId? { diff --git a/nostrdb/NdbTagElem.swift b/nostrdb/NdbTagElem.swift @@ -31,32 +31,60 @@ struct NdbStrIter: IteratorProtocol { } } -struct NdbTagElem: Sequence { +struct NdbTagElem: Sequence, Hashable { + let note: NdbNote let tag: UnsafeMutablePointer<ndb_tag> let index: Int32 + let str: ndb_str + + func hash(into hasher: inout Hasher) { + if str.flag == NDB_PACKED_ID { + hasher.combine(bytes: UnsafeRawBufferPointer(start: str.id, count: 32)) + } else { + hasher.combine(bytes: UnsafeRawBufferPointer(start: str.str, count: strlen(str.str))) + } + } + + static func == (lhs: NdbTagElem, rhs: NdbTagElem) -> Bool { + if lhs.str.flag == NDB_PACKED_ID && rhs.str.flag == NDB_PACKED_ID { + return memcmp(lhs.str.id, rhs.str.id, 32) == 0 + } else if lhs.str.flag == NDB_PACKED_ID || rhs.str.flag == NDB_PACKED_ID { + return false + } + + let l = strlen(lhs.str.str) + let r = strlen(rhs.str.str) + if l != r { return false } + + return memcmp(lhs.str.str, rhs.str.str, r) == 0 + } init(note: NdbNote, tag: UnsafeMutablePointer<ndb_tag>, index: Int32) { self.note = note self.tag = tag self.index = index + self.str = ndb_tag_str(note.note, tag, index) } var is_id: Bool { - return ndb_tag_str(note.note, tag, index).flag == NDB_PACKED_ID + return str.flag == NDB_PACKED_ID } var count: Int { - let r = ndb_tag_str(note.note, tag, index) - if r.flag == NDB_PACKED_ID { + if str.flag == NDB_PACKED_ID { return 32 } else { - return strlen(r.str) + return strlen(str.str) } } func matches_char(_ c: AsciiCharacter) -> Bool { - return ndb_tag_matches_char(note.note, tag, index, c.cchar) == 1 + return str.str[0] == c.cchar && str.str[1] == 0 + } + + var ndbstr: ndb_str { + return ndb_tag_str(note.note, tag, index) } func data() -> NdbData { diff --git a/nostrdb/NdbTagsIterator.swift b/nostrdb/NdbTagsIterator.swift @@ -42,6 +42,12 @@ struct TagsSequence: Sequence { note.note.pointee.tags.count } + func strings() -> [[String]] { + return self.map { tag in + tag.map { t in t.string() } + } + } + // no O(1) indexing on top-level tag lists unfortunately :( // bit it's very fast to iterate over each tag since the number of tags // are stored and the elements are fixed size. diff --git a/nostrdb/Test/NdbTests.swift b/nostrdb/Test/NdbTests.swift @@ -25,8 +25,8 @@ final class NdbTests: XCTestCase { let id = "20d0ff27d6fcb13de8366328c5b1a7af26bcac07f2e558fbebd5e9242e608c09" let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" - XCTAssertEqual(hex_encode(note.id), id) - XCTAssertEqual(hex_encode(note.pubkey), pubkey) + XCTAssertEqual(note.id, id) + XCTAssertEqual(note.pubkey, pubkey) XCTAssertEqual(note.count, 34322) XCTAssertEqual(note.kind, 3) @@ -126,8 +126,8 @@ final class NdbTests: XCTestCase { guard let event else { return } XCTAssertEqual(note.content_len, UInt32(event.content.utf8.count)) - XCTAssertEqual(hex_encode(note.pubkey), event.pubkey) - XCTAssertEqual(hex_encode(note.id), event.id) + XCTAssertEqual(note.pubkey, event.pubkey) + XCTAssertEqual(note.id, event.id) let ev_blocks = event.blocks(nil) let note_blocks = note.blocks(nil) diff --git a/nostrdb/nostrdb.c b/nostrdb/nostrdb.c @@ -3,9 +3,15 @@ #include "jsmn.h" #include "hex.h" #include "cursor.h" +#include "random.h" +#include "sha256.h" #include <stdlib.h> #include <limits.h> +#include "secp256k1.h" +#include "secp256k1_ecdh.h" +#include "secp256k1_schnorrsig.h" + struct ndb_json_parser { const char *json; int json_len; @@ -20,11 +26,10 @@ static inline int cursor_push_tag(struct cursor *cur, struct ndb_tag *tag) return cursor_push_u16(cur, tag->count); } -int ndb_builder_new(struct ndb_builder *builder, unsigned char *buf, - int bufsize) +int ndb_builder_init(struct ndb_builder *builder, unsigned char *buf, + int bufsize) { struct ndb_note *note; - struct cursor mem; int half, size, str_indices_size; // come on bruh @@ -38,20 +43,21 @@ int ndb_builder_new(struct ndb_builder *builder, unsigned char *buf, //debug("size %d half %d str_indices %d\n", size, half, str_indices_size); // make a safe cursor of our available memory - make_cursor(buf, buf + bufsize, &mem); + make_cursor(buf, buf + bufsize, &builder->mem); note = builder->note = (struct ndb_note *)buf; // take slices of the memory into subcursors - if (!(cursor_slice(&mem, &builder->note_cur, half) && - cursor_slice(&mem, &builder->strings, half) && - cursor_slice(&mem, &builder->str_indices, str_indices_size))) { + if (!(cursor_slice(&builder->mem, &builder->note_cur, half) && + cursor_slice(&builder->mem, &builder->strings, half) && + cursor_slice(&builder->mem, &builder->str_indices, str_indices_size))) { return 0; } memset(note, 0, sizeof(*note)); builder->note_cur.p += sizeof(*note); + note->strings = builder->strings.start - buf; note->version = 1; return 1; @@ -82,7 +88,7 @@ static inline int ndb_json_parser_init(struct ndb_json_parser *p, // the more important stuff gets a larger chunk and then it spirals // downward into smaller chunks. Thanks for coming to my TED talk. - if (!ndb_builder_new(&p->builder, buf, half)) + if (!ndb_builder_init(&p->builder, buf, half)) return 0; jsmn_init(&p->json_parser); @@ -99,11 +105,234 @@ static inline int ndb_json_parser_parse(struct ndb_json_parser *p) return p->num_tokens; } -int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note) +static int cursor_push_unescaped_char(struct cursor *cur, char c1, char c2) +{ + switch (c2) { + case 't': return cursor_push_byte(cur, '\t'); + case 'n': return cursor_push_byte(cur, '\n'); + case 'r': return cursor_push_byte(cur, '\r'); + case 'b': return cursor_push_byte(cur, '\b'); + case 'f': return cursor_push_byte(cur, '\f'); + case '\\': return cursor_push_byte(cur, '\\'); + case '"': return cursor_push_byte(cur, '"'); + case 'u': + // these aren't handled yet + return 0; + default: + return cursor_push_byte(cur, c1) && cursor_push_byte(cur, c2); + } +} + +static int cursor_push_escaped_char(struct cursor *cur, char c) +{ + switch (c) { + case '"': return cursor_push_str(cur, "\\\""); + case '\\': return cursor_push_str(cur, "\\\\"); + case '\b': return cursor_push_str(cur, "\\b"); + case '\f': return cursor_push_str(cur, "\\f"); + case '\n': return cursor_push_str(cur, "\\n"); + case '\r': return cursor_push_str(cur, "\\r"); + case '\t': return cursor_push_str(cur, "\\t"); + // TODO: \u hex hex hex hex + } + return cursor_push_byte(cur, c); +} + +static int cursor_push_hex_str(struct cursor *cur, unsigned char *buf, int len) +{ + int i; + + if (len % 2 != 0) + return 0; + + if (!cursor_push_byte(cur, '"')) + return 0; + + for (i = 0; i < len; i++) { + unsigned int c = ((const unsigned char *)buf)[i]; + if (!cursor_push_byte(cur, hexchar(c >> 4))) + return 0; + if (!cursor_push_byte(cur, hexchar(c & 0xF))) + return 0; + } + + if (!cursor_push_byte(cur, '"')) + return 0; + + return 1; +} + +static int cursor_push_jsonstr(struct cursor *cur, const char *str) +{ + int i; + int len; + + len = strlen(str); + + if (!cursor_push_byte(cur, '"')) + return 0; + + for (i = 0; i < len; i++) { + if (!cursor_push_escaped_char(cur, str[i])) + return 0; + } + + if (!cursor_push_byte(cur, '"')) + return 0; + + return 1; +} + + +static inline int cursor_push_json_tag_str(struct cursor *cur, struct ndb_str str) +{ + if (str.flag == NDB_PACKED_ID) + return cursor_push_hex_str(cur, str.id, 32); + + return cursor_push_jsonstr(cur, str.str); +} + +static int cursor_push_json_tag(struct cursor *cur, struct ndb_note *note, + struct ndb_tag *tag) +{ + int i; + + if (!cursor_push_byte(cur, '[')) + return 0; + + for (i = 0; i < tag->count; i++) { + if (!cursor_push_json_tag_str(cur, ndb_note_str(note, &tag->strs[i]))) + return 0; + if (i != tag->count-1 && !cursor_push_byte(cur, ',')) + return 0; + } + + return cursor_push_byte(cur, ']'); +} + +static int cursor_push_json_tags(struct cursor *cur, struct ndb_note *note) +{ + int i; + struct ndb_iterator iter, *it = &iter; + ndb_tags_iterate_start(note, it); + + if (!cursor_push_byte(cur, '[')) + return 0; + + i = 0; + while (ndb_tags_iterate_next(it)) { + if (!cursor_push_json_tag(cur, note, it->tag)) + return 0; + if (i != note->tags.count-1 && !cursor_push_str(cur, ",")) + return 0; + i++; + } + + if (!cursor_push_byte(cur, ']')) + return 0; + + return 1; +} + +static int ndb_event_commitment(struct ndb_note *ev, unsigned char *buf, int buflen) +{ + char timebuf[16] = {0}; + char kindbuf[16] = {0}; + char pubkey[65]; + struct cursor cur; + int ok; + + if (!hex_encode(ev->pubkey, sizeof(ev->pubkey), pubkey, 32)) + return 0; + + make_cursor(buf, buf + buflen, &cur); + + snprintf(timebuf, sizeof(timebuf), "%d", ev->created_at); + snprintf(kindbuf, sizeof(kindbuf), "%d", ev->kind); + + ok = + cursor_push_str(&cur, "[0,\"") && + cursor_push_str(&cur, pubkey) && + cursor_push_str(&cur, "\",") && + cursor_push_str(&cur, timebuf) && + cursor_push_str(&cur, ",") && + cursor_push_str(&cur, kindbuf) && + cursor_push_str(&cur, ",") && + cursor_push_json_tags(&cur, ev) && + cursor_push_str(&cur, ",") && + cursor_push_jsonstr(&cur, ndb_note_str(ev, &ev->content).str) && + cursor_push_str(&cur, "]"); + + if (!ok) + return 0; + + return cur.p - cur.start; +} + +int ndb_calculate_id(struct ndb_note *note, unsigned char *buf, int buflen) { + int len; + + if (!(len = ndb_event_commitment(note, buf, buflen))) + return 0; + + //fprintf(stderr, "%.*s\n", len, buf); + + sha256((struct sha256*)note->id, buf, len); + + return 1; +} + +int ndb_sign_id(struct ndb_keypair *keypair, unsigned char id[32], + unsigned char sig[64]) +{ + unsigned char aux[32]; + secp256k1_keypair *pair = (secp256k1_keypair*) keypair->pair; + + if (!fill_random(aux, sizeof(aux))) + return 0; + + secp256k1_context *ctx = + secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + return secp256k1_schnorrsig_sign32(ctx, sig, id, pair, aux); +} + +int ndb_create_keypair(struct ndb_keypair *kp) +{ + secp256k1_keypair *keypair = (secp256k1_keypair*)kp->pair; + secp256k1_xonly_pubkey pubkey; + + secp256k1_context *ctx = + secp256k1_context_create(SECP256K1_CONTEXT_NONE);; + + /* Try to create a keypair with a valid context, it should only + * fail if the secret key is zero or out of range. */ + if (!secp256k1_keypair_create(ctx, keypair, kp->secret)) + return 0; + + if (!secp256k1_keypair_xonly_pub(ctx, &pubkey, NULL, keypair)) + return 0; + + /* Serialize the public key. Should always return 1 for a valid public key. */ + return secp256k1_xonly_pubkey_serialize(ctx, kp->pubkey, &pubkey); +} + +int ndb_decode_key(const char *secstr, struct ndb_keypair *keypair) +{ + if (!hex_decode(secstr, strlen(secstr), keypair->secret, 32)) { + fprintf(stderr, "could not hex decode secret key\n"); + return 0; + } + + return ndb_create_keypair(keypair); +} + +int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note, + struct ndb_keypair *keypair) { int strings_len = builder->strings.p - builder->strings.start; - unsigned char *end = builder->note_cur.p + strings_len; - int total_size = end - builder->note_cur.start; + unsigned char *note_end = builder->note_cur.p + strings_len; + int total_size = note_end - builder->note_cur.start; // move the strings buffer next to the end of our ndb_note memmove(builder->note_cur.p, builder->strings.start, strings_len); @@ -116,6 +345,19 @@ int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note) *note = builder->note; + // generate id and sign if we're building this manually + if (keypair) { + // use the remaining memory for building our id buffer + unsigned char *end = builder->mem.end; + unsigned char *start = (unsigned char*)(*note) + total_size; + + if (!ndb_calculate_id(*note, start, end - start)) + return 0; + + if (!ndb_sign_id(keypair, (*note)->id, (*note)->sig)) + return 0; + } + return total_size; } @@ -297,44 +539,8 @@ static int ndb_builder_make_json_str(struct ndb_builder *builder, return 0; } - switch (*(p+1)) { - case 't': - if (!cursor_push_byte(&builder->strings, '\t')) - return 0; - break; - case 'n': - if (!cursor_push_byte(&builder->strings, '\n')) - return 0; - break; - case 'r': - if (!cursor_push_byte(&builder->strings, '\r')) - return 0; - break; - case 'b': - if (!cursor_push_byte(&builder->strings, '\b')) - return 0; - break; - case 'f': - if (!cursor_push_byte(&builder->strings, '\f')) - return 0; - break; - case '\\': - if (!cursor_push_byte(&builder->strings, '\\')) - return 0; - break; - case '"': - if (!cursor_push_byte(&builder->strings, '"')) - return 0; - break; - case 'u': - // these aren't handled yet + if (!cursor_push_unescaped_char(&builder->strings, *p, *(p+1))) return 0; - default: - if (!cursor_push_byte(&builder->strings, *p) || - !cursor_push_byte(&builder->strings, *(p+1))) - return 0; - break; - } p++; // Skip the character following the backslash start = p + 1; // Update the start pointer to the next character @@ -482,7 +688,7 @@ int ndb_note_from_json(const char *json, int len, struct ndb_note **note, // sig tok = &parser.toks[i+1]; hex_decode(json + tok->start, toksize(tok), hexbuf, sizeof(hexbuf)); - ndb_builder_set_signature(&parser.builder, hexbuf); + ndb_builder_set_sig(&parser.builder, hexbuf); } else if (start[0] == 'k' && jsoneq(json, tok, tok_len, "kind")) { // kind tok = &parser.toks[i+1]; @@ -524,7 +730,7 @@ int ndb_note_from_json(const char *json, int len, struct ndb_note **note, } } - return ndb_builder_finalize(&parser.builder, note); + return ndb_builder_finalize(&parser.builder, note, NULL); } void ndb_builder_set_pubkey(struct ndb_builder *builder, unsigned char *pubkey) @@ -537,10 +743,9 @@ void ndb_builder_set_id(struct ndb_builder *builder, unsigned char *id) memcpy(builder->note->id, id, 32); } -void ndb_builder_set_signature(struct ndb_builder *builder, - unsigned char *signature) +void ndb_builder_set_sig(struct ndb_builder *builder, unsigned char *sig) { - memcpy(builder->note->signature, signature, 64); + memcpy(builder->note->sig, sig, 64); } void ndb_builder_set_kind(struct ndb_builder *builder, uint32_t kind) @@ -548,6 +753,11 @@ void ndb_builder_set_kind(struct ndb_builder *builder, uint32_t kind) builder->note->kind = kind; } +void ndb_builder_set_created_at(struct ndb_builder *builder, uint32_t created_at) +{ + builder->note->created_at = created_at; +} + int ndb_builder_new_tag(struct ndb_builder *builder) { builder->note->tags.count++; diff --git a/nostrdb/nostrdb.h b/nostrdb/nostrdb.h @@ -12,6 +12,12 @@ struct ndb_str { }; }; +struct ndb_keypair { + unsigned char pubkey[32]; + unsigned char secret[32]; + unsigned char pair[96]; +}; + // these must be byte-aligned, they are directly accessing the serialized data // representation #pragma pack(push, 1) @@ -47,7 +53,7 @@ struct ndb_note { unsigned char padding[3]; // keep things aligned unsigned char id[32]; unsigned char pubkey[32]; - unsigned char signature[64]; + unsigned char sig[64]; uint32_t created_at; uint32_t kind; @@ -62,6 +68,7 @@ struct ndb_note { #pragma pack(pop) struct ndb_builder { + struct cursor mem; struct cursor note_cur; struct cursor strings; struct cursor str_indices; @@ -77,18 +84,24 @@ struct ndb_iterator { int index; }; -// HI BUILDER +// HELPERS +int ndb_calculate_id(struct ndb_note *note, unsigned char *buf, int buflen); +int ndb_sign_id(struct ndb_keypair *keypair, unsigned char id[32], unsigned char sig[64]); +int ndb_create_keypair(struct ndb_keypair *key); +int ndb_decode_key(const char *secstr, struct ndb_keypair *keypair); + +// BUILDER int ndb_note_from_json(const char *json, int len, struct ndb_note **, unsigned char *buf, int buflen); -int ndb_builder_new(struct ndb_builder *builder, unsigned char *buf, int bufsize); -int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note); +int ndb_builder_init(struct ndb_builder *builder, unsigned char *buf, int bufsize); +int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note, struct ndb_keypair *privkey); int ndb_builder_set_content(struct ndb_builder *builder, const char *content, int len); -void ndb_builder_set_signature(struct ndb_builder *builder, unsigned char *signature); +void ndb_builder_set_created_at(struct ndb_builder *builder, uint32_t created_at); +void ndb_builder_set_sig(struct ndb_builder *builder, unsigned char *sig); void ndb_builder_set_pubkey(struct ndb_builder *builder, unsigned char *pubkey); void ndb_builder_set_id(struct ndb_builder *builder, unsigned char *id); void ndb_builder_set_kind(struct ndb_builder *builder, uint32_t kind); int ndb_builder_new_tag(struct ndb_builder *builder); int ndb_builder_push_tag_str(struct ndb_builder *builder, const char *str, int len); -// BYE BUILDER static inline struct ndb_str ndb_note_str(struct ndb_note *note, union ndb_packed_str *pstr) @@ -111,17 +124,6 @@ static inline struct ndb_str ndb_tag_str(struct ndb_note *note, return ndb_note_str(note, &tag->strs[ind]); } -static inline int ndb_tag_matches_char(struct ndb_note *note, - struct ndb_tag *tag, int ind, char c) -{ - struct ndb_str str = ndb_tag_str(note, tag, ind); - if (str.str[0] == '\0') - return 0; - else if (str.str[0] == c) - return 1; - return 0; -} - static inline struct ndb_str ndb_iter_tag_str(struct ndb_iterator *iter, int ind) { @@ -138,9 +140,9 @@ static inline unsigned char * ndb_note_pubkey(struct ndb_note *note) return note->pubkey; } -static inline unsigned char * ndb_note_signature(struct ndb_note *note) +static inline unsigned char * ndb_note_sig(struct ndb_note *note) { - return note->signature; + return note->sig; } static inline uint32_t ndb_note_created_at(struct ndb_note *note) diff --git a/nostrdb/random.h b/nostrdb/random.h @@ -0,0 +1,66 @@ +/************************************************************************* + * Copyright (c) 2020-2021 Elichai Turkel * + * Distributed under the CC0 software license, see the accompanying file * + * EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 * + *************************************************************************/ + +/* + * This file is an attempt at collecting best practice methods for obtaining randomness with different operating systems. + * It may be out-of-date. Consult the documentation of the operating system before considering to use the methods below. + * + * Platform randomness sources: + * Linux -> `getrandom(2)`(`sys/random.h`), if not available `/dev/urandom` should be used. http://man7.org/linux/man-pages/man2/getrandom.2.html, https://linux.die.net/man/4/urandom + * macOS -> `getentropy(2)`(`sys/random.h`), if not available `/dev/urandom` should be used. https://www.unix.com/man-page/mojave/2/getentropy, https://opensource.apple.com/source/xnu/xnu-517.12.7/bsd/man/man4/random.4.auto.html + * FreeBSD -> `getrandom(2)`(`sys/random.h`), if not available `kern.arandom` should be used. https://www.freebsd.org/cgi/man.cgi?query=getrandom, https://www.freebsd.org/cgi/man.cgi?query=random&sektion=4 + * OpenBSD -> `getentropy(2)`(`unistd.h`), if not available `/dev/urandom` should be used. https://man.openbsd.org/getentropy, https://man.openbsd.org/urandom + * Windows -> `BCryptGenRandom`(`bcrypt.h`). https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom + */ + +#if defined(_WIN32) +#include <windows.h> +#include <ntstatus.h> +#include <bcrypt.h> +#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) +//#include <sys/random.h> +#include <Security/SecRandom.h> +#elif defined(__OpenBSD__) +#include <unistd.h> +#else +#error "Couldn't identify the OS" +#endif + +#include <stddef.h> +#include <limits.h> +#include <stdio.h> + + +/* Returns 1 on success, and 0 on failure. */ +static int fill_random(unsigned char* data, size_t size) { +#if defined(_WIN32) + NTSTATUS res = BCryptGenRandom(NULL, data, size, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (res != STATUS_SUCCESS || size > ULONG_MAX) { + return 0; + } else { + return 1; + } +#elif defined(__linux__) || defined(__FreeBSD__) + /* If `getrandom(2)` is not available you should fallback to /dev/urandom */ + ssize_t res = getrandom(data, size, 0); + if (res < 0 || (size_t)res != size ) { + return 0; + } else { + return 1; + } +#elif defined(__APPLE__) || defined(__OpenBSD__) + /* If `getentropy(2)` is not available you should fallback to either + * `SecRandomCopyBytes` or /dev/urandom */ + int res = SecRandomCopyBytes(kSecRandomDefault, size, data); + if (res == 0) { + return 1; + } else { + return 0; + } +#endif + return 0; +} + diff --git a/nostrdb/secp256k1.h b/nostrdb/secp256k1.h @@ -0,0 +1,909 @@ +#ifndef SECP256K1_H +#define SECP256K1_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stddef.h> + +/** Unless explicitly stated all pointer arguments must not be NULL. + * + * The following rules specify the order of arguments in API calls: + * + * 1. Context pointers go first, followed by output arguments, combined + * output/input arguments, and finally input-only arguments. + * 2. Array lengths always immediately follow the argument whose length + * they describe, even if this violates rule 1. + * 3. Within the OUT/OUTIN/IN groups, pointers to data that is typically generated + * later go first. This means: signatures, public nonces, secret nonces, + * messages, public keys, secret keys, tweaks. + * 4. Arguments that are not data pointers go last, from more complex to less + * complex: function pointers, algorithm names, messages, void pointers, + * counts, flags, booleans. + * 5. Opaque data pointers follow the function pointer they are to be passed to. + */ + +/** Opaque data structure that holds context information + * + * The primary purpose of context objects is to store randomization data for + * enhanced protection against side-channel leakage. This protection is only + * effective if the context is randomized after its creation. See + * secp256k1_context_create for creation of contexts and + * secp256k1_context_randomize for randomization. + * + * A secondary purpose of context objects is to store pointers to callback + * functions that the library will call when certain error states arise. See + * secp256k1_context_set_error_callback as well as + * secp256k1_context_set_illegal_callback for details. Future library versions + * may use context objects for additional purposes. + * + * A constructed context can safely be used from multiple threads + * simultaneously, but API calls that take a non-const pointer to a context + * need exclusive access to it. In particular this is the case for + * secp256k1_context_destroy, secp256k1_context_preallocated_destroy, + * and secp256k1_context_randomize. + * + * Regarding randomization, either do it once at creation time (in which case + * you do not need any locking for the other calls), or use a read-write lock. + */ +typedef struct secp256k1_context_struct secp256k1_context; + +/** Opaque data structure that holds rewritable "scratch space" + * + * The purpose of this structure is to replace dynamic memory allocations, + * because we target architectures where this may not be available. It is + * essentially a resizable (within specified parameters) block of bytes, + * which is initially created either by memory allocation or TODO as a pointer + * into some fixed rewritable space. + * + * Unlike the context object, this cannot safely be shared between threads + * without additional synchronization logic. + */ +typedef struct secp256k1_scratch_space_struct secp256k1_scratch_space; + +/** Opaque data structure that holds a parsed and valid public key. + * + * The exact representation of data inside is implementation defined and not + * guaranteed to be portable between different platforms or versions. It is + * however guaranteed to be 64 bytes in size, and can be safely copied/moved. + * If you need to convert to a format suitable for storage or transmission, + * use secp256k1_ec_pubkey_serialize and secp256k1_ec_pubkey_parse. To + * compare keys, use secp256k1_ec_pubkey_cmp. + */ +typedef struct { + unsigned char data[64]; +} secp256k1_pubkey; + +/** Opaque data structured that holds a parsed ECDSA signature. + * + * The exact representation of data inside is implementation defined and not + * guaranteed to be portable between different platforms or versions. It is + * however guaranteed to be 64 bytes in size, and can be safely copied/moved. + * If you need to convert to a format suitable for storage, transmission, or + * comparison, use the secp256k1_ecdsa_signature_serialize_* and + * secp256k1_ecdsa_signature_parse_* functions. + */ +typedef struct { + unsigned char data[64]; +} secp256k1_ecdsa_signature; + +/** A pointer to a function to deterministically generate a nonce. + * + * Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. + * Out: nonce32: pointer to a 32-byte array to be filled by the function. + * In: msg32: the 32-byte message hash being verified (will not be NULL) + * key32: pointer to a 32-byte secret key (will not be NULL) + * algo16: pointer to a 16-byte array describing the signature + * algorithm (will be NULL for ECDSA for compatibility). + * data: Arbitrary data pointer that is passed through. + * attempt: how many iterations we have tried to find a nonce. + * This will almost always be 0, but different attempt values + * are required to result in a different nonce. + * + * Except for test cases, this function should compute some cryptographic hash of + * the message, the algorithm, the key and the attempt. + */ +typedef int (*secp256k1_nonce_function)( + unsigned char *nonce32, + const unsigned char *msg32, + const unsigned char *key32, + const unsigned char *algo16, + void *data, + unsigned int attempt +); + +# if !defined(SECP256K1_GNUC_PREREQ) +# if defined(__GNUC__)&&defined(__GNUC_MINOR__) +# define SECP256K1_GNUC_PREREQ(_maj,_min) \ + ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) +# else +# define SECP256K1_GNUC_PREREQ(_maj,_min) 0 +# endif +# endif + +/* When this header is used at build-time the SECP256K1_BUILD define needs to be set + * to correctly setup export attributes and nullness checks. This is normally done + * by secp256k1.c but to guard against this header being included before secp256k1.c + * has had a chance to set the define (e.g. via test harnesses that just includes + * secp256k1.c) we set SECP256K1_NO_BUILD when this header is processed without the + * BUILD define so this condition can be caught. + */ +#ifndef SECP256K1_BUILD +# define SECP256K1_NO_BUILD +#endif + +/* Symbol visibility. */ +#if defined(_WIN32) + /* GCC for Windows (e.g., MinGW) accepts the __declspec syntax + * for MSVC compatibility. A __declspec declaration implies (but is not + * exactly equivalent to) __attribute__ ((visibility("default"))), and so we + * actually want __declspec even on GCC, see "Microsoft Windows Function + * Attributes" in the GCC manual and the recommendations in + * https://gcc.gnu.org/wiki/Visibility. */ +# if defined(SECP256K1_BUILD) +# if defined(DLL_EXPORT) || defined(SECP256K1_DLL_EXPORT) + /* Building libsecp256k1 as a DLL. + * 1. If using Libtool, it defines DLL_EXPORT automatically. + * 2. In other cases, SECP256K1_DLL_EXPORT must be defined. */ +# define SECP256K1_API extern __declspec (dllexport) +# endif + /* The user must define SECP256K1_STATIC when consuming libsecp256k1 as a static + * library on Windows. */ +# elif !defined(SECP256K1_STATIC) + /* Consuming libsecp256k1 as a DLL. */ +# define SECP256K1_API extern __declspec (dllimport) +# endif +#endif +#ifndef SECP256K1_API +# if defined(__GNUC__) && (__GNUC__ >= 4) && defined(SECP256K1_BUILD) + /* Building libsecp256k1 on non-Windows using GCC or compatible. */ +# define SECP256K1_API extern __attribute__ ((visibility ("default"))) +# else + /* All cases not captured above. */ +# define SECP256K1_API extern +# endif +#endif + +/* Warning attributes + * NONNULL is not used if SECP256K1_BUILD is set to avoid the compiler optimizing out + * some paranoid null checks. */ +# if defined(__GNUC__) && SECP256K1_GNUC_PREREQ(3, 4) +# define SECP256K1_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) +# else +# define SECP256K1_WARN_UNUSED_RESULT +# endif +# if !defined(SECP256K1_BUILD) && defined(__GNUC__) && SECP256K1_GNUC_PREREQ(3, 4) +# define SECP256K1_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x))) +# else +# define SECP256K1_ARG_NONNULL(_x) +# endif + +/* Attribute for marking functions, types, and variables as deprecated */ +#if !defined(SECP256K1_BUILD) && defined(__has_attribute) +# if __has_attribute(__deprecated__) +# define SECP256K1_DEPRECATED(_msg) __attribute__ ((__deprecated__(_msg))) +# else +# define SECP256K1_DEPRECATED(_msg) +# endif +#else +# define SECP256K1_DEPRECATED(_msg) +#endif + +/* All flags' lower 8 bits indicate what they're for. Do not use directly. */ +#define SECP256K1_FLAGS_TYPE_MASK ((1 << 8) - 1) +#define SECP256K1_FLAGS_TYPE_CONTEXT (1 << 0) +#define SECP256K1_FLAGS_TYPE_COMPRESSION (1 << 1) +/* The higher bits contain the actual data. Do not use directly. */ +#define SECP256K1_FLAGS_BIT_CONTEXT_VERIFY (1 << 8) +#define SECP256K1_FLAGS_BIT_CONTEXT_SIGN (1 << 9) +#define SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY (1 << 10) +#define SECP256K1_FLAGS_BIT_COMPRESSION (1 << 8) + +/** Context flags to pass to secp256k1_context_create, secp256k1_context_preallocated_size, and + * secp256k1_context_preallocated_create. */ +#define SECP256K1_CONTEXT_NONE (SECP256K1_FLAGS_TYPE_CONTEXT) + +/** Deprecated context flags. These flags are treated equivalent to SECP256K1_CONTEXT_NONE. */ +#define SECP256K1_CONTEXT_VERIFY (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_VERIFY) +#define SECP256K1_CONTEXT_SIGN (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN) + +/* Testing flag. Do not use. */ +#define SECP256K1_CONTEXT_DECLASSIFY (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY) + +/** Flag to pass to secp256k1_ec_pubkey_serialize. */ +#define SECP256K1_EC_COMPRESSED (SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION) +#define SECP256K1_EC_UNCOMPRESSED (SECP256K1_FLAGS_TYPE_COMPRESSION) + +/** Prefix byte used to tag various encoded curvepoints for specific purposes */ +#define SECP256K1_TAG_PUBKEY_EVEN 0x02 +#define SECP256K1_TAG_PUBKEY_ODD 0x03 +#define SECP256K1_TAG_PUBKEY_UNCOMPRESSED 0x04 +#define SECP256K1_TAG_PUBKEY_HYBRID_EVEN 0x06 +#define SECP256K1_TAG_PUBKEY_HYBRID_ODD 0x07 + +/** A built-in constant secp256k1 context object with static storage duration, to be + * used in conjunction with secp256k1_selftest. + * + * This context object offers *only limited functionality* , i.e., it cannot be used + * for API functions that perform computations involving secret keys, e.g., signing + * and public key generation. If this restriction applies to a specific API function, + * it is mentioned in its documentation. See secp256k1_context_create if you need a + * full context object that supports all functionality offered by the library. + * + * It is highly recommended to call secp256k1_selftest before using this context. + */ +SECP256K1_API const secp256k1_context *secp256k1_context_static; + +/** Deprecated alias for secp256k1_context_static. */ +SECP256K1_API const secp256k1_context *secp256k1_context_no_precomp +SECP256K1_DEPRECATED("Use secp256k1_context_static instead"); + +/** Perform basic self tests (to be used in conjunction with secp256k1_context_static) + * + * This function performs self tests that detect some serious usage errors and + * similar conditions, e.g., when the library is compiled for the wrong endianness. + * This is a last resort measure to be used in production. The performed tests are + * very rudimentary and are not intended as a replacement for running the test + * binaries. + * + * It is highly recommended to call this before using secp256k1_context_static. + * It is not necessary to call this function before using a context created with + * secp256k1_context_create (or secp256k1_context_preallocated_create), which will + * take care of performing the self tests. + * + * If the tests fail, this function will call the default error handler to abort the + * program (see secp256k1_context_set_error_callback). + */ +SECP256K1_API void secp256k1_selftest(void); + + +/** Create a secp256k1 context object (in dynamically allocated memory). + * + * This function uses malloc to allocate memory. It is guaranteed that malloc is + * called at most once for every call of this function. If you need to avoid dynamic + * memory allocation entirely, see secp256k1_context_static and the functions in + * secp256k1_preallocated.h. + * + * Returns: a newly created context object. + * In: flags: Always set to SECP256K1_CONTEXT_NONE (see below). + * + * The only valid non-deprecated flag in recent library versions is + * SECP256K1_CONTEXT_NONE, which will create a context sufficient for all functionality + * offered by the library. All other (deprecated) flags will be treated as equivalent + * to the SECP256K1_CONTEXT_NONE flag. Though the flags parameter primarily exists for + * historical reasons, future versions of the library may introduce new flags. + * + * If the context is intended to be used for API functions that perform computations + * involving secret keys, e.g., signing and public key generation, then it is highly + * recommended to call secp256k1_context_randomize on the context before calling + * those API functions. This will provide enhanced protection against side-channel + * leakage, see secp256k1_context_randomize for details. + * + * Do not create a new context object for each operation, as construction and + * randomization can take non-negligible time. + */ +SECP256K1_API secp256k1_context *secp256k1_context_create( + unsigned int flags +) SECP256K1_WARN_UNUSED_RESULT; + +/** Copy a secp256k1 context object (into dynamically allocated memory). + * + * This function uses malloc to allocate memory. It is guaranteed that malloc is + * called at most once for every call of this function. If you need to avoid dynamic + * memory allocation entirely, see the functions in secp256k1_preallocated.h. + * + * Cloning secp256k1_context_static is not possible, and should not be emulated by + * the caller (e.g., using memcpy). Create a new context instead. + * + * Returns: a newly created context object. + * Args: ctx: an existing context to copy (not secp256k1_context_static) + */ +SECP256K1_API secp256k1_context *secp256k1_context_clone( + const secp256k1_context *ctx +) SECP256K1_ARG_NONNULL(1) SECP256K1_WARN_UNUSED_RESULT; + +/** Destroy a secp256k1 context object (created in dynamically allocated memory). + * + * The context pointer may not be used afterwards. + * + * The context to destroy must have been created using secp256k1_context_create + * or secp256k1_context_clone. If the context has instead been created using + * secp256k1_context_preallocated_create or secp256k1_context_preallocated_clone, the + * behaviour is undefined. In that case, secp256k1_context_preallocated_destroy must + * be used instead. + * + * Args: ctx: an existing context to destroy, constructed using + * secp256k1_context_create or secp256k1_context_clone + * (i.e., not secp256k1_context_static). + */ +SECP256K1_API void secp256k1_context_destroy( + secp256k1_context *ctx +) SECP256K1_ARG_NONNULL(1); + +/** Set a callback function to be called when an illegal argument is passed to + * an API call. It will only trigger for violations that are mentioned + * explicitly in the header. + * + * The philosophy is that these shouldn't be dealt with through a + * specific return value, as calling code should not have branches to deal with + * the case that this code itself is broken. + * + * On the other hand, during debug stage, one would want to be informed about + * such mistakes, and the default (crashing) may be inadvisable. + * When this callback is triggered, the API function called is guaranteed not + * to cause a crash, though its return value and output arguments are + * undefined. + * + * When this function has not been called (or called with fn==NULL), then the + * default handler will be used. The library provides a default handler which + * writes the message to stderr and calls abort. This default handler can be + * replaced at link time if the preprocessor macro + * USE_EXTERNAL_DEFAULT_CALLBACKS is defined, which is the case if the build + * has been configured with --enable-external-default-callbacks. Then the + * following two symbols must be provided to link against: + * - void secp256k1_default_illegal_callback_fn(const char *message, void *data); + * - void secp256k1_default_error_callback_fn(const char *message, void *data); + * The library can call these default handlers even before a proper callback data + * pointer could have been set using secp256k1_context_set_illegal_callback or + * secp256k1_context_set_error_callback, e.g., when the creation of a context + * fails. In this case, the corresponding default handler will be called with + * the data pointer argument set to NULL. + * + * Args: ctx: an existing context object. + * In: fun: a pointer to a function to call when an illegal argument is + * passed to the API, taking a message and an opaque pointer. + * (NULL restores the default handler.) + * data: the opaque pointer to pass to fun above, must be NULL for the default handler. + * + * See also secp256k1_context_set_error_callback. + */ +SECP256K1_API void secp256k1_context_set_illegal_callback( + secp256k1_context *ctx, + void (*fun)(const char *message, void *data), + const void *data +) SECP256K1_ARG_NONNULL(1); + +/** Set a callback function to be called when an internal consistency check + * fails. + * + * The default callback writes an error message to stderr and calls abort + * to abort the program. + * + * This can only trigger in case of a hardware failure, miscompilation, + * memory corruption, serious bug in the library, or other error would can + * otherwise result in undefined behaviour. It will not trigger due to mere + * incorrect usage of the API (see secp256k1_context_set_illegal_callback + * for that). After this callback returns, anything may happen, including + * crashing. + * + * Args: ctx: an existing context object. + * In: fun: a pointer to a function to call when an internal error occurs, + * taking a message and an opaque pointer (NULL restores the + * default handler, see secp256k1_context_set_illegal_callback + * for details). + * data: the opaque pointer to pass to fun above, must be NULL for the default handler. + * + * See also secp256k1_context_set_illegal_callback. + */ +SECP256K1_API void secp256k1_context_set_error_callback( + secp256k1_context *ctx, + void (*fun)(const char *message, void *data), + const void *data +) SECP256K1_ARG_NONNULL(1); + +/** Create a secp256k1 scratch space object. + * + * Returns: a newly created scratch space. + * Args: ctx: an existing context object. + * In: size: amount of memory to be available as scratch space. Some extra + * (<100 bytes) will be allocated for extra accounting. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_scratch_space *secp256k1_scratch_space_create( + const secp256k1_context *ctx, + size_t size +) SECP256K1_ARG_NONNULL(1); + +/** Destroy a secp256k1 scratch space. + * + * The pointer may not be used afterwards. + * Args: ctx: a secp256k1 context object. + * scratch: space to destroy + */ +SECP256K1_API void secp256k1_scratch_space_destroy( + const secp256k1_context *ctx, + secp256k1_scratch_space *scratch +) SECP256K1_ARG_NONNULL(1); + +/** Parse a variable-length public key into the pubkey object. + * + * Returns: 1 if the public key was fully valid. + * 0 if the public key could not be parsed or is invalid. + * Args: ctx: a secp256k1 context object. + * Out: pubkey: pointer to a pubkey object. If 1 is returned, it is set to a + * parsed version of input. If not, its value is undefined. + * In: input: pointer to a serialized public key + * inputlen: length of the array pointed to by input + * + * This function supports parsing compressed (33 bytes, header byte 0x02 or + * 0x03), uncompressed (65 bytes, header byte 0x04), or hybrid (65 bytes, header + * byte 0x06 or 0x07) format public keys. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_parse( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *input, + size_t inputlen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a pubkey object into a serialized byte sequence. + * + * Returns: 1 always. + * Args: ctx: a secp256k1 context object. + * Out: output: a pointer to a 65-byte (if compressed==0) or 33-byte (if + * compressed==1) byte array to place the serialized key + * in. + * In/Out: outputlen: a pointer to an integer which is initially set to the + * size of output, and is overwritten with the written + * size. + * In: pubkey: a pointer to a secp256k1_pubkey containing an + * initialized public key. + * flags: SECP256K1_EC_COMPRESSED if serialization should be in + * compressed format, otherwise SECP256K1_EC_UNCOMPRESSED. + */ +SECP256K1_API int secp256k1_ec_pubkey_serialize( + const secp256k1_context *ctx, + unsigned char *output, + size_t *outputlen, + const secp256k1_pubkey *pubkey, + unsigned int flags +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Compare two public keys using lexicographic (of compressed serialization) order + * + * Returns: <0 if the first public key is less than the second + * >0 if the first public key is greater than the second + * 0 if the two public keys are equal + * Args: ctx: a secp256k1 context object. + * In: pubkey1: first public key to compare + * pubkey2: second public key to compare + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_cmp( + const secp256k1_context *ctx, + const secp256k1_pubkey *pubkey1, + const secp256k1_pubkey *pubkey2 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Parse an ECDSA signature in compact (64 bytes) format. + * + * Returns: 1 when the signature could be parsed, 0 otherwise. + * Args: ctx: a secp256k1 context object + * Out: sig: a pointer to a signature object + * In: input64: a pointer to the 64-byte array to parse + * + * The signature must consist of a 32-byte big endian R value, followed by a + * 32-byte big endian S value. If R or S fall outside of [0..order-1], the + * encoding is invalid. R and S with value 0 are allowed in the encoding. + * + * After the call, sig will always be initialized. If parsing failed or R or + * S are zero, the resulting sig value is guaranteed to fail verification for + * any message and public key. + */ +SECP256K1_API int secp256k1_ecdsa_signature_parse_compact( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sig, + const unsigned char *input64 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Parse a DER ECDSA signature. + * + * Returns: 1 when the signature could be parsed, 0 otherwise. + * Args: ctx: a secp256k1 context object + * Out: sig: a pointer to a signature object + * In: input: a pointer to the signature to be parsed + * inputlen: the length of the array pointed to be input + * + * This function will accept any valid DER encoded signature, even if the + * encoded numbers are out of range. + * + * After the call, sig will always be initialized. If parsing failed or the + * encoded numbers are out of range, signature verification with it is + * guaranteed to fail for every message and public key. + */ +SECP256K1_API int secp256k1_ecdsa_signature_parse_der( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sig, + const unsigned char *input, + size_t inputlen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize an ECDSA signature in DER format. + * + * Returns: 1 if enough space was available to serialize, 0 otherwise + * Args: ctx: a secp256k1 context object + * Out: output: a pointer to an array to store the DER serialization + * In/Out: outputlen: a pointer to a length integer. Initially, this integer + * should be set to the length of output. After the call + * it will be set to the length of the serialization (even + * if 0 was returned). + * In: sig: a pointer to an initialized signature object + */ +SECP256K1_API int secp256k1_ecdsa_signature_serialize_der( + const secp256k1_context *ctx, + unsigned char *output, + size_t *outputlen, + const secp256k1_ecdsa_signature *sig +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Serialize an ECDSA signature in compact (64 byte) format. + * + * Returns: 1 + * Args: ctx: a secp256k1 context object + * Out: output64: a pointer to a 64-byte array to store the compact serialization + * In: sig: a pointer to an initialized signature object + * + * See secp256k1_ecdsa_signature_parse_compact for details about the encoding. + */ +SECP256K1_API int secp256k1_ecdsa_signature_serialize_compact( + const secp256k1_context *ctx, + unsigned char *output64, + const secp256k1_ecdsa_signature *sig +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Verify an ECDSA signature. + * + * Returns: 1: correct signature + * 0: incorrect or unparseable signature + * Args: ctx: a secp256k1 context object. + * In: sig: the signature being verified. + * msghash32: the 32-byte message hash being verified. + * The verifier must make sure to apply a cryptographic + * hash function to the message by itself and not accept an + * msghash32 value directly. Otherwise, it would be easy to + * create a "valid" signature without knowledge of the + * secret key. See also + * https://bitcoin.stackexchange.com/a/81116/35586 for more + * background on this topic. + * pubkey: pointer to an initialized public key to verify with. + * + * To avoid accepting malleable signatures, only ECDSA signatures in lower-S + * form are accepted. + * + * If you need to accept ECDSA signatures from sources that do not obey this + * rule, apply secp256k1_ecdsa_signature_normalize to the signature prior to + * verification, but be aware that doing so results in malleable signatures. + * + * For details, see the comments for that function. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ecdsa_verify( + const secp256k1_context *ctx, + const secp256k1_ecdsa_signature *sig, + const unsigned char *msghash32, + const secp256k1_pubkey *pubkey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Convert a signature to a normalized lower-S form. + * + * Returns: 1 if sigin was not normalized, 0 if it already was. + * Args: ctx: a secp256k1 context object + * Out: sigout: a pointer to a signature to fill with the normalized form, + * or copy if the input was already normalized. (can be NULL if + * you're only interested in whether the input was already + * normalized). + * In: sigin: a pointer to a signature to check/normalize (can be identical to sigout) + * + * With ECDSA a third-party can forge a second distinct signature of the same + * message, given a single initial signature, but without knowing the key. This + * is done by negating the S value modulo the order of the curve, 'flipping' + * the sign of the random point R which is not included in the signature. + * + * Forgery of the same message isn't universally problematic, but in systems + * where message malleability or uniqueness of signatures is important this can + * cause issues. This forgery can be blocked by all verifiers forcing signers + * to use a normalized form. + * + * The lower-S form reduces the size of signatures slightly on average when + * variable length encodings (such as DER) are used and is cheap to verify, + * making it a good choice. Security of always using lower-S is assured because + * anyone can trivially modify a signature after the fact to enforce this + * property anyway. + * + * The lower S value is always between 0x1 and + * 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, + * inclusive. + * + * No other forms of ECDSA malleability are known and none seem likely, but + * there is no formal proof that ECDSA, even with this additional restriction, + * is free of other malleability. Commonly used serialization schemes will also + * accept various non-unique encodings, so care should be taken when this + * property is required for an application. + * + * The secp256k1_ecdsa_sign function will by default create signatures in the + * lower-S form, and secp256k1_ecdsa_verify will not accept others. In case + * signatures come from a system that cannot enforce this property, + * secp256k1_ecdsa_signature_normalize must be called before verification. + */ +SECP256K1_API int secp256k1_ecdsa_signature_normalize( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sigout, + const secp256k1_ecdsa_signature *sigin +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(3); + +/** An implementation of RFC6979 (using HMAC-SHA256) as nonce generation function. + * If a data pointer is passed, it is assumed to be a pointer to 32 bytes of + * extra entropy. + */ +SECP256K1_API const secp256k1_nonce_function secp256k1_nonce_function_rfc6979; + +/** A default safe nonce generation function (currently equal to secp256k1_nonce_function_rfc6979). */ +SECP256K1_API const secp256k1_nonce_function secp256k1_nonce_function_default; + +/** Create an ECDSA signature. + * + * Returns: 1: signature created + * 0: the nonce generation function failed, or the secret key was invalid. + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * Out: sig: pointer to an array where the signature will be placed. + * In: msghash32: the 32-byte message hash being signed. + * seckey: pointer to a 32-byte secret key. + * noncefp: pointer to a nonce generation function. If NULL, + * secp256k1_nonce_function_default is used. + * ndata: pointer to arbitrary data used by the nonce generation function + * (can be NULL). If it is non-NULL and + * secp256k1_nonce_function_default is used, then ndata must be a + * pointer to 32-bytes of additional data. + * + * The created signature is always in lower-S form. See + * secp256k1_ecdsa_signature_normalize for more details. + */ +SECP256K1_API int secp256k1_ecdsa_sign( + const secp256k1_context *ctx, + secp256k1_ecdsa_signature *sig, + const unsigned char *msghash32, + const unsigned char *seckey, + secp256k1_nonce_function noncefp, + const void *ndata +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Verify an ECDSA secret key. + * + * A secret key is valid if it is not 0 and less than the secp256k1 curve order + * when interpreted as an integer (most significant byte first). The + * probability of choosing a 32-byte string uniformly at random which is an + * invalid secret key is negligible. + * + * Returns: 1: secret key is valid + * 0: secret key is invalid + * Args: ctx: pointer to a context object. + * In: seckey: pointer to a 32-byte secret key. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_verify( + const secp256k1_context *ctx, + const unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2); + +/** Compute the public key for a secret key. + * + * Returns: 1: secret was valid, public key stores. + * 0: secret was invalid, try again. + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * Out: pubkey: pointer to the created public key. + * In: seckey: pointer to a 32-byte secret key. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_create( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Negates a secret key in place. + * + * Returns: 0 if the given secret key is invalid according to + * secp256k1_ec_seckey_verify. 1 otherwise + * Args: ctx: pointer to a context object + * In/Out: seckey: pointer to the 32-byte secret key to be negated. If the + * secret key is invalid according to + * secp256k1_ec_seckey_verify, this function returns 0 and + * seckey will be set to some unspecified value. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_negate( + const secp256k1_context *ctx, + unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2); + +/** Same as secp256k1_ec_seckey_negate, but DEPRECATED. Will be removed in + * future versions. */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_privkey_negate( + const secp256k1_context *ctx, + unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) + SECP256K1_DEPRECATED("Use secp256k1_ec_seckey_negate instead"); + +/** Negates a public key in place. + * + * Returns: 1 always + * Args: ctx: pointer to a context object + * In/Out: pubkey: pointer to the public key to be negated. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_negate( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2); + +/** Tweak a secret key by adding tweak to it. + * + * Returns: 0 if the arguments are invalid or the resulting secret key would be + * invalid (only when the tweak is the negation of the secret key). 1 + * otherwise. + * Args: ctx: pointer to a context object. + * In/Out: seckey: pointer to a 32-byte secret key. If the secret key is + * invalid according to secp256k1_ec_seckey_verify, this + * function returns 0. seckey will be set to some unspecified + * value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak, which must be valid according to + * secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly + * random 32-byte tweaks, the chance of being invalid is + * negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_tweak_add( + const secp256k1_context *ctx, + unsigned char *seckey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Same as secp256k1_ec_seckey_tweak_add, but DEPRECATED. Will be removed in + * future versions. */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_privkey_tweak_add( + const secp256k1_context *ctx, + unsigned char *seckey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_DEPRECATED("Use secp256k1_ec_seckey_tweak_add instead"); + +/** Tweak a public key by adding tweak times the generator to it. + * + * Returns: 0 if the arguments are invalid or the resulting public key would be + * invalid (only when the tweak is the negation of the corresponding + * secret key). 1 otherwise. + * Args: ctx: pointer to a context object. + * In/Out: pubkey: pointer to a public key object. pubkey will be set to an + * invalid value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak, which must be valid according to + * secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly + * random 32-byte tweaks, the chance of being invalid is + * negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_tweak_add( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Tweak a secret key by multiplying it by a tweak. + * + * Returns: 0 if the arguments are invalid. 1 otherwise. + * Args: ctx: pointer to a context object. + * In/Out: seckey: pointer to a 32-byte secret key. If the secret key is + * invalid according to secp256k1_ec_seckey_verify, this + * function returns 0. seckey will be set to some unspecified + * value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to + * secp256k1_ec_seckey_verify, this function returns 0. For + * uniformly random 32-byte arrays the chance of being invalid + * is negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_seckey_tweak_mul( + const secp256k1_context *ctx, + unsigned char *seckey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Same as secp256k1_ec_seckey_tweak_mul, but DEPRECATED. Will be removed in + * future versions. */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_privkey_tweak_mul( + const secp256k1_context *ctx, + unsigned char *seckey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) + SECP256K1_DEPRECATED("Use secp256k1_ec_seckey_tweak_mul instead"); + +/** Tweak a public key by multiplying it by a tweak value. + * + * Returns: 0 if the arguments are invalid. 1 otherwise. + * Args: ctx: pointer to a context object. + * In/Out: pubkey: pointer to a public key object. pubkey will be set to an + * invalid value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak. If the tweak is invalid according to + * secp256k1_ec_seckey_verify, this function returns 0. For + * uniformly random 32-byte arrays the chance of being invalid + * is negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_tweak_mul( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Randomizes the context to provide enhanced protection against side-channel leakage. + * + * Returns: 1: randomization successful + * 0: error + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * In: seed32: pointer to a 32-byte random seed (NULL resets to initial state). + * + * While secp256k1 code is written and tested to be constant-time no matter what + * secret values are, it is possible that a compiler may output code which is not, + * and also that the CPU may not emit the same radio frequencies or draw the same + * amount of power for all values. Randomization of the context shields against + * side-channel observations which aim to exploit secret-dependent behaviour in + * certain computations which involve secret keys. + * + * It is highly recommended to call this function on contexts returned from + * secp256k1_context_create or secp256k1_context_clone (or from the corresponding + * functions in secp256k1_preallocated.h) before using these contexts to call API + * functions that perform computations involving secret keys, e.g., signing and + * public key generation. It is possible to call this function more than once on + * the same context, and doing so before every few computations involving secret + * keys is recommended as a defense-in-depth measure. Randomization of the static + * context secp256k1_context_static is not supported. + * + * Currently, the random seed is mainly used for blinding multiplications of a + * secret scalar with the elliptic curve base point. Multiplications of this + * kind are performed by exactly those API functions which are documented to + * require a context that is not secp256k1_context_static. As a rule of thumb, + * these are all functions which take a secret key (or a keypair) as an input. + * A notable exception to that rule is the ECDH module, which relies on a different + * kind of elliptic curve point multiplication and thus does not benefit from + * enhanced protection against side-channel leakage currently. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_context_randomize( + secp256k1_context *ctx, + const unsigned char *seed32 +) SECP256K1_ARG_NONNULL(1); + +/** Add a number of public keys together. + * + * Returns: 1: the sum of the public keys is valid. + * 0: the sum of the public keys is not valid. + * Args: ctx: pointer to a context object. + * Out: out: pointer to a public key object for placing the resulting public key. + * In: ins: pointer to array of pointers to public keys. + * n: the number of public keys to add together (must be at least 1). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ec_pubkey_combine( + const secp256k1_context *ctx, + secp256k1_pubkey *out, + const secp256k1_pubkey * const *ins, + size_t n +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Compute a tagged hash as defined in BIP-340. + * + * This is useful for creating a message hash and achieving domain separation + * through an application-specific tag. This function returns + * SHA256(SHA256(tag)||SHA256(tag)||msg). Therefore, tagged hash + * implementations optimized for a specific tag can precompute the SHA256 state + * after hashing the tag hashes. + * + * Returns: 1 always. + * Args: ctx: pointer to a context object + * Out: hash32: pointer to a 32-byte array to store the resulting hash + * In: tag: pointer to an array containing the tag + * taglen: length of the tag array + * msg: pointer to an array containing the message + * msglen: length of the message array + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_tagged_sha256( + const secp256k1_context *ctx, + unsigned char *hash32, + const unsigned char *tag, + size_t taglen, + const unsigned char *msg, + size_t msglen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_H */ diff --git a/nostrdb/secp256k1_ecdh.h b/nostrdb/secp256k1_ecdh.h @@ -0,0 +1,63 @@ +#ifndef SECP256K1_ECDH_H +#define SECP256K1_ECDH_H + +#include "secp256k1.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** A pointer to a function that hashes an EC point to obtain an ECDH secret + * + * Returns: 1 if the point was successfully hashed. + * 0 will cause secp256k1_ecdh to fail and return 0. + * Other return values are not allowed, and the behaviour of + * secp256k1_ecdh is undefined for other return values. + * Out: output: pointer to an array to be filled by the function + * In: x32: pointer to a 32-byte x coordinate + * y32: pointer to a 32-byte y coordinate + * data: arbitrary data pointer that is passed through + */ +typedef int (*secp256k1_ecdh_hash_function)( + unsigned char *output, + const unsigned char *x32, + const unsigned char *y32, + void *data +); + +/** An implementation of SHA256 hash function that applies to compressed public key. + * Populates the output parameter with 32 bytes. */ +SECP256K1_API const secp256k1_ecdh_hash_function secp256k1_ecdh_hash_function_sha256; + +/** A default ECDH hash function (currently equal to secp256k1_ecdh_hash_function_sha256). + * Populates the output parameter with 32 bytes. */ +SECP256K1_API const secp256k1_ecdh_hash_function secp256k1_ecdh_hash_function_default; + +/** Compute an EC Diffie-Hellman secret in constant time + * + * Returns: 1: exponentiation was successful + * 0: scalar was invalid (zero or overflow) or hashfp returned 0 + * Args: ctx: pointer to a context object. + * Out: output: pointer to an array to be filled by hashfp. + * In: pubkey: a pointer to a secp256k1_pubkey containing an initialized public key. + * seckey: a 32-byte scalar with which to multiply the point. + * hashfp: pointer to a hash function. If NULL, + * secp256k1_ecdh_hash_function_sha256 is used + * (in which case, 32 bytes will be written to output). + * data: arbitrary data pointer that is passed through to hashfp + * (can be NULL for secp256k1_ecdh_hash_function_sha256). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_ecdh( + const secp256k1_context *ctx, + unsigned char *output, + const secp256k1_pubkey *pubkey, + const unsigned char *seckey, + secp256k1_ecdh_hash_function hashfp, + void *data +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_ECDH_H */ diff --git a/nostrdb/secp256k1_extrakeys.h b/nostrdb/secp256k1_extrakeys.h @@ -0,0 +1,247 @@ +#ifndef SECP256K1_EXTRAKEYS_H +#define SECP256K1_EXTRAKEYS_H + +#include "secp256k1.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Opaque data structure that holds a parsed and valid "x-only" public key. + * An x-only pubkey encodes a point whose Y coordinate is even. It is + * serialized using only its X coordinate (32 bytes). See BIP-340 for more + * information about x-only pubkeys. + * + * The exact representation of data inside is implementation defined and not + * guaranteed to be portable between different platforms or versions. It is + * however guaranteed to be 64 bytes in size, and can be safely copied/moved. + * If you need to convert to a format suitable for storage, transmission, use + * use secp256k1_xonly_pubkey_serialize and secp256k1_xonly_pubkey_parse. To + * compare keys, use secp256k1_xonly_pubkey_cmp. + */ +typedef struct { + unsigned char data[64]; +} secp256k1_xonly_pubkey; + +/** Opaque data structure that holds a keypair consisting of a secret and a + * public key. + * + * The exact representation of data inside is implementation defined and not + * guaranteed to be portable between different platforms or versions. It is + * however guaranteed to be 96 bytes in size, and can be safely copied/moved. + */ +typedef struct { + unsigned char data[96]; +} secp256k1_keypair; + +/** Parse a 32-byte sequence into a xonly_pubkey object. + * + * Returns: 1 if the public key was fully valid. + * 0 if the public key could not be parsed or is invalid. + * + * Args: ctx: a secp256k1 context object. + * Out: pubkey: pointer to a pubkey object. If 1 is returned, it is set to a + * parsed version of input. If not, it's set to an invalid value. + * In: input32: pointer to a serialized xonly_pubkey. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_xonly_pubkey_parse( + const secp256k1_context *ctx, + secp256k1_xonly_pubkey *pubkey, + const unsigned char *input32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize an xonly_pubkey object into a 32-byte sequence. + * + * Returns: 1 always. + * + * Args: ctx: a secp256k1 context object. + * Out: output32: a pointer to a 32-byte array to place the serialized key in. + * In: pubkey: a pointer to a secp256k1_xonly_pubkey containing an initialized public key. + */ +SECP256K1_API int secp256k1_xonly_pubkey_serialize( + const secp256k1_context *ctx, + unsigned char *output32, + const secp256k1_xonly_pubkey *pubkey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Compare two x-only public keys using lexicographic order + * + * Returns: <0 if the first public key is less than the second + * >0 if the first public key is greater than the second + * 0 if the two public keys are equal + * Args: ctx: a secp256k1 context object. + * In: pubkey1: first public key to compare + * pubkey2: second public key to compare + */ +SECP256K1_API int secp256k1_xonly_pubkey_cmp( + const secp256k1_context *ctx, + const secp256k1_xonly_pubkey *pk1, + const secp256k1_xonly_pubkey *pk2 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Converts a secp256k1_pubkey into a secp256k1_xonly_pubkey. + * + * Returns: 1 always. + * + * Args: ctx: pointer to a context object. + * Out: xonly_pubkey: pointer to an x-only public key object for placing the converted public key. + * pk_parity: Ignored if NULL. Otherwise, pointer to an integer that + * will be set to 1 if the point encoded by xonly_pubkey is + * the negation of the pubkey and set to 0 otherwise. + * In: pubkey: pointer to a public key that is converted. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_xonly_pubkey_from_pubkey( + const secp256k1_context *ctx, + secp256k1_xonly_pubkey *xonly_pubkey, + int *pk_parity, + const secp256k1_pubkey *pubkey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(4); + +/** Tweak an x-only public key by adding the generator multiplied with tweak32 + * to it. + * + * Note that the resulting point can not in general be represented by an x-only + * pubkey because it may have an odd Y coordinate. Instead, the output_pubkey + * is a normal secp256k1_pubkey. + * + * Returns: 0 if the arguments are invalid or the resulting public key would be + * invalid (only when the tweak is the negation of the corresponding + * secret key). 1 otherwise. + * + * Args: ctx: pointer to a context object. + * Out: output_pubkey: pointer to a public key to store the result. Will be set + * to an invalid value if this function returns 0. + * In: internal_pubkey: pointer to an x-only pubkey to apply the tweak to. + * tweak32: pointer to a 32-byte tweak, which must be valid + * according to secp256k1_ec_seckey_verify or 32 zero + * bytes. For uniformly random 32-byte tweaks, the chance of + * being invalid is negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_xonly_pubkey_tweak_add( + const secp256k1_context *ctx, + secp256k1_pubkey *output_pubkey, + const secp256k1_xonly_pubkey *internal_pubkey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Checks that a tweaked pubkey is the result of calling + * secp256k1_xonly_pubkey_tweak_add with internal_pubkey and tweak32. + * + * The tweaked pubkey is represented by its 32-byte x-only serialization and + * its pk_parity, which can both be obtained by converting the result of + * tweak_add to a secp256k1_xonly_pubkey. + * + * Note that this alone does _not_ verify that the tweaked pubkey is a + * commitment. If the tweak is not chosen in a specific way, the tweaked pubkey + * can easily be the result of a different internal_pubkey and tweak. + * + * Returns: 0 if the arguments are invalid or the tweaked pubkey is not the + * result of tweaking the internal_pubkey with tweak32. 1 otherwise. + * Args: ctx: pointer to a context object. + * In: tweaked_pubkey32: pointer to a serialized xonly_pubkey. + * tweaked_pk_parity: the parity of the tweaked pubkey (whose serialization + * is passed in as tweaked_pubkey32). This must match the + * pk_parity value that is returned when calling + * secp256k1_xonly_pubkey with the tweaked pubkey, or + * this function will fail. + * internal_pubkey: pointer to an x-only public key object to apply the tweak to. + * tweak32: pointer to a 32-byte tweak. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_xonly_pubkey_tweak_add_check( + const secp256k1_context *ctx, + const unsigned char *tweaked_pubkey32, + int tweaked_pk_parity, + const secp256k1_xonly_pubkey *internal_pubkey, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5); + +/** Compute the keypair for a secret key. + * + * Returns: 1: secret was valid, keypair is ready to use + * 0: secret was invalid, try again with a different secret + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * Out: keypair: pointer to the created keypair. + * In: seckey: pointer to a 32-byte secret key. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_keypair_create( + const secp256k1_context *ctx, + secp256k1_keypair *keypair, + const unsigned char *seckey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Get the secret key from a keypair. + * + * Returns: 1 always. + * Args: ctx: pointer to a context object. + * Out: seckey: pointer to a 32-byte buffer for the secret key. + * In: keypair: pointer to a keypair. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_keypair_sec( + const secp256k1_context *ctx, + unsigned char *seckey, + const secp256k1_keypair *keypair +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Get the public key from a keypair. + * + * Returns: 1 always. + * Args: ctx: pointer to a context object. + * Out: pubkey: pointer to a pubkey object, set to the keypair public key. + * In: keypair: pointer to a keypair. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_keypair_pub( + const secp256k1_context *ctx, + secp256k1_pubkey *pubkey, + const secp256k1_keypair *keypair +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Get the x-only public key from a keypair. + * + * This is the same as calling secp256k1_keypair_pub and then + * secp256k1_xonly_pubkey_from_pubkey. + * + * Returns: 1 always. + * Args: ctx: pointer to a context object. + * Out: pubkey: pointer to an xonly_pubkey object, set to the keypair + * public key after converting it to an xonly_pubkey. + * pk_parity: Ignored if NULL. Otherwise, pointer to an integer that will be set to the + * pk_parity argument of secp256k1_xonly_pubkey_from_pubkey. + * In: keypair: pointer to a keypair. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_keypair_xonly_pub( + const secp256k1_context *ctx, + secp256k1_xonly_pubkey *pubkey, + int *pk_parity, + const secp256k1_keypair *keypair +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(4); + +/** Tweak a keypair by adding tweak32 to the secret key and updating the public + * key accordingly. + * + * Calling this function and then secp256k1_keypair_pub results in the same + * public key as calling secp256k1_keypair_xonly_pub and then + * secp256k1_xonly_pubkey_tweak_add. + * + * Returns: 0 if the arguments are invalid or the resulting keypair would be + * invalid (only when the tweak is the negation of the keypair's + * secret key). 1 otherwise. + * + * Args: ctx: pointer to a context object. + * In/Out: keypair: pointer to a keypair to apply the tweak to. Will be set to + * an invalid value if this function returns 0. + * In: tweak32: pointer to a 32-byte tweak, which must be valid according to + * secp256k1_ec_seckey_verify or 32 zero bytes. For uniformly + * random 32-byte tweaks, the chance of being invalid is + * negligible (around 1 in 2^128). + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_keypair_xonly_tweak_add( + const secp256k1_context *ctx, + secp256k1_keypair *keypair, + const unsigned char *tweak32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_EXTRAKEYS_H */ diff --git a/nostrdb/secp256k1_schnorrsig.h b/nostrdb/secp256k1_schnorrsig.h @@ -0,0 +1,190 @@ +#ifndef SECP256K1_SCHNORRSIG_H +#define SECP256K1_SCHNORRSIG_H + +#include "secp256k1.h" +#include "secp256k1_extrakeys.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** This module implements a variant of Schnorr signatures compliant with + * Bitcoin Improvement Proposal 340 "Schnorr Signatures for secp256k1" + * (https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). + */ + +/** A pointer to a function to deterministically generate a nonce. + * + * Same as secp256k1_nonce function with the exception of accepting an + * additional pubkey argument and not requiring an attempt argument. The pubkey + * argument can protect signature schemes with key-prefixed challenge hash + * inputs against reusing the nonce when signing with the wrong precomputed + * pubkey. + * + * Returns: 1 if a nonce was successfully generated. 0 will cause signing to + * return an error. + * Out: nonce32: pointer to a 32-byte array to be filled by the function + * In: msg: the message being verified. Is NULL if and only if msglen + * is 0. + * msglen: the length of the message + * key32: pointer to a 32-byte secret key (will not be NULL) + * xonly_pk32: the 32-byte serialized xonly pubkey corresponding to key32 + * (will not be NULL) + * algo: pointer to an array describing the signature + * algorithm (will not be NULL) + * algolen: the length of the algo array + * data: arbitrary data pointer that is passed through + * + * Except for test cases, this function should compute some cryptographic hash of + * the message, the key, the pubkey, the algorithm description, and data. + */ +typedef int (*secp256k1_nonce_function_hardened)( + unsigned char *nonce32, + const unsigned char *msg, + size_t msglen, + const unsigned char *key32, + const unsigned char *xonly_pk32, + const unsigned char *algo, + size_t algolen, + void *data +); + +/** An implementation of the nonce generation function as defined in Bitcoin + * Improvement Proposal 340 "Schnorr Signatures for secp256k1" + * (https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). + * + * If a data pointer is passed, it is assumed to be a pointer to 32 bytes of + * auxiliary random data as defined in BIP-340. If the data pointer is NULL, + * the nonce derivation procedure follows BIP-340 by setting the auxiliary + * random data to zero. The algo argument must be non-NULL, otherwise the + * function will fail and return 0. The hash will be tagged with algo. + * Therefore, to create BIP-340 compliant signatures, algo must be set to + * "BIP0340/nonce" and algolen to 13. + */ +SECP256K1_API const secp256k1_nonce_function_hardened secp256k1_nonce_function_bip340; + +/** Data structure that contains additional arguments for schnorrsig_sign_custom. + * + * A schnorrsig_extraparams structure object can be initialized correctly by + * setting it to SECP256K1_SCHNORRSIG_EXTRAPARAMS_INIT. + * + * Members: + * magic: set to SECP256K1_SCHNORRSIG_EXTRAPARAMS_MAGIC at initialization + * and has no other function than making sure the object is + * initialized. + * noncefp: pointer to a nonce generation function. If NULL, + * secp256k1_nonce_function_bip340 is used + * ndata: pointer to arbitrary data used by the nonce generation function + * (can be NULL). If it is non-NULL and + * secp256k1_nonce_function_bip340 is used, then ndata must be a + * pointer to 32-byte auxiliary randomness as per BIP-340. + */ +typedef struct { + unsigned char magic[4]; + secp256k1_nonce_function_hardened noncefp; + void *ndata; +} secp256k1_schnorrsig_extraparams; + +#define SECP256K1_SCHNORRSIG_EXTRAPARAMS_MAGIC { 0xda, 0x6f, 0xb3, 0x8c } +#define SECP256K1_SCHNORRSIG_EXTRAPARAMS_INIT {\ + SECP256K1_SCHNORRSIG_EXTRAPARAMS_MAGIC,\ + NULL,\ + NULL\ +} + +/** Create a Schnorr signature. + * + * Does _not_ strictly follow BIP-340 because it does not verify the resulting + * signature. Instead, you can manually use secp256k1_schnorrsig_verify and + * abort if it fails. + * + * This function only signs 32-byte messages. If you have messages of a + * different size (or the same size but without a context-specific tag + * prefix), it is recommended to create a 32-byte message hash with + * secp256k1_tagged_sha256 and then sign the hash. Tagged hashing allows + * providing an context-specific tag for domain separation. This prevents + * signatures from being valid in multiple contexts by accident. + * + * Returns 1 on success, 0 on failure. + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * Out: sig64: pointer to a 64-byte array to store the serialized signature. + * In: msg32: the 32-byte message being signed. + * keypair: pointer to an initialized keypair. + * aux_rand32: 32 bytes of fresh randomness. While recommended to provide + * this, it is only supplemental to security and can be NULL. A + * NULL argument is treated the same as an all-zero one. See + * BIP-340 "Default Signing" for a full explanation of this + * argument and for guidance if randomness is expensive. + */ +SECP256K1_API int secp256k1_schnorrsig_sign32( + const secp256k1_context *ctx, + unsigned char *sig64, + const unsigned char *msg32, + const secp256k1_keypair *keypair, + const unsigned char *aux_rand32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Same as secp256k1_schnorrsig_sign32, but DEPRECATED. Will be removed in + * future versions. */ +SECP256K1_API int secp256k1_schnorrsig_sign( + const secp256k1_context *ctx, + unsigned char *sig64, + const unsigned char *msg32, + const secp256k1_keypair *keypair, + const unsigned char *aux_rand32 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) + SECP256K1_DEPRECATED("Use secp256k1_schnorrsig_sign32 instead"); + +/** Create a Schnorr signature with a more flexible API. + * + * Same arguments as secp256k1_schnorrsig_sign except that it allows signing + * variable length messages and accepts a pointer to an extraparams object that + * allows customizing signing by passing additional arguments. + * + * Equivalent to secp256k1_schnorrsig_sign32(..., aux_rand32) if msglen is 32 + * and extraparams is initialized as follows: + * ``` + * secp256k1_schnorrsig_extraparams extraparams = SECP256K1_SCHNORRSIG_EXTRAPARAMS_INIT; + * extraparams.ndata = (unsigned char*)aux_rand32; + * ``` + * + * Returns 1 on success, 0 on failure. + * Args: ctx: pointer to a context object (not secp256k1_context_static). + * Out: sig64: pointer to a 64-byte array to store the serialized signature. + * In: msg: the message being signed. Can only be NULL if msglen is 0. + * msglen: length of the message. + * keypair: pointer to an initialized keypair. + * extraparams: pointer to an extraparams object (can be NULL). + */ +SECP256K1_API int secp256k1_schnorrsig_sign_custom( + const secp256k1_context *ctx, + unsigned char *sig64, + const unsigned char *msg, + size_t msglen, + const secp256k1_keypair *keypair, + secp256k1_schnorrsig_extraparams *extraparams +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(5); + +/** Verify a Schnorr signature. + * + * Returns: 1: correct signature + * 0: incorrect signature + * Args: ctx: a secp256k1 context object. + * In: sig64: pointer to the 64-byte signature to verify. + * msg: the message being verified. Can only be NULL if msglen is 0. + * msglen: length of the message + * pubkey: pointer to an x-only public key to verify with (cannot be NULL) + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_schnorrsig_verify( + const secp256k1_context *ctx, + const unsigned char *sig64, + const unsigned char *msg, + size_t msglen, + const secp256k1_xonly_pubkey *pubkey +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(5); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_SCHNORRSIG_H */