Nach dem Meltdown: Wie wir Event-Plattformen neu denken können

05.11.2019 – Stefan Adolf

Ein dezentralisierter Ansatz für die Verwaltung von Veranstaltungen, Konferenzen und Teilnehmern

Anfang Oktober erhielten die Veranstalter von lokalen Meetup-Gruppen eine Benachrichtigung von meetup.com, in der sie ankündigten, Änderungen an ihrem Geschäftsmodell vornehmen zu wollen. Verkürzt - und leicht missverständlich - stand da: jeder Teilnehmer sollte ab sofort eine Gebühr von 2 Dollar pro RSVP entrichten und die jährlichen Kosten für Organisatoren auf 24 Dollar gesenkt werden. Was sich nach einer relativ plausiblen Idee anhört, um Noshow-Raten der Teilnehmer zu senken, wurde auf Twitter binnen Minuten als verzweifelter Versuch interpretiert, die verbleibenden Vermögenswerte von meetup.com für den in Schieflage geratenen Mutterkonzern wework zu monetarisieren. Die Zusammenfassung der ganzen Geschichte lässt sich im Detail auch auf The Verge nachlesen.

Dieser Vorfall, den ich persönlich als "Meetup Meltdown" bezeichne, macht eines sehr deutlich: Solange man sich auf eine dritte Partei / Plattform verlässt, ist man voll und ganz auf deren guten Willen angewiesen und ihren neuen Ideen ausgeliefert. Sie legt die Regeln fest, nach denen alle Teilnehmer spielen müssen und sie kann sie im wesentlichen jederzeit ändern. Was aber noch schwerer wiegt: Wenn die Plattform wegen so einer Aktion untergeht, nimmt sie uns alle mit.


Nicht wenige Benutzer und Meetup-Gruppenbetreiber sprangen schon kurz nach dieser Ankündigung von Meetup.com ab - ungeachtet der Tatsache, dass die geplante Gebühr wirklich niemanden umbringen und an anwesende Teilnehmer zurückfließen würde, und auch sonst durchaus Vorteile für den Zusammenhalt von Meetup-Gruppen mitbrächte. Niemand wollte derjenige sein, der die Rechnung für WeWorks katastrophalen Versuch, an die Börse zu gehen, bezahlt. Was eine legitime Entscheidung ist. Aber genau hier liegt das Problem: Wo baut man eine reichweitenstarke Community mit lokalen Meetings auf, wenn nicht auf meetup.com? Über Nacht sprossen die Alternativen nur so aus dem Boden, wie die Blumen nach einem Regenschauer in der Wüste. Gefühlt tausende Tweets von besorgten Meetup-Gastgebern füllten meine Timeline, in denen sie alle fragten: "Welche Alternative würdest du empfehlen?" Und davon gibt es nicht gerade wenige.

Denkt man nur einen Moment darüber nach, wird klar: Jeder Versuch, von Meetup.com auf eine andere Plattform zu wechseln, wird mit digitalem Schweiß und Tränen enden. Denn wenn eine andere Plattform, auf die sich alle einigen, erst einmal die gesamte Last der Meetup-Bewegungen des Planeten Erde bewältigen muss, wird sie früher oder später auch zu dem Schluss kommen, dass es aus dem einen oder anderen Grund eine gute Idee sein könnte, Gebühren zu verlangen. Es ist also Zeit für intelligentere Ansätze, die es uns erlauben, Events neu zu denken.

Der Gründer von FreeCodeCamp Quincy Larson hat nur Stunden nach dem Meltdown einen Tweet gepostet, der seine weltweit aktive Coder-Gemeinschaft ermutigt, sofort mit dem Aufbau einer dezentralen Alternative für FCC Chapter zu beginnen. Und die Community folgte.

Innerhalb von Stunden startete ein dedizierter Discord-Kanal mit etwa tausend mehr oder weniger erfahrenen Hackern aus dem Umfeld von Free Code Camp. Nur einen Tag später öffnete sich ihr Github-Repo und die Diskussion mündete in einem endlosen Strom von Feature Requests. Alle schienen eine gute Idee zu haben, wie die Organisation von Events für lokale Communities verbessert werden könnte. Jeder wollte mit an Bord sein, wenn die ganze Welt beginnt, eine Open-Source Plattform für Meetup-Communities aufzubauen. Das erste Bild des Chapter README-Files zeigte ein Diagramm, das die Beziehungen zwischen Nutzern, Organisatoren, Veranstaltungsorten und Events darstellt – und natürlich wies dieses Entitätenmodell bestechende Ähnlichkeit mit dem Konzept auf, das wir bisher unter dem Namen Meetup.com kannten.

Das FreeCodeCamp "Chapter" Projekt geht jedoch noch einen Schritt weiter. Seine ersten Committer erkannten, dass eine zentralisierte Plattform früher oder später mit ähnlichen Problemen enden wird wie die Plattform, die sie ablösen wollten - also mussten sie die beweglichen Teile auf so viele Schultern wie möglich verteilen. Lokale Communities würden ihre eigenen Nodes betreiben und darauf die containerisierte Chapter-Software installieren.

Der einzige zentralisierte Teil des Systems – zumindest zum Zeitpunkt dieses Artikels – wäre ein Discovery-Service, der es neuen Benutzern ermöglicht, die Nodes ihrer lokalen Community zu finden. Eine hervorragende Idee. Aber man bräuchte immer noch Personen, die bereit und dazu in der Lage sind, einen laufenden Chapter-Node zu installieren und dauerhaft zu betreiben (was natürlich nicht kostenlos ist), und allen ihren bisherigen Mitgliedern zu sagen, dass sie sich nun darauf anmelden sollen.

