No description
  • Python 88.3%
  • HTML 5.8%
  • CSS 4.9%
  • JavaScript 0.8%
  • Dockerfile 0.2%
Find a file
2026-11-06 12:37:45 +01:00
accounts Initial commit 2026-09-28 19:41:55 +02:00
config . 2026-10-12 20:11:07 +02:00
locale/de/LC_MESSAGES . 2026-10-12 20:11:07 +02:00
lottery . 2026-10-12 20:11:07 +02:00
standalone_script Initial commit 2026-09-28 19:41:55 +02:00
static . 2026-10-12 20:11:07 +02:00
templates . 2026-10-12 20:11:07 +02:00
.dockerignore Initial commit 2026-09-28 19:41:55 +02:00
.env.example Initial commit 2026-09-28 19:41:55 +02:00
.gitignore Initial commit 2026-09-28 19:41:55 +02:00
compose.host-network.yaml Initial commit 2026-09-28 19:41:55 +02:00
compose.yaml Initial commit 2026-09-28 19:41:55 +02:00
Dockerfile Collect static files only during web startup 2026-11-06 12:37:45 +01:00
entrypoint.sh Initial commit 2026-09-28 19:41:55 +02:00
manage.py Initial commit 2026-09-28 19:41:55 +02:00
README.md Patch Python, Django, and Gunicorn security dependencies 2026-11-06 12:25:23 +01:00
requirements.txt Patch Python, Django, and Gunicorn security dependencies 2026-11-06 12:25:23 +01:00

USDT-Lotterie – Django und Ethereum ERC-20

Eine bewusst kleine, manuell betriebene USDT-Lotterie mit Django 6 und PostgreSQL. Bestätigte Zahlungen werden über Ethereum JSON-RPC aus ERC-20- Transfer-Logs gelesen und mit ihren Spielzuordnungen gespeichert. Überschüsse bleiben für folgende Spiele erhalten. Es gibt keine Registrierung und keine Hintergrundjobs; eine Admin-Aktion synchronisiert und beendet ausreichend finanzierte Spiele.

Voraussetzungen

  • Ubuntu oder eine vergleichbare Linux-Distribution
  • Docker Engine
  • Docker Compose Plugin (docker compose)

Prüfen, ob beides installiert ist:

docker --version
docker compose version

Python und PostgreSQL müssen nicht direkt auf dem Host installiert sein.

Projekt lokal starten

Im Projektverzeichnis ausführen:

docker compose up --build

Danach ist die Lotterie-Seite erreichbar unter:

http://localhost:8000

Beim ersten Start passiert automatisch Folgendes:

  1. Das Django-Image wird gebaut.
  2. PostgreSQL wird gestartet und auf Bereitschaft geprüft.
  3. Die eingebauten Django-Migrationen werden ausgeführt.
  4. Der Django Development Server startet auf Port 8000.

Lotterie einrichten und bedienen

Zuerst einen Superuser anlegen:

docker compose exec web python manage.py createsuperuser

Anschließend unter http://localhost:8000/admin/:

  1. Globale Lotterie-Konfiguration anlegen.
    • Modus: Simulation, Testnet oder Mainnet
    • Ethereum JSON-RPC URLs: eine HTTP(S)-URL pro Zeile, bevorzugter Anbieter zuerst; alle müssen dieselbe Chain verwenden. Bei Simulation leer lassen.
    • Zielwallet
    • USDT-Contract; Ethereum-Mainnet-USDT ist als Default gesetzt
    • USDT-Dezimalstellen: normalerweise 6
    • erforderliche Blockbestätigungen: Default 12
  2. Spiel anlegen. Standardwerte sind 100 USDT Zielbetrag und 1 USDT pro Los. Beim Anlegen wird nur lokal gespeichert, ohne RPC-Aufruf. Zahlungen und Überträge werden erst über die gemeinsame Synchronisieren-/Beenden-Aktion zugeordnet. Ein Spiel lässt sich daher auch bei nicht erreichbarem RPC anlegen. Optional einen Startblock (inklusiv) eingeben. Beim ersten Spiel wird ohne Eingabe die Erstellungszeit verwendet, später der vorherige Endblock + 1. Modus, Contract und Wallet werden dabei festgeschrieben. Ein gestartetes Spiel kann danach nicht verändert werden.
  3. Spiel auswählen und „Zahlungen synchronisieren und bei erreichtem Ziel beenden“ ausführen. Die Aktion speichert bestätigte Zahlungen und ordnet Überträge zu. Unter dem Zielbetrag bleibt das Spiel offen; später dieselbe Aktion erneut ausführen. Ist das Ziel erreicht und sind Startblock und Lose gültig, werden Endblock und Seed gespeichert und das Spiel beendet. Überzählige Beträge bleiben verfügbar. „Zugeordnet (Token)“ und „Noch verfügbar für nächste Spiele (Token)“ zeigen den gespeicherten Stand. Beispiel: 90 von 100 USDT → bleibt offen; nach weiteren 10 USDT dieselbe Aktion → beendet.
  4. Das beendete Spiel auswählen und „Gewinner auslosen und als CSV herunterladen“ ausführen.

Der CSV-Dateiname enthält Spiel-ID und, falls vergeben, den Namen, z. B. gewinner-spiel-5-september-test-game.csv.

Die CSV enthält ausschließlich wallet, betrag_usdt und preis mit einer Zeile pro Gewinner-Wallet. Mehrere Gewinne derselben Wallet werden in betrag_usdt addiert; preis listet alle gewonnenen Preise mit Semikolon getrennt auf. Alle CSV-Felder sind in doppelte Anführungszeichen eingeschlossen. Beim Import in LibreOffice Komma als Feldtrenner und " als Texttrenner verwenden, damit die Preisbezeichnungen in einer Zelle bleiben. So reicht eine Auszahlung pro Wallet, unabhängig von der Teilnehmerzahl. Die Auszahlung erfolgt manuell; diese Anwendung kennt und speichert keinen Private Key.

Startseite und Darstellung

