Ich habe kürzlich einen Artikel über Karma Money geschrieben, ein alternatives Währungssystem, das auf einem einzigartigen ERC-20-Token basiert. Ich habe mir Karma Money als geschlossenes System vorgestellt, bei dem Benutzer auch Transaktionsgebühren mit Karma bezahlen können. Der Aufbau einer eigenen Blockchain wäre eine Möglichkeit, dies zu verwirklichen, aber es ist eine anspruchsvolle Aufgabe. Um die Sicherheit und Glaubwürdigkeit der Blockchain zu gewährleisten, müssten wir die nötige Infrastruktur und eine ausreichend große Community aufbauen. Es wäre viel einfacher, eine bestehende Blockchain zu nutzen. Es gibt Ketten wie Gnosis oder Polygon , die vollständig mit Ethereum kompatibel sind und sehr niedrige Transaktionsgebühren haben. Die Gebühr für eine ERC20-Transaktion auf diesen Ketten beträgt typischerweise weniger als 1 Cent. Das Problem besteht darin, dass diese Gebühr in der eigenen Kryptowährung der Kette bezahlt werden muss, was die Nutzung der Kette für Benutzer erschweren kann. Glücklicherweise gibt es eine Überbrückungslösung, die EIP-712- Metalltransaktionen.
Im Falle einer Metatransaktion erstellt der Benutzer eine Struktur, die eine Transaktion beschreibt, und signiert sie dann digital mit seinem privaten Schlüssel. Es ist vergleichbar mit dem Ausstellen eines Schecks für jemanden. Die digital signierte Transaktion wird dann an einen Relay-Knoten gesendet, der sie an einen Smart Contract weiterleitet. Der Vertrag überprüft die Signatur und führt die Transaktion aus, wenn sie gültig ist. Der Relay-Knoten zahlt für die Vertragsausführung.
Bei einer Karma-Transaktion gibt der Benutzer beispielsweise den Transaktionsbetrag (z. B. 10 Karma-Dollar), die Ethereum-Adresse, an die er den Betrag senden möchte, und eine Transaktionsgebühr (in Karma-Dollar) an, die er anzubieten bereit ist für die Transaktion. Diese Struktur wird digital signiert und an einen Relay-Knoten gesendet. Wenn der Knoten die Transaktionsgebühr für akzeptabel hält, übermittelt er die digital signierte Struktur an den Karma-Vertrag, der die Signatur überprüft und die Transaktion ausführt. Da die Transaktionsgebühr vom Relay-Knoten in der nativen Währung der Blockchain bezahlt wird, sieht es für den Benutzer so aus, als würde er die Transaktion mit Karma-Dollar bezahlen, ohne eine eigene Blockchain zu benötigen.
Nach der Theorie werfen wir einen Blick auf die Praxis.
Der EIP-712-Standard definiert, wie strukturierte Datenpakete standardisiert signiert werden. MetaMask zeigt diese strukturierten Daten in einem für den Benutzer lesbaren Format an. Eine EIP-712-kompatible Struktur, wie sie auf MetaMask gezeigt wird ( kann unter dieser URL getestet werden ), sieht folgendermaßen aus:
Die obige Transaktion wurde mit dem folgenden einfachen Code generiert:
async function main() { if (!window.ethereum || !window.ethereum.isMetaMask) { console.log("Please install MetaMask") return } const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); const chainId = await window.ethereum.request({ method: 'eth_chainId' }); const eip712domain_type_definition = { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ] } const karma_request_domain = { "name": "Karma Request", "version": "1", "chainId": chainId, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" } document.getElementById('transfer_request')?.addEventListener("click", async function () { const transfer_request = { "types": { ...eip712domain_type_definition, "TransferRequest": [ { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" } ] }, "primaryType": "TransferRequest", "domain": karma_request_domain, "message": { "to": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", "amount": 1234 } } let signature = await window.ethereum.request({ "method": "eth_signTypedData_v4", "params": [ accounts[0], transfer_request ] }) alert("Signature: " + signature) }) } main()
Die eip712domain_type_definition ist eine Beschreibung einer allgemeinen Struktur, die die Metadaten enthält. Das Namensfeld ist der Name der Struktur, das Versionsfeld ist die Definitionsversion der Struktur und die Felder „chainId“ und „VerificationContract“ bestimmen, für welchen Vertrag die Nachricht bestimmt ist. Der ausführende Vertrag überprüft diese Metadaten, um sicherzustellen, dass die unterzeichnete Transaktion nur für den Zielvertrag ausgeführt wird.
Die karma_request_domain enthält den spezifischen Wert der Metadaten, die durch die EIP712Domain-Struktur definiert sind.
Die eigentliche Struktur, die wir zur Signatur an MetaMask senden, ist in der Variablen transfer_request enthalten. Der Typenblock enthält die Typdefinitionen. Hier ist das erste Element die obligatorische EIP712Domain-Definition, die die Metadaten beschreibt. Darauf folgt die eigentliche Strukturdefinition, in diesem Fall der TransferRequest. Dies ist die Struktur, die dem Benutzer in MetaMask angezeigt wird. Der Domänenblock enthält den spezifischen Wert der Metadaten, während die Nachricht die spezifische Struktur enthält, die wir mit dem Benutzer signieren möchten.
Wenn es um Karma-Geld geht, sieht ein Beispiel dafür, wie eine Metatransaktion zusammengestellt und an den Smart Contract gesendet wird, so aus:
const types = { "TransferRequest": [ { "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "fee", "type": "uint256" }, { "name": "nonce", "type": "uint256" } ] } let nonce = await contract.connect(MINER).getNonce(ALICE.address) const message = { "from": ALICE.address, "to": JOHN.address, "amount": 10, "fee": 1, "nonce": nonce } const signature = await ALICE.signTypedData(karma_request_domain, types, message) await contract.connect(MINER).metaTransfer(ALICE.address, JOHN.address, 10, 1, nonce, signature) assert.equal(await contract.balanceOf(ALICE.address), ethers.toBigInt(11))
Die Variable „types“ definiert die Struktur der Transaktion. Das „Von“ ist die Adresse des Absenders, während das „An“ die Adresse des Empfängers ist. Der Betrag stellt die Menge der zu übertragenden Token dar. Die Gebühr ist die „Menge“ an Token, die wir dem Relay-Knoten als Gegenleistung für die Ausführung unserer Transaktion und die Deckung der Kosten in der Landeswährung der Kette anbieten. Die „Nonce“ dient als Zähler, um die Einzigartigkeit der Transaktion sicherzustellen. Ohne dieses Feld könnte eine Transaktion mehrmals ausgeführt werden. Dank der Nonce kann eine signierte Transaktion jedoch nur einmal ausgeführt werden.
Die von ethers.js bereitgestellte Funktion signTypedData erleichtert das Signieren von EIP-712-Strukturen. Es macht dasselbe wie der zuvor vorgestellte Code, jedoch mit einer einfacheren Verwendung.
Der MetaTransfer ist die Methode des Karma-Vertrags zur Ausführung einer Meta-Transaktion. Mal sehen, wie es funktioniert:
function metaTransfer( address from, address to, uint256 amount, uint256 fee, uint256 nonce, bytes calldata signature ) public virtual returns (bool) { uint256 currentNonce = _useNonce(from, nonce); (address recoveredAddress, ECDSA.RecoverError err) = ECDSA.tryRecover( _hashTypedDataV4( keccak256( abi.encode( TRANSFER_REQUEST_TYPEHASH, from, to, amount, fee, currentNonce ) ) ), signature ); require( err == ECDSA.RecoverError.NoError && recoveredAddress == from, "Signature error" ); _transfer(recoveredAddress, to, amount); _transfer(recoveredAddress, msg.sender, fee); return true; }
Um die Signatur zu validieren, müssen wir zunächst den Hash der Struktur generieren. Die genauen Schritte hierfür sind ausführlich im EIP-712-Standard beschrieben, der einen Beispiel-Smart-Vertrag und einen Beispiel-Javascript-Code enthält.
Zusammenfassend besteht das Wesentliche darin, dass wir den TYPEHASH (der der Hash der Strukturbeschreibung ist) mit den Feldern der Struktur mithilfe von abi.encode kombinieren. Erzeugt dann einen keccak256-Hash. Der Hash wird an die Methode _hashTypedDataV4 übergeben, die vom EIP712 OpenZeppelin-Vertrag im Karma-Vertrag geerbt wurde. Diese Funktion fügt unserer Struktur Metadaten hinzu und generiert den endgültigen Hash, wodurch die Strukturvalidierung sehr einfach und transparent wird. Die äußerste Funktion ist ECDSA.tryRecover, die versucht, die Adresse des Unterzeichners aus dem Hash und der Signatur wiederherzustellen. Wenn sie mit der Adresse des „from“-Parameters übereinstimmt, ist die Signatur gültig. Am Ende des Codes wird die eigentliche Transaktion ausgeführt und der Relay-Knoten, der die Transaktion durchführt, erhält die Gebühr.
EIP-712 ist ein allgemeiner Standard zum Signieren von Strukturen und damit nur eine von vielen Anwendungen zur Implementierung von Metatransaktionen. Da die Signatur nicht nur bei Smart Contracts validiert werden kann, kann sie auch in Nicht-Blockchain-Anwendungen sehr nützlich sein. Es kann beispielsweise zur serverseitigen Authentifizierung verwendet werden, bei der sich der Benutzer mit seinem privaten Schlüssel identifiziert. Ein solches System kann ein hohes Maß an Sicherheit bieten, das typischerweise mit Kryptowährungen verbunden ist, und ermöglicht die Möglichkeit, eine Webanwendung nur mit einem Hardwareschlüssel zu verwenden. Darüber hinaus können mit Hilfe von MetaMask auch einzelne API-Aufrufe signiert werden.
Ich hoffe, dass dieser kurze Überblick über den EIP-712-Standard viele inspiriert hat und dass Sie ihn sowohl in Blockchain-basierten als auch in Nicht-Blockchain-Projekten nutzen können.
Jeder Code ist im GitHub-Repo von Karma Money verfügbar.