Diese Art von verteilten, verknüpften Netzwerken ist nicht ganz neu: Mastodon und Matrix (das Rückgrat von Riot.im) bauen ziemlich erfolgreich Twitter- und Telegram-Ersatzdienste auf. Kazaa und Diaspora zeigten schon früh, dass P2P-basierte Filesysteme und soziale Netzwerke, die auf global verteilten Peer-Nodes laufen, perspektivisch die Schranken der Zentralisierung durchbrechen können.

Aber wollen wir wirklich im Jahr 2020 Software mit relationalen Datenbanken und server-orientiertem Anwendungs-Hosting entwickeln? SQL stammt aus dem Jahr 1974, und in Zeiten von Serverless Computing hinterlässt der Betrieb eines Servers auch hier das flaue Gefühl, mit Zombie-Technologie zu arbeiten.

Die Anfänge der Dezentralisierung

Ein Gespenst streift durchs Web-Land und sein Name ist "Web3": Eine offensichtliche Anknüpfung an das "Web2.0", das bis heute als Synonym für asynchrone und interaktive Web-Anwendungen gilt, und seinerzeit vor allem vom aufstrebenden "AJAX"-Verfahren angetrieben wurde, mit dem man Informationsfragmente im Hintergrund zwischen Clients und Servern austauscht. Bei Web3 geht es dagegen um dezentralisierte Anwendungen und Protokolle: Nicht ein einzelner Server, ein Unternehmen oder ein Entwickler sollte für die Verfügbarket, Assets und Daten verantwortlich sein, sondern das Netzwerk als Ganzes. Web3 hat seit seiner ersten Nennung durch Gavin Wood 2014 große Schritte in Richtung Produktivbetrieb unternommen und verändert die Art und Weise, wie wir verteilten Code ausführen und vertrauenswürdigen State unserer Anwendungen zwischen Peers synchronisieren.

Eine der bekanntesten und bewährtesten dezentralen Anwendungsplattformen ist die Ethereum Virtual Machine (EVM). Noch heute verstehen vermutlich viele die auf einer Blockchain basierende Ethereum-Plattform falsch – als einen bloßen ICO-fähigen, für von Crypto-Kiddie betriebenes Token-Mining gedachten, ökologisch höchst bedenklichen Krypto-Betrug. Oder einfach gesagt, als einen Bitcoin auf Steroiden. Nur, dass es das nicht ist.

Ethereum wurde von seinen Schöpfern als verteilter "Welt-Computer" erdacht, daher läuft auf jedem Ethereum-Node ein Bytecode Interpreter. Miner führen den auf der Ethereum-Blockchain verankerten Code gleichzeitig und deterministisch aus, während sie neue Blöcke minen. Nutzer können Anfragen an ausführbaren EVM-Bytecode zu ihren Transaktionen hinzufügen, die während des Minings ausgeführt werden. Da Anfragen und Binärcode Bestandteil der signierten Blockchain-Transaktionshistorie sind, sind sie nach dem Konsens unveränderbar, unfälschbar und unzerstörbar. Daher bezeichnet man diese Art von Code als smart contract: nicht verhandelbare Regelwerke, die auf immer und ewig in Stein gemeißelt sind (bzw. solange wenigstens ein Node auf dem Planeten noch eine Kopie der Ethereum-Blockchain kennt).

Ein kleiner Exkurs am Rande: Der Begriff "smart contract" wurde in den 90er Jahren von dem Mathematiker Nick Szabo geprägt, lange bevor überhaupt jemand auf die Blockchain-Idee kam. Nicht wenige nehmen an, dass Szabo selbst der Kopf hinter dem ominösen unbekannten "Satoshi Nakamato" ist, der einst mit seinem Whitepaper den Anstoß für den Bitcoin gab (was Szabo seitdem bestreitet). Das hält die Ethereum-Community nicht davon ab, den billionsten Teil eines Ethers als Hommage an seinen theoretischen Beitrag als 1 "Szabo" zu bezeichnen.

Verlässt man die philosophische Ebene, fühlt sich das Entwickeln eines Smart Contracts kaum anders an als eine Klasse in Typescript, C++ oder Java zu schreiben. Während es jedem freigestellt bleibt, seine eigene zu entwickeln, hat die Ethereum Foundation bereits 2014 die mittlerweile weit verbreite Smart Contract-Sprache "Solidity" eingeführt, die zu Bytecode für die EVM kompiliert. Smart Contracts mit Solidity haben einen intrinsischen Zustand, öffentliche und private Methoden, um den Zustand des Contracts mittels neuer Transaktionen verändern zu können, sie können Ereignisse auslösen, selbst Ethereum-Tokens besitzen, unterstützen pure functions und können Code von anderen Contracts und Interfaces erben. Für die meisten Entwickler sollten sich Smart Contracts mit Solidity daher recht vertraut anfühlen: Sie sind mehr oder weniger Klassen. Oder präzise ausgedrückt: Klassen für persistente Objekte, die Verhalten und einen veränderbaren Zustand repräsentieren. Jeder Account des Ethereum-Netzwerks kann nach dem Deployment eines Smart Contracts Instanzen von ihm erstellen und mit ihm per Transaktion interagieren.

pragma solidity ^0.4.24;

import "./IERC20.sol";
import "../../math/SafeMath.sol";

contract ERC20 is IERC20 {
using SafeMath for uint256;
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) private _allowed;
uint256 private _totalSupply;

function totalSupply() public view returns (uint256) {
return _totalSupply;
}

function balanceOf(address owner) public view returns (uint256) {
return _balances[owner];
}

function allowance(address owner, address spender) public view returns (uint256)
{
return _allowed[owner][spender];
}