Die Startseite von luckynumber.one zeigt den Hauptgewinn und den Zielbetrag in USDT der aktuellen Runde. Beträge und Lospreis kommen aus dem Spiel; Designbeispiele sind keine festen Werte. Bei einem Ziel von 1.000 USDT zeigt sie „Zielbetrag: 1.000 USDT“ und die verbleibenden Lose. Bei Teilbeträgen oder einem nicht durch den Lospreis teilbaren Ziel zeigt sie stattdessen die Anzahl gültiger Lose. Ein Fortschrittsbalken zeigt den gespeicherten Finanzierungsstand; der numerische Prozentwert wird nicht eingeblendet. Nach den bestehenden Regeln beträgt der Hauptgewinn dieser Runde 300 USDT (50 % des 60-%-Preispools).

Der Zahlungsstand wird nur bei der Admin-Aktion aktualisiert, nicht beim Seitenaufruf. Sobald das Ziel erreicht ist, zeigt die Seite einen entsprechenden Hinweis. Mehrere Preise können dieselbe Wallet treffen; die Zahl verschiedener Gewinner hängt von Teilnehmerzahl und Ziehung ab. Die Ziehung wird vom Betreiber ausgelöst.

„Lose kaufen“ öffnet die Zahlungsanleitung mit QR-Code, Zielwallet, Netzwerk und Token-Contract. Es können beliebig viele Lose mit einer Zahlung gekauft werden; Beträge über dem Rundenziel zählen für folgende Spiele. Es wird keine Transaktion automatisch gesendet. Die Simulation zeigt keine Zahlungsaufforderung. Die Startseite und die Ergebnisse unterstützen Hell- und Dunkelmodus: zunächst nach Systemeinstellung, dann nach der über den Schalter gespeicherten Browser-Präferenz. Dafür sind keine Datenbankfelder nötig.

Öffentliche Ergebnisse

Die öffentlichen Seiten sind standardmäßig auf Englisch: / zeigt das aktuelle Spiel, /completed-games/ die Ergebnisse. Der EN / DE-Schalter wechselt die Sprache und behält die aktuelle Seite bei. Deutsch ist unter /de/ und /de/abgeschlossene-spiele/ erreichbar; dort bleibt der bisherige deutsche Text erhalten. Navigation, Zahlenformatierung, Kopiermeldungen und Beschriftungen des Hell-/Dunkelmodus folgen der Sprache in der URL. Der bisherige Pfad /abgeschlossene-spiele/ leitet dauerhaft auf die deutsche Ergebnisseite weiter.

Die deutschen Übersetzungen liegen in locale/de/LC_MESSAGES/django.po; die kompilierte django.mo wird mit ausgeliefert. Nach Änderungen an den Übersetzungen vor dem Deployment python manage.py compilemessages -l de ausführen (benötigt GNU gettext).

Der Link „Completed games“ / „Abgeschlossene Spiele“ bleibt auch ohne aktives Spiel sichtbar. Die Seite zeigt die letzten drei beendeten Spiele, neueste zuerst: Spielname, Netzwerk, Gewinnsumme und die vollständigen öffentlichen Gewinner-Adressen mit Kopierbutton. Mehrere Preise derselben Wallet werden wie in der CSV zu einem Gesamtgewinn addiert.

Die Ergebnisse werden aus den gespeicherten Zuordnungen und dem unveränderten Ziehungs-Seed berechnet. Es gibt keine RPC-Aufrufe, neue Tabellen oder zusätzlichen Admin-Schritte. Simulation und Testnet sind entsprechend gekennzeichnet. Alte Spiele ohne gespeicherte Zuordnungen sowie unvollständige Ziehungen erscheinen mit einem Hinweis, dass ihre Gewinner hier nicht verfügbar sind. Ein beendetes Spiel ist kein Nachweis einer erfolgten Auszahlung; ein Auszahlungsstatus wird nicht gespeichert.

Los- und Auszahlungsregeln

Alle diesem Spiel zugeordneten Beträge derselben Absender-Wallet werden zuerst addiert. Erst danach wird auf ganze Lose abgerundet. Der ursprüngliche Zahlungsblock darf bei übertragenem Guthaben vor dem Startblock liegen:

  • 0,1 USDT ergibt 0 Lose.
  • 1,2 USDT ergibt 1 Los.
  • 0,3 + 0,7 USDT im selben Spiel ergibt 1 Los.
  • Bereits zugeordnete 0,8 USDT im ersten Spiel und zugeordnete 0,4 USDT im zweiten ergeben jeweils 0 Lose. Diese Los-Bruchteile werden nicht erneut benutzt.
  • Noch nicht zugeordnete 0,5 USDT Übertrag plus neue 0,5 USDT derselben Wallet ergeben im nächsten Spiel ein Los.

Bei einem Zielbetrag von 100 USDT beträgt der Preispool 60 USDT:

  • 30 USDT Hauptgewinn
  • 15 USDT Gewinn
  • 6 USDT Gewinn
  • 3 USDT Gewinn
  • sechs unterschiedliche Wallets erhalten jeweils 1 USDT zurück

Die vier Hauptpreise werden losgewichtet gezogen. Eine Wallet kann mehrfach gewinnen. Gibt es weniger als vier Lose, werden nach Verbrauch aller Lose diese erneut für die übrigen Hauptpreise verwendet. Die Rückzahlungen werden auf bis zu sechs unterschiedliche Wallets verteilt; bei weniger Wallets wird der gesamte Rückzahlungstopf unter diesen aufgeteilt. Ein vollständiges Los von einer einzigen Wallet reicht zum Beenden und Auswerten aus, sofern der Zielbetrag erreicht ist. Ohne vollständiges Los bleibt das Spiel offen. Auszahlungen werden in kleinsten Token-Einheiten berechnet; Rundungsreste der Hauptpreise fließen in den Rückzahlungstopf. Der beim Beenden gespeicherte Seed macht einen erneuten CSV-Export desselben Spiels reproduzierbar.

Modi und RPC

