Przejdź do głównej zawartości

Python Bitcoin bsv-sdk: szyfrowanie wiadomości

Szyfrowanie, szyfrowanie

Bitcoin kojarzy się większości osób z przelewaniem.. bitcoina. Wysyłasz transakcję, ktoś ją odbiera. W rzeczywistości sieci takie jak Bitcoin SV potrafią znacznie więcej. Każda transakcja może nieść ze sobą dane. A skoro dane - to również wiadomości.

Python py-sdk bsv

Wyobraź sobie, że możesz wysłać komuś zaszyfrowaną wiadomość: bez serwera, bez pośrednika, bez możliwości ocenzurowania i zapisaną na zawsze w blockchainie. Mechanizm, który to umożliwia, nazywa się OP_RETURN (pisałem o nim tutaj).  W tym wpisie pokażę, jak wykorzystać OP_RETURN jako kanał komunikacji - zapisując zaszyfrowaną wiadomość, którą odczytać może tylko wybrany odbiorca. Całość zrobimy w Pythonie, używając biblioteki py-sdk-bsv.

Mógłbyś zapytać: ale po co ktoś miałby prowadzić komunikację, na publicznym blockchainie, szyfrując wiadomości? 

Zaszyfrowana wiadomość w OP_RETURN ma kilka unikalnych cech:

  • niezmienność
  • globalna dostępność
  • brak cenzury
  • prywatność

To połączenie jest bardzo rzadkie. Możesz wysyłać zaszyfrowane wiadomości komuś innemu albo opublikować zaszyfrowaną wiadomość lub nawet wielotomowe historie, wysyłając je na jakiś swój (pusty) nowo utworzony adres, a po kilku latach ujawnić klucz publicznie i treści z nim związane. Możesz też wysłać sam sobie taką zaszyfrowaną wiadomość, podpisać się, zapisać prototyp, pomysły, gromadzić dowody lub prowadzić dziennik a gdy przyjdzie potrzeba, udowodnić to już według formalnych procedur (które z pewnością kiedyś nadejdą).

Zaczniemy od wykorzystania poprzednich skryptów, bo tak naprawdę nic nowego nam nie dojdzie - no może kilka linijek :) Na początek trochę importów.

*Jedno zastrzeżenie - dodajemy w naszym imporcie EncryptedMessage i WhatsOnChainBroadcaster, zmienimy go też w linii kodu z rozgłoszeniem transakcji.

import nest_asyncio
nest_asyncio.apply()
import asyncio
from bsv import PrivateKey, P2PKH, ARC, Transaction, TransactionInput, TransactionOutput, OpReturn, WhatsOnChainBroadcaster
from bsv.encrypted_message import EncryptedMessage
import requests import json
Teraz kilka linijek na szybkie zrozumienie szyfrowania:
moj_wif = "xxxxxxxxxxxxxxxxxxx"
klucz_prywatny = PrivateKey(moj_wif)
address_testnet = klucz_prywatny.address(network="testnet")
url = f"https://api.whatsonchain.com/v1/bsv/test/address/{address_testnet}/confirmed/balance"
response = requests.get(url)
data = response.json()
saldo = data['confirmed']

# Szyfrowanie / Odszyfrowanie
nadawca = klucz_prywatny # Nadawca odb_wif = "xxxxxxxxxx" # Odbiorca odbiorca = PrivateKey(odb_wif) message = "To nasza wiadomość" encrypted = nadawca.public_key().encrypt_text(message) print(f"Wiadomość od A: {message}") print(f"Zaszyfrowana wiadomość A: {encrypted}") print(f"Odszyfrowana wiadomość przez B: {odbiorca.decrypt_text(encrypted)}")
Teraz wykonuję kod 4 razy. Zobacz co się stało:

Wykonanie kodu
Powyższy przykład  jest "słaby kryptograficznie" do prywatnej komunikacji, bo używa prostego szyfrowania kluczem publicznym bez dodatkowego kontekstu, bez unikalnego klucza i identyfikatora. Tylko jako ciekawostkę go podałem abyś zobaczył, że klucze używają losowych nonces. Dlatego ten sam tekst po zaszyfrowaniu, będzie za każdym razem wyglądał inaczej.

Ok, teraz zróbmy to jak należy z użyciem klasy EncryptMessage:
# Szyfrowanie / Odszyfrowanie
nadawca = klucz_prywatny # Nadawca
odb_wif = "xxxxxxxxxx"
odbiorca = PrivateKey(odb_wif)