function transfer(address to, uint256 value) public returns (bool) {
require(value <= _balances[msg.sender]);
require(to != address(0));

_balances[msg.sender] = _balances[msg.sender].sub(value);
_balances[to] = _balances[to].add(value);
emit Transfer(msg.sender, to, value);
return true;
}

function approve(address spender, uint256 value) public returns (bool) {
require(spender != address(0));

_allowed[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}

function transferFrom(address from, address to, uint256 value) public returns (bool)
{
require(value <= _balances[from]);
require(value <= _allowed[from][msg.sender]);
require(to != address(0));

_balances[from] = _balances[from].sub(value);
_balances[to] = _balances[to].add(value);
_allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
emit Transfer(from, to, value);
return true;
}

function increaseAllowance(address spender, uint256 addedValue) public returns (bool)
{
require(spender != address(0));

_allowed[msg.sender][spender] = (
_allowed[msg.sender][spender].add(addedValue));
emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
return true;
}

function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool)
{
require(spender != address(0));

_allowed[msg.sender][spender] = (
_allowed[msg.sender][spender].sub(subtractedValue));
emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
return true;
}

function _mint(address account, uint256 amount) internal {
require(account != 0);
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}

function _burn(address account, uint256 amount) internal {
require(account != 0);
require(amount <= _balances[account]);

_totalSupply = _totalSupply.sub(amount);
_balances[account] = _balances[account].sub(amount);
emit Transfer(account, address(0), amount);
}

function _burnFrom(address account, uint256 amount) internal {
require(amount <= _allowed[account][msg.sender]);
_allowed[account][msg.sender] = _allowed[account][msg.sender].sub(
amount);
_burn(account, amount);
}
}

Ein vollständiges Beispiel für einen ERC-20 Token in Solidity von OpenZeppelin

Die Interaktion mit einem Smart Contract ist nicht kostenlos, sofern man seinen Zustand verändern möchte. Miner, die den Code der Contracts ausführen, müssen CPU-Zyklen bereitstellen, um den Bytecode auszuführen und den neuen Zustand zu speichern. Ethereum führt daher das Konzept von "Gas" ein, um die Gebühren darzustellen, die für jeden Schritt der Code-Ausführung anfallen. Abhängig von der Komplexität der Operation schätzt ein Ethereum-Client vorab eine Menge an Gas, die für die Berechnung benötigt wird. Zum Zeitpunkt der Ausführung übersetzt ein Miner das Gas in "echte" Ether und erwartet, dass diese "Gebühr" zusammen mit der vertragsaufrufenden Transaktion gesendet wurde – sollten das gesendete Ether nicht ausreichen, lehnen die Miner die Transaktion ab. Jeder Nutzer, der mit einem Smart Contract in schreibender Richtung interagieren will, muss daher eine entsprechende Menge Ether mitbringen, um die Gas-Kosten zu begleichen.

Smart Contracts sind per Definition "Code der sich nicht stoppen lässt" und ermöglichen die Erstellung von so genannten dezentralen Apps (oder kurz Đapps). Ganz praktisch gesprochen sind das reine Frontend-Anwendungen, die den Ethereum-Ledger als Event-Source und vertrauenswürdige Datenbank nutzen.

Anfänge einer dezentralisierten Veranstaltungsplattform

Anfang August 2019, während einer internen Hacking-Veranstaltung namens "BREAKOUT", bildete ein Team von 5 Turbine Kreuzberg-Ingenieuren, POs und Agile Coaches das Team "Ethickets", um die Konzepte von Ethereum kennenzulernen und einen einfachen Meetup-Klon zu erstellen.

Unsere erste Idee war, nur eine Teilnehmerliste auf dem Ledger zu speichern und alle Veranstaltungsdetails in einer Symfony basierten Backend-Anwendung zu pflegen, die mit Ethereum auf einem lokalen Geth-Knoten über eine Elixir-basierte Gateway-Anwendung interagiert. Ein React Native Client sollte es den Benutzern dann ermöglichen, Meetups in seiner Nähe zu finden sich seinen Platz zu sichern. An der "Tür" der Veranstaltung könnte dann jemand manuell prüfen, ob ein Teilnehmer die öffentliche Adresse kontrolliert, die im Smart Contract für seinen "RSVP" hinterlegt ist.

Wer etwas Erfahrung mit Web3 hat, wird schnell die kleinen Mängel unseres allerersten Ansatzes bemerken: Die Symfony-Backend-Anwendung kann als zentralisierte Komponente jederzeit ausfallen. Weiterhin hätte man den Elixir-Teil ganz weglassen und sein Backend (den man für die Administration von Meetups benötigt) gleich auf die PHP-Seite verschieben können. Wir haben das nicht getan, weil unser Symfony-Entwickler sich dabei unwohl fühlte, aber es ist sicherlich möglich.

Ein deutlich kniffligerer Teil unseres Ansatzes war die Verwendung von React Native: Wie sich herausstellte, ist das Ausführen eines Web3-Clients, der mit Ethereum interagiert nicht die einfachste Sache, gar nicht so einfach, wenn Node.js dazwischensteht. Die crypto-Bibliothek von node ist nicht direkt mit der Web-Plattform kompatibel und muss für die Nutzung in React Native mit entsprechenden nativen Plugins ausgetauscht werden. Das führte uns tief in den Kaninchenbau von iOS-Pods und Android-JARs, und wir schmorten viele Stunden in der Dependency Hell des React Natvie Ökosystems. Wer wirklich mit Etherum von einem Web/nativen Client aus interagieren muss, können wir nur ans Herz legen, es statt mit web3.js mit ethers.js zu versuchen, aber diese Alternative blieb während der BREAKOUT-Week von uns unentdeckt.