Simulation läuft vollständig lokal und verwendet die RPC-URL nicht. Beim Beenden werden bis zu zehn lokale Test-Wallets und die zum Zielbetrag gehörenden Lose erzeugt; damit können Ziehung und CSV ohne Blockchain getestet werden.

Testnet und Mainnet verwenden Ethereum JSON-RPC. Im Admin-Feld Ethereum JSON-RPC URLs können mehrere Anbieter hinterlegt werden:

https://rpc-anbieter-1.example/endpoint
https://rpc-anbieter-2.example/endpoint

Eine bestehende einzelne URL bleibt gültig. Leerzeilen werden ignoriert. Bei Verbindungsfehlern, Zeitüberschreitungen (30 Sekunden pro Anfrage), HTTP-Fehlern, ungültigen JSON-Antworten oder JSON-RPC-Fehlern wird der nächste Anbieter versucht. Ein funktionierender Anbieter wird für weitere Anfragen derselben Aktion bevorzugt; die nächste Aktion beginnt wieder mit dem ersten Eintrag. Beim Anbieterwechsel wird die Chain-ID mit der zu Beginn der Aktion ermittelten Chain verglichen. Schlagen alle Anbieter fehl, wird die Aktion mit einer Fehlermeldung abgebrochen. Die RPC-URLs können auch während eines aktiven Spiels geändert werden. Modus, Wallet, Contract, Dezimalstellen und Bestätigungen bleiben währenddessen gesperrt.

Beim Synchronisieren und Beenden prüft die Anwendung die Chain-ID und den bestätigten Endblock. Beim ersten erfolgreichen Scan wird die Chain festgeschrieben und die Token-Dezimalzahl geprüft. Viele Public RPCs begrenzen die Blockspanne von eth_getLogs; die Anwendung fragt deshalb in Blöcken von 2.000 Blöcken ab. Lehnen alle Anbieter diese Spanne ab, wird der RPC-Fehler angezeigt und das Spiel bleibt offen; die Blockspanne muss dann zu einem Anbieter passen (LOG_BLOCK_CHUNK).

Übertrag zwischen Spielen: Regeln und Beispiele

  1. Ab dem Start des ersten Spiels wird jede passende, bestätigte positive Zahlung erfasst. Andere Contracts/Empfänger, entfernte Logs, Nullbeträge und Selbstüberweisungen der Zielwallet werden nicht als Zahlungen verwendet.
  2. Zahlungen werden nach Block und Log-Index verarbeitet, unabhängig von der Reihenfolge der RPC-Antwort. Eine Transaktion kann mehrere Transfer-Logs enthalten; jeder Log wird separat erkannt und nur einmal gespeichert.
  3. Ein Spiel erhält höchstens seinen Zielbetrag. Die Zahlung, die das Ziel überschreitet, wird exakt aufgeteilt. Der Rest bleibt beim ursprünglichen Sender.
  4. Guthaben gilt nur für dieselbe Chain, denselben Token-Contract und dieselbe Zielwallet. Ein Konfigurationswechsel verbraucht kein Guthaben eines anderen Zahlungsbereichs. Bei Rückkehr zu diesem Bereich wird die Erfassung fortgesetzt.
  5. Erfasste Blöcke werden über den gespeicherten Scan-Stand verfolgt, auch wenn sie keine Zahlungen enthielten. Der nächste Scan beginnt beim Folgeblock. Ein manuell gewählter Spielstart setzt diesen Stand weder vor noch zurück.
  6. Erreichen allein schließt ein Spiel nicht automatisch. Der Admin beendet es. Mindestens ein vollständiges Los und ein ausreichend bestätigter Spielstart sind weiterhin erforderlich. Die vier Hauptpreise und Rückzahlungen bleiben zusammen 60 % des Ziels; der übrige 40-%-Anteil ist kein Übertrag.

Beispiele bei Ziel 100 USDT und Lospreis 1 USDT:

Eingegangene Zahlungen Aktuelles Spiel Folgende Spiele
A zahlt 40, B zahlt 60 A: 40 Lose, B: 60 Lose Kein Übertrag
A zahlt 125 A: 100 Lose 25 USDT für A
A zahlt 98, danach B 5 A: 98 Lose, B: 2 Lose 3 USDT für B
A zahlt 250 100 USDT für Spiel 1 100 für Spiel 2, 50 für Spiel 3
A zahlt 90 Offen mit 90 USDT Weitere 10 nötig
A zahlt 99,5, danach B 1 100 USDT zugeordnet; ganze Lose je Wallet 0,5 USDT für B
RPC liefert denselben Transfer zweimal Zahlung einmal gespeichert Kein doppeltes Guthaben

Startblock 1000 → nächstes Spiel 1100: Spiel 1 verbraucht seine 100 USDT und wird nach einem Scan bis Block 1020 beendet. B zahlt 25 USDT in Block 1050. Beim ersten Synchronisieren oder Beenden von Spiel 2 mit Startblock 1100 läuft der Scan ab 1021, sodass B seine 25 USDT zugeordnet bekommt. Neue 75 USDT in Block 1100 füllen das zweite Spiel. Auch überschüssige Transfers im selben Block wie die letzte Zahlung des ersten Spiels bleiben erhalten. Bei einem zukünftigen Startblock kann schon Guthaben zugeordnet werden; beenden lässt sich das Spiel erst, wenn dieser Block bestätigt ist.

„Fortlaufend“ bedeutet hier lückenlose Blockabdeckung bei der nächsten Aktion, keinen permanent laufenden Hintergrundprozess. Auch nach einer Pause oder zwischen Spielen werden die zwischenzeitlichen Zahlungen beim nächsten Scan abgeholt.

Admin-Ablauf für Spiel 2: Spiel 1 beenden → Gewinner-CSV exportieren → neues Spiel mit Ziel/Lospreis und optionalem Startblock anlegen → dieselbe gemeinsame Aktion ausführen. Dabei wird der Übertrag zugeordnet; manuelle Kopien oder Token-Transaktionen sind nicht nötig. Bei unzureichender Finanzierung weitere Zahlungen abwarten und die Aktion wiederholen. Bei ausreichend Guthaben wird das Spiel beendet, sobald der Startblock bestätigt ist und vollständige Lose vorliegen. Danach die Gewinner-CSV exportieren.

