From b62931b26ae2e8f2662831df1cde9135d7194a53 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Tue, 13 Aug 2024 10:25:43 -0400 Subject: [PATCH] Lucidless signing --- src/game.ts | 73 ++++++++++++++++++----------------------------- src/headless.html | 26 +++++++++++------ src/keys.ts | 5 ++-- 3 files changed, 47 insertions(+), 57 deletions(-) diff --git a/src/game.ts b/src/game.ts index 621152ee..b0e30791 100644 --- a/src/game.ts +++ b/src/game.ts @@ -29,6 +29,9 @@ import { appendTx, session, updateUI } from "./stats"; import * as ed25519 from "@noble/ed25519"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import { blake2b } from "@noble/hashes/blake2b"; +import { sha512 } from "@noble/hashes/sha512"; + +ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m)); let gameServerUrl = process.env.SERVER_URL; if (!gameServerUrl) { @@ -38,9 +41,9 @@ if (!gameServerUrl) { ); } let lucid = await Lucid.new(undefined, "Preprod"); -let { sessionKey: privateKey } = keys; +let { sessionKey, privateKey, sessionPk } = keys; const address = await lucid - .selectWalletFromPrivateKey(privateKey) + .selectWalletFromPrivateKey(sessionKey) .wallet.address(); const pkh = lucid.utils.getAddressDetails(address).paymentCredential?.hash!; console.log(`Using session key with address: ${address}`); @@ -134,9 +137,6 @@ export async function fetchNewGame(region: string) { // so we return it from the newGameResponse and set it manually here latestUTxO.datum = newGameResponse.player_utxo_datum_hex; - lucid = await Lucid.new(hydra, "Preprod"); - lucid.selectWalletFromPrivateKey(privateKey); - // This is temporary, the initial game state is stored in a UTxO created by the control plane. // We need to add the ability to parse game state from the datum here. gameData = initialGameData(pkh, player_pkh!); @@ -250,7 +250,7 @@ export async function hydraSend( redeemerQueue.push(cmd); if (frameNumber % 1 == 0) { - const [newUtxo, tx] = await buildTx( + const [newUtxo, tx] = buildTx( latestUTxO!, encodeRedeemer(redeemerQueue), buildDatum(gameData), @@ -258,7 +258,7 @@ export async function hydraSend( ); sessionStats.transactions++; - sessionStats.bytes += tx.txSigned.to_bytes().length; + sessionStats.bytes += tx.length / 2; sessionStats.total_kills = gameData.player.totalStats.killCount; sessionStats.total_items = gameData.player.totalStats.itemCount; sessionStats.total_secrets = gameData.player.totalStats.secretCount; @@ -268,7 +268,7 @@ export async function hydraSend( ); updateUI(session, sessionStats); - await hydra.submitTx(tx.toString()); + await hydra.submitTx(tx); latestUTxO = newUtxo; redeemerQueue = []; // console.log(`submitted ${tx.toHash()}, took ${performance.now() - hydraSendStart}ms`); @@ -363,12 +363,12 @@ const decodeRedeemer = (redeemer: string): Cmd[] => { }) as Cmd, ); }; -const buildTx = async ( +const buildTx = ( inputUtxo: UTxO, redeemer: string, datum: string, collateralUtxo: UTxO, -): Promise<[UTxO, TxSigned]> => { +): [UTxO, string] => { // Hand-roll transaction creation for more performance // NOTE: Redeemer is always using max ex units const redeemerBlock = `81840000${redeemer}821a00d59f801b00000002540be400`; @@ -387,45 +387,26 @@ const buildTx = async ( `0d81825820${collateralUtxo.txHash}0${collateralUtxo.outputIndex}` + // Collatteral Input `0e81581c${pkh}` + // Required Signers `1281825820${scriptRef!.split("#")[0]}0${scriptRef!.split("#")[1]}`; // Reference inputs - const witnessSetByHand = `a105${redeemerBlock}`; // a single redeemer in witness set + + const txId = bytesToHex( + blake2b(hexToBytes(txBodyByHand), { dkLen: 256 / 8 }), + ); + const signature = bytesToHex(ed25519.sign(txId, privateKey)); + + const witnessSetByHand = `a20081825820${sessionPk}5840${signature}05${redeemerBlock}`; // a single redeemer in witness set const txByHand = `84${txBodyByHand}${witnessSetByHand}f5f6`; - // Still use lucid for signing with configured key - const tx = lucid.fromTx(txByHand); - const signedTx = await tx.sign().complete(); - - const body = signedTx.txSigned.body(); - const outputs = body.outputs(); - body.free(); - let newUtxo: UTxO | null = null; - for (let i = 0; i < outputs.len(); i++) { - const output = outputs.get(i); - const address = output.address(); - if (address.to_bech32("addr_test") === scriptAddress) { - const amount = output.amount(); - const datum = output.datum()!; - const data = datum.as_data()!; - - newUtxo = { - txHash: signedTx.toHash(), - outputIndex: i, - address: address.to_bech32("addr_test"), - assets: valueToAssets(amount), - datumHash: null, - datum: toHex(data.to_bytes()).substring(8), - scriptRef: null, - }; - amount.free(); - output.free(); - data.free(); - address.free(); - break; - } - outputs.free(); - body.free(); - } + const newUtxo: UTxO = { + txHash: txId, + outputIndex: 0, + address: scriptAddress, + assets: { lovelace: 0n }, + datumHash: null, + datum: datum, + scriptRef: null, + }; - return [newUtxo!, signedTx]; + return [newUtxo, txByHand]; }; const subtractPlayerStats = (left?: PlayerStats, right?: PlayerStats) => { diff --git a/src/headless.html b/src/headless.html index 0098a4cc..ce59a590 100644 --- a/src/headless.html +++ b/src/headless.html @@ -44,6 +44,8 @@

import * as ed from "https://esm.sh/@noble/ed25519@2.1.0"; import { blake2b } from "https://esm.sh/@noble/hashes@1.4.0/blake2b"; import { sha512 } from "https://esm.sh/@noble/hashes@1.4.0/sha512"; + import { bech32 } from "https://esm.sh/bech32@2.0.0"; + import { bytesToHex, hexToBytes, @@ -95,6 +97,10 @@

async function requestGame() { let lucid = await Lucid.new(undefined, "Preprod"); const ephemeralKey = lucid.utils.generatePrivateKey(); + const privateKey = new Uint8Array( + bech32.fromWords(bech32.decode(ephemeralKey).words), + ); + const publicKey = bytesToHex(ed.getPublicKey(privateKey)); const address = await lucid .selectWalletFromPrivateKey(ephemeralKey) .wallet.address(); @@ -107,7 +113,9 @@

await rawResponse.json(); return { lucid, - ephemeralKey, + ephemeralKey: ephemeralKey, + privateKey, + publicKey: publicKey, pkh, ip, player_utxo, @@ -259,7 +267,8 @@

async function buildTx(gameId) { const { - ephemeralKey, + privateKey, + publicKey, pkh, script_ref, player_utxo, @@ -285,14 +294,13 @@

`0d81825820${collateralUtxo.split("#")[0]}0${collateralUtxo.split("#")[1]}` + // Collatteral Input `0e81581c${pkh}` + // Required Signers `1281825820${script_ref.split("#")[0]}0${script_ref.split("#")[1]}`; // Reference inputs - const witnessSetByHand = `a105${redeemerBlock}`; // a single redeemer in witness set + const txIdRaw = blake2b(hexToBytes(txBodyByHand), { dkLen: 256 / 8 }); + const txId = bytesToHex(txIdRaw); + const signature = bytesToHex(ed.sign(txIdRaw, privateKey)); + + const witnessSetByHand = `a20081825820${publicKey}5840${signature}05${redeemerBlock}`; // a single redeemer in witness set const txByHand = `84${txBodyByHand}${witnessSetByHand}f5f6`; - const txId = bytesToHex( - blake2b(hexToBytes(txBodyByHand), { dkLen: 256 / 8 }), - ); - const tx = games[gameId].lucid.fromTx(txByHand); - const signedTx = await tx.sign().complete(); - return { tx: signedTx.toString(), new_utxo: `${txId}#0` }; + return { tx: txByHand, new_utxo: `${txId}#0` }; } diff --git a/src/keys.ts b/src/keys.ts index 0c23099e..a27e9c5b 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -2,7 +2,7 @@ import * as ed25519 from "@noble/ed25519"; import * as bech32 from "bech32-buffer"; import { encode } from "cbor-x"; import { Lucid } from "lucid-cardano"; -import { blake2b } from "@noble/hashes/blake2b" +import { blake2b } from "@noble/hashes/blake2b"; import { sha512 } from "@noble/hashes/sha512"; ed25519.etc.sha512Async = (...m) => @@ -34,7 +34,7 @@ const decodedSessionKey = Array.from(bech32.decode(sessionKey).data) .map(toHex) .join(""); const sessionPk = await ed25519.getPublicKeyAsync(decodedSessionKey); -const sessionPkh = blake2b(sessionPk, { dkLen: 224/8 }); +const sessionPkh = blake2b(sessionPk, { dkLen: 224 / 8 }); function toHex(i: number) { return ("0" + i.toString(16)).slice(-2); @@ -42,6 +42,7 @@ function toHex(i: number) { export const keys = { sessionKey, + privateKey: decodedSessionKey, sessionPk: ed25519.etc.bytesToHex(sessionPk), sessionPkh: ed25519.etc.bytesToHex(sessionPkh), cabinetKey,