Am Ende unseres Projekts stand dennoch ein funktionierender Prototyp, dessen einfacher Smart Contract etwa so aussah:

pragma solidity >=0.4.22 <0.6.0;
contract ETHicketEvent {
address payable hoster;

mapping (address => uint8) tickets;

constructor() public {
hoster = msg.sender;
}

function requestTicket(address attendee) payable public {
require (msg.value > 1000000);
hoster.transfer(msg.value);
tickets[attendee] += 1;
}

function getTicketCount(address requester) public view returns (uint8) {
require(msg.sender == hoster);

return tickets[requester];
}
}

Um grob zu erklären, was hier vor sich geht: Jeder, der ein Meetup veranstalten möchte, kann eine neue Instanz dieses Smart Contracts erzeugen und wird so zu seinem Eigentümer (Hoster = msg.sender). Um ein Ticket zu "kaufen", rufen die Teilnehmer die Methode requestTicket auf, die mindestens ein MWei mitschickt. Der Ticketpreis wird sofort an den Veranstalter überwiesen. Ein internes Mapping (mapping(address => uint8) tickets) merkt sich, welcher Teilnehmer ein Ticket gekauft hat.

Die Idee war, dass das zentralisierte Symfony Backend lediglich den Überblick über alle Smart Contract Instanzen behält. Veranstalter würden es nur für das Management aller Veranstaltungsdetails nutzen, der Ethereum Smart Contract wäre ausschließlich für Verkauf und Nachweis von "Tickets" der Teilnehmer verantwortlich. Die Eigentümer von Veranstaltungen würden nicht unbedingt Kenntnisse über die Blockchain in diesem Konzept benötigen. Dank der ewigen Verfügbarkeit einer Blokchain können wir auch heute noch in der Transaktionshistorie nachschlagen, welche virtuellen Tickets wir für ein virtuelles Event während der Breakout-Woche gekauft haben.

Nachdem unsere Interaktionsversuche mit Web3 in React Native gescheitert waren, schwenkten wir unser Frontend blitzschnell auf das seinerzeit brandneue ionic / React um und waren damit auch sofort mobil kompatibel zur bekanntesten Ethereum-Wallet MetaMask. Beim Start lädt unsere für Mobiltelefone optimierte Webanwendung alle Ereignisse vom zentralen API-Endpunkt und erstellt eine neue Web3-Wallet mit einem Ethereum Account, indem sie einen injizierten (in unserem Fall: fest eingebundenen) privaten Schlüssel verwendet. Von hier aus konnten wir Anfragen an die Adressen der Smart Contracts aus der API stellen, so dass wir effektiv in der Lage waren, Tickets von einem festen Benutzerkonto aus zu "kaufen". Wir nutzten hierfür das Ropsten-Testnet über einen JSON-RPC Infura-Node, konnten die App aber ebenso gut mit unserem eigenen geth-basierten Ropsten-Node testen. Leider erlaubte es unser Netzwerk nicht, diesen per Wifi zu verbinden, sodass wir zu Demonstrationszwecken schließlich auf Infura blieben.

Our smart contract in the Remix IDE with active MetaMask plugin

Exkurs: Đappcon

Der letzte Augenöffner
Was wir auf jeden Fall gelernt haben: Entweder man geht dezentralisiert oder nicht. Jeder zentralisierte Teil im gesamten Systems würde jeden anderen zerstören. Damit im Sinne habe ich Đappcon im August 2019 besucht.


Eine dreitägige Veranstaltung, die sich vollständig auf die neuesten Entwicklungen in der Welt des Ethereum konzentrierte, und sie war keine Enttäuschung. Hunderte von internationalen Gästen kamen in meine Heimatstadt, und ich fühlte mich plötzlich sehr verstanden – ich merkte, dass wir uns nicht auf irgendeinem esoterischen Pfad auf einem mysteriösen Alien-Tech-Stack befanden, sondern dass wir nicht gut genug auf das hörten, was so viele der Teilnehmer von Đappcon bereits verstanden: das Web2.0, wie wir es kennen, ist tot, die Zukunft hat schon vor Jahren begonnen. Smart Contracts in einer Art und Weise zu schreiben, wie wir es tun, könnte schon als alte Schule angesehen werden Dezentralisierte Autonome Organisationen übernehmen bereits (mehr oder weniger) unser Verständnis von Aktiengesellschaften und Gesellschaften mit beschränkter Haftung, wie wir sie kennen. Ich habe digitale Nomaden kennengelernt, die vom Bau von Ethereum-Apps leben, und sie werden mit Ethereum und Dai (einer von Ethereum unterstützten, auf USD ausgerichteten Stablecoin) bezahlt. Das war mein persönlicher Wendepunkt: **Ich musste diesen Drachen zum Fliegen bringen und ich musste ein Team dafür finden**.

The ĐOor — eine Einreichung für EthBerlinZwei

Nur einen Tag später stand der EthBerlinZwei hackathon kurz vor dem Abflug und ich war bereit, Geschichte zu schreiben. Ich machte einen Vorschlag, um Leute zu finden, mit denen man sich zusammentun konnte, und fand drei erstaunliche Musketiere, die sich der Partei anschlossen: Tam (Berlin/Israel), Ben (Litauen) und Niels (Hamburg) haben die Idee fröhlich adaptiert und wir begannen mit dem Remix. Kurz gesagt, wir wollten den letzten Teil des Puzzles lösen:

Wie identifiziert man jemanden, der zu einer Veranstaltung kommt, effektiv als gültigen (und kostenpflichtigen) Teilnehmer?