Gespeicherte Daten und Umstieg bestehender Spiele

Das MVP ergänzt genau zwei Tabellen:

  • Payment: Chain-ID, Contract, Empfänger, Transaktionshash, Block, Log-Index, Absender und Betrag in ganzzahligen Token-Basiseinheiten.
  • GameAllocation: Spiel, Zahlung und der diesem Spiel zugeordnete Teilbetrag.

Keine separate Guthaben-, Teilnehmer-, Ticket- oder Gewinner-Tabelle. Guthaben ist Zahlungsbetrag minus Summe der Zuordnungen. Game.last_scanned_block speichert den Scan-Stand; Game.uses_payment_ledger unterscheidet alte und neue Ziehungsregeln. DecimalField(78, 0) speichert auch große ERC-20-Basiseinheiten exakt in PostgreSQL. Payments und Game allocations sind im Admin ausschließlich lesbar. In Payments zeigt „Spiele / Zahlungsaufteilung“ alle zugehörigen Spiele mit Link und zugeordnetem Betrag. Beispiel: Zahlung 250 Token → Spiel A: 100, Spiel B: 100, für künftige Spiele reserviert: 50. Der Rest ist noch keinem konkreten Spiel zugeordnet. Ohne bisherige Zuordnung wird der Betrag in Basiseinheiten angezeigt, da die Zahlung selbst keine Token-Dezimalzahl speichert.

Scans, Zuordnungen und Beenden laufen in einer Datenbanktransaktion mit einer Sperre auf der Konfiguration. Parallele Aktionen können dasselbe Guthaben nicht doppelt verbrauchen. Bei RPC-Fehlern wird der Scan einschließlich neuer Zahlungen und Scan-Stand zurückgerollt. Ein erfolgreicher, aber unterfinanzierter Scan bleibt gespeichert; das Spiel bleibt offen. Für geschlossene neue Spiele verwenden Ziehung und CSV ausschließlich gespeicherte Zuordnungen und den bestehenden Seed. Ein erneuter Export benötigt dafür keinen RPC.

Vor Verwendung die Migration ausführen:

sudo docker compose -f compose.yaml -f compose.host-network.yaml exec web python manage.py migrate

Bereits geschlossene Spiele behalten ihre bisherigen Ergebnisse und benötigen für den Export weiterhin ihre alten RPC-Blockbereiche. Für neue Guthaben beginnt der erste Scan nach dem höchsten alten Endblock desselben Zahlungsbereichs. Alte Überschüsse werden nicht rückwirkend erneut verwendet, weil die alte Ziehung bereits sämtliche Zahlungen bis zu ihrem Endblock berücksichtigt hat. Ein bereits offenes Spiel wechselt zur neuen Zuordnung. Die Migration selbst ruft keinen RPC auf.

Die Tests in lottery/test_ledger.py prüfen die Beispiele, Zeiträume zwischen Spielen, Wiederholungen, RPC-Ausfälle, Grenzbeträge, Admin-Ablauf und den Umstieg. Zusätzlich zur schnellen SQLite-Suite können alle Backend-Tests auf einer separaten PostgreSQL-Testdatenbank ausgeführt werden:

sudo docker compose -f compose.yaml -f compose.host-network.yaml exec web python manage.py test lottery --settings=config.postgres_test_settings

Nur die PostgreSQL-Suite prüft echte Zeilensperren bei gleichzeitigen Aktionen und den exakten Übertrag einer einzelnen Basiseinheit bei einem 18-Dezimalstellen-Token. Die normale Anwendungsdatenbank bleibt unverändert; Django erstellt und entfernt eine eigene Testdatenbank. Die eigenständige Anwendung in standalone_script/ wurde nicht auf diese neuen Django-Übertragsregeln umgestellt.

Lokal testen: 1 mUSDT, ein Los, eine Wallet

Hier bedeutet mUSDT einen ganzen Token des eigenen Mock-USDT-Contracts, nicht 0,001 USDT. Die bisherigen USDT-Feldnamen und die CSV-Spalte betrag_usdt beziehen sich immer auf den im Spiel gespeicherten Contract.

  1. Lokal starten und Migrationen anwenden:

    docker compose up --build -d
    docker compose exec web python manage.py migrate
    
  2. Unter http://localhost:8000/admin/ die vorhandene Konfiguration prüfen: Testnet, dessen RPC-URL, eigener mUSDT-Contract, Zielwallet und die tatsächlichen Dezimalstellen des Contracts (häufig 6 oder 18). Mit der Listenaktion „RPC und Token-Contract prüfen (nur lesen)“ die Verbindung prüfen, oder im Terminal:

    docker compose exec web python manage.py check_lottery_rpc
    

    Der Check liest Chain-ID, bestätigten Block, Contract-Code, decimals() und Transfer-Logs des bestätigten Blocks. Er ändert keine Daten und sendet keine Transaktion. Eine falsche Dezimalstellen-Konfiguration wird gemeldet. Die ausgegebene Chain-ID mit dem gewünschten Testnet vergleichen.

  3. Spiel anlegen: Name 1 mUSDT Test, Zielbetrag 1, Preis pro Los 1. Als Startblock den gewünschten ersten Block oder den Block einer bereits erfolgten Testzahlung eingeben. Dieser Block zählt mit. Bereits in einem anderen Spiel verwendete Zahlungen nicht erneut einschließen. Alternativ Startblock leer lassen und erst nach dem Anlegen zahlen.

  4. Aus einer anderen Wallet 1 mUSDT des konfigurierten Contracts an die Zielwallet senden. Bei 6 Dezimalstellen entspricht das 1000000 Basiseinheiten, bei 18 Dezimalstellen 1000000000000000000. Danach die konfigurierten Bestätigungen abwarten. Überweisungen der Zielwallet an sich selbst zählen nicht.

  5. Spiel auswählen und „Zahlungen synchronisieren und bei erreichtem Ziel beenden“ ausführen, danach „Gewinner auslosen und als CSV herunterladen“. Bei genau einer teilnehmenden Wallet enthält die CSV eine Auszahlungszeile über 0,60 mUSDT (bei mindestens zwei Dezimalstellen). In preis stehen alle fünf Gewinne: Hauptgewinn 50 %;Gewinn 25 %;Gewinn 10 %;Gewinn 5 %;Einsatz zurück 1/1. Die Auszahlung bleibt manuell.