message = b"To nasza wiadomość" encrypted = EncryptedMessage.encrypt( message=message, sender=nadawca, recipient=odbiorca.public_key()) print(f"Wiadomość od A: {message}") print(f"Zaszyfrowana wiadomość A: {encrypted}") try: decrypted = EncryptedMessage.decrypt( message=encrypted, recipient=odbiorca)
# w 3 wykonaniu zamieniam na nadawcę :) print(f"Odszyfrowana wiadomość przez B: {decrypted.decode()}") except Exception as e: print("Nie udało się odszyfrować wiadomości") print("Powód:", str(e))
Co się teraz zadzieje jak wykonany kod? Sprawdźmy!
Wykonuję go 2 razy z użyciem recipient = odbiorca i 1 raz recipient = nadawca.

Wykonanie poprawnego kodu + błąd

Widzimy, że B odszyfrował wiadomość dwukrotnie, za trzecim razem gdy podstawiłem klucz prywatny pochodzacy od A, odszyfrowanie się nie powiodło. Nasza wersja szyfrowania z EncryptMessage jest dużo lepsza, bo nie traktuje każdej wiadomości jak jednorazowego zaszyfrowania tekstu, tylko jak część większego systemu komunikacji między dwiema osobami. 

W prostym podejściu szyfrujemy wiadomość w sposób dość bezpośredni - używamy klucza publicznego odbiorcy i dostajemy zaszyfrowany tekst. To działa, ale jest dość.. płaskie: każda wiadomość jest niezależna, a mechanizm nie ma dodatkowego kontekstu ani dodatkowych warstw bezpieczeństwa.

W drugiej wersji [EncryptedMessage] dzieje się coś bardziej zaawansowanego. Zamiast jednego prostego klucza, system buduje wspólny sekret pomiędzy nadawcą i odbiorcą, który jest tworzony osobno dla każdej wiadomości. Dzięki temu nawet jeśli ktoś przechwyci jedną zaszyfrowaną wiadomość, nie ma żadnej możliwości wykorzystania jej do odszyfrowania innych. Klasa EncryptedMessage znajduje się w tym miejscu naszej biblioteki py-sdk.

Rozgłoszenie transakcji z wiadomością w sieci Bitcoin SV.

No w tym przykładzie, polecimy już w internety. Nasza zaszyfrowaną wiadomość od A do B, zapakujemy i wyślemy w OP_RETURN. Dodatkowo dla B wyślemy 2 satoshi, żeby poinformować, że posiada niewydane UTXO.
address_odbiorcy = odbiorca.address(network="testnet")

url = f"https://api.whatsonchain.com/v1/bsv/test/address/{address_testnet}/unspent/all"
response = requests.get(url)
data = response.json()
niewydane_utxo = len(data['result'])

print(f"Adres testnet : {address_testnet}")
print(f"Saldo : {saldo} satoshi / {saldo / 100000000} BSV")
print(f"Ilość niewydanych UTXO: {niewydane_utxo}")
for i in data['result']:
    print(f"Index : {i['tx_pos']}, Hash : {i['tx_hash']}, Satoshi : {i['value']}")

txid = data['result'][1]['tx_hash']
tx_pos = data['result'][1]['tx_pos']
print(f"\nTransakcja z niewydanym UTXO: {txid}")
print(f"Indeks transakcji: {tx_pos}\n")

url = f"https://api.whatsonchain.com/v1/bsv/test/tx/{txid}/hex"
response = requests.get(url)
tx_hex = response.text
print(f"HEX : {tx_hex}\n")

op_ret = OpReturn()
block_script = op_ret.lock([encrypted])
do_wyslania = 2 # Poinformujemy odbiorcę, że ma jakieś nowe UTXO :)

async def create_and_broadcast_transaction():
    
    tx = Transaction()

    tx.add_input(TransactionInput(
        source_transaction = Transaction.from_hex(tx_hex),
        source_output_index = tx_pos,
        unlocking_script_template = P2PKH().unlock(klucz_prywatny)))

    tx.add_output(TransactionOutput(
            locking_script=P2PKH().lock(address_odbiorcy),
            satoshis=do_wyslania)) # <-- 2 satoshi

    tx.add_output(TransactionOutput(
        locking_script = block_script,
        satoshis = 0))

    tx.add_output(TransactionOutput(
        locking_script = P2PKH().lock(address_testnet),
        change = True))

    tx.fee()
    tx.sign()
    
    response = await tx.broadcast(WhatsOnChainBroadcaster("test"))
    print(f"Status rozgłoszenia: {response.status}")
    try:
        print(f"Opis rozgłoszenia: {response.description}")
    except:
        pass
    print(f"ID transakcji: {tx.txid()}")

if __name__ == "__main__":
    asyncio.run(create_and_broadcast_transaction())