Darum haben wir unser Projekt ĐOor genannt. Es ist potentiell als "Türöffner" für jede Art von physischer und nicht-physischer Zugangsbeschränkung verwendbar und besteht aus drei Teilen:

  • Veranstaltungsmanagement. Jeder kann eine neue Veranstaltung anlegen und die Teilnehmerliste der Veranstaltung, die Ticketpreise und die Inhaltsdaten kontrollieren.
  • Ticketverkauf / RSVPs. Jeder kann alle erstellten Veranstaltungen entdecken und sich für sie anmelden, indem er Ether als Ticketpreis mitschickt.
  • der Rausschmeißer-Anwendungsfall. An der Tür wird die Identität eines Teilnehmers anhand der Teilnehmerliste überprüft. Wenn sie auf der Liste steht, wird der Eintritt gewährt.

Wir haben uns wieder für die Smart Contracts von Solidity entschieden, aber wir haben dafür OpenZeppelin Tools verwendet: Sie kommen mit einer großen Anzahl von geprüften Contracts, die nachweislich als großartige Grundlage für alle gängigen Anwendungsanforderungen funktionieren. Außerdem bauten wir unser Frontend auf einfachen Web-Technologien auf und entschieden uns für die Verwendung von Vue.js als Frontend-Bibliothek (was ich persönlich wegen der unnötigen konventionellen Komplexität im Vergleich zu React für eine eher ungünstige Wahl halte; aber das ist eine ganz andere Geschichte).


Eventmanagement

Um neue Ereignisse zu erstellen, interagieren die Benutzer mit einer einzigartigen Instanz einer so genannten DoorFactory, die in ihrem Namen neue ĐOor Contracts hervorbringt. Ein Ersteller ist der erste Eigentümer dieses Vertrags, so dass alle Gebühren auf sein Konto überwiesen werden. Dank des vorgefertigten Ownable-Basisvertrags von OpenZeppelin kann ĐOors leicht auf neue Eigentümer übertragen werden. Hier sind die interessanteren Teile des Codes, beginnend mit dem Door-Werksvertrag:

pragma solidity ^0.5.0;
import "./Ownership.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";
contract DoorFactory {
address[] public doorAddresses;
event NewDoorCreated(
address indexed doorOwner,
address indexed doorAddress,
string indexed doorName,
uint ticketPrice,
bool allowDisposeLeftovers
);
function createNewDoor(uint256 _price,
string memory eventName,
bool allowDisposeLeftovers)
public returns(address)
{
Door door = new Door();
door.initialize(_price, eventName, allowDisposeLeftovers);
door.transferOwnership(msg.sender);
doorAddresses.push(address(door));
emit NewDoorCreated(
msg.sender,
address(door),
eventName,
_price,
allowDisposeLeftovers
);
return address(door);
}
...
}

und die entstandenen Door-"Instanzen"-Contracts:

contract Door is Ownable, Initializable {
string public nameOfEvent;
uint public ticketPrice;

uint attendeesCount;
uint256 shares;
enum AttendanceTypes { NONE, REGISTERED, ATTENDED }
struct UserStruct {
AttendanceTypes ticketStatus;
}
mapping(address => UserStruct) public users;
function initialize(uint256 _price, string memory eventName)
public initializer payable
{
ticketPrice = _price;
nameOfEvent = eventName;
}

function buyEventTicket() public payable
{
require(users[msg.sender].ticketStatus == AttendanceTypes.NONE, 'User already has a ticket');
require(
msg.value == ticketPrice,
msg.value does not meet the ticket price.
);
this._owner.transfer(msg.value);
users[msg.sender].ticketStatus = AttendanceTypes.REGISTERED;
}
...
}

Teilnahme

Jede Client-Anwendung (z.B. unser Vue-Frontend) kann nun alle bestehenden Door Contracts, die erstellt wurden, über seine Vertragsadresse, die ABI des Contracts und den Aufruf seiner getAllDoors-Methode ermitteln. Sobald man sie entdeckt hat, kann man die buyEventTicket-Methode von Door aufrufen und eine entsprechende Ticketgebühr in Ethers senden. Jede Door führt eine eigene Teilnehmerliste und leitet das Geld direkt an den aktuellen Door-Besitzer weiter.

Ben führte eine Wendung in die Geschichte ein, die ein wenig tiefer geht: Wenn man eine Veranstaltung/Door als "abhebbar" markiert, müssen die Teilnehmer eine Vorauszahlung leisten, auch wenn die Veranstaltung frei ist (nicht ohne Bezug zur Idee von meetup.com). Sobald der Besitzer einen Teilnehmer als Teilnehmer der Veranstaltung identifiziert, hat der Teilnehmer das Recht, seine Anzahlung zurückzubekommen. Das ist eine ziemlich kluge Strategie, um die No-Show-Raten zu senken: Wenn Sie nicht zu einem Meetup gehen, ist Ihre Kaution verloren.

Das recht populäre "kickback" Projekt hatte diese Idee zuerst und führte eine ganze Reihe von Veranstaltungen durch, die die No-Show-Raten bereits auf ein Allzeittief senkten. Bens kleine Abhebungsmethode erlaubt es jedem Teilnehmer, seine Einlage abzuheben, nachdem er vom Gastgeber als anwesend markiert wurde und die Veranstaltung offiziell beendet ist.


Teilnahmenachweis