Um die eingegangene Zahlung ab einem konkreten Block zu prüfen:

docker compose exec web python manage.py check_lottery_rpc --start-block 123456

123456 durch den eigenen Startblock ersetzen. Der Check summiert bestätigte Zahlungen bis zum aktuellen bestätigten Block, ohne das Spiel zu beenden.

Für einen Test ohne Blockchain vor dem Anlegen Simulation wählen und Zielbetrag und Lospreis ebenfalls auf 1 setzen. Beenden und CSV-Export erzeugen dann lokal genau eine teilnehmende Wallet mit einem Los.

Automatisierte Tests (isolierte SQLite-Testdatenbank; die konfigurierte PostgreSQL-Datenbank bleibt unverändert):

docker compose exec web python manage.py test --settings=config.test_settings

Alternativ ohne Docker mit Python 3.12+:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python manage.py test --settings=config.test_settings

Die Tests umfassen Simulation und den vollständigen Admin-Ablauf gegen einen lokalen HTTP-JSON-RPC-Testserver. Den echten Testnet-Endpunkt separat mit dem obigen Check prüfen. Die Implementierung verwendet die Ethereum JSON-RPC API und ERC-20 Transfer-Events.

Sicherheitsprüfung vom 6. November 2026

Die Mindestversion ist Django 6.0.10. Die Django-Sicherheitsupdates vom Oktober und November beheben unter anderem DoS-Lücken in Headern, Sprachverarbeitung und Multipart-Uploads. Das Docker-Image verwendet Python 3.12.16. Gunicorn 26.2.2 enthält weitere HTTP-Parser-Korrekturen und wird bis zur Veröffentlichung auf PyPI aus dem offiziellen Repository installiert, fest auf Commit und SHA-256-Prüfsumme gebunden. Der Image-Build benötigt deshalb Zugriff auf GitHub. Next.js und Node-Paketmanifeste sind nicht vorhanden. Diese Prüfung ersetzt keinen Scan des gebauten Container-Images oder der Host-Pakete. Für die aktualisierte Python-Version das Image neu bauen; lokale virtuelle Umgebungen übernehmen ihren Python-Interpreter weiterhin vom Host.

Teilnehmer sollten aus einer Wallet zahlen, deren Adresse sie selbst kontrollieren. Bei einer direkten Auszahlung von einer zentralen Börse ist on-chain möglicherweise nur die Hot-Wallet der Börse sichtbar. Diese kann nicht zuverlässig einer Person als Gewinner zugeordnet werden.

Der Betrieb einer entgeltlichen Lotterie und die Verwendung von Kryptowährungen können erlaubnis-, steuer-, Verbraucher- und geldwäscherechtlichen Anforderungen unterliegen. Vor einem öffentlichen Mainnet-Betrieb ist eine rechtliche Prüfung erforderlich.

Mit Ctrl+C werden die laufenden Container beendet. Im Hintergrund starten:

docker compose up --build -d

Logs anzeigen:

docker compose logs -f web

Schrittweise Lotterie-Logs

Die Anwendung schreibt ab der nächsten Aktion nach logs/lottery.log. Im lokalen Compose-Setup liegt die Datei durch den Projekt-Bind-Mount auch direkt im Projektordner auf dem Host. Live verfolgen:

tail -F logs/lottery.log

Alternativ im Container:

sudo docker compose -f compose.yaml -f compose.host-network.yaml exec web tail -F /app/logs/lottery.log

Währenddessen den RPC-Check ausführen oder im Admin ein Spiel beenden bzw. Gewinner exportieren. Die Datei enthält Zeitstempel, Log-Level, Prozess/Thread und die einzelnen Schritte:

  • Spiel-ID, gespeicherter Token-Contract, Zielwallet und Startblock
  • RPC-Methode, Aufruf-ID, Laufzeit, HTTP-Status oder Netzwerkfehlertyp/Fehlernummer
  • Chain-ID, aktueller Block, Bestätigungen und letzter berücksichtigter Block
  • automatische Startblock-Suche und abgefragte Blockbereiche
  • Anzahl gefundener Logs, akzeptierte Zahlungen und Gründe für übersprungene Logs
  • empfangener Betrag und Zielbetrag in Token-Basiseinheiten, Losanzahl
  • abgelehntes oder erfolgreiches Beenden, Ziehung und CSV-Export

Bei 6 Dezimalstellen entsprechen 1000000 Basiseinheiten einem Token. Die Logs zeigen nur Ereignisse innerhalb des abgefragten Contracts, Empfängers und Blockbereichs; andere Zahlungen kommen vom RPC nicht zurück. Die Anwendung loggt keine vollständigen RPC-URLs, API-Keys, rohen RPC-Antworten oder Ziehungs-Seeds. Öffentliche Wallet-Adressen und Zahlungsbeträge stehen jedoch in dieser Diagnose-Datei. Frühere Aktionen werden nicht nachträglich protokolliert. Automatisierte Tests verwenden separate temporäre Logdateien.

Optional lassen sich LOTTERY_LOG_DIR (Standard: /app/logs im Container) und LOTTERY_LOG_LEVEL (Standard: INFO; DEBUG zeigt auch die einzelnen Block-Zeitstempel der Startblock-Suche) als Prozess-Umgebungsvariablen setzen. Für Compose diese bei Bedarf unter web.environment eintragen. Die Datei wird fortlaufend ergänzt. Für längeren Betrieb kann eine externe Rotation, beispielsweise mit logrotate, eingerichtet werden; der verwendete WatchedFileHandler öffnet eine ersetzte Logdatei automatisch neu. Logs werden weder in Git aufgenommen noch ins Docker-Image kopiert.

