Merge branch 'main' into claude/marmot-group-management-Jpiux
This commit is contained in:
@@ -874,6 +874,7 @@
|
|||||||
<string name="settings">Nastavení</string>
|
<string name="settings">Nastavení</string>
|
||||||
<string name="account_settings">Nastavení účtu</string>
|
<string name="account_settings">Nastavení účtu</string>
|
||||||
<string name="app_settings">Nastavení aplikace</string>
|
<string name="app_settings">Nastavení aplikace</string>
|
||||||
|
<string name="danger_zone">Nebezpečná zóna</string>
|
||||||
<string name="connectivity_type_always">Vždy</string>
|
<string name="connectivity_type_always">Vždy</string>
|
||||||
<string name="connectivity_type_wifi_only">Pouze Wi-Fi</string>
|
<string name="connectivity_type_wifi_only">Pouze Wi-Fi</string>
|
||||||
<string name="connectivity_type_unmetered_wifi_only">Neomezená WiFi</string>
|
<string name="connectivity_type_unmetered_wifi_only">Neomezená WiFi</string>
|
||||||
@@ -1290,6 +1291,7 @@
|
|||||||
<string name="inbox_relays_not_found_examples">Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
<string name="inbox_relays_not_found_examples">Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||||
<string name="dm_upload">Nahrát v DM</string>
|
<string name="dm_upload">Nahrát v DM</string>
|
||||||
<string name="relay_settings">Nastavení relé</string>
|
<string name="relay_settings">Nastavení relé</string>
|
||||||
|
<string name="relay_reorder">Přeřadit relé</string>
|
||||||
<string name="public_home_section">Veřejná domácí relé</string>
|
<string name="public_home_section">Veřejná domácí relé</string>
|
||||||
<string name="public_home_section_explainer_profile">Uživatel zveřejňuje svůj obsah na těchto přenašečích</string>
|
<string name="public_home_section_explainer_profile">Uživatel zveřejňuje svůj obsah na těchto přenašečích</string>
|
||||||
<string name="public_home_section_explainer">Tento typ relé ukládá veškerý váš obsah. Amethyst sem pošle vaše příspěvky a ostatní použijí tato relé, aby našli váš obsah. Vložte mezi 1–3 relé. Mohou to být osobní relé, placená relé nebo veřejná relé.</string>
|
<string name="public_home_section_explainer">Tento typ relé ukládá veškerý váš obsah. Amethyst sem pošle vaše příspěvky a ostatní použijí tato relé, aby našli váš obsah. Vložte mezi 1–3 relé. Mohou to být osobní relé, placená relé nebo veřejná relé.</string>
|
||||||
@@ -1676,6 +1678,7 @@
|
|||||||
<string name="kind_shorts">Krátká videa</string>
|
<string name="kind_shorts">Krátká videa</string>
|
||||||
<string name="kind_voice_msg">Hlasová zpráva</string>
|
<string name="kind_voice_msg">Hlasová zpráva</string>
|
||||||
<string name="kind_voice_reply">Hlasová odpověď</string>
|
<string name="kind_voice_reply">Hlasová odpověď</string>
|
||||||
|
<string name="kind_wake">Probuzení</string>
|
||||||
<string name="kind_web_bookmark">Webová záložka</string>
|
<string name="kind_web_bookmark">Webová záložka</string>
|
||||||
<string name="kind_wiki">Wiki</string>
|
<string name="kind_wiki">Wiki</string>
|
||||||
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Začněte se skvělým feedem tím, že budete sledovat stejné lidi jako někdo, komu důvěřujete.</string>
|
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Začněte se skvělým feedem tím, že budete sledovat stejné lidi jako někdo, komu důvěřujete.</string>
|
||||||
|
|||||||
@@ -879,6 +879,7 @@ anz der Bedingungen ist erforderlich</string>
|
|||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</string>
|
||||||
<string name="account_settings">Kontoeinstellungen</string>
|
<string name="account_settings">Kontoeinstellungen</string>
|
||||||
<string name="app_settings">App-Einstellungen</string>
|
<string name="app_settings">App-Einstellungen</string>
|
||||||
|
<string name="danger_zone">Gefahrenzone</string>
|
||||||
<string name="connectivity_type_always">Immer</string>
|
<string name="connectivity_type_always">Immer</string>
|
||||||
<string name="connectivity_type_wifi_only">Nur WLAN</string>
|
<string name="connectivity_type_wifi_only">Nur WLAN</string>
|
||||||
<string name="connectivity_type_unmetered_wifi_only">Unbegrenztes WLAN</string>
|
<string name="connectivity_type_unmetered_wifi_only">Unbegrenztes WLAN</string>
|
||||||
@@ -1295,6 +1296,7 @@ anz der Bedingungen ist erforderlich</string>
|
|||||||
<string name="inbox_relays_not_found_examples">Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
<string name="inbox_relays_not_found_examples">Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||||
<string name="dm_upload">DM-Upload</string>
|
<string name="dm_upload">DM-Upload</string>
|
||||||
<string name="relay_settings">Relaiseinstellungen</string>
|
<string name="relay_settings">Relaiseinstellungen</string>
|
||||||
|
<string name="relay_reorder">Relay neu anordnen</string>
|
||||||
<string name="public_home_section">Öffentliche Heimrelais</string>
|
<string name="public_home_section">Öffentliche Heimrelais</string>
|
||||||
<string name="public_home_section_explainer_profile">Der Benutzer veröffentlicht seine Inhalte auf diesen Relays</string>
|
<string name="public_home_section_explainer_profile">Der Benutzer veröffentlicht seine Inhalte auf diesen Relays</string>
|
||||||
<string name="public_home_section_explainer">Dieser Relais-Typ speichert alle Ihre Inhalte. Amethyst sendet Ihre Beiträge hierher und andere werden diese Relais verwenden, um Ihre Inhalte zu finden. Fügen Sie 1–3 Relais ein. Sie können persönliche Relais, bezahlte Relais oder öffentliche Relais sein.</string>
|
<string name="public_home_section_explainer">Dieser Relais-Typ speichert alle Ihre Inhalte. Amethyst sendet Ihre Beiträge hierher und andere werden diese Relais verwenden, um Ihre Inhalte zu finden. Fügen Sie 1–3 Relais ein. Sie können persönliche Relais, bezahlte Relais oder öffentliche Relais sein.</string>
|
||||||
@@ -1681,6 +1683,7 @@ anz der Bedingungen ist erforderlich</string>
|
|||||||
<string name="kind_shorts">Shorts</string>
|
<string name="kind_shorts">Shorts</string>
|
||||||
<string name="kind_voice_msg">Sprachnachricht</string>
|
<string name="kind_voice_msg">Sprachnachricht</string>
|
||||||
<string name="kind_voice_reply">Sprachantwort</string>
|
<string name="kind_voice_reply">Sprachantwort</string>
|
||||||
|
<string name="kind_wake">Aufwachen</string>
|
||||||
<string name="kind_web_bookmark">Web-Lesezeichen</string>
|
<string name="kind_web_bookmark">Web-Lesezeichen</string>
|
||||||
<string name="kind_wiki">Wiki</string>
|
<string name="kind_wiki">Wiki</string>
|
||||||
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Starte mit einem großartigen Feed, indem du dieselben Personen folgst wie jemand, dem du vertraust.</string>
|
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Starte mit einem großartigen Feed, indem du dieselben Personen folgst wie jemand, dem du vertraust.</string>
|
||||||
|
|||||||
@@ -386,6 +386,8 @@
|
|||||||
<string name="migrate_bookmarks_success">A könyvjelzők átköltöztetése sikeresen befejeződött</string>
|
<string name="migrate_bookmarks_success">A könyvjelzők átköltöztetése sikeresen befejeződött</string>
|
||||||
<string name="drafts">Piszkozatok</string>
|
<string name="drafts">Piszkozatok</string>
|
||||||
<string name="polls">Szavazások</string>
|
<string name="polls">Szavazások</string>
|
||||||
|
<string name="open_polls">Megnyitás</string>
|
||||||
|
<string name="closed_polls">Lezárva</string>
|
||||||
<string name="pictures">Képek</string>
|
<string name="pictures">Képek</string>
|
||||||
<string name="shorts">Rövidek</string>
|
<string name="shorts">Rövidek</string>
|
||||||
<string name="longs">Videók</string>
|
<string name="longs">Videók</string>
|
||||||
@@ -876,6 +878,7 @@
|
|||||||
<string name="settings">Beállítások</string>
|
<string name="settings">Beállítások</string>
|
||||||
<string name="account_settings">Fiókbeállítások</string>
|
<string name="account_settings">Fiókbeállítások</string>
|
||||||
<string name="app_settings">Alkalmazásbeállítások</string>
|
<string name="app_settings">Alkalmazásbeállítások</string>
|
||||||
|
<string name="danger_zone">Kritikus beállítások</string>
|
||||||
<string name="connectivity_type_always">Mindig</string>
|
<string name="connectivity_type_always">Mindig</string>
|
||||||
<string name="connectivity_type_wifi_only">Csak Wi-Fi-n</string>
|
<string name="connectivity_type_wifi_only">Csak Wi-Fi-n</string>
|
||||||
<string name="connectivity_type_unmetered_wifi_only">Korlátlan Wi-Fi</string>
|
<string name="connectivity_type_unmetered_wifi_only">Korlátlan Wi-Fi</string>
|
||||||
@@ -1293,6 +1296,7 @@
|
|||||||
<string name="inbox_relays_not_found_examples">Jó választási lehetőségek:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
<string name="inbox_relays_not_found_examples">Jó választási lehetőségek:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||||
<string name="dm_upload">Feltöltés közvetlen üzenetbe</string>
|
<string name="dm_upload">Feltöltés közvetlen üzenetbe</string>
|
||||||
<string name="relay_settings">Átjátszók beállításai</string>
|
<string name="relay_settings">Átjátszók beállításai</string>
|
||||||
|
<string name="relay_reorder">Átjátszók átrendezése</string>
|
||||||
<string name="public_home_section">Nyilvános kimenő és saját átjátszók</string>
|
<string name="public_home_section">Nyilvános kimenő és saját átjátszók</string>
|
||||||
<string name="public_home_section_explainer_profile">A felhasználó a bejegyzéseit ezeken az átjátszókon teszi közzé</string>
|
<string name="public_home_section_explainer_profile">A felhasználó a bejegyzéseit ezeken az átjátszókon teszi közzé</string>
|
||||||
<string name="public_home_section_explainer">Ez az átjátszótípus tárolja az összes tartalmat. Az Amethyst ide küldi az Ön bejegyzéseit, és mások ezeket az átjátszókat fogják használni, hogy megtalálják az Ön tartalmát. Adjon hozzá 1–3 átjátszót. Ezek lehetnek személyes-, fizetett- vagy nyilvános átjátszók.</string>
|
<string name="public_home_section_explainer">Ez az átjátszótípus tárolja az összes tartalmat. Az Amethyst ide küldi az Ön bejegyzéseit, és mások ezeket az átjátszókat fogják használni, hogy megtalálják az Ön tartalmát. Adjon hozzá 1–3 átjátszót. Ezek lehetnek személyes-, fizetett- vagy nyilvános átjátszók.</string>
|
||||||
@@ -1436,6 +1440,7 @@
|
|||||||
<string name="select_list_to_filter">Szempont kiválasztása a hírfolyam szűréséhez</string>
|
<string name="select_list_to_filter">Szempont kiválasztása a hírfolyam szűréséhez</string>
|
||||||
<string name="feed_group_feeds">Hírfolyamok</string>
|
<string name="feed_group_feeds">Hírfolyamok</string>
|
||||||
<string name="feed_group_hashtags">Kulcsszavak</string>
|
<string name="feed_group_hashtags">Kulcsszavak</string>
|
||||||
|
<string name="feed_group_locations">Helyszínek</string>
|
||||||
<string name="feed_group_communities">Közösségek</string>
|
<string name="feed_group_communities">Közösségek</string>
|
||||||
<string name="feed_group_lists">Listák</string>
|
<string name="feed_group_lists">Listák</string>
|
||||||
<string name="feed_group_relays">Átjátszók</string>
|
<string name="feed_group_relays">Átjátszók</string>
|
||||||
@@ -1536,6 +1541,7 @@
|
|||||||
<string name="connected">Kapcsolódva</string>
|
<string name="connected">Kapcsolódva</string>
|
||||||
<string name="social_proof">Közösségi megerősítés</string>
|
<string name="social_proof">Közösségi megerősítés</string>
|
||||||
<string name="poll_submit">Beküldés</string>
|
<string name="poll_submit">Beküldés</string>
|
||||||
|
<string name="poll_view_results">Eredmények megtekintése</string>
|
||||||
<string name="restart">Újraindítás</string>
|
<string name="restart">Újraindítás</string>
|
||||||
<string name="chess_accept">Elfogadás</string>
|
<string name="chess_accept">Elfogadás</string>
|
||||||
<string name="chess_decline">Elutasítás</string>
|
<string name="chess_decline">Elutasítás</string>
|
||||||
@@ -1677,6 +1683,7 @@
|
|||||||
<string name="kind_shorts">Rövidek</string>
|
<string name="kind_shorts">Rövidek</string>
|
||||||
<string name="kind_voice_msg">Hangüzenet</string>
|
<string name="kind_voice_msg">Hangüzenet</string>
|
||||||
<string name="kind_voice_reply">Hangos válasz</string>
|
<string name="kind_voice_reply">Hangos válasz</string>
|
||||||
|
<string name="kind_wake">Ébredés</string>
|
||||||
<string name="kind_web_bookmark">Webes könyvjelző</string>
|
<string name="kind_web_bookmark">Webes könyvjelző</string>
|
||||||
<string name="kind_wiki">Wiki</string>
|
<string name="kind_wiki">Wiki</string>
|
||||||
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Kezdje egy remek hírfolyammal, követve azokat az embereket, akikben megbízik.</string>
|
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Kezdje egy remek hírfolyammal, követve azokat az embereket, akikben megbízik.</string>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -874,6 +874,7 @@
|
|||||||
<string name="settings">Configurações</string>
|
<string name="settings">Configurações</string>
|
||||||
<string name="account_settings">Configurações da Conta</string>
|
<string name="account_settings">Configurações da Conta</string>
|
||||||
<string name="app_settings">Configurações do App</string>
|
<string name="app_settings">Configurações do App</string>
|
||||||
|
<string name="danger_zone">Zona de Perigo</string>
|
||||||
<string name="connectivity_type_always">Sempre</string>
|
<string name="connectivity_type_always">Sempre</string>
|
||||||
<string name="connectivity_type_wifi_only">Somente wifi</string>
|
<string name="connectivity_type_wifi_only">Somente wifi</string>
|
||||||
<string name="connectivity_type_unmetered_wifi_only">WiFi não medida</string>
|
<string name="connectivity_type_unmetered_wifi_only">WiFi não medida</string>
|
||||||
@@ -1290,6 +1291,7 @@
|
|||||||
<string name="inbox_relays_not_found_examples">Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
<string name="inbox_relays_not_found_examples">Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||||
<string name="dm_upload">Envio de DM</string>
|
<string name="dm_upload">Envio de DM</string>
|
||||||
<string name="relay_settings">Configurações de Relay</string>
|
<string name="relay_settings">Configurações de Relay</string>
|
||||||
|
<string name="relay_reorder">Reordenar relay</string>
|
||||||
<string name="public_home_section">Relés Públicos de Casa</string>
|
<string name="public_home_section">Relés Públicos de Casa</string>
|
||||||
<string name="public_home_section_explainer_profile">O usuário está publicando seu conteúdo nesses relays</string>
|
<string name="public_home_section_explainer_profile">O usuário está publicando seu conteúdo nesses relays</string>
|
||||||
<string name="public_home_section_explainer">Esse tipo de relé armazena todo o seu conteúdo. Amethyst enviará suas postagens aqui e outros usarão esses relés para encontrar seu conteúdo. Insira entre 1–3 relés. Eles podem ser relés pessoais, relés pagos ou relés públicos.</string>
|
<string name="public_home_section_explainer">Esse tipo de relé armazena todo o seu conteúdo. Amethyst enviará suas postagens aqui e outros usarão esses relés para encontrar seu conteúdo. Insira entre 1–3 relés. Eles podem ser relés pessoais, relés pagos ou relés públicos.</string>
|
||||||
@@ -1676,6 +1678,7 @@
|
|||||||
<string name="kind_shorts">Shorts</string>
|
<string name="kind_shorts">Shorts</string>
|
||||||
<string name="kind_voice_msg">Mensagem de voz</string>
|
<string name="kind_voice_msg">Mensagem de voz</string>
|
||||||
<string name="kind_voice_reply">Resposta de voz</string>
|
<string name="kind_voice_reply">Resposta de voz</string>
|
||||||
|
<string name="kind_wake">Despertar</string>
|
||||||
<string name="kind_web_bookmark">Marcador Web</string>
|
<string name="kind_web_bookmark">Marcador Web</string>
|
||||||
<string name="kind_wiki">Wiki</string>
|
<string name="kind_wiki">Wiki</string>
|
||||||
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Comece com um ótimo feed seguindo as mesmas pessoas que alguém em quem você confia.</string>
|
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Comece com um ótimo feed seguindo as mesmas pessoas que alguém em quem você confia.</string>
|
||||||
|
|||||||
@@ -873,6 +873,7 @@
|
|||||||
<string name="settings">Inställningar</string>
|
<string name="settings">Inställningar</string>
|
||||||
<string name="account_settings">Kontoinställningar</string>
|
<string name="account_settings">Kontoinställningar</string>
|
||||||
<string name="app_settings">Appinställningar</string>
|
<string name="app_settings">Appinställningar</string>
|
||||||
|
<string name="danger_zone">Riskzon</string>
|
||||||
<string name="connectivity_type_always">Alltid</string>
|
<string name="connectivity_type_always">Alltid</string>
|
||||||
<string name="connectivity_type_wifi_only">Endast Wi-Fi</string>
|
<string name="connectivity_type_wifi_only">Endast Wi-Fi</string>
|
||||||
<string name="connectivity_type_unmetered_wifi_only">Obegränsad WiFi</string>
|
<string name="connectivity_type_unmetered_wifi_only">Obegränsad WiFi</string>
|
||||||
@@ -1289,6 +1290,7 @@
|
|||||||
<string name="inbox_relays_not_found_examples">Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
<string name="inbox_relays_not_found_examples">Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||||
<string name="dm_upload">DM uppladdning</string>
|
<string name="dm_upload">DM uppladdning</string>
|
||||||
<string name="relay_settings">Relä inställningar</string>
|
<string name="relay_settings">Relä inställningar</string>
|
||||||
|
<string name="relay_reorder">Ordna om relä</string>
|
||||||
<string name="public_home_section">Offentliga hemreläer</string>
|
<string name="public_home_section">Offentliga hemreläer</string>
|
||||||
<string name="public_home_section_explainer_profile">Användaren publicerar sitt innehåll på dessa reläer</string>
|
<string name="public_home_section_explainer_profile">Användaren publicerar sitt innehåll på dessa reläer</string>
|
||||||
<string name="public_home_section_explainer">Denna typ av relä lagrar allt ditt innehåll. Amethyst skickar dina inlägg hit och andra kommer att använda dessa reläer för att hitta ditt innehåll. Sätt in mellan 1–3 reläer. De kan vara personliga reläer, betalda reläer eller offentliga reläer.</string>
|
<string name="public_home_section_explainer">Denna typ av relä lagrar allt ditt innehåll. Amethyst skickar dina inlägg hit och andra kommer att använda dessa reläer för att hitta ditt innehåll. Sätt in mellan 1–3 reläer. De kan vara personliga reläer, betalda reläer eller offentliga reläer.</string>
|
||||||
@@ -1675,6 +1677,7 @@
|
|||||||
<string name="kind_shorts">Shorts</string>
|
<string name="kind_shorts">Shorts</string>
|
||||||
<string name="kind_voice_msg">Röstmeddelande</string>
|
<string name="kind_voice_msg">Röstmeddelande</string>
|
||||||
<string name="kind_voice_reply">Röstsvar</string>
|
<string name="kind_voice_reply">Röstsvar</string>
|
||||||
|
<string name="kind_wake">Väckning</string>
|
||||||
<string name="kind_web_bookmark">Webbbokmärke</string>
|
<string name="kind_web_bookmark">Webbbokmärke</string>
|
||||||
<string name="kind_wiki">Wiki</string>
|
<string name="kind_wiki">Wiki</string>
|
||||||
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Kom igång med ett bra flöde genom att följa samma personer som någon du litar på.</string>
|
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Kom igång med ett bra flöde genom att följa samma personer som någon du litar på.</string>
|
||||||
|
|||||||
@@ -1683,6 +1683,7 @@
|
|||||||
<string name="kind_shorts">短篇</string>
|
<string name="kind_shorts">短篇</string>
|
||||||
<string name="kind_voice_msg">语音消息</string>
|
<string name="kind_voice_msg">语音消息</string>
|
||||||
<string name="kind_voice_reply">语音回复</string>
|
<string name="kind_voice_reply">语音回复</string>
|
||||||
|
<string name="kind_wake">唤醒</string>
|
||||||
<string name="kind_web_bookmark">网络书签</string>
|
<string name="kind_web_bookmark">网络书签</string>
|
||||||
<string name="kind_wiki">维基</string>
|
<string name="kind_wiki">维基</string>
|
||||||
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">关注你信任的人所关注的人来开启优质的源。</string>
|
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">关注你信任的人所关注的人来开启优质的源。</string>
|
||||||
|
|||||||
+1
@@ -26,5 +26,6 @@ internal actual class ScratchLocal<T> actual constructor(
|
|||||||
) {
|
) {
|
||||||
private val tl = ThreadLocal.withInitial(initializer)
|
private val tl = ThreadLocal.withInitial(initializer)
|
||||||
|
|
||||||
|
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||||
actual fun get(): T = tl.get()
|
actual fun get(): T = tl.get()
|
||||||
}
|
}
|
||||||
|
|||||||
+185
-252
@@ -24,112 +24,29 @@ package com.vitorpamplona.quartz.utils.secp256k1
|
|||||||
// ELLIPTIC CURVE POINT OPERATIONS ON secp256k1
|
// ELLIPTIC CURVE POINT OPERATIONS ON secp256k1
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
//
|
//
|
||||||
// This file implements point arithmetic on the secp256k1 elliptic curve: y² = x³ + 7 (mod p).
|
// Point arithmetic on the secp256k1 curve: y² = x³ + 7 (mod p).
|
||||||
// It provides point addition, doubling, scalar multiplication, and serialization.
|
// See PointTypes.kt for MutablePoint, AffinePoint, and PointScratch.
|
||||||
//
|
|
||||||
// JACOBIAN COORDINATES
|
|
||||||
// ====================
|
|
||||||
// Points are stored in Jacobian projective coordinates (X, Y, Z) which represent the
|
|
||||||
// affine point (X/Z², Y/Z³). This avoids expensive field inversions during intermediate
|
|
||||||
// steps of scalar multiplication — inversion is only needed once at the very end to
|
|
||||||
// convert back to affine (x, y) form.
|
|
||||||
//
|
|
||||||
// The "point at infinity" (identity element) is represented by Z = 0.
|
|
||||||
//
|
//
|
||||||
// POINT FORMULAS
|
// POINT FORMULAS
|
||||||
// ==============
|
// ==============
|
||||||
// - doublePoint: 3M+4S (uses fe_half for L=(3/2)·X², same as libsecp256k1)
|
// - doublePoint: 3M+4S (uses fe_half for L=(3/2)·X², same as libsecp256k1)
|
||||||
// - addMixed (Jacobian + Affine): 8M+3S (used for precomputed table lookups)
|
// - addMixed (Jacobian + Affine): 8M+3S (precomputed table lookups)
|
||||||
// - addPoints (Jacobian + Jacobian): 11M+5S (used when both points are Jacobian)
|
// - addPoints (Jacobian + Jacobian): 11M+5S (both points Jacobian)
|
||||||
//
|
//
|
||||||
// SCALAR MULTIPLICATION STRATEGIES
|
// SCALAR MULTIPLICATION
|
||||||
// ================================
|
// =====================
|
||||||
// Three methods are used depending on the context:
|
// 1. mulG (Generator): Comb method, only 3 doublings + ~43 table lookups.
|
||||||
|
// 2. mul (Arbitrary): GLV + wNAF-5, ~130 shared doublings.
|
||||||
|
// 3. mulDoubleG (Verify: s·G + e·P): Strauss/Shamir + GLV + wNAF, 4 streams.
|
||||||
//
|
//
|
||||||
// 1. mulG (Generator multiplication): Comb method (Hamburg 2012).
|
// PRECOMPUTED TABLES (lazily initialized)
|
||||||
// Arranges scalar bits into a 4×66 matrix, processes 4 rows with 11 table lookups
|
// =======================================
|
||||||
// each. Only 3 doublings total. Uses a precomputed 704-entry affine table (~45KB).
|
// - combTable: 704 affine points for mulG (~45KB)
|
||||||
// Cost: ~43 mixed additions + 3 doublings ≈ 494 field ops.
|
// - gOddTable: 1024 affine points for G-side wNAF-12 (~128KB)
|
||||||
// Used by: pubkeyCreate, signSchnorr.
|
// - gLamTable: 1024 affine points for λ(G)-side wNAF-12 (~128KB)
|
||||||
//
|
// - pTableCache: 256-entry cache of P-side wNAF tables (~256KB, for verify)
|
||||||
// 2. mul (Arbitrary point multiplication): GLV endomorphism + wNAF-5 (Glv.kt).
|
|
||||||
// Splits the 256-bit scalar into two ~128-bit halves via the secp256k1 endomorphism,
|
|
||||||
// then processes both with wNAF encoding in a single pass of ~130 shared doublings.
|
|
||||||
// P-side tables are batch-inverted to affine (effective-affine technique) so the
|
|
||||||
// main loop uses addMixed (8M+3S) instead of addPoints (11M+5S), saving ~4M per add.
|
|
||||||
// Used by: pubKeyTweakMul (ECDH), ecdhXOnly.
|
|
||||||
//
|
|
||||||
// 3. mulDoubleG (Verification: s·G + e·P): Strauss/Shamir trick with GLV + wNAF.
|
|
||||||
// Splits both scalars via GLV into 4 half-scalar streams. G-side uses a precomputed
|
|
||||||
// 1024-entry affine wNAF-12 table (~128KB); P-side tables batch-inverted to affine.
|
|
||||||
// All 4 streams share ~130 doublings with mixed additions throughout.
|
|
||||||
// Used by: verifySchnorr.
|
|
||||||
//
|
|
||||||
// BATCH INVERSION
|
|
||||||
// ===============
|
|
||||||
// Montgomery's trick: convert n Jacobian→affine with 1 inversion + 3(n-1) muls instead
|
|
||||||
// of n individual inversions. Used for wNAF table construction and G table initialization.
|
|
||||||
//
|
|
||||||
// PRECOMPUTED TABLES
|
|
||||||
// ==================
|
|
||||||
// All tables are lazily initialized on first use (Kotlin `by lazy`):
|
|
||||||
// - combTable: 704 affine points for mulG (~45KB, built once per process)
|
|
||||||
// - gOddTable: 1024 affine points for G-side wNAF-12 (~128KB, batch-inverted)
|
|
||||||
// - gLamTable: 1024 affine points for λ(G)-side wNAF-12 (~128KB, derived from gOddTable)
|
|
||||||
//
|
|
||||||
// Note: C libsecp256k1 uses WINDOW_G=15 (8192 entries, 1MB) as compile-time .rodata.
|
|
||||||
// On JVM, w=15 is slower due to cache pressure from heap-allocated AffinePoint objects.
|
|
||||||
// w=12 (1024 entries, ~128KB) is the sweet spot — fits in L2, fewer additions than w=8.
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutable Jacobian point for in-place computation.
|
|
||||||
*
|
|
||||||
* Points are mutable to avoid allocating new objects during the inner loop of scalar
|
|
||||||
* multiplication, which performs thousands of doublings and additions per operation.
|
|
||||||
*/
|
|
||||||
internal class MutablePoint(
|
|
||||||
val x: LongArray = LongArray(4),
|
|
||||||
val y: LongArray = LongArray(4),
|
|
||||||
val z: LongArray = LongArray(4),
|
|
||||||
) {
|
|
||||||
fun isInfinity(): Boolean = U256.isZero(z)
|
|
||||||
|
|
||||||
fun setInfinity() {
|
|
||||||
for (i in 0 until 4) {
|
|
||||||
x[i] = 0L
|
|
||||||
z[i] = 0L
|
|
||||||
}
|
|
||||||
y[0] = 1L
|
|
||||||
for (i in 1 until 4) y[i] = 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyFrom(other: MutablePoint) {
|
|
||||||
other.x.copyInto(x)
|
|
||||||
other.y.copyInto(y)
|
|
||||||
other.z.copyInto(z)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAffine(
|
|
||||||
ax: LongArray,
|
|
||||||
ay: LongArray,
|
|
||||||
) {
|
|
||||||
ax.copyInto(x)
|
|
||||||
ay.copyInto(y)
|
|
||||||
z[0] = 1L
|
|
||||||
for (i in 1 until 4) z[i] = 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Affine point (x, y) — no Z coordinate.
|
|
||||||
* Used for precomputed tables where we want compact storage and mixed addition.
|
|
||||||
*/
|
|
||||||
internal class AffinePoint(
|
|
||||||
val x: LongArray = LongArray(4),
|
|
||||||
val y: LongArray = LongArray(4),
|
|
||||||
)
|
|
||||||
|
|
||||||
internal object ECPoint {
|
internal object ECPoint {
|
||||||
// ==================== Generator point G ====================
|
// ==================== Generator point G ====================
|
||||||
|
|
||||||
@@ -176,6 +93,34 @@ internal object ECPoint {
|
|||||||
Array(G_TABLE_SIZE) { AffinePoint(FieldP.mul(gOddTable[it].x, Glv.BETA), gOddTable[it].y.copyOf()) }
|
Array(G_TABLE_SIZE) { AffinePoint(FieldP.mul(gOddTable[it].x, Glv.BETA), gOddTable[it].y.copyOf()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== P-side wNAF table cache ====================
|
||||||
|
//
|
||||||
|
// In mulDoubleG (verify), we build an 8-entry wNAF-5 affine table for the
|
||||||
|
// public key P on every call: [1P, 3P, 5P, ..., 15P] plus their GLV λ
|
||||||
|
// counterparts. This costs ~437 field ops (~27% of mulDoubleG, ~20% of verify).
|
||||||
|
//
|
||||||
|
// For Nostr, the same pubkeys are verified repeatedly (many events per author).
|
||||||
|
// This cache stores the P-side affine tables keyed by the point's x-coordinate,
|
||||||
|
// so repeated verifications for the same pubkey skip the table build entirely.
|
||||||
|
//
|
||||||
|
// 256 entries × 16 AffinePoints × 64 bytes = ~256KB total cache.
|
||||||
|
|
||||||
|
// 1024 entries to cover ~1000 followed pubkeys with minimal collisions.
|
||||||
|
// Memory: 1024 × 16 AffinePoints × 64 bytes = ~1MB. Acceptable for mobile.
|
||||||
|
private const val P_TABLE_CACHE_SIZE = 1024
|
||||||
|
private const val P_TABLE_CACHE_MASK = P_TABLE_CACHE_SIZE - 1
|
||||||
|
|
||||||
|
private class CachedPTable(
|
||||||
|
val px: LongArray, // x-coordinate of the point (cache key, 4 limbs)
|
||||||
|
val pOdd: Array<AffinePoint>, // 8 affine odd-multiples of P
|
||||||
|
val pLamOdd: Array<AffinePoint>, // 8 affine odd-multiples of λ(P)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val pTableCache = arrayOfNulls<CachedPTable>(P_TABLE_CACHE_SIZE)
|
||||||
|
|
||||||
|
/** Hash a field element to a cache slot index. */
|
||||||
|
private fun cacheSlot(px: LongArray): Int = (px[0].toInt() xor px[1].toInt().shl(3)) and P_TABLE_CACHE_MASK
|
||||||
|
|
||||||
private fun buildGOddTable(): Array<AffinePoint> {
|
private fun buildGOddTable(): Array<AffinePoint> {
|
||||||
val g = MutablePoint()
|
val g = MutablePoint()
|
||||||
g.setAffine(GX, GY)
|
g.setAffine(GX, GY)
|
||||||
@@ -302,51 +247,7 @@ internal object ECPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Thread-local scratch buffers ====================
|
// ==================== Thread-local scratch ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* Scratch space for point operations. Each thread gets its own set of temporary
|
|
||||||
* field elements and a wide buffer to avoid allocation and ThreadLocal lookups
|
|
||||||
* in the inner loops. The 12 temp buffers (t[0]..t[11]) are shared across
|
|
||||||
* doublePoint and addPoints — this is safe because these functions only call
|
|
||||||
* each other in the equal-point degenerate case, which returns immediately
|
|
||||||
* after the recursive call without using the temps further.
|
|
||||||
*
|
|
||||||
* The wide buffer (LongArray(8)) is pre-fetched once per top-level operation
|
|
||||||
* and passed through to FieldP.mul/sqr, avoiding ~500+ ThreadLocal.get() calls
|
|
||||||
* per scalar multiplication (~20-30ns each on JVM).
|
|
||||||
*/
|
|
||||||
internal class PointScratch {
|
|
||||||
val t = Array(12) { LongArray(4) }
|
|
||||||
val dblCopy = MutablePoint() // Copy buffer for in-place doubling (out === input)
|
|
||||||
val w = LongArray(8) // Wide buffer for FieldP.mul/sqr — shared, avoids ThreadLocal
|
|
||||||
|
|
||||||
// Pre-allocated scratch for wNAF encoding (avoids IntArray allocation per call).
|
|
||||||
// Size 145 = 129 (max bits after GLV split) + 15 (max window) + 1 (headroom).
|
|
||||||
val wnaf1 = IntArray(145)
|
|
||||||
val wnaf2 = IntArray(145)
|
|
||||||
val wnaf3 = IntArray(145) // mulDoubleG needs 4 wNAF arrays
|
|
||||||
val wnaf4 = IntArray(145)
|
|
||||||
val wnafTmp = LongArray(4) // scratch for wnaf scalar copy (GLV scalars are up to 4 limbs)
|
|
||||||
|
|
||||||
// Pre-allocated scratch for wNAF mixed addition
|
|
||||||
val mixTmp = MutablePoint()
|
|
||||||
val mixNegY = LongArray(4)
|
|
||||||
|
|
||||||
// Pre-allocated P-side tables for mul/mulDoubleG (avoids ~80 LongArray allocs per call)
|
|
||||||
val pOddJac = Array(8) { MutablePoint() }
|
|
||||||
val pLamOddJac = Array(8) { MutablePoint() }
|
|
||||||
val pOddAff = Array(8) { AffinePoint() }
|
|
||||||
val pLamOddAff = Array(8) { AffinePoint() }
|
|
||||||
val p2 = MutablePoint() // doublePoint temp for table building
|
|
||||||
|
|
||||||
// Pre-allocated batch inversion temps (avoids 12 LongArray allocs per call)
|
|
||||||
val cumZ = Array(8) { LongArray(4) }
|
|
||||||
val batchInv = LongArray(4)
|
|
||||||
val batchZInv = LongArray(4)
|
|
||||||
val batchZInv2 = LongArray(4)
|
|
||||||
val batchZInv3 = LongArray(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scratch = ScratchLocal { PointScratch() }
|
private val scratch = ScratchLocal { PointScratch() }
|
||||||
|
|
||||||
@@ -563,8 +464,8 @@ internal object ECPoint {
|
|||||||
val wnd = 5
|
val wnd = 5
|
||||||
val tableSize = 1 shl (wnd - 2) // 8 entries
|
val tableSize = 1 shl (wnd - 2) // 8 entries
|
||||||
|
|
||||||
// Split scalar via GLV: scalar = k₁ + k₂·λ
|
// Split scalar via GLV: scalar = k₁ + k₂·λ (allocation-free)
|
||||||
val split = Glv.splitScalar(scalar)
|
val split = Glv.splitScalarInto(s.splitK1, s.splitK2, scalar, s.splitWide, s.splitT1, s.splitT2)
|
||||||
Glv.wnafInto(s.wnaf1, s.wnafTmp, split.k1, wnd, 129)
|
Glv.wnafInto(s.wnaf1, s.wnafTmp, split.k1, wnd, 129)
|
||||||
Glv.wnafInto(s.wnaf2, s.wnafTmp, split.k2, wnd, 129)
|
Glv.wnafInto(s.wnaf2, s.wnafTmp, split.k2, wnd, 129)
|
||||||
val wnaf1 = s.wnaf1
|
val wnaf1 = s.wnaf1
|
||||||
@@ -596,15 +497,31 @@ internal object ECPoint {
|
|||||||
bits--
|
bits--
|
||||||
}
|
}
|
||||||
|
|
||||||
out.setInfinity()
|
// Ping-pong: alternate between two point buffers to avoid copyFrom after
|
||||||
val tmp = s.mixTmp
|
// every addition. Saves ~20 copyFroms per call (each = 12 Long copies).
|
||||||
|
// Also avoids the internal copy in doublePoint (out===inp path).
|
||||||
|
var cur = out
|
||||||
|
var alt = s.mixTmp
|
||||||
|
cur.setInfinity()
|
||||||
val negY = s.mixNegY
|
val negY = s.mixNegY
|
||||||
|
|
||||||
for (i in bits - 1 downTo 0) {
|
for (i in bits - 1 downTo 0) {
|
||||||
doublePoint(out, out, s)
|
doublePoint(alt, cur, s)
|
||||||
addWnafMixed(out, tmp, negY, wnaf1, i, pOdd, split.negK1, s)
|
var t = cur
|
||||||
addWnafMixed(out, tmp, negY, wnaf2, i, pLamOdd, split.negK2, s)
|
cur = alt
|
||||||
|
alt = t
|
||||||
|
if (addWnafMixedPP(cur, alt, negY, wnaf1, i, pOdd, split.negK1, s)) {
|
||||||
|
t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
}
|
}
|
||||||
|
if (addWnafMixedPP(cur, alt, negY, wnaf2, i, pLamOdd, split.negK2, s)) {
|
||||||
|
t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cur !== out) out.copyFrom(cur)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -628,12 +545,20 @@ internal object ECPoint {
|
|||||||
|
|
||||||
val s = scratch.get()
|
val s = scratch.get()
|
||||||
val table = combTable
|
val table = combTable
|
||||||
out.setInfinity()
|
|
||||||
val tmp = MutablePoint()
|
// Ping-pong: alternate between out and s.mixTmp to avoid copyFrom after
|
||||||
|
// every addMixed and the internal copy in in-place doublePoint.
|
||||||
|
// Also eliminates the MutablePoint() allocation that was here before.
|
||||||
|
var cur = out
|
||||||
|
var alt = s.mixTmp
|
||||||
|
cur.setInfinity()
|
||||||
|
|
||||||
for (combOff in COMB_SPACING - 1 downTo 0) {
|
for (combOff in COMB_SPACING - 1 downTo 0) {
|
||||||
if (combOff < COMB_SPACING - 1) {
|
if (combOff < COMB_SPACING - 1) {
|
||||||
doublePoint(out, out, s)
|
doublePoint(alt, cur, s)
|
||||||
|
val t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
}
|
}
|
||||||
for (block in 0 until COMB_BLOCKS) {
|
for (block in 0 until COMB_BLOCKS) {
|
||||||
var mask = 0
|
var mask = 0
|
||||||
@@ -645,11 +570,14 @@ internal object ECPoint {
|
|||||||
}
|
}
|
||||||
if (mask != 0) {
|
if (mask != 0) {
|
||||||
val entry = table[block * COMB_POINTS + mask]
|
val entry = table[block * COMB_POINTS + mask]
|
||||||
addMixed(tmp, out, entry.x, entry.y, s)
|
addMixed(alt, cur, entry.x, entry.y, s)
|
||||||
out.copyFrom(tmp)
|
val t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (cur !== out) out.copyFrom(cur)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -674,13 +602,14 @@ internal object ECPoint {
|
|||||||
val wP = 5 // Window for P-side (table built per-call, keep small)
|
val wP = 5 // Window for P-side (table built per-call, keep small)
|
||||||
val pTableSize = 1 shl (wP - 2) // 8 entries for P
|
val pTableSize = 1 shl (wP - 2) // 8 entries for P
|
||||||
|
|
||||||
// Split scalars via GLV decomposition
|
// Split scalars via GLV decomposition (allocation-free).
|
||||||
val sSplit = Glv.splitScalar(s)
|
// sSplit writes into splitK1/K2, then wNAF encodes them immediately
|
||||||
val eSplit = Glv.splitScalar(e)
|
// before eSplit overwrites the same scratch buffers.
|
||||||
|
val sSplit = Glv.splitScalarInto(sc.splitK1, sc.splitK2, s, sc.splitWide, sc.splitT1, sc.splitT2)
|
||||||
// Build wNAF: G-side uses wider window (cached table), P-side uses w=5
|
|
||||||
Glv.wnafInto(sc.wnaf1, sc.wnafTmp, sSplit.k1, WINDOW_G, 129)
|
Glv.wnafInto(sc.wnaf1, sc.wnafTmp, sSplit.k1, WINDOW_G, 129)
|
||||||
Glv.wnafInto(sc.wnaf2, sc.wnafTmp, sSplit.k2, WINDOW_G, 129)
|
Glv.wnafInto(sc.wnaf2, sc.wnafTmp, sSplit.k2, WINDOW_G, 129)
|
||||||
|
// Now safe to reuse splitK1/K2 for the e scalar
|
||||||
|
val eSplit = Glv.splitScalarInto(sc.splitK1, sc.splitK2, e, sc.splitWide, sc.splitT1, sc.splitT2)
|
||||||
Glv.wnafInto(sc.wnaf3, sc.wnafTmp, eSplit.k1, wP, 129)
|
Glv.wnafInto(sc.wnaf3, sc.wnafTmp, eSplit.k1, wP, 129)
|
||||||
Glv.wnafInto(sc.wnaf4, sc.wnafTmp, eSplit.k2, wP, 129)
|
Glv.wnafInto(sc.wnaf4, sc.wnafTmp, eSplit.k2, wP, 129)
|
||||||
val wnafS1 = sc.wnaf1
|
val wnafS1 = sc.wnaf1
|
||||||
@@ -692,7 +621,18 @@ internal object ECPoint {
|
|||||||
val gOdd = gOddTable
|
val gOdd = gOddTable
|
||||||
val gLam = gLamTable
|
val gLam = gLamTable
|
||||||
|
|
||||||
// P odd-multiples [1P, 3P, 5P, ..., 15P] — uses pre-allocated scratch tables
|
// P-side tables: check cache first, build only on miss.
|
||||||
|
// On cache hit, copies 16 affine points from cache (~trivial vs ~437 field ops to build).
|
||||||
|
val pOdd: Array<AffinePoint>
|
||||||
|
val pLamOdd: Array<AffinePoint>
|
||||||
|
val cacheSlot = cacheSlot(p.x)
|
||||||
|
val cached = pTableCache[cacheSlot]
|
||||||
|
if (cached != null && U256.cmp(cached.px, p.x) == 0) {
|
||||||
|
// Cache hit — use cached affine tables directly (no copy needed)
|
||||||
|
pOdd = cached.pOdd
|
||||||
|
pLamOdd = cached.pLamOdd
|
||||||
|
} else {
|
||||||
|
// Cache miss — build tables and store in cache
|
||||||
doublePoint(sc.p2, p, sc)
|
doublePoint(sc.p2, p, sc)
|
||||||
val pOddJac = sc.pOddJac
|
val pOddJac = sc.pOddJac
|
||||||
pOddJac[0].copyFrom(p)
|
pOddJac[0].copyFrom(p)
|
||||||
@@ -703,11 +643,23 @@ internal object ECPoint {
|
|||||||
pOddJac[i].y.copyInto(pLamOddJac[i].y)
|
pOddJac[i].y.copyInto(pLamOddJac[i].y)
|
||||||
pOddJac[i].z.copyInto(pLamOddJac[i].z)
|
pOddJac[i].z.copyInto(pLamOddJac[i].z)
|
||||||
}
|
}
|
||||||
|
// Batch-convert to affine (into scratch arrays)
|
||||||
|
batchToAffinePair(pOddJac, pLamOddJac, sc.pOddAff, sc.pLamOddAff, sc)
|
||||||
|
|
||||||
// Effective-affine: batch-convert P-side tables (shared Z inversion)
|
// Store in cache (allocate new arrays so they're independent of scratch)
|
||||||
val pOdd = sc.pOddAff
|
val cachedPOdd =
|
||||||
val pLamOdd = sc.pLamOddAff
|
Array(pTableSize) {
|
||||||
batchToAffinePair(pOddJac, pLamOddJac, pOdd, pLamOdd, sc)
|
AffinePoint(sc.pOddAff[it].x.copyOf(), sc.pOddAff[it].y.copyOf())
|
||||||
|
}
|
||||||
|
val cachedPLamOdd =
|
||||||
|
Array(pTableSize) {
|
||||||
|
AffinePoint(sc.pLamOddAff[it].x.copyOf(), sc.pLamOddAff[it].y.copyOf())
|
||||||
|
}
|
||||||
|
pTableCache[cacheSlot] = CachedPTable(p.x.copyOf(), cachedPOdd, cachedPLamOdd)
|
||||||
|
|
||||||
|
pOdd = cachedPOdd
|
||||||
|
pLamOdd = cachedPLamOdd
|
||||||
|
}
|
||||||
|
|
||||||
// Find highest non-zero digit across all 4 streams
|
// Find highest non-zero digit across all 4 streams
|
||||||
var bits = 129 + WINDOW_G // max possible wNAF length
|
var bits = 129 + WINDOW_G // max possible wNAF length
|
||||||
@@ -717,75 +669,75 @@ internal object ECPoint {
|
|||||||
bits--
|
bits--
|
||||||
}
|
}
|
||||||
|
|
||||||
out.setInfinity()
|
// Ping-pong: alternate between out and sc.mixTmp to avoid copyFrom after
|
||||||
val tmp = sc.mixTmp
|
// every addition (~170 copies per verify → at most 1). Also avoids the
|
||||||
|
// internal copy buffer in doublePoint's out===inp path (~130 per verify).
|
||||||
|
var cur = out
|
||||||
|
var alt = sc.mixTmp
|
||||||
|
cur.setInfinity()
|
||||||
val negY = sc.mixNegY
|
val negY = sc.mixNegY
|
||||||
|
|
||||||
for (i in bits - 1 downTo 0) {
|
for (i in bits - 1 downTo 0) {
|
||||||
doublePoint(out, out, sc)
|
doublePoint(alt, cur, sc)
|
||||||
|
var t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
// Streams 1-2: G-side (affine tables, mixed addition)
|
// Streams 1-2: G-side (affine tables, mixed addition)
|
||||||
addWnafMixed(out, tmp, negY, wnafS1, i, gOdd, sSplit.negK1, sc)
|
if (addWnafMixedPP(cur, alt, negY, wnafS1, i, gOdd, sSplit.negK1, sc)) {
|
||||||
addWnafMixed(out, tmp, negY, wnafS2, i, gLam, sSplit.negK2, sc)
|
t = cur
|
||||||
// Streams 3-4: P-side (affine tables via effective-affine, mixed addition)
|
cur = alt
|
||||||
addWnafMixed(out, tmp, negY, wnafE1, i, pOdd, eSplit.negK1, sc)
|
alt = t
|
||||||
addWnafMixed(out, tmp, negY, wnafE2, i, pLamOdd, eSplit.negK2, sc)
|
|
||||||
}
|
}
|
||||||
|
if (addWnafMixedPP(cur, alt, negY, wnafS2, i, gLam, sSplit.negK2, sc)) {
|
||||||
|
t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
|
}
|
||||||
|
// Streams 3-4: P-side (affine tables via effective-affine, mixed addition)
|
||||||
|
if (addWnafMixedPP(cur, alt, negY, wnafE1, i, pOdd, eSplit.negK1, sc)) {
|
||||||
|
t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
|
}
|
||||||
|
if (addWnafMixedPP(cur, alt, negY, wnafE2, i, pLamOdd, eSplit.negK2, sc)) {
|
||||||
|
t = cur
|
||||||
|
cur = alt
|
||||||
|
alt = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cur !== out) out.copyFrom(cur)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process one wNAF digit with mixed addition.
|
* Process one wNAF digit with mixed addition (ping-pong version).
|
||||||
* The effective sign is: (wNAF digit sign) XOR (GLV negation flag).
|
* Reads from `cur`, writes result to `alt`. Returns true if an addition was
|
||||||
* Positive = add as-is, negative = negate the table entry's y.
|
* performed (caller should swap cur/alt references).
|
||||||
|
*
|
||||||
|
* This avoids the copyFrom after every addition — the caller swaps references
|
||||||
|
* instead (free: just local variable reassignment).
|
||||||
*/
|
*/
|
||||||
private fun addWnafMixed(
|
private fun addWnafMixedPP(
|
||||||
out: MutablePoint,
|
cur: MutablePoint,
|
||||||
tmp: MutablePoint,
|
alt: MutablePoint,
|
||||||
negY: LongArray,
|
negY: LongArray,
|
||||||
wnafDigits: IntArray,
|
wnafDigits: IntArray,
|
||||||
bitIndex: Int,
|
bitIndex: Int,
|
||||||
table: Array<AffinePoint>,
|
table: Array<AffinePoint>,
|
||||||
glvNeg: Boolean,
|
glvNeg: Boolean,
|
||||||
s: PointScratch,
|
s: PointScratch,
|
||||||
) {
|
): Boolean {
|
||||||
if (bitIndex >= wnafDigits.size) return
|
if (bitIndex >= wnafDigits.size) return false
|
||||||
val d = wnafDigits[bitIndex]
|
val d = wnafDigits[bitIndex]
|
||||||
if (d == 0) return
|
if (d == 0) return false
|
||||||
val idx = (if (d > 0) d else -d) / 2
|
val idx = (if (d > 0) d else -d) / 2
|
||||||
val effectiveNeg = (d < 0) xor glvNeg
|
val effectiveNeg = (d < 0) xor glvNeg
|
||||||
if (!effectiveNeg) {
|
if (!effectiveNeg) {
|
||||||
addMixed(tmp, out, table[idx].x, table[idx].y, s)
|
addMixed(alt, cur, table[idx].x, table[idx].y, s)
|
||||||
} else {
|
} else {
|
||||||
FieldP.neg(negY, table[idx].y)
|
FieldP.neg(negY, table[idx].y)
|
||||||
addMixed(tmp, out, table[idx].x, negY, s)
|
addMixed(alt, cur, table[idx].x, negY, s)
|
||||||
}
|
}
|
||||||
out.copyFrom(tmp)
|
return true
|
||||||
}
|
|
||||||
|
|
||||||
/** Process one wNAF digit with full Jacobian addition (for P-side tables). */
|
|
||||||
private fun addWnafJacobian(
|
|
||||||
out: MutablePoint,
|
|
||||||
tmp: MutablePoint,
|
|
||||||
negScratch: MutablePoint,
|
|
||||||
wnafDigits: IntArray,
|
|
||||||
bitIndex: Int,
|
|
||||||
table: Array<MutablePoint>,
|
|
||||||
glvNeg: Boolean,
|
|
||||||
s: PointScratch,
|
|
||||||
) {
|
|
||||||
if (bitIndex >= wnafDigits.size) return
|
|
||||||
val d = wnafDigits[bitIndex]
|
|
||||||
if (d == 0) return
|
|
||||||
val idx = (if (d > 0) d else -d) / 2
|
|
||||||
val effectiveNeg = (d < 0) xor glvNeg
|
|
||||||
if (!effectiveNeg) {
|
|
||||||
addPoints(tmp, out, table[idx], s)
|
|
||||||
} else {
|
|
||||||
table[idx].x.copyInto(negScratch.x)
|
|
||||||
FieldP.neg(negScratch.y, table[idx].y)
|
|
||||||
table[idx].z.copyInto(negScratch.z)
|
|
||||||
addPoints(tmp, out, negScratch, s)
|
|
||||||
}
|
|
||||||
out.copyFrom(tmp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Batch Affine Conversion (Montgomery's Trick) ====================
|
// ==================== Batch Affine Conversion (Montgomery's Trick) ====================
|
||||||
@@ -905,11 +857,7 @@ internal object ECPoint {
|
|||||||
|
|
||||||
// ==================== Coordinate Conversion ====================
|
// ==================== Coordinate Conversion ====================
|
||||||
|
|
||||||
/**
|
/** Convert Jacobian → affine (convenience, allocates temps). For one-time init paths. */
|
||||||
* Convert from Jacobian (X, Y, Z) to affine (x, y) = (X/Z², Y/Z³).
|
|
||||||
* Requires one field inversion (the most expensive single operation).
|
|
||||||
* Returns false if the point is at infinity.
|
|
||||||
*/
|
|
||||||
fun toAffine(
|
fun toAffine(
|
||||||
p: MutablePoint,
|
p: MutablePoint,
|
||||||
outX: LongArray,
|
outX: LongArray,
|
||||||
@@ -927,47 +875,32 @@ internal object ECPoint {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Convert Jacobian → affine using pre-allocated scratch (hot path). */
|
||||||
* Convert from Jacobian to affine, returning only the x-coordinate: x = X/Z².
|
fun toAffine(
|
||||||
* Saves 2 multiplications vs full toAffine (no zInv3, no outY computation).
|
|
||||||
* Used by ecdhXOnly where only the x-coordinate of the shared point is needed.
|
|
||||||
*/
|
|
||||||
fun toAffineX(
|
|
||||||
p: MutablePoint,
|
p: MutablePoint,
|
||||||
outX: LongArray,
|
outX: LongArray,
|
||||||
|
outY: LongArray,
|
||||||
|
s: PointScratch,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (p.isInfinity()) return false
|
if (p.isInfinity()) return false
|
||||||
val zInv = LongArray(4)
|
FieldP.inv(s.zInv, p.z)
|
||||||
val zInv2 = LongArray(4)
|
FieldP.sqr(s.zInv2, s.zInv)
|
||||||
FieldP.inv(zInv, p.z)
|
FieldP.mul(s.zInv3, s.zInv2, s.zInv)
|
||||||
FieldP.sqr(zInv2, zInv)
|
FieldP.mul(outX, p.x, s.zInv2)
|
||||||
FieldP.mul(outX, p.x, zInv2)
|
FieldP.mul(outY, p.y, s.zInv3)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Key Encoding (delegates to KeyCodec) ====================
|
/** Convert Jacobian → affine x-only using pre-allocated scratch (hot path). */
|
||||||
|
fun toAffineX(
|
||||||
fun liftX(
|
p: MutablePoint,
|
||||||
outX: LongArray,
|
outX: LongArray,
|
||||||
outY: LongArray,
|
s: PointScratch,
|
||||||
x: LongArray,
|
): Boolean {
|
||||||
) = KeyCodec.liftX(outX, outY, x)
|
if (p.isInfinity()) return false
|
||||||
|
FieldP.inv(s.zInv, p.z)
|
||||||
fun hasEvenY(y: LongArray) = KeyCodec.hasEvenY(y)
|
FieldP.sqr(s.zInv2, s.zInv)
|
||||||
|
FieldP.mul(outX, p.x, s.zInv2)
|
||||||
fun parsePublicKey(
|
return true
|
||||||
pubkey: ByteArray,
|
}
|
||||||
outX: LongArray,
|
|
||||||
outY: LongArray,
|
|
||||||
) = KeyCodec.parsePublicKey(pubkey, outX, outY)
|
|
||||||
|
|
||||||
fun serializeUncompressed(
|
|
||||||
x: LongArray,
|
|
||||||
y: LongArray,
|
|
||||||
) = KeyCodec.serializeUncompressed(x, y)
|
|
||||||
|
|
||||||
fun serializeCompressed(
|
|
||||||
x: LongArray,
|
|
||||||
y: LongArray,
|
|
||||||
) = KeyCodec.serializeCompressed(x, y)
|
|
||||||
}
|
}
|
||||||
@@ -67,27 +67,54 @@ internal object FieldP {
|
|||||||
val carry = U256.addTo(out, a, b)
|
val carry = U256.addTo(out, a, b)
|
||||||
if (carry != 0) {
|
if (carry != 0) {
|
||||||
// Overflow past 2^256: add 2^256 mod p = 2^32 + 977 = 0x1000003D1
|
// Overflow past 2^256: add 2^256 mod p = 2^32 + 977 = 0x1000003D1
|
||||||
// This fits in 33 bits. Add to limb[0] with carry propagation.
|
val s1 = out[0] + 4294968273L
|
||||||
val s1 = out[0] + 4294968273L // 2^32 + 977
|
|
||||||
val c1 = if (s1.toULong() < out[0].toULong()) 1L else 0L
|
val c1 = if (s1.toULong() < out[0].toULong()) 1L else 0L
|
||||||
out[0] = s1
|
out[0] = s1
|
||||||
if (c1 != 0L) {
|
if (c1 != 0L) {
|
||||||
for (i in 1 until 4) {
|
out[1]++
|
||||||
out[i]++
|
if (out[1] == 0L) {
|
||||||
if (out[i] != 0L) break
|
out[2]++
|
||||||
|
if (out[2] == 0L) out[3]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reduceSelf(out)
|
reduceSelf(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* out = a - b mod p. Specialized add-back for P = [P0, -1, -1, -1]:
|
||||||
|
* adding -1 to limbs 1-3 with carry=1 is identity, so only the carry=0
|
||||||
|
* case needs work (subtract 1 with borrow propagation). ~500 calls/verify.
|
||||||
|
*/
|
||||||
fun sub(
|
fun sub(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
b: LongArray,
|
b: LongArray,
|
||||||
) {
|
) {
|
||||||
val borrow = U256.subTo(out, a, b)
|
val borrow = U256.subTo(out, a, b)
|
||||||
if (borrow != 0) U256.addTo(out, out, P)
|
if (borrow != 0) {
|
||||||
|
// Add P = [P0, -1, -1, -1].
|
||||||
|
val s0 = out[0] + P0
|
||||||
|
val c0 = if (s0.toULong() < out[0].toULong()) 1L else 0L
|
||||||
|
out[0] = s0
|
||||||
|
// For limbs 1-3: adding P[i]=-1 with carry c:
|
||||||
|
// c=1 → result unchanged, carry out=1 (identity propagation)
|
||||||
|
// c=0 → result = out[i]-1, carry out = (out[i] != 0) ? 1 : 0
|
||||||
|
// So if c0=1, limbs 1-3 are untouched. If c0=0, subtract 1 with borrow:
|
||||||
|
if (c0 == 0L) {
|
||||||
|
if (out[1] != 0L) {
|
||||||
|
out[1]--
|
||||||
|
} else {
|
||||||
|
out[1] = -1L // 0-1 wraps
|
||||||
|
if (out[2] != 0L) {
|
||||||
|
out[2]--
|
||||||
|
} else {
|
||||||
|
out[2] = -1L
|
||||||
|
out[3]--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Multiply with ThreadLocal wide buffer (convenience for non-hot paths). */
|
/** Multiply with ThreadLocal wide buffer (convenience for non-hot paths). */
|
||||||
@@ -132,39 +159,80 @@ internal object FieldP {
|
|||||||
reduceWide(out, w)
|
reduceWide(out, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* out = -a mod p = P - a. Specialized for P = [P0, -1, -1, -1]:
|
||||||
|
* P[i]-a[i] = ~a[i] for i>=1 (bitwise NOT), with borrow from limb 0.
|
||||||
|
* Avoids generic U256.subTo + P array reads (~260 calls/verify).
|
||||||
|
*/
|
||||||
fun neg(
|
fun neg(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
) {
|
) {
|
||||||
if (U256.isZero(a)) {
|
if (U256.isZero(a)) {
|
||||||
for (i in 0 until 4) out[i] = 0L
|
out[0] = 0L
|
||||||
} else {
|
out[1] = 0L
|
||||||
U256.subTo(out, P, a)
|
out[2] = 0L
|
||||||
|
out[3] = 0L
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
// P - a: limb 0 is P0 - a[0], limbs 1-3 are (-1) - a[i] = ~a[i]
|
||||||
|
out[0] = P0 - a[0]
|
||||||
|
val borrow = if (a[0].toULong() > P0.toULong()) 1L else 0L
|
||||||
|
// ~a[i] - borrow. New borrow only if ~a[i] == 0 (i.e., a[i] == -1) and borrow == 1
|
||||||
|
out[1] = a[1].inv() - borrow
|
||||||
|
val b1 = if (a[1] == -1L && borrow != 0L) 1L else 0L
|
||||||
|
out[2] = a[2].inv() - b1
|
||||||
|
val b2 = if (a[2] == -1L && b1 != 0L) 1L else 0L
|
||||||
|
out[3] = a[3].inv() - b2
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* out = a / 2 mod p. Branchless: if odd, add p first (p is odd → a+p is even).
|
* out = a / 2 mod p. Branchless: if odd, add p first (p is odd → a+p is even).
|
||||||
|
* Unrolled, with P[1..3]=-1 inlined as `mask` (since -1 & mask = mask).
|
||||||
*/
|
*/
|
||||||
fun half(
|
fun half(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
) {
|
) {
|
||||||
val mask = -(a[0] and 1L) // all 1s if odd, all 0s if even
|
val mask = -(a[0] and 1L) // all 1s if odd, all 0s if even
|
||||||
var carry = 0L
|
val p0 = P0 and mask // P[0] masked; P[1..3] are -1, so P[i]&mask = mask
|
||||||
for (i in 0 until 4) {
|
var s1: Long
|
||||||
val pMasked = P[i] and mask
|
var s2: Long
|
||||||
val s1 = a[i] + pMasked
|
var c1: Long
|
||||||
val c1 = if (s1.toULong() < a[i].toULong()) 1L else 0L
|
var c2: Long
|
||||||
val s2 = s1 + carry
|
|
||||||
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
// Conditional add: out = a + (P & mask), unrolled
|
||||||
out[i] = s2
|
// Limb 0
|
||||||
|
s1 = a[0] + p0
|
||||||
|
c1 = if (s1.toULong() < a[0].toULong()) 1L else 0L
|
||||||
|
out[0] = s1
|
||||||
|
var carry = c1
|
||||||
|
// Limb 1
|
||||||
|
s1 = a[1] + mask
|
||||||
|
c1 = if (s1.toULong() < a[1].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[1] = s2
|
||||||
carry = c1 + c2
|
carry = c1 + c2
|
||||||
}
|
// Limb 2
|
||||||
// Right-shift by 1
|
s1 = a[2] + mask
|
||||||
for (i in 0 until 3) {
|
c1 = if (s1.toULong() < a[2].toULong()) 1L else 0L
|
||||||
out[i] = (out[i] ushr 1) or (out[i + 1] shl 63)
|
s2 = s1 + carry
|
||||||
}
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[2] = s2
|
||||||
|
carry = c1 + c2
|
||||||
|
// Limb 3
|
||||||
|
s1 = a[3] + mask
|
||||||
|
c1 = if (s1.toULong() < a[3].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[3] = s2
|
||||||
|
carry = c1 + c2
|
||||||
|
|
||||||
|
// Right-shift by 1 (unrolled)
|
||||||
|
out[0] = (out[0] ushr 1) or (out[1] shl 63)
|
||||||
|
out[1] = (out[1] ushr 1) or (out[2] shl 63)
|
||||||
|
out[2] = (out[2] ushr 1) or (out[3] shl 63)
|
||||||
out[3] = (out[3] ushr 1) or (carry shl 63)
|
out[3] = (out[3] ushr 1) or (carry shl 63)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,81 +366,122 @@ internal object FieldP {
|
|||||||
|
|
||||||
// ==================== Reduction ====================
|
// ==================== Reduction ====================
|
||||||
|
|
||||||
|
// P[0] cached as a constant to avoid array load in the hot reduceSelf path.
|
||||||
|
private const val P0 = -4294968273L // 0xFFFFFFFEFFFFFC2F
|
||||||
|
|
||||||
fun reduceSelf(a: LongArray) {
|
fun reduceSelf(a: LongArray) {
|
||||||
// Exploit P's structure: P = [P0, -1, -1, -1] where P[1..3] = 0xFFFFFFFFFFFFFFFF.
|
// Exploit P's structure: P = [P0, -1, -1, -1] where P[1..3] = 0xFFFFFFFFFFFFFFFF.
|
||||||
// a >= P only if a[3]==a[2]==a[1]==-1 AND a[0] >= P[0]. The first check (a[3]==-1)
|
// a >= P only if a[3]==a[2]==a[1]==-1 AND a[0] >= P[0]. The first check (a[3]==-1)
|
||||||
// fails >99.99% of the time for random field elements, making this a single branch.
|
// fails >99.99% of the time for random field elements, making this a single branch.
|
||||||
if (a[3] == -1L && a[2] == -1L && a[1] == -1L &&
|
if (a[3] == -1L && a[2] == -1L && a[1] == -1L &&
|
||||||
(a[0] xor Long.MIN_VALUE) >= (P[0] xor Long.MIN_VALUE)
|
(a[0] xor Long.MIN_VALUE) >= (P0 xor Long.MIN_VALUE)
|
||||||
) {
|
) {
|
||||||
U256.subTo(a, a, P)
|
// Inline P subtraction: when a[1..3] = -1 and a[0] >= P0,
|
||||||
|
// a - P = [a[0] - P0, 0, 0, 0] (no borrows since P[1..3] = -1).
|
||||||
|
a[0] -= P0
|
||||||
|
a[1] = 0L
|
||||||
|
a[2] = 0L
|
||||||
|
a[3] = 0L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduce 512-bit value mod p.
|
* Reduce 512-bit value mod p. Fully unrolled for ART JIT.
|
||||||
*
|
*
|
||||||
* Uses hi × 2^256 ≡ hi × C (mod p) where C = 2^32 + 977 = 4294968273.
|
* Uses hi × 2^256 ≡ hi × C (mod p) where C = 2^32 + 977 = 4294968273.
|
||||||
* Since C < 2^33, hi[i] × C fits in 97 bits. We use unsignedMultiplyHigh
|
* Three stages: fold 512→~260 bits, fold carry×C, final reduceSelf.
|
||||||
* to get the upper 64 bits of each limb×C product.
|
|
||||||
*
|
|
||||||
* Three stages:
|
|
||||||
* 1. Fold 512→~260 bits: lo + hi × C, producing at most ~34-bit carry
|
|
||||||
* 2. Fold carry × C back into limb[0..3]; propagate carries (may overflow 256 bits)
|
|
||||||
* 3. If round 2 overflowed, fold the single-bit overflow (≡ C) once more
|
|
||||||
* Final reduceSelf handles the at-most-one subtraction of p.
|
|
||||||
*/
|
*/
|
||||||
fun reduceWide(
|
fun reduceWide(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
w: LongArray,
|
w: LongArray,
|
||||||
) {
|
) {
|
||||||
// Round 1: acc = lo + hi × C
|
|
||||||
val c = 4294968273L // 2^32 + 977
|
val c = 4294968273L // 2^32 + 977
|
||||||
var carry = 0L
|
var hcLo: Long
|
||||||
for (i in 0 until 4) {
|
var hcHi: Long
|
||||||
val hcLo = w[i + 4] * c
|
var s1: Long
|
||||||
val hcHi = unsignedMultiplyHigh(w[i + 4], c)
|
var s2: Long
|
||||||
|
var c1: Long
|
||||||
|
var c2: Long
|
||||||
|
|
||||||
// acc = w[i] + hcLo + carry
|
// Round 1: acc = lo + hi × C (4 limbs, unrolled)
|
||||||
val s1 = w[i] + hcLo
|
|
||||||
val c1 = if (s1.toULong() < w[i].toULong()) 1L else 0L
|
// Limb 0 (no carry input)
|
||||||
val s2 = s1 + carry
|
hcLo = w[4] * c
|
||||||
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
hcHi = unsignedMultiplyHigh(w[4], c)
|
||||||
out[i] = s2
|
s1 = w[0] + hcLo
|
||||||
|
c1 = if (s1.toULong() < w[0].toULong()) 1L else 0L
|
||||||
|
out[0] = s1
|
||||||
|
var carry = hcHi + c1
|
||||||
|
|
||||||
|
// Limb 1
|
||||||
|
hcLo = w[5] * c
|
||||||
|
hcHi = unsignedMultiplyHigh(w[5], c)
|
||||||
|
s1 = w[1] + hcLo
|
||||||
|
c1 = if (s1.toULong() < w[1].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[1] = s2
|
||||||
|
carry = hcHi + c1 + c2
|
||||||
|
|
||||||
|
// Limb 2
|
||||||
|
hcLo = w[6] * c
|
||||||
|
hcHi = unsignedMultiplyHigh(w[6], c)
|
||||||
|
s1 = w[2] + hcLo
|
||||||
|
c1 = if (s1.toULong() < w[2].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[2] = s2
|
||||||
|
carry = hcHi + c1 + c2
|
||||||
|
|
||||||
|
// Limb 3
|
||||||
|
hcLo = w[7] * c
|
||||||
|
hcHi = unsignedMultiplyHigh(w[7], c)
|
||||||
|
s1 = w[3] + hcLo
|
||||||
|
c1 = if (s1.toULong() < w[3].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[3] = s2
|
||||||
carry = hcHi + c1 + c2
|
carry = hcHi + c1 + c2
|
||||||
}
|
|
||||||
|
|
||||||
// Round 2: if carry > 0, fold carry × C back in
|
// Round 2: if carry > 0, fold carry × C back in
|
||||||
if (carry != 0L) {
|
if (carry != 0L) {
|
||||||
val ccLo = carry * c
|
val ccLo = carry * c
|
||||||
val ccHi = unsignedMultiplyHigh(carry, c)
|
val ccHi = unsignedMultiplyHigh(carry, c)
|
||||||
val s1 = out[0] + ccLo
|
s1 = out[0] + ccLo
|
||||||
val c1 = if (s1.toULong() < out[0].toULong()) 1L else 0L
|
c1 = if (s1.toULong() < out[0].toULong()) 1L else 0L
|
||||||
out[0] = s1
|
out[0] = s1
|
||||||
|
// Propagate carry (unrolled, with early exit)
|
||||||
var prop = ccHi + c1
|
var prop = ccHi + c1
|
||||||
for (i in 1 until 4) {
|
|
||||||
if (prop == 0L) break
|
|
||||||
val s = out[i] + prop
|
|
||||||
prop = if (s.toULong() < out[i].toULong()) 1L else 0L
|
|
||||||
out[i] = s
|
|
||||||
}
|
|
||||||
// Round 2 carry propagation may overflow past 256 bits.
|
|
||||||
// This happens when out[0..3] were all 0xFF..FF and the add cascades.
|
|
||||||
// Overflow of 1 means 2^256 ≡ C (mod p), so add C to out[0..3].
|
|
||||||
if (prop != 0L) {
|
if (prop != 0L) {
|
||||||
val s2 = out[0] + c
|
s1 = out[1] + prop
|
||||||
val c2 = if (s2.toULong() < out[0].toULong()) 1L else 0L
|
prop = if (s1.toULong() < out[1].toULong()) 1L else 0L
|
||||||
out[0] = s2
|
out[1] = s1
|
||||||
if (c2 != 0L) {
|
if (prop != 0L) {
|
||||||
for (i in 1 until 4) {
|
s1 = out[2] + prop
|
||||||
out[i]++
|
prop = if (s1.toULong() < out[2].toULong()) 1L else 0L
|
||||||
if (out[i] != 0L) break
|
out[2] = s1
|
||||||
|
if (prop != 0L) {
|
||||||
|
s1 = out[3] + prop
|
||||||
|
prop = if (s1.toULong() < out[3].toULong()) 1L else 0L
|
||||||
|
out[3] = s1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Overflow past 256 bits: 2^256 ≡ C (mod p)
|
||||||
|
if (prop != 0L) {
|
||||||
|
s1 = out[0] + c
|
||||||
|
c1 = if (s1.toULong() < out[0].toULong()) 1L else 0L
|
||||||
|
out[0] = s1
|
||||||
|
if (c1 != 0L) {
|
||||||
|
out[1]++
|
||||||
|
if (out[1] == 0L) {
|
||||||
|
out[2]++
|
||||||
|
if (out[2] == 0L) out[3]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final: at most one subtraction of p
|
|
||||||
reduceSelf(out)
|
reduceSelf(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,58 @@ internal object Glv {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocation-free splitScalar using pre-allocated scratch from PointScratch.
|
||||||
|
* Eliminates ~26 LongArray allocations per call (called 2× per verify = ~52 allocs saved).
|
||||||
|
*/
|
||||||
|
fun splitScalarInto(
|
||||||
|
outK1: LongArray,
|
||||||
|
outK2: LongArray,
|
||||||
|
k: LongArray,
|
||||||
|
w: LongArray,
|
||||||
|
t1: LongArray,
|
||||||
|
t2: LongArray,
|
||||||
|
): Split {
|
||||||
|
// c1 = mulShift384(k, G1)
|
||||||
|
mulShift384Into(t1, k, G1, w)
|
||||||
|
// c2 = mulShift384(k, G2)
|
||||||
|
mulShift384Into(t2, k, G2, w)
|
||||||
|
|
||||||
|
// r2 = add(mul(c1, MINUS_B1), mul(c2, MINUS_B2))
|
||||||
|
// Use outK1 as temp for mul(c1, MINUS_B1), outK2 as temp for mul(c2, MINUS_B2)
|
||||||
|
ScalarN.mulTo(outK1, t1, MINUS_B1, w)
|
||||||
|
ScalarN.mulTo(outK2, t2, MINUS_B2, w)
|
||||||
|
ScalarN.addTo(outK2, outK1, outK2) // outK2 = r2
|
||||||
|
|
||||||
|
// r1 = add(mul(r2, MINUS_LAMBDA), k)
|
||||||
|
ScalarN.mulTo(outK1, outK2, MINUS_LAMBDA, w)
|
||||||
|
ScalarN.addTo(outK1, outK1, k) // outK1 = r1
|
||||||
|
|
||||||
|
val neg1 = U256.cmp(outK1, N_HALF) > 0
|
||||||
|
val neg2 = U256.cmp(outK2, N_HALF) > 0
|
||||||
|
if (neg1) ScalarN.negTo(outK1, outK1)
|
||||||
|
if (neg2) ScalarN.negTo(outK2, outK2)
|
||||||
|
return Split(outK1, outK2, neg1, neg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Allocation-free mulShift384. */
|
||||||
|
private fun mulShift384Into(
|
||||||
|
out: LongArray,
|
||||||
|
k: LongArray,
|
||||||
|
g: LongArray,
|
||||||
|
w: LongArray,
|
||||||
|
) {
|
||||||
|
U256.mulWide(w, k, g)
|
||||||
|
out[0] = w[6]
|
||||||
|
out[1] = w[7]
|
||||||
|
out[2] = 0L
|
||||||
|
out[3] = 0L
|
||||||
|
if (w[5] < 0) { // bit 63 of w[5] = bit 383 (rounding)
|
||||||
|
out[0]++
|
||||||
|
if (out[0] == 0L) out[1]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== wNAF Encoding ====================
|
// ==================== wNAF Encoding ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -61,6 +61,23 @@ internal object KeyCodec {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** liftX with caller-provided temp buffer (avoids 1 LongArray alloc). */
|
||||||
|
fun liftX(
|
||||||
|
outX: LongArray,
|
||||||
|
outY: LongArray,
|
||||||
|
x: LongArray,
|
||||||
|
tmp: LongArray,
|
||||||
|
): Boolean {
|
||||||
|
if (U256.cmp(x, FieldP.P) >= 0) return false
|
||||||
|
FieldP.sqr(tmp, x)
|
||||||
|
FieldP.mul(tmp, tmp, x)
|
||||||
|
FieldP.add(tmp, tmp, B)
|
||||||
|
if (!FieldP.sqrt(outY, tmp)) return false
|
||||||
|
U256.copyInto(outX, x)
|
||||||
|
if (outY[0] and 1L != 0L) FieldP.neg(outY, outY)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/** Check if y-coordinate is even (LSB = 0). */
|
/** Check if y-coordinate is even (LSB = 0). */
|
||||||
fun hasEvenY(y: LongArray): Boolean = y[0] and 1L == 0L
|
fun hasEvenY(y: LongArray): Boolean = y[0] and 1L == 0L
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -45,12 +45,28 @@ internal expect fun unsignedMultiplyHigh(
|
|||||||
): Long
|
): Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback: unsigned multiply high from signed multiply high + correction.
|
* Fallback: unsigned multiply high computed directly from 32-bit sub-products.
|
||||||
|
*
|
||||||
|
* Unlike the old approach (signed multiplyHigh + correction), this computes the
|
||||||
|
* unsigned result directly, avoiding the signed correction branches (if a < 0,
|
||||||
|
* if b < 0) and the unsigned correction terms (+ (a & (b >> 63)) + (b & (a >> 63))).
|
||||||
|
* Saves ~8 instructions per call on Android < API 31, where this is the hot path
|
||||||
|
* (~30,000 calls per signature verify).
|
||||||
*/
|
*/
|
||||||
internal fun unsignedMultiplyHighFallback(
|
internal fun unsignedMultiplyHighFallback(
|
||||||
a: Long,
|
a: Long,
|
||||||
b: Long,
|
b: Long,
|
||||||
): Long = multiplyHigh(a, b) + (a and (b shr 63)) + (b and (a shr 63))
|
): Long {
|
||||||
|
val aLo = a and 0xFFFFFFFFL
|
||||||
|
val aHi = a ushr 32
|
||||||
|
val bLo = b and 0xFFFFFFFFL
|
||||||
|
val bHi = b ushr 32
|
||||||
|
val mid1 = aHi * bLo
|
||||||
|
val mid2 = aLo * bHi
|
||||||
|
val low = aLo * bLo
|
||||||
|
val carry = ((low ushr 32) + (mid1 and 0xFFFFFFFFL) + (mid2 and 0xFFFFFFFFL)) ushr 32
|
||||||
|
return (aHi * bHi) + (mid1 ushr 32) + (mid2 ushr 32) + carry
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pure-Kotlin fallback for multiplyHigh, using four 32-bit sub-products.
|
* Pure-Kotlin fallback for multiplyHigh, using four 32-bit sub-products.
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.utils.secp256k1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutable Jacobian point for in-place computation.
|
||||||
|
*
|
||||||
|
* Points are stored in Jacobian projective coordinates (X, Y, Z) which represent
|
||||||
|
* the affine point (X/Z², Y/Z³). This avoids expensive field inversions during
|
||||||
|
* intermediate steps — inversion is only needed once at the end to convert back
|
||||||
|
* to affine (x, y) form.
|
||||||
|
*
|
||||||
|
* The "point at infinity" (identity element) is represented by Z = 0.
|
||||||
|
*
|
||||||
|
* Mutable to avoid allocating new objects during the inner loop of scalar
|
||||||
|
* multiplication, which performs thousands of doublings and additions per operation.
|
||||||
|
*/
|
||||||
|
internal class MutablePoint(
|
||||||
|
val x: LongArray = LongArray(4),
|
||||||
|
val y: LongArray = LongArray(4),
|
||||||
|
val z: LongArray = LongArray(4),
|
||||||
|
) {
|
||||||
|
fun isInfinity(): Boolean = U256.isZero(z)
|
||||||
|
|
||||||
|
fun setInfinity() {
|
||||||
|
for (i in 0 until 4) {
|
||||||
|
x[i] = 0L
|
||||||
|
z[i] = 0L
|
||||||
|
}
|
||||||
|
y[0] = 1L
|
||||||
|
for (i in 1 until 4) y[i] = 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyFrom(other: MutablePoint) {
|
||||||
|
other.x.copyInto(x)
|
||||||
|
other.y.copyInto(y)
|
||||||
|
other.z.copyInto(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAffine(
|
||||||
|
ax: LongArray,
|
||||||
|
ay: LongArray,
|
||||||
|
) {
|
||||||
|
ax.copyInto(x)
|
||||||
|
ay.copyInto(y)
|
||||||
|
z[0] = 1L
|
||||||
|
for (i in 1 until 4) z[i] = 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affine point (x, y) — no Z coordinate.
|
||||||
|
* Used for precomputed tables where we want compact storage and mixed addition.
|
||||||
|
*/
|
||||||
|
internal class AffinePoint(
|
||||||
|
val x: LongArray = LongArray(4),
|
||||||
|
val y: LongArray = LongArray(4),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-allocated scratch space for point operations. Each thread gets its own
|
||||||
|
* instance via [ScratchLocal] to avoid allocation and ThreadLocal lookups in the
|
||||||
|
* inner loops of scalar multiplication.
|
||||||
|
*
|
||||||
|
* The 12 temp buffers (t[0]..t[11]) are shared across doublePoint and addPoints —
|
||||||
|
* this is safe because these functions only call each other in the equal-point
|
||||||
|
* degenerate case, which returns immediately after the recursive call without
|
||||||
|
* using the temps further.
|
||||||
|
*
|
||||||
|
* The wide buffer (LongArray(8)) is pre-fetched once per top-level operation and
|
||||||
|
* passed through to FieldP.mul/sqr, avoiding ~500+ ThreadLocal.get() calls per
|
||||||
|
* scalar multiplication (~20-30ns each on JVM).
|
||||||
|
*/
|
||||||
|
internal class PointScratch {
|
||||||
|
val t = Array(12) { LongArray(4) }
|
||||||
|
val dblCopy = MutablePoint() // Copy buffer for in-place doubling (out === input)
|
||||||
|
val w = LongArray(8) // Wide buffer for FieldP.mul/sqr — shared, avoids ThreadLocal
|
||||||
|
|
||||||
|
// Pre-allocated scratch for wNAF encoding (avoids IntArray allocation per call).
|
||||||
|
// Size 145 = 129 (max bits after GLV split) + 15 (max window) + 1 (headroom).
|
||||||
|
val wnaf1 = IntArray(145)
|
||||||
|
val wnaf2 = IntArray(145)
|
||||||
|
val wnaf3 = IntArray(145) // mulDoubleG needs 4 wNAF arrays
|
||||||
|
val wnaf4 = IntArray(145)
|
||||||
|
val wnafTmp = LongArray(4) // scratch for wnaf scalar copy (GLV scalars are up to 4 limbs)
|
||||||
|
|
||||||
|
// Pre-allocated scratch for wNAF mixed addition
|
||||||
|
val mixTmp = MutablePoint()
|
||||||
|
val mixNegY = LongArray(4)
|
||||||
|
|
||||||
|
// Pre-allocated P-side tables for mul/mulDoubleG (avoids ~80 LongArray allocs per call)
|
||||||
|
val pOddJac = Array(8) { MutablePoint() }
|
||||||
|
val pLamOddJac = Array(8) { MutablePoint() }
|
||||||
|
val pOddAff = Array(8) { AffinePoint() }
|
||||||
|
val pLamOddAff = Array(8) { AffinePoint() }
|
||||||
|
val p2 = MutablePoint() // doublePoint temp for table building
|
||||||
|
|
||||||
|
// Pre-allocated batch inversion temps (avoids 12 LongArray allocs per call)
|
||||||
|
val cumZ = Array(8) { LongArray(4) }
|
||||||
|
val batchInv = LongArray(4)
|
||||||
|
val batchZInv = LongArray(4)
|
||||||
|
val batchZInv2 = LongArray(4)
|
||||||
|
val batchZInv3 = LongArray(4)
|
||||||
|
|
||||||
|
// Pre-allocated scratch for Glv.splitScalar (avoids ~26 LongArray allocs per call)
|
||||||
|
val splitWide = LongArray(8) // mulShift384 and ScalarN.mulTo scratch
|
||||||
|
val splitT1 = LongArray(4) // temporary for mul results
|
||||||
|
val splitT2 = LongArray(4) // temporary for mul results
|
||||||
|
val splitK1 = LongArray(4) // output k1
|
||||||
|
val splitK2 = LongArray(4) // output k2
|
||||||
|
|
||||||
|
// Pre-allocated scratch for toAffine / toAffineX (avoids 3 LongArray allocs per call)
|
||||||
|
val zInv = LongArray(4)
|
||||||
|
val zInv2 = LongArray(4)
|
||||||
|
val zInv3 = LongArray(4)
|
||||||
|
|
||||||
|
// Pre-allocated scratch for Secp256k1 entry points (avoids per-call allocations)
|
||||||
|
val entryPx = LongArray(4) // liftX / parsePublicKey output
|
||||||
|
val entryPy = LongArray(4)
|
||||||
|
val entryPoint = MutablePoint() // pubkeyCreate, signSchnorr, ecdhXOnly
|
||||||
|
val entryResult = MutablePoint() // mulG / mul output
|
||||||
|
val entryTmp = LongArray(4) // liftX temp, auxrand XOR, nonce, etc.
|
||||||
|
val entryTmp2 = LongArray(4) // secondary temp for signSchnorr R-point
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ package com.vitorpamplona.quartz.utils.secp256k1
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Arithmetic modulo the secp256k1 group order n using LongArray(4) limbs.
|
* Arithmetic modulo the secp256k1 group order n using LongArray(4) limbs.
|
||||||
|
*
|
||||||
|
* Provides both allocating (convenience) and in-place (hot-path) variants.
|
||||||
|
* The in-place variants write results to caller-provided output arrays, avoiding
|
||||||
|
* allocation in the inner loops of scalar multiplication and GLV decomposition.
|
||||||
*/
|
*/
|
||||||
internal object ScalarN {
|
internal object ScalarN {
|
||||||
val N =
|
val N =
|
||||||
@@ -64,12 +68,21 @@ internal object ScalarN {
|
|||||||
b: LongArray,
|
b: LongArray,
|
||||||
): LongArray {
|
): LongArray {
|
||||||
val r = LongArray(4)
|
val r = LongArray(4)
|
||||||
val carry = U256.addTo(r, a, b)
|
addTo(r, a, b)
|
||||||
if (carry != 0) U256.addTo(r, r, N_COMPLEMENT)
|
|
||||||
reduceSelf(r)
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** In-place add: out = (a + b) mod n. */
|
||||||
|
fun addTo(
|
||||||
|
out: LongArray,
|
||||||
|
a: LongArray,
|
||||||
|
b: LongArray,
|
||||||
|
) {
|
||||||
|
val carry = U256.addTo(out, a, b)
|
||||||
|
if (carry != 0) U256.addTo(out, out, N_COMPLEMENT)
|
||||||
|
reduceSelf(out)
|
||||||
|
}
|
||||||
|
|
||||||
fun sub(
|
fun sub(
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
b: LongArray,
|
b: LongArray,
|
||||||
@@ -89,6 +102,17 @@ internal object ScalarN {
|
|||||||
return reduceWide(w)
|
return reduceWide(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** In-place multiply: out = (a * b) mod n. Uses caller-provided wide buffer. */
|
||||||
|
fun mulTo(
|
||||||
|
out: LongArray,
|
||||||
|
a: LongArray,
|
||||||
|
b: LongArray,
|
||||||
|
w: LongArray,
|
||||||
|
) {
|
||||||
|
U256.mulWide(w, a, b)
|
||||||
|
reduceWideTo(out, w)
|
||||||
|
}
|
||||||
|
|
||||||
fun neg(a: LongArray): LongArray {
|
fun neg(a: LongArray): LongArray {
|
||||||
if (U256.isZero(a)) return LongArray(4)
|
if (U256.isZero(a)) return LongArray(4)
|
||||||
val r = LongArray(4)
|
val r = LongArray(4)
|
||||||
@@ -96,6 +120,18 @@ internal object ScalarN {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** In-place negate: out = (-a) mod n. Safe for out === a. */
|
||||||
|
fun negTo(
|
||||||
|
out: LongArray,
|
||||||
|
a: LongArray,
|
||||||
|
) {
|
||||||
|
if (U256.isZero(a)) {
|
||||||
|
for (i in 0 until 4) out[i] = 0L
|
||||||
|
} else {
|
||||||
|
U256.subTo(out, N, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun inv(a: LongArray): LongArray {
|
fun inv(a: LongArray): LongArray {
|
||||||
require(!U256.isZero(a))
|
require(!U256.isZero(a))
|
||||||
return powModN(a, N_MINUS_2)
|
return powModN(a, N_MINUS_2)
|
||||||
@@ -106,87 +142,128 @@ internal object ScalarN {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduce 512-bit product mod n.
|
* Reduce 512-bit product mod n (allocating version).
|
||||||
* Uses hi × 2^256 ≡ hi × N_COMPLEMENT (mod n). N_COMPLEMENT is ~129 bits.
|
* Uses hi × 2^256 ≡ hi × N_COMPLEMENT (mod n). N_COMPLEMENT is ~129 bits.
|
||||||
*/
|
*/
|
||||||
private fun reduceWide(w: LongArray): LongArray {
|
private fun reduceWide(w: LongArray): LongArray {
|
||||||
val lo = LongArray(4)
|
val result = LongArray(4)
|
||||||
val hi = LongArray(4)
|
reduceWideTo(result, w)
|
||||||
for (i in 0 until 4) {
|
return result
|
||||||
lo[i] = w[i]
|
|
||||||
hi[i] = w[i + 4]
|
|
||||||
}
|
}
|
||||||
if (U256.isZero(hi)) {
|
|
||||||
reduceSelf(lo)
|
/**
|
||||||
return lo
|
* Reduce 512-bit product mod n into caller-provided output.
|
||||||
|
* Reuses the wide buffer w as scratch (caller must not need it after this call).
|
||||||
|
*/
|
||||||
|
private fun reduceWideTo(
|
||||||
|
out: LongArray,
|
||||||
|
w: LongArray,
|
||||||
|
) {
|
||||||
|
// Split into lo (w[0..3]) and hi (w[4..7])
|
||||||
|
val hasHi = w[4] != 0L || w[5] != 0L || w[6] != 0L || w[7] != 0L
|
||||||
|
if (!hasHi) {
|
||||||
|
for (i in 0 until 4) out[i] = w[i]
|
||||||
|
reduceSelf(out)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round 1: lo + hi × N_COMPLEMENT
|
// Round 1: lo + hi × N_COMPLEMENT
|
||||||
val hiTimesNC = LongArray(8)
|
// We reuse w[0..7] as scratch for hiTimesNC by saving lo first
|
||||||
U256.mulWide(hiTimesNC, hi, N_COMPLEMENT)
|
val lo0 = w[0]
|
||||||
val sum = LongArray(8)
|
val lo1 = w[1]
|
||||||
|
val lo2 = w[2]
|
||||||
|
val lo3 = w[3]
|
||||||
|
// Use `out` as temporary storage for hi limbs (avoids longArrayOf allocation)
|
||||||
|
out[0] = w[4]
|
||||||
|
out[1] = w[5]
|
||||||
|
out[2] = w[6]
|
||||||
|
out[3] = w[7]
|
||||||
|
|
||||||
|
val hiTimesNC = w // reuse w as scratch
|
||||||
|
U256.mulWide(hiTimesNC, out, N_COMPLEMENT)
|
||||||
|
|
||||||
|
// sum = hiTimesNC + lo
|
||||||
var carry = 0L
|
var carry = 0L
|
||||||
for (i in 0 until 8) {
|
for (i in 0 until 8) {
|
||||||
val s1 = hiTimesNC[i] + if (i < 4) lo[i] else 0L
|
val loVal =
|
||||||
|
if (i == 0) {
|
||||||
|
lo0
|
||||||
|
} else if (i == 1) {
|
||||||
|
lo1
|
||||||
|
} else if (i == 2) {
|
||||||
|
lo2
|
||||||
|
} else if (i == 3) {
|
||||||
|
lo3
|
||||||
|
} else {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
val s1 = hiTimesNC[i] + loVal
|
||||||
val c1 = if (s1.toULong() < hiTimesNC[i].toULong()) 1L else 0L
|
val c1 = if (s1.toULong() < hiTimesNC[i].toULong()) 1L else 0L
|
||||||
val s2 = s1 + carry
|
val s2 = s1 + carry
|
||||||
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
sum[i] = s2
|
w[i] = s2
|
||||||
carry = c1 + c2
|
carry = c1 + c2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round 2 if still > 256 bits
|
// Check if round 2 needed
|
||||||
val lo2 = LongArray(4)
|
val hasHi2 = w[4] != 0L || w[5] != 0L || w[6] != 0L || w[7] != 0L
|
||||||
val hi2 = LongArray(4)
|
if (!hasHi2) {
|
||||||
for (i in 0 until 4) {
|
for (i in 0 until 4) out[i] = w[i]
|
||||||
lo2[i] = sum[i]
|
reduceSelf(out)
|
||||||
hi2[i] = sum[i + 4]
|
return
|
||||||
}
|
|
||||||
if (U256.isZero(hi2)) {
|
|
||||||
reduceSelf(lo2)
|
|
||||||
return lo2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val hi2NC = LongArray(8)
|
// Round 2: reuse out for hi2 limbs (avoids longArrayOf allocation)
|
||||||
U256.mulWide(hi2NC, hi2, N_COMPLEMENT)
|
out[0] = w[4]
|
||||||
|
out[1] = w[5]
|
||||||
|
out[2] = w[6]
|
||||||
|
out[3] = w[7]
|
||||||
|
val saved0 = w[0]
|
||||||
|
val saved1 = w[1]
|
||||||
|
val saved2 = w[2]
|
||||||
|
val saved3 = w[3]
|
||||||
|
val hi2NC = w
|
||||||
|
U256.mulWide(hi2NC, out, N_COMPLEMENT)
|
||||||
|
|
||||||
var c2 = 0L
|
var c2 = 0L
|
||||||
val result = LongArray(4)
|
|
||||||
for (i in 0 until 4) {
|
for (i in 0 until 4) {
|
||||||
val s1 = lo2[i] + hi2NC[i]
|
val loVal =
|
||||||
val c1 = if (s1.toULong() < lo2[i].toULong()) 1L else 0L
|
if (i == 0) {
|
||||||
|
saved0
|
||||||
|
} else if (i == 1) {
|
||||||
|
saved1
|
||||||
|
} else if (i == 2) {
|
||||||
|
saved2
|
||||||
|
} else {
|
||||||
|
saved3
|
||||||
|
}
|
||||||
|
val s1 = loVal + hi2NC[i]
|
||||||
|
val c1 = if (s1.toULong() < loVal.toULong()) 1L else 0L
|
||||||
val s2 = s1 + c2
|
val s2 = s1 + c2
|
||||||
val cc = if (s2.toULong() < s1.toULong()) 1L else 0L
|
val cc = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
result[i] = s2
|
out[i] = s2
|
||||||
c2 = c1 + cc
|
c2 = c1 + cc
|
||||||
}
|
}
|
||||||
// Handle remaining overflow from hi2NC[4..7] + carry
|
|
||||||
// hi2NC[4..7] should be small (hi2 is ~129 bits, NC is ~129 bits → product ≤ 258 bits)
|
|
||||||
// So hi2NC[4] might be non-zero but hi2NC[5..7] should be zero.
|
|
||||||
// Fold: overflow * N_COMPLEMENT into result
|
|
||||||
var ov = c2 + hi2NC[4]
|
var ov = c2 + hi2NC[4]
|
||||||
for (i in 5 until 8) ov += hi2NC[i]
|
for (i in 5 until 8) ov += hi2NC[i]
|
||||||
if (ov != 0L) {
|
if (ov != 0L) {
|
||||||
// ov × NC[0]
|
|
||||||
val c0lo = ov * N_COMPLEMENT[0]
|
val c0lo = ov * N_COMPLEMENT[0]
|
||||||
val c0hi = unsignedMultiplyHigh(ov, N_COMPLEMENT[0])
|
val c0hi = unsignedMultiplyHigh(ov, N_COMPLEMENT[0])
|
||||||
// ov × NC[1]
|
|
||||||
val c1lo = ov * N_COMPLEMENT[1]
|
val c1lo = ov * N_COMPLEMENT[1]
|
||||||
val c1hi = unsignedMultiplyHigh(ov, N_COMPLEMENT[1])
|
val c1hi = unsignedMultiplyHigh(ov, N_COMPLEMENT[1])
|
||||||
// ov × NC[2] = ov × 1 = ov
|
val s0 = out[0] + c0lo
|
||||||
val s0 = result[0] + c0lo
|
val carry0 = if (s0.toULong() < out[0].toULong()) 1L else 0L
|
||||||
val carry0 = if (s0.toULong() < result[0].toULong()) 1L else 0L
|
out[0] = s0
|
||||||
result[0] = s0
|
val s1 = out[1] + c0hi + c1lo + carry0
|
||||||
val s1 = result[1] + c0hi + c1lo + carry0
|
val carry1 = if (s1.toULong() < out[1].toULong()) 1L else 0L
|
||||||
val carry1 = if (s1.toULong() < result[1].toULong()) 1L else 0L
|
out[1] = s1
|
||||||
result[1] = s1
|
val s2 = out[2] + c1hi + ov + carry1
|
||||||
val s2 = result[2] + c1hi + ov + carry1
|
val carry2 = if (s2.toULong() < out[2].toULong()) 1L else 0L
|
||||||
val carry2 = if (s2.toULong() < result[2].toULong()) 1L else 0L
|
out[2] = s2
|
||||||
result[2] = s2
|
out[3] += carry2
|
||||||
result[3] += carry2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (U256.cmp(result, N) >= 0) U256.subTo(result, result, N)
|
while (U256.cmp(out, N) >= 0) U256.subTo(out, out, N)
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun powModN(
|
private fun powModN(
|
||||||
|
|||||||
+255
-112
@@ -25,59 +25,37 @@ import com.vitorpamplona.quartz.utils.sha256.sha256
|
|||||||
/**
|
/**
|
||||||
* Pure Kotlin implementation of secp256k1 elliptic curve operations for Nostr.
|
* Pure Kotlin implementation of secp256k1 elliptic curve operations for Nostr.
|
||||||
*
|
*
|
||||||
* This replaces the native fr.acinq.secp256k1 JNI bindings with a portable KMP
|
* Portable KMP implementation that runs on all Kotlin targets (JVM, Android, iOS,
|
||||||
* implementation that runs on all Kotlin targets (JVM, Android, iOS, Linux) without
|
* Linux) without requiring platform-specific native libraries.
|
||||||
* requiring platform-specific native libraries.
|
|
||||||
*
|
*
|
||||||
* Provides only the operations used by Nostr:
|
* Provides only the operations used by Nostr:
|
||||||
* - [pubkeyCreate] / [pubKeyCompress]: Key generation
|
* - [pubkeyCreate] / [pubKeyCompress]: Key generation
|
||||||
* - [secKeyVerify]: Key validation
|
* - [secKeyVerify]: Key validation
|
||||||
* - [signSchnorr] / [verifySchnorr]: BIP-340 Schnorr signatures (NIP-01)
|
* - [signSchnorr] / [signSchnorrWithPubKey] / [verifySchnorr]: BIP-340 Schnorr (NIP-01)
|
||||||
* - [privKeyTweakAdd]: BIP-32 key derivation (NIP-06)
|
* - [privKeyTweakAdd]: BIP-32 key derivation (NIP-06)
|
||||||
* - [pubKeyTweakMul] / [ecdhXOnly]: ECDH shared secrets (NIP-04, NIP-44)
|
* - [pubKeyTweakMul] / [ecdhXOnly]: ECDH shared secrets (NIP-04, NIP-44)
|
||||||
*
|
*
|
||||||
* Performance on Java 21 (vs native C/JNI secp256k1, well-warmed):
|
* Performance on JVM (vs native C/JNI secp256k1, 2000+ warmup, 3000-5000 iterations):
|
||||||
* verify ~8,000 ops/s (3.4× native) — Strauss + GLV + wNAF-12
|
* verify ~15,000 ops/s (1.7× native, with pubkey cache)
|
||||||
* sign ~26,000 ops/s (1.1× native) — comb method (cached pubkey)
|
* sign ~18,000 ops/s (1.5× native)
|
||||||
* pubCreate ~36,000 ops/s (1.6× native) — comb method, 3 doublings
|
* sign(cached) ~28,000 ops/s (0.9× — FASTER than native)
|
||||||
* ECDH ~11,000 ops/s (2.8× native) — GLV + wNAF-5, effective-affine
|
* pubCreate ~38,000 ops/s (1.3× native)
|
||||||
* compress ~7M ops/s (1.7× FASTER) — pure Kotlin, no JNI overhead
|
* ECDH ~14,000 ops/s (2.0× native)
|
||||||
* secKeyVerify ~8M ops/s (1.2× FASTER) — scalar range check, no JNI
|
|
||||||
*
|
*
|
||||||
* Architecture:
|
* Key optimizations:
|
||||||
* Field arithmetic uses 4×64-bit limbs (LongArray(4)) with Math.unsignedMultiplyHigh
|
* - Unrolled 4×64-bit field arithmetic (mulWide, sqrWide, addTo, subTo)
|
||||||
* (Java 18+, single UMULH instruction) for 64×64→128-bit products. 16 products per
|
* - GLV endomorphism + wNAF + comb method + Strauss/Shamir
|
||||||
* field multiply vs C's 25 (5×52-bit limbs), but each C product is a single native
|
* - Ping-pong point buffers (eliminates copyFrom in scalar mul loops)
|
||||||
* 128-bit MUL instruction vs our UMULH + MUL + carry propagation (~7 insns total).
|
* - Pre-allocated ThreadLocal scratch (eliminates ~130 allocs/operation)
|
||||||
|
* - Pubkey decompression cache (skips sqrt for repeated pubkeys)
|
||||||
|
* - P-side wNAF table cache (skips table build for repeated pubkeys)
|
||||||
|
* - Direct unsigned multiplyHigh fallback (faster on Android < API 31)
|
||||||
*
|
*
|
||||||
* Per-doublePoint cost analysis (instruction-level, vs C libsecp256k1):
|
* Remaining gap vs C libsecp256k1:
|
||||||
* mul/sqr (7 ops): Kotlin ~1,204 insns vs C ~455 insns (2.6× — UMULH overhead)
|
* - No lazy reduction (4×64 limbs fully packed; C's 5×52 have 12-bit headroom)
|
||||||
* add/neg/half: Kotlin ~312 insns vs C ~75 insns (4.2× — no lazy reduction)
|
* - Fermat inversion instead of safegcd (safegcd slower on JVM)
|
||||||
* Total: Kotlin ~1,516 insns vs C ~530 insns (2.9× — matches benchmarks)
|
* - WINDOW_G=12 vs 15 (JVM heap tables cause cache pressure at w=15)
|
||||||
*
|
* - No constant-time guarantees (not needed for Nostr)
|
||||||
* Optimizations implemented (matching or adapted from libsecp256k1):
|
|
||||||
* - Math.unsignedMultiplyHigh (Java 18+): eliminates 4-insn signed→unsigned correction
|
|
||||||
* - GLV endomorphism: splits 256-bit scalars into 2×128-bit halves
|
|
||||||
* - wNAF encoding: windowed non-adjacent form for sparse addition patterns
|
|
||||||
* - Comb method: generator multiplication with only 3 doublings (Hamburg 2012)
|
|
||||||
* - Strauss/Shamir: interleaved multi-scalar multiplication for verification
|
|
||||||
* - Effective-affine: batch-inverts wNAF tables for cheaper mixed adds (saves ~4M/add)
|
|
||||||
* - Shared Z inversion: GLV table pairs share Z coords, one inversion for both
|
|
||||||
* - Batch inversion: Montgomery's trick (1 inv + 3(n-1) muls for n inversions)
|
|
||||||
* - Pre-allocated scratch: ThreadLocal PointScratch eliminates ~130 allocs/operation
|
|
||||||
* - Dedicated squaring: 10 products vs 16 for general multiplication
|
|
||||||
* - secp256k1-specific reduceSelf: single branch on a[3]==-1 (>99.99% fast path)
|
|
||||||
*
|
|
||||||
* Differences from C libsecp256k1 (due to JVM constraints):
|
|
||||||
* - No lazy reduction (4×64 limbs have no headroom; C's 5×52 limbs have 12-bit spare
|
|
||||||
* capacity per limb, allowing 3-8 chained add/sub without normalizing — this accounts
|
|
||||||
* for 24% of the remaining per-operation gap)
|
|
||||||
* - Fermat inversion (255 sqr + 15 mul) instead of safegcd (safegcd is slower on JVM
|
|
||||||
* due to 128-bit arithmetic overhead in the inner divstep matrix multiply)
|
|
||||||
* - WINDOW_G=12 instead of 15 (JVM heap-allocated tables cause cache pressure at
|
|
||||||
* larger sizes; C uses contiguous compile-time .rodata arrays)
|
|
||||||
* - No constant-time guarantees (not needed for Nostr — secrets are nonces, not
|
|
||||||
* long-term keys exposed to timing side-channels)
|
|
||||||
*/
|
*/
|
||||||
object Secp256k1 {
|
object Secp256k1 {
|
||||||
// ==================== Cached BIP-340 tag hash prefixes ====================
|
// ==================== Cached BIP-340 tag hash prefixes ====================
|
||||||
@@ -99,6 +77,59 @@ object Secp256k1 {
|
|||||||
h + h
|
h + h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Pubkey decompression cache ====================
|
||||||
|
//
|
||||||
|
// liftX (square root on secp256k1) costs ~280 field ops per call. In Nostr,
|
||||||
|
// the same pubkeys are verified repeatedly (every event from the same author).
|
||||||
|
// This cache maps x-only pubkey bytes → decompressed (x, y) coordinates,
|
||||||
|
// saving the sqrt for repeated pubkeys (~13% of verify cost per cache hit).
|
||||||
|
//
|
||||||
|
// Simple fixed-size direct-mapped cache (no LRU overhead). Size must be power of 2.
|
||||||
|
// 1024 entries covers most follow lists (~1000 users) with few collisions.
|
||||||
|
// Memory: 1024 × ~96 bytes = ~96KB.
|
||||||
|
private const val PUBKEY_CACHE_SIZE = 1024 // power of 2
|
||||||
|
private const val PUBKEY_CACHE_MASK = PUBKEY_CACHE_SIZE - 1
|
||||||
|
|
||||||
|
private class CachedPubkey(
|
||||||
|
val keyBytes: ByteArray, // 32-byte x-only pubkey (for equality check)
|
||||||
|
val px: LongArray, // decompressed x (4 limbs)
|
||||||
|
val py: LongArray, // decompressed y (4 limbs)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val pubkeyCache = arrayOfNulls<CachedPubkey>(PUBKEY_CACHE_SIZE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* liftX with caching. Returns true and fills outX/outY if the pubkey is valid.
|
||||||
|
* On cache hit, copies the cached coordinates (2 array copies, ~trivial).
|
||||||
|
* On cache miss, computes sqrt and stores the result.
|
||||||
|
*/
|
||||||
|
private fun liftXCached(
|
||||||
|
outX: LongArray,
|
||||||
|
outY: LongArray,
|
||||||
|
pub: ByteArray,
|
||||||
|
): Boolean {
|
||||||
|
// Hash the pubkey bytes to a cache slot (use first 4 bytes as index)
|
||||||
|
val slot =
|
||||||
|
(
|
||||||
|
(pub[0].toInt() and 0xFF) or
|
||||||
|
((pub[1].toInt() and 0xFF) shl 8)
|
||||||
|
) and PUBKEY_CACHE_MASK
|
||||||
|
|
||||||
|
val cached = pubkeyCache[slot]
|
||||||
|
if (cached != null && cached.keyBytes.contentEquals(pub)) {
|
||||||
|
// Cache hit — copy pre-computed coordinates
|
||||||
|
cached.px.copyInto(outX)
|
||||||
|
cached.py.copyInto(outY)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss — compute sqrt and store
|
||||||
|
if (!KeyCodec.liftX(outX, outY, U256.fromBytes(pub))) return false
|
||||||
|
|
||||||
|
pubkeyCache[slot] = CachedPubkey(pub.copyOf(), outX.copyOf(), outY.copyOf())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Key operations ====================
|
// ==================== Key operations ====================
|
||||||
|
|
||||||
/** Create a 65-byte uncompressed public key (04 || x || y) from a 32-byte secret key. */
|
/** Create a 65-byte uncompressed public key (04 || x || y) from a 32-byte secret key. */
|
||||||
@@ -106,12 +137,10 @@ object Secp256k1 {
|
|||||||
require(seckey.size == 32)
|
require(seckey.size == 32)
|
||||||
val scalar = U256.fromBytes(seckey)
|
val scalar = U256.fromBytes(seckey)
|
||||||
require(ScalarN.isValid(scalar))
|
require(ScalarN.isValid(scalar))
|
||||||
val p = MutablePoint()
|
val sc = ECPoint.getScratch()
|
||||||
ECPoint.mulG(p, scalar)
|
ECPoint.mulG(sc.entryResult, scalar)
|
||||||
val x = LongArray(4)
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
val y = LongArray(4)
|
return KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
||||||
check(ECPoint.toAffine(p, x, y))
|
|
||||||
return ECPoint.serializeUncompressed(x, y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,14 +245,12 @@ object Secp256k1 {
|
|||||||
require(ScalarN.isValid(d0))
|
require(ScalarN.isValid(d0))
|
||||||
|
|
||||||
// Derive public key (one G multiplication + one inversion)
|
// Derive public key (one G multiplication + one inversion)
|
||||||
val pubPoint = MutablePoint()
|
val sc = ECPoint.getScratch()
|
||||||
ECPoint.mulG(pubPoint, d0)
|
ECPoint.mulG(sc.entryResult, d0)
|
||||||
val px = LongArray(4)
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
val py = LongArray(4)
|
|
||||||
check(ECPoint.toAffine(pubPoint, px, py))
|
|
||||||
|
|
||||||
val xOnlyPub = U256.toBytes(px)
|
val xOnlyPub = U256.toBytes(sc.entryPx)
|
||||||
return signSchnorrInternal(data, d0, xOnlyPub, ECPoint.hasEvenY(py), auxrand)
|
return signSchnorrInternal(data, d0, xOnlyPub, KeyCodec.hasEvenY(sc.entryPy), auxrand)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,23 +294,31 @@ object Secp256k1 {
|
|||||||
pubKeyHasEvenY: Boolean,
|
pubKeyHasEvenY: Boolean,
|
||||||
auxrand: ByteArray?,
|
auxrand: ByteArray?,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
val d = if (pubKeyHasEvenY) d0 else ScalarN.neg(d0)
|
val sc = ECPoint.getScratch()
|
||||||
|
val tmp = sc.entryTmp
|
||||||
|
|
||||||
|
val d =
|
||||||
|
if (pubKeyHasEvenY) {
|
||||||
|
d0
|
||||||
|
} else {
|
||||||
|
ScalarN.negTo(tmp, d0)
|
||||||
|
tmp
|
||||||
|
}
|
||||||
val dBytes = U256.toBytes(d)
|
val dBytes = U256.toBytes(d)
|
||||||
|
|
||||||
val t =
|
val tBytes: ByteArray
|
||||||
if (auxrand != null) {
|
if (auxrand != null) {
|
||||||
require(auxrand.size == 32)
|
require(auxrand.size == 32)
|
||||||
val auxHash = sha256(AUX_PREFIX + auxrand)
|
val auxHash = sha256(AUX_PREFIX + auxrand)
|
||||||
val tArr = LongArray(4)
|
U256.xorTo(sc.entryTmp2, U256.fromBytes(dBytes), U256.fromBytes(auxHash))
|
||||||
U256.xorTo(tArr, U256.fromBytes(dBytes), U256.fromBytes(auxHash))
|
tBytes = U256.toBytes(sc.entryTmp2)
|
||||||
U256.toBytes(tArr)
|
|
||||||
} else {
|
} else {
|
||||||
dBytes
|
tBytes = dBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
val nonceInput = ByteArray(64 + 32 + 32 + data.size)
|
val nonceInput = ByteArray(64 + 32 + 32 + data.size)
|
||||||
NONCE_PREFIX.copyInto(nonceInput, 0)
|
NONCE_PREFIX.copyInto(nonceInput, 0)
|
||||||
t.copyInto(nonceInput, 64)
|
tBytes.copyInto(nonceInput, 64)
|
||||||
pBytes.copyInto(nonceInput, 96)
|
pBytes.copyInto(nonceInput, 96)
|
||||||
data.copyInto(nonceInput, 128)
|
data.copyInto(nonceInput, 128)
|
||||||
val rand = sha256(nonceInput)
|
val rand = sha256(nonceInput)
|
||||||
@@ -291,13 +326,12 @@ object Secp256k1 {
|
|||||||
require(!U256.isZero(k0))
|
require(!U256.isZero(k0))
|
||||||
|
|
||||||
// R = k0·G
|
// R = k0·G
|
||||||
val rPoint = MutablePoint()
|
ECPoint.mulG(sc.entryResult, k0)
|
||||||
ECPoint.mulG(rPoint, k0)
|
val rx = sc.entryPx
|
||||||
val rx = LongArray(4)
|
val ry = sc.entryPy
|
||||||
val ry = LongArray(4)
|
check(ECPoint.toAffine(sc.entryResult, rx, ry, sc))
|
||||||
check(ECPoint.toAffine(rPoint, rx, ry))
|
|
||||||
|
|
||||||
val k = if (ECPoint.hasEvenY(ry)) k0 else ScalarN.neg(k0)
|
val k = if (KeyCodec.hasEvenY(ry)) k0 else ScalarN.neg(k0)
|
||||||
|
|
||||||
// Challenge: e = H(R || P || msg)
|
// Challenge: e = H(R || P || msg)
|
||||||
val chalInput = ByteArray(64 + 32 + 32 + data.size)
|
val chalInput = ByteArray(64 + 32 + 32 + data.size)
|
||||||
@@ -338,13 +372,16 @@ object Secp256k1 {
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
if (signature.size != 64 || pub.size != 32) return false
|
if (signature.size != 64 || pub.size != 32) return false
|
||||||
|
|
||||||
val px = LongArray(4)
|
// Use thread-local scratch to avoid per-verify allocations.
|
||||||
val py = LongArray(4)
|
// Saves ~10 LongArray(4) + 2 MutablePoint = ~14 object allocations per call.
|
||||||
if (!ECPoint.liftX(px, py, U256.fromBytes(pub))) return false
|
val sc = ECPoint.getScratch()
|
||||||
|
if (!liftXCached(sc.entryPx, sc.entryPy, pub)) return false
|
||||||
|
|
||||||
val r = U256.fromBytes(signature, 0)
|
val r = sc.entryTmp
|
||||||
|
U256.fromBytesInto(r, signature, 0)
|
||||||
if (U256.cmp(r, FieldP.P) >= 0) return false
|
if (U256.cmp(r, FieldP.P) >= 0) return false
|
||||||
val s = U256.fromBytes(signature, 32)
|
val s = sc.entryTmp2
|
||||||
|
U256.fromBytesInto(s, signature, 32)
|
||||||
if (U256.cmp(s, ScalarN.N) >= 0) return false
|
if (U256.cmp(s, ScalarN.N) >= 0) return false
|
||||||
|
|
||||||
// Build challenge hash input in a single array: prefix(64) + r(32) + pub(32) + data(N)
|
// Build challenge hash input in a single array: prefix(64) + r(32) + pub(32) + data(N)
|
||||||
@@ -354,21 +391,20 @@ object Secp256k1 {
|
|||||||
pub.copyInto(hashInput, 96)
|
pub.copyInto(hashInput, 96)
|
||||||
data.copyInto(hashInput, 128)
|
data.copyInto(hashInput, 128)
|
||||||
val eHash = sha256(hashInput)
|
val eHash = sha256(hashInput)
|
||||||
val e = ScalarN.reduce(U256.fromBytes(eHash))
|
// Reuse entryPx for e (liftX result already copied into pPoint below)
|
||||||
|
val e = sc.zInv // safe: zInv not used until toAffine after mulDoubleG
|
||||||
|
U256.fromBytesInto(e, eHash, 0)
|
||||||
|
if (U256.cmp(e, ScalarN.N) >= 0) U256.subTo(e, e, ScalarN.N) // inline reduce
|
||||||
|
|
||||||
// R = s·G + (-e)·P via Shamir's trick
|
// R = s·G + (-e)·P via Shamir's trick
|
||||||
val negE = ScalarN.neg(e)
|
ScalarN.negTo(e, e) // negate in-place
|
||||||
val pPoint = MutablePoint()
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy) // copies px/py, so entryPx is free
|
||||||
pPoint.setAffine(px, py)
|
ECPoint.mulDoubleG(sc.entryResult, s, sc.entryPoint, e)
|
||||||
val result = MutablePoint()
|
|
||||||
ECPoint.mulDoubleG(result, s, pPoint, negE)
|
|
||||||
|
|
||||||
if (result.isInfinity()) return false
|
if (sc.entryResult.isInfinity()) return false
|
||||||
val rx = LongArray(4)
|
if (!ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc)) return false
|
||||||
val ry = LongArray(4)
|
if (!KeyCodec.hasEvenY(sc.entryPy)) return false
|
||||||
if (!ECPoint.toAffine(result, rx, ry)) return false
|
return U256.cmp(sc.entryPx, r) == 0
|
||||||
if (!ECPoint.hasEvenY(ry)) return false
|
|
||||||
return U256.cmp(rx, r) == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Tweak operations ====================
|
// ==================== Tweak operations ====================
|
||||||
@@ -390,24 +426,19 @@ object Secp256k1 {
|
|||||||
tweak: ByteArray,
|
tweak: ByteArray,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
require(tweak.size == 32)
|
require(tweak.size == 32)
|
||||||
val x = LongArray(4)
|
val sc = ECPoint.getScratch()
|
||||||
val y = LongArray(4)
|
check(KeyCodec.parsePublicKey(pubkey, sc.entryPx, sc.entryPy))
|
||||||
check(ECPoint.parsePublicKey(pubkey, x, y))
|
|
||||||
val scalar = U256.fromBytes(tweak)
|
val scalar = U256.fromBytes(tweak)
|
||||||
require(ScalarN.isValid(scalar))
|
require(ScalarN.isValid(scalar))
|
||||||
|
|
||||||
val p = MutablePoint()
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
||||||
p.setAffine(x, y)
|
ECPoint.mul(sc.entryResult, sc.entryPoint, scalar)
|
||||||
val result = MutablePoint()
|
check(ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc))
|
||||||
ECPoint.mul(result, p, scalar)
|
|
||||||
val rx = LongArray(4)
|
|
||||||
val ry = LongArray(4)
|
|
||||||
check(ECPoint.toAffine(result, rx, ry))
|
|
||||||
|
|
||||||
return if (pubkey.size == 33) {
|
return if (pubkey.size == 33) {
|
||||||
ECPoint.serializeCompressed(rx, ry)
|
KeyCodec.serializeCompressed(sc.entryPx, sc.entryPy)
|
||||||
} else {
|
} else {
|
||||||
ECPoint.serializeUncompressed(rx, ry)
|
KeyCodec.serializeUncompressed(sc.entryPx, sc.entryPy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,25 +460,22 @@ object Secp256k1 {
|
|||||||
scalar: ByteArray,
|
scalar: ByteArray,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
require(xOnlyPub.size == 32 && scalar.size == 32)
|
require(xOnlyPub.size == 32 && scalar.size == 32)
|
||||||
val x = U256.fromBytes(xOnlyPub)
|
val sc = ECPoint.getScratch()
|
||||||
require(U256.cmp(x, FieldP.P) < 0)
|
U256.fromBytesInto(sc.entryTmp, xOnlyPub, 0)
|
||||||
|
require(U256.cmp(sc.entryTmp, FieldP.P) < 0)
|
||||||
val k = U256.fromBytes(scalar)
|
val k = U256.fromBytes(scalar)
|
||||||
require(ScalarN.isValid(k))
|
require(ScalarN.isValid(k))
|
||||||
|
|
||||||
// Compute y = sqrt(x³ + 7). We need SOME valid y for EC point operations,
|
// Compute y = sqrt(x³ + 7). We need SOME valid y for EC point operations,
|
||||||
// but the result's x-coordinate is the same regardless of y sign.
|
// but the result's x-coordinate is the same regardless of y sign.
|
||||||
// Use liftX which returns the even-y variant.
|
check(KeyCodec.liftX(sc.entryPx, sc.entryPy, sc.entryTmp, sc.entryTmp2)) {
|
||||||
val px = LongArray(4)
|
"Not a valid x-coordinate on secp256k1"
|
||||||
val py = LongArray(4)
|
}
|
||||||
check(ECPoint.liftX(px, py, x)) { "Not a valid x-coordinate on secp256k1" }
|
|
||||||
|
|
||||||
val p = MutablePoint()
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy)
|
||||||
p.setAffine(px, py)
|
ECPoint.mul(sc.entryResult, sc.entryPoint, k)
|
||||||
val result = MutablePoint()
|
check(ECPoint.toAffineX(sc.entryResult, sc.entryPx, sc))
|
||||||
ECPoint.mul(result, p, k)
|
return U256.toBytes(sc.entryPx)
|
||||||
val rx = LongArray(4)
|
|
||||||
check(ECPoint.toAffineX(result, rx))
|
|
||||||
return U256.toBytes(rx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** BIP-340 tagged hash (for tags not cached above). */
|
/** BIP-340 tagged hash (for tags not cached above). */
|
||||||
@@ -458,4 +486,119 @@ object Secp256k1 {
|
|||||||
val tagHash = sha256(tag.encodeToByteArray())
|
val tagHash = sha256(tag.encodeToByteArray())
|
||||||
return sha256(tagHash + tagHash + msg)
|
return sha256(tagHash + tagHash + msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Same-Pubkey Batch Verification ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch-verify multiple BIP-340 Schnorr signatures from the SAME public key
|
||||||
|
* using scalar and point summation. Returns true if ALL signatures are valid.
|
||||||
|
*
|
||||||
|
* Instead of n individual mulDoubleG calls (each with ~130 doublings + toAffine),
|
||||||
|
* this combines everything into scalar sums + one point sum + one mulDoubleG:
|
||||||
|
*
|
||||||
|
* S = Σ sᵢ mod n (scalar addition — trivial)
|
||||||
|
* E = Σ eᵢ mod n (scalar addition — trivial)
|
||||||
|
* R_sum = Σ liftX(rᵢ) (point addition — n-1 addMixed calls)
|
||||||
|
* Check: S·G - E·P - R_sum == O (one mulDoubleG + point subtraction)
|
||||||
|
*
|
||||||
|
* This works because valid Schnorr signatures are linear:
|
||||||
|
* sᵢ·G = Rᵢ + eᵢ·P → (Σsᵢ)·G = (ΣRᵢ) + (Σeᵢ)·P
|
||||||
|
*
|
||||||
|
* Performance: ~1,350 + 11·n field ops vs n × ~1,620 individual (with caches).
|
||||||
|
* For n=16: ~1,526 vs ~25,920 = ~17x throughput improvement.
|
||||||
|
*
|
||||||
|
* Security: by linearity, if any signature is invalid (sᵢ·G ≠ Rᵢ + eᵢ·P),
|
||||||
|
* the sum fails — errors cannot cancel without solving the discrete log.
|
||||||
|
* For extra hardening with duplicate events from multiple relays, the caller
|
||||||
|
* can verify duplicates individually to detect relay manipulation.
|
||||||
|
*
|
||||||
|
* @param pub 32-byte x-only public key (same for all events)
|
||||||
|
* @param signatures list of 64-byte signatures (R.x || s)
|
||||||
|
* @param messages list of message byte arrays (same order as signatures)
|
||||||
|
* @return true if all signatures are valid for this pubkey
|
||||||
|
*/
|
||||||
|
fun verifySchnorrBatch(
|
||||||
|
pub: ByteArray,
|
||||||
|
signatures: List<ByteArray>,
|
||||||
|
messages: List<ByteArray>,
|
||||||
|
): Boolean {
|
||||||
|
val n = signatures.size
|
||||||
|
require(n == messages.size) { "signatures and messages must have same size" }
|
||||||
|
if (n == 0) return true
|
||||||
|
if (n == 1) return verifySchnorr(signatures[0], messages[0], pub)
|
||||||
|
if (pub.size != 32) return false
|
||||||
|
|
||||||
|
val sc = ECPoint.getScratch()
|
||||||
|
|
||||||
|
// Decompress pubkey P once (uses liftX cache)
|
||||||
|
val px = sc.entryPx
|
||||||
|
val py = sc.entryPy
|
||||||
|
if (!liftXCached(px, py, pub)) return false
|
||||||
|
|
||||||
|
// Accumulators for the scalar sums
|
||||||
|
val sSum = LongArray(4) // Σ sᵢ mod n
|
||||||
|
val eSum = LongArray(4) // Σ eᵢ mod n
|
||||||
|
|
||||||
|
// Accumulator for R point sum (Jacobian)
|
||||||
|
val rSum = MutablePoint()
|
||||||
|
rSum.setInfinity()
|
||||||
|
val rTmp = sc.entryResult // reuse as temp for addMixed
|
||||||
|
|
||||||
|
for (i in 0 until n) {
|
||||||
|
val sig = signatures[i]
|
||||||
|
val msg = messages[i]
|
||||||
|
if (sig.size != 64) return false
|
||||||
|
|
||||||
|
// Parse r, s from signature
|
||||||
|
val r = U256.fromBytes(sig, 0)
|
||||||
|
if (U256.cmp(r, FieldP.P) >= 0) return false
|
||||||
|
val s = U256.fromBytes(sig, 32)
|
||||||
|
if (U256.cmp(s, ScalarN.N) >= 0) return false
|
||||||
|
|
||||||
|
// Accumulate s: sSum += sᵢ mod n
|
||||||
|
ScalarN.addTo(sSum, sSum, s)
|
||||||
|
|
||||||
|
// Compute challenge eᵢ = H(rᵢ || pub || msgᵢ)
|
||||||
|
val hashInput = ByteArray(64 + 32 + 32 + msg.size)
|
||||||
|
CHALLENGE_PREFIX.copyInto(hashInput, 0)
|
||||||
|
sig.copyInto(hashInput, 64, 0, 32)
|
||||||
|
pub.copyInto(hashInput, 96)
|
||||||
|
msg.copyInto(hashInput, 128)
|
||||||
|
val eHash = sha256(hashInput)
|
||||||
|
val e = ScalarN.reduce(U256.fromBytes(eHash))
|
||||||
|
|
||||||
|
// Accumulate e: eSum += eᵢ mod n
|
||||||
|
ScalarN.addTo(eSum, eSum, e)
|
||||||
|
|
||||||
|
// Decompress Rᵢ = liftX(rᵢ) and accumulate into rSum
|
||||||
|
val rx = LongArray(4)
|
||||||
|
val ry = LongArray(4)
|
||||||
|
if (!KeyCodec.liftX(rx, ry, r)) return false
|
||||||
|
|
||||||
|
// rSum += Rᵢ (mixed addition: Rᵢ is affine)
|
||||||
|
if (rSum.isInfinity()) {
|
||||||
|
rSum.setAffine(rx, ry)
|
||||||
|
} else {
|
||||||
|
ECPoint.addMixed(rTmp, rSum, rx, ry, sc)
|
||||||
|
rSum.copyFrom(rTmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute Q = sSum·G + (-eSum)·P via Shamir's trick (one mulDoubleG)
|
||||||
|
ScalarN.negTo(eSum, eSum)
|
||||||
|
val pPoint = sc.entryPoint
|
||||||
|
pPoint.setAffine(px, py)
|
||||||
|
val q = MutablePoint()
|
||||||
|
ECPoint.mulDoubleG(q, sSum, pPoint, eSum)
|
||||||
|
|
||||||
|
// Check: Q - R_sum == O → Q + (-R_sum) == O
|
||||||
|
// Negate R_sum: just negate its Y coordinate
|
||||||
|
FieldP.neg(rSum.y, rSum.y)
|
||||||
|
|
||||||
|
// Add Q + (-R_sum) and check if result is infinity
|
||||||
|
val result = MutablePoint()
|
||||||
|
ECPoint.addPoints(result, q, rSum, sc)
|
||||||
|
|
||||||
|
return result.isInfinity()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,131 +64,407 @@ internal object U256 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/** out = a + b. Returns carry (0 or 1). Safe for aliasing. */
|
/** out = a + b. Returns carry (0 or 1). Safe for aliasing. Unrolled for ART JIT. */
|
||||||
fun addTo(
|
fun addTo(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
b: LongArray,
|
b: LongArray,
|
||||||
): Int {
|
): Int {
|
||||||
var carry = 0L
|
var s1: Long
|
||||||
for (i in 0 until 4) {
|
var s2: Long
|
||||||
val s1 = a[i] + b[i]
|
var c1: Long
|
||||||
val c1 = if (s1.toULong() < a[i].toULong()) 1L else 0L
|
var c2: Long
|
||||||
val s2 = s1 + carry
|
|
||||||
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
// Limb 0 (no carry input)
|
||||||
out[i] = s2
|
s1 = a[0] + b[0]
|
||||||
|
c1 = if (s1.toULong() < a[0].toULong()) 1L else 0L
|
||||||
|
out[0] = s1
|
||||||
|
var carry = c1
|
||||||
|
|
||||||
|
// Limb 1
|
||||||
|
s1 = a[1] + b[1]
|
||||||
|
c1 = if (s1.toULong() < a[1].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[1] = s2
|
||||||
carry = c1 + c2
|
carry = c1 + c2
|
||||||
}
|
|
||||||
|
// Limb 2
|
||||||
|
s1 = a[2] + b[2]
|
||||||
|
c1 = if (s1.toULong() < a[2].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[2] = s2
|
||||||
|
carry = c1 + c2
|
||||||
|
|
||||||
|
// Limb 3
|
||||||
|
s1 = a[3] + b[3]
|
||||||
|
c1 = if (s1.toULong() < a[3].toULong()) 1L else 0L
|
||||||
|
s2 = s1 + carry
|
||||||
|
c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
||||||
|
out[3] = s2
|
||||||
|
carry = c1 + c2
|
||||||
|
|
||||||
return carry.toInt()
|
return carry.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** out = a - b. Returns borrow (0 or 1). Safe for aliasing. */
|
/** out = a - b. Returns borrow (0 or 1). Safe for aliasing. Unrolled for ART JIT. */
|
||||||
fun subTo(
|
fun subTo(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
b: LongArray,
|
b: LongArray,
|
||||||
): Int {
|
): Int {
|
||||||
var borrow = 0L
|
var d1: Long
|
||||||
for (i in 0 until 4) {
|
var d2: Long
|
||||||
val d1 = a[i] - b[i]
|
var c1: Long
|
||||||
val c1 = if (a[i].toULong() < b[i].toULong()) 1L else 0L
|
var c2: Long
|
||||||
val d2 = d1 - borrow
|
|
||||||
val c2 = if (d1.toULong() < borrow.toULong()) 1L else 0L
|
// Limb 0 (no borrow input)
|
||||||
out[i] = d2
|
d1 = a[0] - b[0]
|
||||||
|
c1 = if (a[0].toULong() < b[0].toULong()) 1L else 0L
|
||||||
|
out[0] = d1
|
||||||
|
var borrow = c1
|
||||||
|
|
||||||
|
// Limb 1
|
||||||
|
d1 = a[1] - b[1]
|
||||||
|
c1 = if (a[1].toULong() < b[1].toULong()) 1L else 0L
|
||||||
|
d2 = d1 - borrow
|
||||||
|
c2 = if (d1.toULong() < borrow.toULong()) 1L else 0L
|
||||||
|
out[1] = d2
|
||||||
borrow = c1 + c2
|
borrow = c1 + c2
|
||||||
}
|
|
||||||
|
// Limb 2
|
||||||
|
d1 = a[2] - b[2]
|
||||||
|
c1 = if (a[2].toULong() < b[2].toULong()) 1L else 0L
|
||||||
|
d2 = d1 - borrow
|
||||||
|
c2 = if (d1.toULong() < borrow.toULong()) 1L else 0L
|
||||||
|
out[2] = d2
|
||||||
|
borrow = c1 + c2
|
||||||
|
|
||||||
|
// Limb 3
|
||||||
|
d1 = a[3] - b[3]
|
||||||
|
c1 = if (a[3].toULong() < b[3].toULong()) 1L else 0L
|
||||||
|
d2 = d1 - borrow
|
||||||
|
c2 = if (d1.toULong() < borrow.toULong()) 1L else 0L
|
||||||
|
out[3] = d2
|
||||||
|
borrow = c1 + c2
|
||||||
|
|
||||||
return borrow.toInt()
|
return borrow.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 4×4 schoolbook multiplication: out = a × b (512-bit result in LongArray(8)).
|
* 4×4 schoolbook multiplication: out = a × b (512-bit result in LongArray(8)).
|
||||||
*
|
*
|
||||||
* Uses unsignedMultiplyHigh for the upper 64 bits of each 64×64→128-bit product.
|
* Fully unrolled: all 16 products are explicit, eliminating loop control overhead
|
||||||
* On JVM, this is a hardware intrinsic (single instruction). Total: 16 products
|
* and array bounds checks. This significantly helps ART JIT on Android, which is
|
||||||
* vs 64 for the previous 8×32-bit representation.
|
* less aggressive at loop optimization than HotSpot. Called ~1,900× per verify.
|
||||||
*/
|
*/
|
||||||
fun mulWide(
|
fun mulWide(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
b: LongArray,
|
b: LongArray,
|
||||||
) {
|
) {
|
||||||
for (i in 0 until 8) out[i] = 0L
|
val a0 = a[0]
|
||||||
|
val a1 = a[1]
|
||||||
|
val a2 = a[2]
|
||||||
|
val a3 = a[3]
|
||||||
|
val b0 = b[0]
|
||||||
|
val b1 = b[1]
|
||||||
|
val b2 = b[2]
|
||||||
|
val b3 = b[3]
|
||||||
|
var lo: Long
|
||||||
|
var hi: Long
|
||||||
|
var prev: Long
|
||||||
|
var s: Long
|
||||||
|
var c1: Long
|
||||||
|
var c2: Long
|
||||||
|
var carry: Long
|
||||||
|
|
||||||
for (i in 0 until 4) {
|
// Row 0: a0 × [b0,b1,b2,b3] → out[0..4] (out starts empty, no prev accumulation)
|
||||||
var carry = 0L
|
lo = a0 * b0
|
||||||
val ai = a[i]
|
out[0] = lo
|
||||||
for (j in 0 until 4) {
|
carry = unsignedMultiplyHigh(a0, b0)
|
||||||
val lo = ai * b[j]
|
|
||||||
val hi = unsignedMultiplyHigh(ai, b[j])
|
|
||||||
|
|
||||||
val prev = out[i + j]
|
lo = a0 * b1
|
||||||
val s1 = prev + lo
|
s = lo + carry
|
||||||
val c1 = if (s1.toULong() < prev.toULong()) 1L else 0L
|
c1 = if (s.toULong() < lo.toULong()) 1L else 0L
|
||||||
val s2 = s1 + carry
|
out[1] = s
|
||||||
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
carry = unsignedMultiplyHigh(a0, b1) + c1
|
||||||
out[i + j] = s2
|
|
||||||
|
lo = a0 * b2
|
||||||
|
s = lo + carry
|
||||||
|
c1 = if (s.toULong() < lo.toULong()) 1L else 0L
|
||||||
|
out[2] = s
|
||||||
|
carry = unsignedMultiplyHigh(a0, b2) + c1
|
||||||
|
|
||||||
|
lo = a0 * b3
|
||||||
|
s = lo + carry
|
||||||
|
c1 = if (s.toULong() < lo.toULong()) 1L else 0L
|
||||||
|
out[3] = s
|
||||||
|
out[4] = unsignedMultiplyHigh(a0, b3) + c1
|
||||||
|
|
||||||
|
// Row 1: a1 × [b0,b1,b2,b3] accumulated into out[1..5]
|
||||||
|
lo = a1 * b0
|
||||||
|
hi = unsignedMultiplyHigh(a1, b0)
|
||||||
|
prev = out[1]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
out[1] = s
|
||||||
|
carry = hi + c1
|
||||||
|
|
||||||
|
lo = a1 * b1
|
||||||
|
hi = unsignedMultiplyHigh(a1, b1)
|
||||||
|
prev = out[2]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[2] = s
|
||||||
carry = hi + c1 + c2
|
carry = hi + c1 + c2
|
||||||
}
|
|
||||||
out[i + 4] = carry
|
lo = a1 * b2
|
||||||
}
|
hi = unsignedMultiplyHigh(a1, b2)
|
||||||
|
prev = out[3]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[3] = s
|
||||||
|
carry = hi + c1 + c2
|
||||||
|
|
||||||
|
lo = a1 * b3
|
||||||
|
hi = unsignedMultiplyHigh(a1, b3)
|
||||||
|
prev = out[4]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[4] = s
|
||||||
|
out[5] = hi + c1 + c2
|
||||||
|
|
||||||
|
// Row 2: a2 × [b0,b1,b2,b3] accumulated into out[2..6]
|
||||||
|
lo = a2 * b0
|
||||||
|
hi = unsignedMultiplyHigh(a2, b0)
|
||||||
|
prev = out[2]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
out[2] = s
|
||||||
|
carry = hi + c1
|
||||||
|
|
||||||
|
lo = a2 * b1
|
||||||
|
hi = unsignedMultiplyHigh(a2, b1)
|
||||||
|
prev = out[3]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[3] = s
|
||||||
|
carry = hi + c1 + c2
|
||||||
|
|
||||||
|
lo = a2 * b2
|
||||||
|
hi = unsignedMultiplyHigh(a2, b2)
|
||||||
|
prev = out[4]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[4] = s
|
||||||
|
carry = hi + c1 + c2
|
||||||
|
|
||||||
|
lo = a2 * b3
|
||||||
|
hi = unsignedMultiplyHigh(a2, b3)
|
||||||
|
prev = out[5]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[5] = s
|
||||||
|
out[6] = hi + c1 + c2
|
||||||
|
|
||||||
|
// Row 3: a3 × [b0,b1,b2,b3] accumulated into out[3..7]
|
||||||
|
lo = a3 * b0
|
||||||
|
hi = unsignedMultiplyHigh(a3, b0)
|
||||||
|
prev = out[3]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
out[3] = s
|
||||||
|
carry = hi + c1
|
||||||
|
|
||||||
|
lo = a3 * b1
|
||||||
|
hi = unsignedMultiplyHigh(a3, b1)
|
||||||
|
prev = out[4]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[4] = s
|
||||||
|
carry = hi + c1 + c2
|
||||||
|
|
||||||
|
lo = a3 * b2
|
||||||
|
hi = unsignedMultiplyHigh(a3, b2)
|
||||||
|
prev = out[5]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[5] = s
|
||||||
|
carry = hi + c1 + c2
|
||||||
|
|
||||||
|
lo = a3 * b3
|
||||||
|
hi = unsignedMultiplyHigh(a3, b3)
|
||||||
|
prev = out[6]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[6] = s
|
||||||
|
out[7] = hi + c1 + c2
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dedicated squaring: out = a² (512-bit result in LongArray(8)).
|
* Dedicated squaring: out = a² (512-bit result in LongArray(8)).
|
||||||
* Exploits symmetry: 6 cross-products doubled + 4 diagonal = 10 multiplyHigh calls.
|
* Exploits symmetry: 6 cross-products doubled + 4 diagonal = 10 multiplyHigh calls.
|
||||||
|
* Fully unrolled for ART JIT optimization.
|
||||||
*/
|
*/
|
||||||
fun sqrWide(
|
fun sqrWide(
|
||||||
out: LongArray,
|
out: LongArray,
|
||||||
a: LongArray,
|
a: LongArray,
|
||||||
) {
|
) {
|
||||||
for (i in 0 until 8) out[i] = 0L
|
val a0 = a[0]
|
||||||
|
val a1 = a[1]
|
||||||
|
val a2 = a[2]
|
||||||
|
val a3 = a[3]
|
||||||
|
var lo: Long
|
||||||
|
var hi: Long
|
||||||
|
var prev: Long
|
||||||
|
var s: Long
|
||||||
|
var c1: Long
|
||||||
|
var c2: Long
|
||||||
|
var carry: Long
|
||||||
|
var v: Long
|
||||||
|
|
||||||
// Pass 1: cross-products a[i]*a[j] for i < j (single)
|
// Pass 1: cross-products a[i]*a[j] for i < j (single, before doubling)
|
||||||
for (i in 0 until 4) {
|
|
||||||
var carry = 0L
|
// Row i=0: a0 × [a1, a2, a3] → out[1..4]
|
||||||
val ai = a[i]
|
out[0] = 0L
|
||||||
for (j in i + 1 until 4) {
|
lo = a0 * a1
|
||||||
val lo = ai * a[j]
|
out[1] = lo
|
||||||
val hi = unsignedMultiplyHigh(ai, a[j])
|
carry = unsignedMultiplyHigh(a0, a1)
|
||||||
val prev = out[i + j]
|
|
||||||
val s1 = prev + lo
|
lo = a0 * a2
|
||||||
val c1 = if (s1.toULong() < prev.toULong()) 1L else 0L
|
s = lo + carry
|
||||||
val s2 = s1 + carry
|
c1 = if (s.toULong() < lo.toULong()) 1L else 0L
|
||||||
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
out[2] = s
|
||||||
out[i + j] = s2
|
carry = unsignedMultiplyHigh(a0, a2) + c1
|
||||||
carry = hi + c1 + c2
|
|
||||||
}
|
lo = a0 * a3
|
||||||
out[i + 4] = carry
|
s = lo + carry
|
||||||
}
|
c1 = if (s.toULong() < lo.toULong()) 1L else 0L
|
||||||
|
out[3] = s
|
||||||
|
out[4] = unsignedMultiplyHigh(a0, a3) + c1
|
||||||
|
|
||||||
|
// Row i=1: a1 × [a2, a3] → accumulated into out[3..5]
|
||||||
|
lo = a1 * a2
|
||||||
|
hi = unsignedMultiplyHigh(a1, a2)
|
||||||
|
prev = out[3]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
out[3] = s
|
||||||
|
carry = hi + c1
|
||||||
|
|
||||||
|
lo = a1 * a3
|
||||||
|
hi = unsignedMultiplyHigh(a1, a3)
|
||||||
|
prev = out[4]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
s += carry
|
||||||
|
c2 = if (s.toULong() < carry.toULong()) 1L else 0L
|
||||||
|
out[4] = s
|
||||||
|
out[5] = hi + c1 + c2
|
||||||
|
|
||||||
|
// Row i=2: a2 × [a3] → accumulated into out[5..6]
|
||||||
|
lo = a2 * a3
|
||||||
|
hi = unsignedMultiplyHigh(a2, a3)
|
||||||
|
prev = out[5]
|
||||||
|
s = prev + lo
|
||||||
|
c1 = if (s.toULong() < prev.toULong()) 1L else 0L
|
||||||
|
out[5] = s
|
||||||
|
out[6] = hi + c1
|
||||||
|
|
||||||
// Pass 2: double all cross-products (shift left by 1 bit)
|
// Pass 2: double all cross-products (shift left by 1 bit)
|
||||||
var shiftCarry = 0L
|
v = out[1]
|
||||||
for (i in 1 until 8) {
|
out[1] = v shl 1
|
||||||
val v = out[i]
|
var shiftCarry = v ushr 63
|
||||||
out[i] = (v shl 1) or shiftCarry
|
v = out[2]
|
||||||
|
out[2] = (v shl 1) or shiftCarry
|
||||||
shiftCarry = v ushr 63
|
shiftCarry = v ushr 63
|
||||||
}
|
v = out[3]
|
||||||
|
out[3] = (v shl 1) or shiftCarry
|
||||||
|
shiftCarry = v ushr 63
|
||||||
|
v = out[4]
|
||||||
|
out[4] = (v shl 1) or shiftCarry
|
||||||
|
shiftCarry = v ushr 63
|
||||||
|
v = out[5]
|
||||||
|
out[5] = (v shl 1) or shiftCarry
|
||||||
|
shiftCarry = v ushr 63
|
||||||
|
v = out[6]
|
||||||
|
out[6] = (v shl 1) or shiftCarry
|
||||||
|
shiftCarry = v ushr 63
|
||||||
|
out[7] = shiftCarry
|
||||||
|
|
||||||
// Pass 3: add diagonal products a[i]²
|
// Pass 3: add diagonal products a[i]²
|
||||||
var dCarry = 0L
|
|
||||||
for (i in 0 until 4) {
|
|
||||||
val lo = a[i] * a[i]
|
|
||||||
val hi = unsignedMultiplyHigh(a[i], a[i])
|
|
||||||
val pos = 2 * i
|
|
||||||
|
|
||||||
val s1 = out[pos] + lo
|
// i=0: a0², pos=0
|
||||||
val c1 = if (s1.toULong() < out[pos].toULong()) 1L else 0L
|
lo = a0 * a0
|
||||||
val s2 = s1 + dCarry
|
hi = unsignedMultiplyHigh(a0, a0)
|
||||||
val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L
|
out[0] = lo // out[0] was 0
|
||||||
out[pos] = s2
|
s = out[1] + hi
|
||||||
|
c1 = if (s.toULong() < out[1].toULong()) 1L else 0L
|
||||||
|
out[1] = s
|
||||||
|
var dCarry = c1
|
||||||
|
|
||||||
val s3 = out[pos + 1] + hi
|
// i=1: a1², pos=2
|
||||||
val c3 = if (s3.toULong() < out[pos + 1].toULong()) 1L else 0L
|
lo = a1 * a1
|
||||||
val s4 = s3 + c1 + c2
|
hi = unsignedMultiplyHigh(a1, a1)
|
||||||
val c4 = if (s4.toULong() < s3.toULong()) 1L else 0L
|
s = out[2] + lo
|
||||||
out[pos + 1] = s4
|
c1 = if (s.toULong() < out[2].toULong()) 1L else 0L
|
||||||
dCarry = c3 + c4
|
s += dCarry
|
||||||
}
|
c2 = if (s.toULong() < dCarry.toULong()) 1L else 0L
|
||||||
|
out[2] = s
|
||||||
|
prev = out[3] + hi
|
||||||
|
val c3a = if (prev.toULong() < out[3].toULong()) 1L else 0L
|
||||||
|
prev += c1 + c2
|
||||||
|
val c4a = if (prev.toULong() < (c1 + c2).toULong()) 1L else 0L
|
||||||
|
out[3] = prev
|
||||||
|
dCarry = c3a + c4a
|
||||||
|
|
||||||
|
// i=2: a2², pos=4
|
||||||
|
lo = a2 * a2
|
||||||
|
hi = unsignedMultiplyHigh(a2, a2)
|
||||||
|
s = out[4] + lo
|
||||||
|
c1 = if (s.toULong() < out[4].toULong()) 1L else 0L
|
||||||
|
s += dCarry
|
||||||
|
c2 = if (s.toULong() < dCarry.toULong()) 1L else 0L
|
||||||
|
out[4] = s
|
||||||
|
prev = out[5] + hi
|
||||||
|
val c3b = if (prev.toULong() < out[5].toULong()) 1L else 0L
|
||||||
|
prev += c1 + c2
|
||||||
|
val c4b = if (prev.toULong() < (c1 + c2).toULong()) 1L else 0L
|
||||||
|
out[5] = prev
|
||||||
|
dCarry = c3b + c4b
|
||||||
|
|
||||||
|
// i=3: a3², pos=6
|
||||||
|
lo = a3 * a3
|
||||||
|
hi = unsignedMultiplyHigh(a3, a3)
|
||||||
|
s = out[6] + lo
|
||||||
|
c1 = if (s.toULong() < out[6].toULong()) 1L else 0L
|
||||||
|
s += dCarry
|
||||||
|
c2 = if (s.toULong() < dCarry.toULong()) 1L else 0L
|
||||||
|
out[6] = s
|
||||||
|
prev = out[7] + hi
|
||||||
|
val c3c = if (prev.toULong() < out[7].toULong()) 1L else 0L
|
||||||
|
prev += c1 + c2
|
||||||
|
out[7] = prev
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Serialization ====================
|
// ==================== Serialization ====================
|
||||||
@@ -201,9 +477,19 @@ internal object U256 {
|
|||||||
offset: Int,
|
offset: Int,
|
||||||
): LongArray {
|
): LongArray {
|
||||||
val r = LongArray(4)
|
val r = LongArray(4)
|
||||||
|
fromBytesInto(r, bytes, offset)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode big-endian 32 bytes into a pre-allocated LongArray(4). */
|
||||||
|
fun fromBytesInto(
|
||||||
|
out: LongArray,
|
||||||
|
bytes: ByteArray,
|
||||||
|
offset: Int,
|
||||||
|
) {
|
||||||
for (i in 0 until 4) {
|
for (i in 0 until 4) {
|
||||||
val o = offset + 24 - i * 8
|
val o = offset + 24 - i * 8
|
||||||
r[i] = ((bytes[o].toLong() and 0xFF) shl 56) or
|
out[i] = ((bytes[o].toLong() and 0xFF) shl 56) or
|
||||||
((bytes[o + 1].toLong() and 0xFF) shl 48) or
|
((bytes[o + 1].toLong() and 0xFF) shl 48) or
|
||||||
((bytes[o + 2].toLong() and 0xFF) shl 40) or
|
((bytes[o + 2].toLong() and 0xFF) shl 40) or
|
||||||
((bytes[o + 3].toLong() and 0xFF) shl 32) or
|
((bytes[o + 3].toLong() and 0xFF) shl 32) or
|
||||||
@@ -212,7 +498,6 @@ internal object U256 {
|
|||||||
((bytes[o + 6].toLong() and 0xFF) shl 8) or
|
((bytes[o + 6].toLong() and 0xFF) shl 8) or
|
||||||
(bytes[o + 7].toLong() and 0xFF)
|
(bytes[o + 7].toLong() and 0xFF)
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toBytes(a: LongArray): ByteArray {
|
fun toBytes(a: LongArray): ByteArray {
|
||||||
|
|||||||
+11
-11
@@ -286,10 +286,10 @@ class PointTest {
|
|||||||
fun liftXGenerator() {
|
fun liftXGenerator() {
|
||||||
val x = LongArray(4)
|
val x = LongArray(4)
|
||||||
val y = LongArray(4)
|
val y = LongArray(4)
|
||||||
assertTrue(ECPoint.liftX(x, y, ECPoint.GX))
|
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
|
||||||
assertEquals(toHex(ECPoint.GX), toHex(x))
|
assertEquals(toHex(ECPoint.GX), toHex(x))
|
||||||
// liftX returns even y
|
// liftX returns even y
|
||||||
assertTrue(ECPoint.hasEvenY(y))
|
assertTrue(KeyCodec.hasEvenY(y))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -297,27 +297,27 @@ class PointTest {
|
|||||||
// p itself is not a valid x coordinate
|
// p itself is not a valid x coordinate
|
||||||
val x = LongArray(4)
|
val x = LongArray(4)
|
||||||
val y = LongArray(4)
|
val y = LongArray(4)
|
||||||
assertFalse(ECPoint.liftX(x, y, FieldP.P))
|
assertFalse(KeyCodec.liftX(x, y, FieldP.P))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Serialization round-trips ====================
|
// ==================== Serialization round-trips ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun compressDecompressRoundTrip() {
|
fun compressDecompressRoundTrip() {
|
||||||
val compressed = ECPoint.serializeCompressed(ECPoint.GX, ECPoint.GY)
|
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
|
||||||
val x = LongArray(4)
|
val x = LongArray(4)
|
||||||
val y = LongArray(4)
|
val y = LongArray(4)
|
||||||
assertTrue(ECPoint.parsePublicKey(compressed, x, y))
|
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
|
||||||
assertEquals(toHex(ECPoint.GX), toHex(x))
|
assertEquals(toHex(ECPoint.GX), toHex(x))
|
||||||
assertEquals(toHex(ECPoint.GY), toHex(y))
|
assertEquals(toHex(ECPoint.GY), toHex(y))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun uncompressedRoundTrip() {
|
fun uncompressedRoundTrip() {
|
||||||
val uncompressed = ECPoint.serializeUncompressed(ECPoint.GX, ECPoint.GY)
|
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
|
||||||
val x = LongArray(4)
|
val x = LongArray(4)
|
||||||
val y = LongArray(4)
|
val y = LongArray(4)
|
||||||
assertTrue(ECPoint.parsePublicKey(uncompressed, x, y))
|
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
|
||||||
assertEquals(toHex(ECPoint.GX), toHex(x))
|
assertEquals(toHex(ECPoint.GX), toHex(x))
|
||||||
assertEquals(toHex(ECPoint.GY), toHex(y))
|
assertEquals(toHex(ECPoint.GY), toHex(y))
|
||||||
}
|
}
|
||||||
@@ -326,8 +326,8 @@ class PointTest {
|
|||||||
fun parseInvalidKey() {
|
fun parseInvalidKey() {
|
||||||
val x = LongArray(4)
|
val x = LongArray(4)
|
||||||
val y = LongArray(4)
|
val y = LongArray(4)
|
||||||
assertFalse(ECPoint.parsePublicKey(ByteArray(10), x, y))
|
assertFalse(KeyCodec.parsePublicKey(ByteArray(10), x, y))
|
||||||
assertFalse(ECPoint.parsePublicKey(ByteArray(33), x, y)) // wrong prefix (0x00)
|
assertFalse(KeyCodec.parsePublicKey(ByteArray(33), x, y)) // wrong prefix (0x00)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -366,9 +366,9 @@ class PointTest {
|
|||||||
assertEquals(0x03.toByte(), compressed[0]) // Odd y → 03 prefix
|
assertEquals(0x03.toByte(), compressed[0]) // Odd y → 03 prefix
|
||||||
val x = LongArray(4)
|
val x = LongArray(4)
|
||||||
val y = LongArray(4)
|
val y = LongArray(4)
|
||||||
assertTrue(ECPoint.parsePublicKey(compressed, x, y))
|
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
|
||||||
// Round-trip: compress again should give same result
|
// Round-trip: compress again should give same result
|
||||||
val recompressed = ECPoint.serializeCompressed(x, y)
|
val recompressed = KeyCodec.serializeCompressed(x, y)
|
||||||
assertEquals(compressed.toList(), recompressed.toList())
|
assertEquals(compressed.toList(), recompressed.toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -336,4 +336,67 @@ class Secp256k1Test {
|
|||||||
.sha256(tagHash + tagHash + msg)
|
.sha256(tagHash + tagHash + msg)
|
||||||
assertEquals(expected.toHexKey(), result.toHexKey())
|
assertEquals(expected.toHexKey(), result.toHexKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Same-pubkey batch verification
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun batchSamePubkeyAllValid() {
|
||||||
|
val seckey = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
|
||||||
|
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
|
||||||
|
val sigs = mutableListOf<ByteArray>()
|
||||||
|
val msgs = mutableListOf<ByteArray>()
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
val msg = ByteArray(32) { (i * 7 + it).toByte() }
|
||||||
|
val sig = Secp256k1.signSchnorr(msg, seckey, null)
|
||||||
|
assertTrue(Secp256k1.verifySchnorr(sig, msg, pub), "Individual verify failed for event $i")
|
||||||
|
sigs.add(sig)
|
||||||
|
msgs.add(msg)
|
||||||
|
}
|
||||||
|
assertTrue(Secp256k1.verifySchnorrBatch(pub, sigs, msgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun batchSamePubkeyWithInvalid() {
|
||||||
|
val seckey = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
|
||||||
|
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
|
||||||
|
val msg1 = ByteArray(32) { 0x01 }
|
||||||
|
val msg2 = ByteArray(32) { 0x02 }
|
||||||
|
val sig1 = Secp256k1.signSchnorr(msg1, seckey, null)
|
||||||
|
val sig2 = Secp256k1.signSchnorr(msg2, seckey, null)
|
||||||
|
// Corrupt sig2
|
||||||
|
val badSig2 = sig2.copyOf()
|
||||||
|
badSig2[63] = (badSig2[63].toInt() xor 0x01).toByte()
|
||||||
|
assertFalse(Secp256k1.verifySchnorrBatch(pub, listOf(sig1, badSig2), listOf(msg1, msg2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun batchSamePubkeyEmpty() {
|
||||||
|
val pub = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
|
||||||
|
assertTrue(Secp256k1.verifySchnorrBatch(pub, emptyList(), emptyList()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun batchSamePubkeySingleFallback() {
|
||||||
|
val seckey = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
|
||||||
|
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
|
||||||
|
val msg = ByteArray(32) { 0x42 }
|
||||||
|
val sig = Secp256k1.signSchnorr(msg, seckey, null)
|
||||||
|
assertTrue(Secp256k1.verifySchnorrBatch(pub, listOf(sig), listOf(msg)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun batchSamePubkeyLargeBatch() {
|
||||||
|
val seckey = "3982F19BEF1615BCCFBB05E321C10E1D4CBA3DF0E841C2E41EEB6016347653C3".hexToByteArray()
|
||||||
|
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
|
||||||
|
val sigs = mutableListOf<ByteArray>()
|
||||||
|
val msgs = mutableListOf<ByteArray>()
|
||||||
|
for (i in 0 until 32) {
|
||||||
|
val msg = ByteArray(64) { (i * 13 + it).toByte() }
|
||||||
|
sigs.add(Secp256k1.signSchnorr(msg, seckey, null))
|
||||||
|
msgs.add(msg)
|
||||||
|
}
|
||||||
|
assertTrue(Secp256k1.verifySchnorrBatch(pub, sigs, msgs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-22
@@ -134,8 +134,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "verifySchnorr",
|
name = "verifySchnorr",
|
||||||
warmup = 200,
|
warmup = 2000,
|
||||||
iterations = 500,
|
iterations = 5000,
|
||||||
nativeOp = { native.verifySchnorr(nativeSig, msg32, nativeXOnlyPub) },
|
nativeOp = { native.verifySchnorr(nativeSig, msg32, nativeXOnlyPub) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.verifySchnorr(
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.verifySchnorr(
|
||||||
@@ -150,8 +150,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "signSchnorr",
|
name = "signSchnorr",
|
||||||
warmup = 100,
|
warmup = 1000,
|
||||||
iterations = 200,
|
iterations = 3000,
|
||||||
nativeOp = { native.signSchnorr(msg32, privKey, auxRand) },
|
nativeOp = { native.signSchnorr(msg32, privKey, auxRand) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.signSchnorr(
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.signSchnorr(
|
||||||
@@ -166,8 +166,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "signSchnorr (cached pk)",
|
name = "signSchnorr (cached pk)",
|
||||||
warmup = 100,
|
warmup = 1000,
|
||||||
iterations = 200,
|
iterations = 5000,
|
||||||
nativeOp = { native.signSchnorr(msg32, privKey, auxRand) },
|
nativeOp = { native.signSchnorr(msg32, privKey, auxRand) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.signSchnorrWithPubKey(
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.signSchnorrWithPubKey(
|
||||||
@@ -183,8 +183,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "pubkeyCreate",
|
name = "pubkeyCreate",
|
||||||
warmup = 100,
|
warmup = 1000,
|
||||||
iterations = 500,
|
iterations = 5000,
|
||||||
nativeOp = { native.pubkeyCreate(privKey) },
|
nativeOp = { native.pubkeyCreate(privKey) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
@@ -200,8 +200,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "pubKeyCompress",
|
name = "pubKeyCompress",
|
||||||
warmup = 200,
|
warmup = 2000,
|
||||||
iterations = 1000,
|
iterations = 50000,
|
||||||
nativeOp = { native.pubKeyCompress(uncompressedNative) },
|
nativeOp = { native.pubKeyCompress(uncompressedNative) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
@@ -213,8 +213,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "pubKeyTweakMul (ECDH)",
|
name = "pubKeyTweakMul (ECDH)",
|
||||||
warmup = 100,
|
warmup = 1000,
|
||||||
iterations = 200,
|
iterations = 3000,
|
||||||
nativeOp = { native.pubKeyTweakMul(pubKey2Uncompressed.copyOf(), privKey) },
|
nativeOp = { native.pubKeyTweakMul(pubKey2Uncompressed.copyOf(), privKey) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.pubKeyTweakMul(
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.pubKeyTweakMul(
|
||||||
@@ -228,8 +228,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "privKeyTweakAdd",
|
name = "privKeyTweakAdd",
|
||||||
warmup = 100,
|
warmup = 1000,
|
||||||
iterations = 1000,
|
iterations = 50000,
|
||||||
nativeOp = { native.privKeyTweakAdd(privKey.copyOf(), privKey2) },
|
nativeOp = { native.privKeyTweakAdd(privKey.copyOf(), privKey2) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.privKeyTweakAdd(
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.privKeyTweakAdd(
|
||||||
@@ -243,8 +243,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "secKeyVerify",
|
name = "secKeyVerify",
|
||||||
warmup = 100,
|
warmup = 5000,
|
||||||
iterations = 10000,
|
iterations = 200000,
|
||||||
nativeOp = { native.secKeyVerify(privKey) },
|
nativeOp = { native.secKeyVerify(privKey) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
@@ -256,8 +256,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "compressedPubKeyFor",
|
name = "compressedPubKeyFor",
|
||||||
warmup = 10,
|
warmup = 1000,
|
||||||
iterations = 100,
|
iterations = 5000,
|
||||||
nativeOp = { native.pubKeyCompress(native.pubkeyCreate(privKey)) },
|
nativeOp = { native.pubKeyCompress(native.pubkeyCreate(privKey)) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.pubKeyCompress(
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.pubKeyCompress(
|
||||||
@@ -272,8 +272,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "tweakMulCompact (old)",
|
name = "tweakMulCompact (old)",
|
||||||
warmup = 100,
|
warmup = 1000,
|
||||||
iterations = 200,
|
iterations = 3000,
|
||||||
nativeOp = { native.pubKeyTweakMul(h02 + pub2xOnly, privKey).copyOfRange(1, 33) },
|
nativeOp = { native.pubKeyTweakMul(h02 + pub2xOnly, privKey).copyOfRange(1, 33) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
@@ -288,8 +288,8 @@ class Secp256k1Benchmark {
|
|||||||
results +=
|
results +=
|
||||||
bench(
|
bench(
|
||||||
name = "ecdhXOnly (Nostr)",
|
name = "ecdhXOnly (Nostr)",
|
||||||
warmup = 100,
|
warmup = 1000,
|
||||||
iterations = 200,
|
iterations = 3000,
|
||||||
nativeOp = { native.pubKeyTweakMul(h02 + pub2xOnly, privKey).copyOfRange(1, 33) },
|
nativeOp = { native.pubKeyTweakMul(h02 + pub2xOnly, privKey).copyOfRange(1, 33) },
|
||||||
kotlinOp = {
|
kotlinOp = {
|
||||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
@@ -306,6 +306,63 @@ class Secp256k1Benchmark {
|
|||||||
println(r)
|
println(r)
|
||||||
}
|
}
|
||||||
println("=".repeat(90))
|
println("=".repeat(90))
|
||||||
|
|
||||||
|
// ==================== Batch verification benchmark ====================
|
||||||
|
// Same pubkey, n events — the typical Nostr pattern (feed from one author)
|
||||||
|
val batchPub = kotlinXOnlyPub
|
||||||
|
for (batchSize in intArrayOf(4, 8, 16, 32)) {
|
||||||
|
val sigs = mutableListOf<ByteArray>()
|
||||||
|
val msgs = mutableListOf<ByteArray>()
|
||||||
|
for (i in 0 until batchSize) {
|
||||||
|
val m = ByteArray(32) { (i * 7 + it).toByte() }
|
||||||
|
sigs.add(
|
||||||
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
|
.signSchnorr(m, privKey, auxRand),
|
||||||
|
)
|
||||||
|
msgs.add(m)
|
||||||
|
}
|
||||||
|
// Warmup both paths
|
||||||
|
repeat(500) {
|
||||||
|
for (j in 0 until batchSize) {
|
||||||
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
|
.verifySchnorr(sigs[j], msgs[j], batchPub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repeat(500) {
|
||||||
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
|
.verifySchnorrBatch(batchPub, sigs, msgs)
|
||||||
|
}
|
||||||
|
// Time individual
|
||||||
|
val iters = 1000
|
||||||
|
val indivStart = System.nanoTime()
|
||||||
|
repeat(iters) {
|
||||||
|
for (j in 0 until batchSize) {
|
||||||
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
|
.verifySchnorr(sigs[j], msgs[j], batchPub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val indivNs = System.nanoTime() - indivStart
|
||||||
|
val indivPerEvent = iters.toLong() * batchSize * 1_000_000_000L / indivNs
|
||||||
|
// Time batch
|
||||||
|
val batchStart = System.nanoTime()
|
||||||
|
repeat(iters) {
|
||||||
|
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||||
|
.verifySchnorrBatch(batchPub, sigs, msgs)
|
||||||
|
}
|
||||||
|
val batchNs = System.nanoTime() - batchStart
|
||||||
|
val batchPerEvent = iters.toLong() * batchSize * 1_000_000_000L / batchNs
|
||||||
|
val speedup = indivNs.toDouble() / batchNs.toDouble()
|
||||||
|
println(
|
||||||
|
String.format(
|
||||||
|
" batch(%2d): individual %,7d ev/s batch %,7d ev/s speedup %.1fx",
|
||||||
|
batchSize,
|
||||||
|
indivPerEvent,
|
||||||
|
batchPerEvent,
|
||||||
|
speedup,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
println("=".repeat(90))
|
||||||
println()
|
println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user