Und jetzt kommt der schwierige Teil. Wie kann man beweisen, dass jemand persönlich an einer Veranstaltung teilgenommen hat? Zunächst muss die Teilnehmerin an der Tür stehen und ihren Public Key irgendwie vorzeigen können, denn das ist der Datenbestand, den ein ĐOor Contract als Indikator für ihr "Ticket" speichert. Aber was hält sie davon ab, einem Freund (oder der ganzen Welt) einfach ihren Public Key zu geben? Immerhin ist er sogar in der Blockchain-History öffentlich sichtbar gespeichert, so dass jeder einfach einen öffentlichen Schlüssel, der sich angemeldet hat, aufheben und versuchen kann, mit ihm in die Veranstaltung zu gelangen.

Die einfachste Präventionsstrategie wäre es, eine öffentliche Ansprache, die an der Door vorbeigeht, als "besucht" zu markieren und jeden anderen Teilnehmer zu blockieren, der versucht, dieselbe Adresse erneut zu verwenden. Das würde jedoch bedeuten, dass wir jetzt möglicherweise den wahren Ticketinhaber ausschließen: Wer zuerst kommt, mahlt zuerst war noch nie eine gute Idee, außer in der Biologie.

Eine recht vielversprechende Variante ist die Ausgabe von ERC20 Tokens als Veranstaltungstickets. Jeder Token würde eine Eintrittskarte darstellen, und um in den Veranstaltungsort zu gelangen, überträgt man den Token einfach in die Wallet des Türstehers. Sobald diese Transaktion bestätigt ist, öffnet sich die Tür. Eine gute Idee, aber was hält jemanden davon ab, den Token einfach auf einen Freund zu übertragen? Das sollte bei kostenlosen Veranstaltungen kein Thema sein, aber für exklusive Veranstaltungen vielleicht nicht genau das, wonach man sucht. Nicht fungible ERC721 Tokens (NFTs) können dieses Problem beheben, aber sie widersprechen der Realität: Außer der aufgedruckten Nummer ist ein Ticket für die nächste Madonna-Show austauschbar. Das Gute an NFTs ist: Man könnte nahtlos nachweisen, wie es verwendet wurde, und es ist absolut einzigartig für den ersten Aussteller und Besitzer. Eigentlich eine großartige Idee für Anwendungen auf dem sekundären Ticketmarkt, aber hier ist der Vorbehalt: Die Transaktionen sind langsam.

Während es im Ropsten-Testnetz etwa 30 Sekunden dauert, bis eine Transaktion als bestätigt gelten kann, kann eine Token-Transaktion im Ethereum-Hauptnetz bis zu 2 Minuten dauern (durchschnittliche Blockzeit von 15s x 7 Bestätigungen). Wenn man auf seiner ĐOor kostenlosen Kaffee serviert, könnte das eine Option sein, aber sicherlich nicht für die Masse.
Wie sich herausstellt, hat die asymmetrische Kryptographie eine eingebaute Lösung für dieses Problem und es sind Private Key Signatures.



Sesam öffne Dich!

Hier ist also unsere Lösung: Wenn ein Teilnehmer auf ĐOor anklopft, erstellt der Türsteher eine zufällige 4-stellige Zeichenfolge und präsentiert sie dem Teilnehmer. Der Teilnehmer signiert dieses kurzlebige geteilte Geheimnis mit seinem privaten Schlüssel und erstellt einen QR-Code, der die Signatur enthält. Der Türsteher scannt den QR-Code auf dem Gerät des Gasts und ruft die ecRecover-Methode von Ethereum mit der ihm bekannten zufälligen Zeichenfolge auf.

Einchecken mit ĐOor

Diese Methode ergibt den Public Key des Teilnehmers, der in diesem Moment an der Tür steht. Er kann überprüfen, ob der öffentliche Schlüssel dem Vertrag von ĐOor bekannt ist (was eine Frage von Millisekunden ist, da er keine Statusänderung beinhaltet) und eine neue Transaktion auslösen, um den Status der Adresse auf "besucht" zu setzen. Da alle Operationen entweder außerhalb der Blockchain ablaufen oder nur den Vertragsstatus lesen, dauert der gesamte Verifizierungsprozess nicht länger als 5 Sekunden und ist noch schneller, wenn der Türsteher eine lokale Kopie aller Teilnehmeradressen behält.

Das ist die grobe Zusammenfassung dessen, was unser Team bei EthBerlinZwei erreicht hat. Unsere DevPost-Einreichung enthält Links zu allen Codes und unseren vorläufigen Demos.

Team Meeting: Eventmanagement und Discovery

Die Idee von ĐOor ist zwar ein fabelhaftes Instrument, um die "Tickets" eines Teilnehmers zu überprüfen, aber eine Frage bleibt offen: Sollten wir wirklich Veranstaltungen und ihre Metadaten auf dieser Plattform verwalten?

Wenn wir das täten, müssten Veranstalter ein Frontend verwenden, das uns gehört, um neue Veranstaltungen von einer Factory hinzuzufügen, die von uns in einem Format bereitgestellt wird, das wir uns ausdenken müssen. Kurz: Wir würden schließlich auch als das nächste meetup.com enden – mit einer dezentralisierten Datenbank, aber immer noch mit einem zentralen Konzept davon, wie Veranstaltungsdaten aussehen sollten.

Zeit für einen weiteren Hackathon: Unser völlig neues Team (mit mir als einzigem Teilnehmer, der vom Team ĐOor dazukam, die anderen sind Sascha, Jean Daniel, Sebastian und Hendrik) machte sich bei der Diffusion 2019 (20. Oktober) Gedanken darüber, wie wir Veranstaltungen auffindbar machen können, ohne sie zu besitzen oder zu definieren. Da es schwierig ist, ein völlig neues Team davon zu überzeugen, an etwas zu hacken, was ein Team zuvor hinterlassen hatte, gingen wir mit dem neuen Namen "Team Meeting" an den Start und begannen von vorne.


Nutzen, was schon da ist: das Semantic Web