Container stoppen:

docker compose down

Standalone-Betrieb ohne Django, Docker oder PostgreSQL

lottery_standalone.py ist eine eigenständige Datei für Python 3.10+. Sie benötigt keine zusätzlichen Python-Pakete und kann zusammen mit einer Konfigurationsdatei auf einen anderen Rechner kopiert werden. Für Testnet/Mainnet ist weiterhin ein erreichbarer RPC erforderlich; Simulation und der erneute Export bereits gespeicherter Gewinner funktionieren offline.

Konfiguration setzen

Die Beispieldatei enthält den oben getesteten Sepolia-mUSDT-Contract und die Zielwallet. Bei Bedarf anpassen:

cp lottery-standalone.example.json lottery-standalone.json
python3 lottery_standalone.py configure --file lottery-standalone.json
python3 lottery_standalone.py check
{
  "network": "testnet",
  "rpc_url": "https://ethereum-sepolia-rpc.publicnode.com",
  "usdt_contract": "0xb5fDdeA9DE705e47756D21C981f6afD6982F3405",
  "target_wallet": "0xc35F8197D914C78eB3a0886Bf2746Dec76ccc4be",
  "token_decimals": 6,
  "confirmations": 12
}

Für einen vollständig lokalen Probelauf network auf simulation und rpc_url auf "" setzen. configure speichert die Angaben lokal; die Django-Konfiguration wird nicht ausgelesen oder verändert.

Spiel starten, beenden und Gewinner erhalten

python3 lottery_standalone.py start --name "1 mUSDT Test" --target 1 --ticket-price 1
python3 lottery_standalone.py status

Der Startbefehl gibt die Spiel-ID und den Startblock aus. Ohne --start-block beginnt ein Blockchain-Spiel mit dem nächsten Block nach dem aktuellen Chain-Stand; Zahlungen erst danach senden. Anders als Django benötigt der Standalone-Start einen funktionierenden RPC und speichert die Chain-ID sofort. Alternativ einen expliziten inklusiven Startblock setzen:

python3 lottery_standalone.py start --name "1 mUSDT Test" --target 1 --ticket-price 1 --start-block 123456

Nur einen der beiden Startbefehle ausführen und 123456 durch den gewünschten Block ersetzen. Es gibt maximal ein aktives Standalone-Spiel. Sobald die Zahlung bestätigt und der Zielbetrag erreicht ist, Spiel beenden (hier Spiel-ID 1):

python3 lottery_standalone.py end 1

Das schreibt standalone-data/winners-game-1.csv. Die Regeln entsprechen der Django-Anwendung: 60 % Preispool, ein vollständiges Los reicht, eine CSV-Zeile pro Gewinner-Wallet und alle Preisnamen mit ; verbunden. In Simulation wird bei Zielbetrag/Lospreis 1 ein lokales Testlos erzeugt.

Eigener CSV-Pfad und späterer Export:

python3 lottery_standalone.py end 1 --output ./winners.csv
python3 lottery_standalone.py export 1 --output ./winners-again.csv

Beim erfolgreichen Beenden werden Endblock, Seed und die endgültige CSV in einer SQLite-Transaktion gespeichert. Wiederholtes end zieht keine neuen Gewinner. export benötigt weder RPC noch Backend. Falls das Schreiben der CSV-Datei fehlschlägt, bleiben die gespeicherten Gewinner erhalten und lassen sich mit export erneut ausgeben. Auszahlungstransaktionen werden nicht gesendet.

Daten, Logs und Grenzen des Ersatzbetriebs

Neben dem Skript wird standardmäßig standalone-data/ angelegt:

  • lottery.sqlite3: Konfiguration, Spiele, Seeds und abgeschlossene Gewinner-CSV
  • lottery.log: RPC-Schritte, Transfers, Spielaktionen und Fehler
  • winners-game-ID.csv: Auszahlungen nach Wallet gruppiert
tail -F standalone-data/lottery.log

Ein anderes Datenverzeichnis mit --data-dir vor dem Unterbefehl wählen:

python3 lottery_standalone.py --data-dir /pfad/lotterie configure --file lottery-standalone.json
python3 lottery_standalone.py --data-dir /pfad/lotterie status

Immer dasselbe Verzeichnis verwenden. Die SQLite-Datei sichern, während kein Standalone-Befehl läuft; sie enthält auch die RPC-Konfiguration und privaten Seeds. Ein neuer Ordner enthält keine bisherigen Spiele. Neu angelegte Dateien sind nur für den ausführenden Benutzer zugänglich. Lokale Daten und Konfiguration werden von Git und Docker-Builds ausgeschlossen.

Django und Standalone haben getrennte Spielstände. Vorhandene Django-Spiele werden nicht automatisch übernommen oder synchronisiert. Für den Ersatzbetrieb einen eigenen neuen Spielzeitraum verwenden. Bereits ausgewertete Zahlungen nicht noch einmal über die andere Anwendung abrechnen. Das Skript verhindert innerhalb seiner eigenen Historie überlappende Blockbereiche für dieselbe Chain, denselben Contract und dieselbe Zielwallet. Solange ein Standalone-Spiel aktiv ist, bleibt seine Konfiguration unveränderlich.

Eigenständige Tests ohne Django:

python3 -S -m unittest test_lottery_standalone

Die Tests kopieren nur das Skript in ein temporäres Verzeichnis und prüfen Simulation, HTTP-RPC, Fehlerfälle, gespeicherte Gewinner und Offline-Export. Die Django-Tests vergleichen zusätzlich Ziehungsregeln und CSV-Format zwischen beiden Anwendungen.

Fehlerbehebung: RPC meldet „Network is unreachable“