Uruchamiamy kod, nasza transakcja została rozgłoszona w sieci, możemy zobaczyć nasz OP_RETURN, 2 satoshi jako poinformowanie adresu i naszą resztę:
Rozgłoszona transakcja

Szybki podgląd: dane [ASCII] to surowy bajtowy string, więc explorer próbuje go na siłę wyświetlić jako UTF-8 i wychodzą śmieci; lepszy jest HEX i to jego będziemy pobierać do odszyfrowania wiadomości:
op_return i hex

Pobranie i odszyfrowanie wiadomości z blockchain.

Teraz stawiamy się na miejscu odbiorcy. Wiemy, że ktoś wpłacił nam 2 satoshi - dość nietypowe 2 satoshi :P - dlatego przyjrzyjmy się bliżej co to była za transakcja, pobierzmy ją poprzez API, oddzielmy sobie nasz ładunek w OP_RETURN (pamiętamy, że pobieramy hex) i odszyfrujmy. 
from bsv.encrypted_message import EncryptedMessage
import requests
import json


odb_wif = "klucz_prywatny_odbiorcy" # Odbiorca
odbiorca = PrivateKey(odb_wif)
address_odbiorcy = odbiorca.address(network="testnet")
url = f"https://api.whatsonchain.com/v1/bsv/test/address/{address_odbiorcy}/unspent/all"
response = requests.get(url)
data = response.json()
niewydane_utxo = len(data['result'])

print(f"Adres testnet : {address_odbiorcy}")
print(f"Ilość niewydanych UTXO: {niewydane_utxo}")
for i in data['result']:
    print(f"Index : {i['tx_pos']}, Hash : {i['tx_hash']}, Satoshi : {i['value']}")
txid = input("Wybierz UTXO gdzie siedzi 2 satoshi:")
# <-- Tu wkleisz swoje txid
url = f"https://api.whatsonchain.com/v1/bsv/test/tx/hash/{txid}" response = requests.get(url) data = response.json() wiadomosc = data['vout'][1]['scriptPubKey']['hex'] data = bytes.fromhex(wiadomosc) payload = data[3:]
# <-- ucinamy 3 pierwsze bajty try: decrypted = EncryptedMessage.decrypt( message=payload, recipient=odbiorca) print(f"Odszyfrowana wiadomość przez B z Blockchain: {decrypted.decode()}") except Exception as e: print("Nie udało się odszyfrować wiadomości") print("Powód:", str(e))
Po uruchomieniu kodu mamy taki oto wynik:

Wynik z blockchain BSV

Na adresie odbiorcy miałem 4 UTXO, wybrałem czwarte: 9d3c8.. gdzie siedziało 2 satoshi, po drodze wyświetliłem jeszcze nasz surowy ładunek a na koniec odszyfrowałem. 
Wynikiem jest treść: "To nasza wiadomosc".
W tej sytuacji jaką widzisz, odbiorca nawet nie musi posiadać jakiegoś zaawansowanego portfela czy aplikacji, no i przede wszystkim nie musi za nic płacić. Gdyby komunikacja odbywała się A <--> B wtedy płaciliby oboje. 

Ok, czy to było trudne? No nie wydaje mi się. Tak prawdę mówiąc to większość przykładów, które tu widzisz na blogu - jest tylko u mnie. Nie istnieją też w oficjalnej dokumentacji z przykładami. Dlatego jak chcesz opanować py-sdk to radzę nie czytać przykładów przed snem :P  Kilka miesięcy temu, zgłosiłem też ciekawy przykład na wykorzystanie skryptów Bitcoina dla transakcji OP_CAT. Nie zgadniesz :) Tak jest już wprowadzony do biblioteki py-sdk i na tym blogu pokażę jak go używać. Jeszcze nie wiem kiedy zabiorę się za poradniczek z OP_CAT ale zaglądaj, odwiedzaj mnie, to może go spotkasz. Tak sobie teraz myślę, że jeszcze zapomniałem napisać o "wybijaniu" NFT :). Takie transakcje przechodzą w dość prosty sposób z wykorzystaniem 1SatOridinals - można umieszczać tekst lub zdjęcia. Fajna sprawa i też będzie niebawem opisana na blogu. Btw - mój adres odbiorcy ma chyba UTXO z jakimś NFT jaks się nie mylę.
No dobra spadam. Do następnego! Cześć!



 **Gwarantuję Ci niezmienność moich treści**

Hash artykułu:

ID transakcji: sprawdź OP_RETURN i porównaj jego hash

Komentarze

Popularne posty

Status w życiu: confirmed. Dobra, super, a co dalej?

Discord kontra Forum – dlaczego Twój mózg tęskni za phpBB

Cyfrowy minimalizm - mniej pingów, więcej spokoju