Für den Datenteil kamen wir auf die Idee, mit dem loszulaufen, was bereits verfügbar ist, und wie sich herausstellt, ist die Idee eines Semantic Web sehr lebendig. Wahrscheinlich aus SEO-Gründen reichern viele Sites, die Veranstaltungsseiten anzeigen und hosten, ihr Markup mit Micro Formats oder strukturierten Daten an – kleinen, aber gut definierten Informationen, die entweder als zusätzliche Attribute innerhalb des Markups, als Meta-Header oder als Meta-Blöcke in Skript-Tags versteckt und als JSON-LD formatiert sind.

Ein Browser, der eine Seite besucht, die schema.org Microdata Formats enthält, erkennt sofort, dass der Benutzer eine Ereignisseite durchsucht und alle Metadaten daraus extrahieren könnte: Titel, Veranstaltungsort oder Öffnungszeiten sind alle in einem maschinenlesbaren Format verfügbar. Gut für uns, dass meetup.com und Eventbrite das stark ausnutzen: Sie stellen viele Veranstaltungsinhalte auf ihren Veranstaltungsseiten aus, in der Hoffnung, dass der GoogleBot sie sinnvoll nutzen kann. Hier ist ein Beispiel für JSON-LD-formatierte Metadaten auf Eventbrite für den kommenden React.Day:

Warum also sollte GoogleBot der einzige Client sein, der diese Daten sinnvoll nutzt?

Wir beschlossen, eine Chrome-Erweiterung zu schreiben, die Mikrodatenformate erkennt und Ereignisdaten aus Seiten extrahiert. Sobald sie ein Ereignis erkennt, bietet sie dem Benutzer eine "RSVP"-Aktion an (oder möglicherweise eine "Ticket kaufen"-Schaltfläche, wenn das Ereignis nicht kostenlos ist). Sobald ein Benutzer auf diese Schaltfläche klickt, werden die Veranstaltungsdaten an einen freien, dezentralisierten und globalen Speicher übertragen: IPFS.