check_lottery_rpc meldet diesen Fehler, wenn die Netzwerkverbindung zum konfigurierten RPC-Endpunkt scheitert. Die Contract-Prüfung kann dann noch nicht beginnen. Mögliche Ursachen sind VPN-/Firewall-Regeln für Docker, fehlende IPv4-/IPv6-Routen oder ein nur lokal erreichbarer RPC-Endpunkt.

Bei Eddie/AirVPN kann der Host Internetzugriff haben, während Verbindungen aus dem Docker-Bridge-Netz scheitern. build.network: host in compose.yaml gilt nur beim Image-Build, nicht für den laufenden Web-Container.

Allgemeinen HTTPS-Zugriff auf Host und im Container vergleichen:

curl -4 -I --max-time 10 https://ethereum.org
sudo docker compose exec web python -c 'import urllib.request; print(urllib.request.urlopen("https://ethereum.org", timeout=10).status)'

Diese Probe verwendet eine öffentliche Website, nicht den konfigurierten RPC. Funktioniert sie nur auf dem Host, kann für den lokalen Linux-Betrieb das vorbereitete Host-Netzwerk-Override verwendet werden:

sudo docker compose -f compose.yaml -f compose.host-network.yaml up -d
sudo docker compose -f compose.yaml -f compose.host-network.yaml exec web python manage.py check_lottery_rpc

Dabei werden die Container bei Bedarf neu erstellt und das bestehende postgres_data-Volume weiterverwendet. Django ist unter http://127.0.0.1:8000 erreichbar. PostgreSQL wird ausschließlich auf 127.0.0.1:55432 veröffentlicht, damit Django im Host-Netzwerk darauf zugreifen kann. Falls dieser Port belegt ist, LOCAL_POSTGRES_PORT in .env anpassen. Das Override ist für die lokale Linux-Entwicklung vorgesehen. Docker beschreibt das Verhalten unter Host networking.

Bei weiteren up-Befehlen beide -f-Optionen verwenden, solange dieses Setup gewünscht ist. Zur ursprünglichen Bridge-Konfiguration zurückkehren:

sudo docker compose -f compose.yaml up -d

Falls der RPC-Check auch mit Host-Netzwerk scheitert, die konfigurierte RPC-Adresse und die Erreichbarkeit ihres Hosts prüfen. Der Host benötigt eine passende Route zur IPv4- oder IPv6-Adresse des Anbieters. Ein erfolgreicher Zugriff auf eine andere Website bestätigt noch nicht die Erreichbarkeit dieses RPC-Endpunkts.

RPC meldet „Temporary failure in name resolution“

Der RPC-Hostname konnte nicht aufgelöst werden. Das Host-Netzwerk-Override bindet /etc/resolv.conf vom Host schreibgeschützt in den Web-Container ein, damit dieser den gleichen DNS-Resolver wie der Host verwendet. Bei aktiviertem Eddie-VPN ist dies der vom VPN konfigurierte Resolver. Nach dieser Konfigurationsänderung den Web-Container neu erstellen:

sudo docker compose -f compose.yaml -f compose.host-network.yaml up -d --force-recreate web
sudo docker compose -f compose.yaml -f compose.host-network.yaml exec web python manage.py check_lottery_rpc

Falls ein VPN-Wechsel die Host-Datei ersetzt, den Web-Container ebenfalls neu erstellen, um die aktuelle Datei einzubinden. Die Datenbank bleibt erhalten. Bei anhaltendem Fehler die Namensauflösung vergleichen:

getent ahostsv4 ethereum-sepolia-rpc.publicnode.com
sudo docker compose -f compose.yaml -f compose.host-network.yaml exec web getent ahostsv4 ethereum-sepolia-rpc.publicnode.com

Schlägt bereits der Host-Test fehl, zuerst die Host-/VPN-DNS-Verbindung prüfen. Hintergrund: Docker DNS-Konfiguration.

RPC antwortet mit HTTP 403 Forbidden

Eine HTTP-Antwort bedeutet, dass ein Server oder Proxy erreicht wurde. 403 meldet eine Zugriffsverweigerung; daraus lässt sich die genaue Ursache allein noch nicht ableiten. Die Fehlermeldung nennt zusätzlich die betroffene RPC-Methode, ohne die möglicherweise geheime RPC-URL auszugeben.

Für https://ethereum-sepolia-rpc.publicnode.com wurde eine Ablehnung des Standard-Headers Python-urllib reproduziert (HTTP 403, Cloudflare-Code 1010). Die Anwendung sendet deshalb einen Firefox-User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0. Mit diesem Header wurde derselbe eth_chainId-Aufruf erfolgreich geprüft. Nach Aktualisierung des Codes den Check erneut ausführen; bei der lokalen Compose-Konfiguration ist wegen des eingebundenen Quellcodes kein Image-Build erforderlich.

Im Dashboard des RPC-Anbieters prüfen:

  • Ist die konfigurierte URL der vollständige HTTP-JSON-RPC-Endpunkt des gewünschten Testnets, einschließlich eines eventuell erforderlichen API-Keys?
  • Ist der Key aktiv und für dieses Netzwerk freigegeben?
  • Beschränkt der Anbieter den Zugriff auf bestimmte öffentliche IP-Adressen oder Origins? Bei Nutzung eines VPNs ist dessen öffentliche Ausgangs-IP relevant, nicht 127.0.0.1 oder die interne Docker-IP.
  • Zeigen die Anbieter-Logs eine Firewall- oder Zugriffsverweigerung?

Beispielsweise dokumentiert Alchemy IP- und Origin-Beschränkungen. Bei einer Fehlersuche nur Anbieter und Hostname teilen, keine API-Keys oder geheimen URL-Pfade. Ein erneuter Image-Build behebt keine Zugriffsregel des Anbieters.

Fehlerbehebung: DNS beim Image-Build

Die Meldung Temporary failure in name resolution bedeutet, dass Docker Domainnamen wie pypi.org nicht auflösen kann. Es handelt sich nicht um einen Django- oder pip-Fehler.

Zuerst prüfen, ob die Namensauflösung auf dem Ubuntu-Host funktioniert:

getent hosts pypi.org

Anschließend die Namensauflösung in einem Container testen:

docker run --rm busybox nslookup pypi.org

Funktioniert DNS auf dem Host, aber nicht im Container, können DNS-Server für den Docker-Daemon konfiguriert werden. Falls /etc/docker/daemon.json bereits existiert, vorhandene Einstellungen beibehalten und nur den Schlüssel dns ergänzen:

{
  "dns": ["1.1.1.1", "8.8.8.8"]
}

Danach Docker neu starten und das Image ohne alten Build-Cache bauen:

sudo systemctl restart docker
docker compose build --no-cache web
docker compose up

In Firmen-, VPN- oder Proxy-Netzwerken müssen gegebenenfalls die DNS- beziehungsweise Proxy-Vorgaben des Netzwerks verwendet werden. Öffentliche DNS-Server können dort gesperrt sein.

Automatisches Neuladen bei Codeänderungen

Das gesamte Projektverzeichnis ist als Bind Mount unter /app in den Web-Container eingebunden. Django läuft im Entwicklungsmodus mit seinem automatischen Reloader.

Änderungen an Python-, Template- oder CSS-Dateien stehen deshalb direkt im Container zur Verfügung. Bei Python-Änderungen startet Django den Development Server automatisch neu. Nach Änderungen an HTML oder CSS muss die Seite im Browser neu geladen werden.

Ein zusätzlicher Image-Build ist nur erforderlich, wenn sich requirements.txt oder der Dockerfile ändert:

docker compose up --build

Umgebungsvariablen

Das Projekt funktioniert ohne zusätzliche Konfiguration mit lokalen Standardwerten. Optional kann eine .env angelegt werden:

cp .env.example .env

Anschließend können in .env diese Werte angepasst werden:

DJANGO_SECRET_KEY=replace-me-for-local-development
POSTGRES_DB=django
POSTGRES_USER=django
POSTGRES_PASSWORD=django

Die .env wird durch .gitignore und .dockerignore ausgeschlossen und soll nicht versioniert werden.

Mit DJANGO_ADMIN_URL=verwaltung/ ist das Admin-Panel unter /verwaltung/ erreichbar. Auch verschachtelte Pfade wie intern/verwaltung/ sind möglich; führende und abschließende Schrägstriche sind optional. Ohne Wert oder bei leerem Wert gilt /admin/. Bei einem eigenen Pfad entfällt die bisherige /admin/-Route. Lokal die Variable in .env, in Coolify als Laufzeit-Umgebungsvariable setzen. Danach den Web-Container mit docker compose up -d --force-recreate web neu erstellen beziehungsweise die Anwendung in Coolify neu deployen.

Nützliche Befehle

Django-Systemprüfung ausführen:

docker compose exec web python manage.py check

Migrationen manuell ausführen:

docker compose exec web python manage.py migrate

Superuser ausschließlich über die CLI erstellen:

docker compose exec web python manage.py createsuperuser

Eine öffentliche Registrierung ist nicht vorhanden. Auch über die Admin-Oberfläche können keine weiteren Benutzer angelegt werden.

Django-Shell öffnen:

docker compose exec web python manage.py shell

Datenbank zurücksetzen

PostgreSQL-Daten bleiben im Docker-Volume postgres_data erhalten. Zum vollständigen Löschen der lokalen Datenbank:

docker compose down -v

Beim nächsten docker compose up wird die Datenbank neu angelegt.

Projektstruktur

.
├── compose.yaml
├── Dockerfile
├── manage.py
├── requirements.txt
├── accounts/
│   ├── admin.py
│   ├── apps.py
│   └── tests.py
├── config/
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
├── templates/
│   └── index.html
└── static/
    └── css/
        └── styles.css

Hinweis zum Einsatz

Die lokale Compose-Konfiguration verwendet weiterhin runserver und DEBUG=True. Das Dockerfile startet dagegen Gunicorn und ist für die Bereitstellung mit Coolify vorbereitet.

Deployment auf Coolify

1. PostgreSQL anlegen

In Coolify eine PostgreSQL-Ressource erstellen. Die Datenbank muss nicht öffentlich erreichbar sein. Anwendung und Datenbank müssen demselben Coolify-Projekt bzw. demselben Zielserver-Netzwerk zugeordnet sein.

2. Anwendung anlegen

Eine neue Anwendung aus diesem Git-Repository erstellen und als Build Pack Dockerfile wählen. Folgende Werte konfigurieren:

  • Dockerfile: /Dockerfile
  • Port: 8000
  • Health Check Path: /
  • Domain: die gewünschte Domain mit https://, zum Beispiel https://app.example.com

Coolify übernimmt Reverse Proxy und TLS. In der Anwendung wird kein zusätzlicher Nginx- oder Caddy-Container benötigt.

3. Umgebungsvariablen setzen

In Coolify unter Environment Variables konfigurieren:

DJANGO_SECRET_KEY=<langer-zufaelliger-wert>
DJANGO_DEBUG=False
POSTGRES_DB=django
DJANGO_ALLOWED_HOSTS=app.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://app.example.com
POSTGRES_USER=django
POSTGRES_[PASSWORD] POSTGRES_HOST=<interner-coolify-datenbank-host>
POSTGRES_PORT=5432

Für POSTGRES_HOST den internen Hostnamen der Coolify-PostgreSQL-Ressource eintragen. localhost ist falsch, weil PostgreSQL in einem anderen Container läuft. Coolify zeigt den Host bei den internen Verbindungsdaten der PostgreSQL-Ressource an.

4. Deployen

Beim Containerstart werden Migrationen und collectstatic automatisch ausgeführt; anschließend startet Gunicorn auf Port 8000. Nach dem ersten erfolgreichen Deployment kann in Coolifys Terminal ein Administrator erstellt werden:

python manage.py createsuperuser

Vor Updates sollte über die Coolify-PostgreSQL-Ressource ein Backup eingerichtet und dessen Wiederherstellung getestet werden.