Dank der Alpha-Version von ipfs-js, die ohne die Notwendigkeit funktioniert, einen eigenen oder einen dedizierten IPFS-Knoten zu betreiben – die Peer-Erkennung und das Hochladen geschieht nur im Browser (ipfs-js hält nur einige konstante Root Nodes, um Peers zu entdecken). Einmal auf IPFS erhält jedes Ereignis einen eindeutigen Inhaltsbezeichner, das CID. Nach einigen Sekunden werden die Informationen auf dem weltweiten dezentralen Speichernetzwerk gespeichert (z.B. https://ipfs.io/ipfs/QmevnNxM2qTRJp6DFdq7Xh2g5GoMXWLoNGAKSjTtKQRv1w).

Da wir nicht auf einen MetaMask / Web3-Kontext aus dem Bereich der Browser-Erweiterung zugreifen können, leiten wir den Benutzer auf eine statisch gehostete Đapp Landing Page um, die einen Web3-Kontext anfordert und mit der Original-URL der Ereignisseite und ihrem IPFS-CID ausgestattet ist. Hier kann sich der Benutzer schließlich für das Ereignis anmelden, indem er eine kleine Transaktion an die rsvpForEvent-Methoden von dedizierten Smart Contracts sendet:

pragma solidity ^0.5.8;
contract Meeting {
struct EventStruct {
address[] rsvps;
bytes cid;
bool isCanceled;
}
mapping(bytes32 => EventStruct) public events;
mapping(bytes32 => uint8) public meetups;
mapping(bytes32 => uint8) public attendees;
event MeetupCreated(string url, bytes cid);
event MeetupRSVPee(string url, address attendee);
function rsvpForEvent(string memory url, bytes memory cid) public {
bytes32 id = keccak256(abi.encode(url));
meetups[id] = meetups[id] + 1; // cheap existence check
if (meetups[id] > 1) {
events[id].rsvps.push(msg.sender);
} else {
emit MeetupCreated(url, cid);
}
bytes32 attend = keccak256(abi.encode(url, msg.sender));
attendees[attend] = 2; // code for attending
emit MeetupRSVPee(url, msg.sender);
}
function rsvpForExistingEvent(string memory url) public {
bytes32 id = keccak256(abi.encode(url));
require(meetups[id] > 0, "event needs to be created first");
bytes32 attend = keccak256(abi.encode(url, msg.sender));
attendees[attend] = 2;
emit MeetupRSVPee(url, msg.sender);
}
function isAttending(string memory url) public view returns (bool attending) {
attending = isOtherAttending(url, msg.sender);
}
function isOtherAttending(string memory url, address identity) public view returns (bool attending) {
bytes32 attend = keccak256(abi.encode(url, identity));
attending = attendees[attend] == 2;
}
function isRegistered(string memory url) public view returns (bool registered) {
bytes32 id = keccak256(abi.encode(url));
registered = meetups[id] > 0;
}
function countAttendees(string memory url) public view returns (uint users) {
bytes32 id = keccak256(abi.encode(url));
users = meetups[id];
}
}

Das ist ein mehr oder weniger funktionierender Contract, um neue Veranstaltungen und deren Teilnehmerliste im Ledger zu verankern. Was fehlt, ist der Entdeckungsteil (zeige mir Veranstaltungen in der Nähe von 'Berlin'). Dafür mussten wir etwas tiefer in die Geheimniskiste greifen:

The Graph: Ableitung einer GraphQL-API aus Ethereum-Events

Angenommen, man möchte Ereignisse in der Nähe nur durch Iteration von Ethereum-Transaktionen finden und deren verwandte Inhalte einzeln abrufen. Wenn die Menge der gespeicherten Ereignisse groß wird, wäre es ein massiver Aufwand, dies bei jeder Anfrage zu tun, und deshalb muss man sich eine (möglicherweise zentralisierte) Projektion ausdenken, die den erzeugten Zustand des Ereignisspeichers in einer abfragbaren Datenbank fortsetzt. Wenn wir die Ereignisdaten nur auf IPFS indizieren würden, könnte OrbitDB eine geeignete Option sein, aber da wir dabei sind, die Projektion zu aktualisieren, sobald ein RSVP auftritt, haben wir uns für The Graph entschieden.

GraphQL API inferred by events emitted by our smart contract

The Graph ist ein YML-konfigurierter GraphQL-SaaS / selbst gehosteter Dienst, der auf Events auf Ethereum und IPFS hört und seinen intern persistierenden Zustand gemäß den Eventdaten aktualisiert. Sie können Handler und Mappings geschrieben werden, die aufgerufen werden, sobald ein Ereignis ausgelöst wird, und innerhalb dieser kann man fast alles tun, was man brauchen, um Daten abzurufen, die mit dem Ereignis in Zusammenhang stehen. Die Graph-Mappings sind in AssemblyScript geschrieben, einer Untermenge von TypeScript, und zu einer ausführbaren WASM-Binärdatei kompiliert.

Es gibt zwei obligatorische Teile, um eine Graph-Instanz zu konfigurieren und zu erstellen: Zuerst muss man eine GraphQL-Schema-Definition für die projizierten Entitäten definieren. Der Graph leitet eine ORM-ähnliche Entitätsschicht ab, die man innerhalb Ihrer Handler zum Lesen und Schreiben von Entitäten in die von The Graph bereitgestellte "Speicher"-Abstraktion verwenden können. Der wichtigste Teil ist die "Subgraph"-Konfiguration, die The Graph verwendet, um Ereignis-Listener für Ihre intelligenten Verträge einzurichten und Ihre benutzerdefinierten Handler an Ereignisse und Entitäten zu binden:

specVersion: 0.0.2
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: Contract
network: ropsten
source:
address: "0xe742EF468584506C32B86541d0c3d4878857Af66"
abi: Contract
mapping:
kind: ethereum/events
apiVersion: 0.0.3
language: wasm/assemblyscript
entities:
- MeetupCreated
- MeetupRSVPee
abis:
- name: Contract
file: ./abis/Contract.json
eventHandlers:
- event: MeetupCreated(string,bytes)
handler: handleMeetupCreated
- event: MeetupRSVPee(string,address)
handler: handleMeetupRSVPee
file: ./src/mapping.ts

Bei dieser Konfiguration richtet The Graph Dienste ein, die MeetupCreated- oder MeetupRSVPee-Events des Contracts abhören und alle diese Daten in eine gehostete Datenbank (die unter der Haube tatsächlich Postgresql verwendet) mit einem automatisch generierten GraphQL-API einspeisen, die darüber liegt. Das Frontend kann einfach nach tiefen Grafikdaten fragen, z.B. um anstehende Veranstaltungen mit bestimmten Tags in einer bestimmten Stadt zu filtern.

Unsere letzte Devpost-Einreichung für "Team Meeting" kann hier gefunden werden. Derzeit befindet sich der Code noch im Hackathon-Zustand, funktioniert aber mehr oder weniger wie oben erklärt.

Konsolidierung & Schlussfolgerung

Was wir entwickelt haben und was wir im Begriff sind zu bauen

All der oben genannte Codes und die Informationen sind das Ergebnis von drei Wochenenden ohne Schlaf von etwa 10 Personen, die sich noch nie in ihrem Leben getroffen haben. Bitte habt also Geduld mit uns: Dies ist noch keine funktionierende Software.

Zum Zeitpunkt des Schreibens sind wir gerade dabei, herauszufinden, wie wir dieses Projekt weiterführen. Wir bleiben höchstwahrscheinlich auf der Ethereum-Chain, obwohl eine lebhafte Diskussion darüber geführt wird, ob Parity / Substrate, NEAR, elastic Eth Sidechains oder Eth2.0 / Serenity die Stacks sind, auf die man warten oder auf denen man aufbauen kann. Persönlich würde ich gerne beim bewährten Ethereum-Stack bleiben und MetaMask als "Identitäts"-Wallet und obligatorische Abhängigkeit für Interaktionen annehmen.

Wir werden sicherlich etwas Energie in den Ereigniserkennung / Speicherung / Entdeckung Teil der ganzen Geschichte investieren, da dies der am meisten prioritäre, föderierte und in Silos gestellte Teil des derzeitigen Ökosystems der Meetups/Events ist. Die Leute, die hinter unserer ĐOor Idee stehen, wollen noch einen Schritt weiter gehen und weitere, ereignisnahe Features hinzufügen (z.B. Kickback-ähnliche RSVP-Einlagen, die nach dem Nachweis der Teilnahme an ihren Ursprung zurücktransferiert werden), während Team Meeting es vorzieht, die Funktionalität so weit wie möglich zu entkoppeln, um nur mit der einfachen, in der Chain verankerten Ereignisdatenbank zu helfen.

Wer Teil der Diskussion sein möchte oder sich durch unseren Code wühlen oder zu ihm beitragen will, oder wer ihn abspalten und ein eigenes neues meetup.com aufbauen möchte: nur zu, der Weg ist frei – hier ist unsere "ĐOor" Organisation, die derzeit sowohl unser ĐOor als auch unser Team Meeting Repository pflegt: github.com/d0or . Wer uns in irgendeiner Weise unterstützen oder integrieren möchte, bitte keine Scheu. Nehmt Kontakt mit uns auf!