diff --git a/package.json b/package.json index be25c58f..b5a44c90 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@noble/hashes": "^1.5.0", "@tanstack/react-query": "^5.55.4", "bech32-buffer": "^0.2.1", + "cbor-x": "^1.6.0", "classnames": "^2.5.1", "lodash": "^4.17.21", "lucid-cardano": "^0.10.10", diff --git a/referee/package.json b/referee/package.json index 2b66be08..73997b77 100644 --- a/referee/package.json +++ b/referee/package.json @@ -8,6 +8,7 @@ "@noble/ed25519": "^2.1.0", "@noble/hashes": "^1.5.0", "bech32-buffer": "^0.2.1", + "cbor-x": "^1.6.0", "lucid-cardano": "^0.10.10", "prom-client": "^15.1.3", "readline": "^1.3.0" diff --git a/referee/referee.ts b/referee/referee.ts index 39d59228..cbe9da7e 100644 --- a/referee/referee.ts +++ b/referee/referee.ts @@ -93,12 +93,14 @@ const module = await createModule({ }, }); global.Module = module; -global.HydraMultiplayer = new HydraMultiplayerServer({ + +const hydra = new HydraMultiplayerServer({ key: keys, address: keys.address, url: HYDRA_NODE, module, }); +global.HydraMultiplayer = hydra; let playerCount = 0; global.gameStarted = async () => { @@ -208,6 +210,14 @@ try { console.warn("Failed to fetch and parse node utxos: ", e); } +// Log a new game or player joined transaction if we see it +hydra.onNewGame = (gameId, players, bots, ephemeralKey) => { + console.log("New game: ", gameId, players, bots, ephemeralKey); +}; +hydra.onPlayerJoin = (gameId, ephemeralKeys) => { + console.log("Join: ", gameId, ephemeralKeys); +}; + const args = [ "-server", "-altdeath", @@ -220,7 +230,7 @@ const args = [ "-extratics", "1", "-nodes", - "2", + "3", "-nodraw", "-nomouse", "-nograbmouse", diff --git a/referee/yarn.lock b/referee/yarn.lock index 7454c7db..77738c30 100644 --- a/referee/yarn.lock +++ b/referee/yarn.lock @@ -463,6 +463,36 @@ "@smithy/types" "^3.7.1" tslib "^2.6.2" +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== + +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== + +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== + +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== + +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== + +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== + "@noble/ed25519@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-2.1.0.tgz#4bf661de9ee0ad775d41fcacbfc9aeec491f459c" @@ -976,11 +1006,37 @@ bowser@^2.11.0: resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== + dependencies: + node-gyp-build-optional-packages "5.1.1" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" + +cbor-x@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.6.0.tgz#89c35d2d805efc30e09a28349425cc05d57aacd7" + integrity sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg== + optionalDependencies: + cbor-extract "^2.2.0" + data-uri-to-buffer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + fast-xml-parser@4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" @@ -1026,6 +1082,13 @@ node-fetch@^3.2.3: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-gyp-build-optional-packages@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== + dependencies: + detect-libc "^2.0.1" + prom-client@^15.1.3: version "15.1.3" resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2" diff --git a/src/utils/HydraMultiplayer/base.ts b/src/utils/HydraMultiplayer/base.ts index 04d1b329..fac760fb 100644 --- a/src/utils/HydraMultiplayer/base.ts +++ b/src/utils/HydraMultiplayer/base.ts @@ -1,18 +1,10 @@ -import { - Constr, - Data, - fromHex, - toHex, - TxComplete, - TxHash, - UTxO, -} from "lucid-cardano"; +import { Constr, Data, fromHex, toHex, TxHash, UTxO } from "lucid-cardano"; import { Hydra } from ".././hydra"; import * as ed25519 from "@noble/ed25519"; import { sha512 } from "@noble/hashes/sha512"; import { EmscriptenModule } from "../../types"; -import { Keys } from "../../hooks/useKeys"; +import { Keys } from "../../types"; ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m)); export abstract class HydraMultiplayer { @@ -24,6 +16,16 @@ export abstract class HydraMultiplayer { module: EmscriptenModule; networkId: number; + gameId?: string; + + onNewGame?: ( + gameId: string, + players: number, + bots: number, + ephemeralKey: string, + ) => void; + onPlayerJoin?: (gameId: string, ephemeralKeys: string[]) => void; + constructor({ key, url, @@ -74,15 +76,32 @@ export abstract class HydraMultiplayer { this.packetQueue = []; } - public onTxSeen(_txId: TxHash, tx: TxComplete): void { - // TODO: tolerate other txs here + public onTxSeen(txId: TxHash, tx: any): void { try { - const output = tx.txComplete.body().outputs().get(0); - const packetsRaw = output?.datum()?.as_data()?.get().to_bytes(); - if (!packetsRaw) { + const body = tx[0]; + const outputs = body["1"]; + const output = outputs[0]; + const datumRaw: Uint8Array = output["2"][1].value; + if (!datumRaw) { + return; + } + const packets = decodePackets(datumRaw); + if (!packets) { + // We failed to decode packets, so this might be a new game or join game tx + const game = decodeGame(datumRaw); + if (this.gameId) { + this.onPlayerJoin?.(this.gameId, game.players); + } else { + this.gameId = txId; + this.onNewGame?.( + txId, + Number(game.playerCount), + Number(game.botCount), + game.players[0], + ); + } return; } - const packets = decodePackets(packetsRaw); for (const packet of packets) { if (packet.to == this.myIP) { const buf = this.module._malloc!(packet.data.length); @@ -118,14 +137,74 @@ function encodePackets(packets: Packet[]): string { ); } -function decodePackets(raw: Uint8Array): Packet[] { +function decodePackets(raw: Uint8Array): Packet[] | undefined { const packets = Data.from(toHex(raw)) as Constr[]; - return packets.map((packet) => { - const [to, from, data] = packet.fields; - return { - to: Number(to), - from: Number(from), - data: fromHex(data as string), - }; - }); + return packets instanceof Array + ? packets.map((packet) => { + const [to, from, data] = packet.fields; + return { + to: Number(to), + from: Number(from), + data: fromHex(data as string), + }; + }) + : undefined; +} + +interface Game { + referee_key_hash: string; + playerCount: bigint; + botCount: bigint; + players: string[]; + state: "Lobby" | "Running" | "Cheated" | "Finished" | "Aborted"; + winner?: string; + cheater?: string; +} + +function decodeGame(raw: Uint8Array): Game { + const game = Data.from(toHex(raw)) as Constr; + const [ + referee_payment, + playerCountRaw, + botCountRaw, + player_payments, + stateTag, + winnerRaw, + cheaterRaw, + ] = game.fields; + const referee_key_hash = (referee_payment as Constr) + .fields[0] as string; + const playerCount = playerCountRaw as bigint; + const botCount = botCountRaw as bigint; + const players = (player_payments as Constr[]).map( + (player) => player.fields[0] as string, + ); + let state: Game["state"] = "Aborted"; + switch ((stateTag as Constr).index) { + case 0: + state = "Lobby"; + break; + case 1: + state = "Running"; + break; + case 2: + state = "Cheated"; + break; + case 3: + state = "Finished"; + break; + default: + state = "Aborted"; + } + const winner = winnerRaw as Constr; + const cheater = cheaterRaw as Constr; + return { + referee_key_hash: referee_key_hash, + playerCount, + botCount, + players, + state: state, + winner: winner.index == 0 ? (winner.fields[0] as string) : undefined, + cheater: cheater.index == 0 ? (cheater.fields[0] as string) : undefined, + }; } diff --git a/src/utils/hydra.ts b/src/utils/hydra.ts index f71e16cc..a97f404e 100644 --- a/src/utils/hydra.ts +++ b/src/utils/hydra.ts @@ -7,6 +7,7 @@ import { OutRef, ProtocolParameters, toHex, + fromHex, Transaction, TxHash, Unit, @@ -15,6 +16,7 @@ import { TxComplete, Credential as Cred, } from "lucid-cardano"; +import { decode } from "cbor-x"; const NETWORK_ID = typeof process !== "undefined" @@ -45,7 +47,7 @@ export class Hydra { utxos: { [txRef: string]: UTxO }; tombstones: { [txRef: string]: boolean }; - onTxSeen?: (txId: TxHash, tx: TxComplete) => void; + onTxSeen?: (txId: TxHash, tx: any) => void; onTxConfirmed?: (txid: TxHash) => void; onTxInvalid?: (txid: TxHash) => void; @@ -119,29 +121,9 @@ export class Hydra { this.tx_timings[txid].seen = seenTime; // console.log(`seen ${txid} after ${seenTime}ms`); } - const tx = tx_parser.fromTx(data.transaction.cborHex); - for (const input of tx.txComplete.body().inputs().to_js_value()) { - const ref = `${input.transaction_id}#${input.index}`; - if (this.utxos[ref]) { - delete this.utxos[ref]; - } else { - this.tombstones[ref] = true; - } - } - let idx = 0; - for (const output of tx.txComplete.body().outputs().to_js_value()) { - const ref = `${tx.toHash()}#${idx}`; - if (!this.tombstones[ref]) { - this.utxos[ref] = hydraUtxoToLucidUtxo( - tx.toHash(), - idx, - output, - ); - } - idx++; - } + const cbor = fromHex(data.transaction.cborHex); + const tx = decode(cbor); this.onTxSeen?.(txid, tx); - tx.txComplete.free(); } break; case "TxInvalid": diff --git a/yarn.lock b/yarn.lock index d8c06453..93d818c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -186,6 +186,36 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== + +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== + +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== + +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== + +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== + +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -981,6 +1011,27 @@ caniuse-lite@^1.0.30001646: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138" integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== + dependencies: + node-gyp-build-optional-packages "5.1.1" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" + +cbor-x@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.6.0.tgz#89c35d2d805efc30e09a28349425cc05d57aacd7" + integrity sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg== + optionalDependencies: + cbor-extract "^2.2.0" + chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1128,6 +1179,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -1908,6 +1964,13 @@ node-fetch@^3.2.3: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-gyp-build-optional-packages@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== + dependencies: + detect-libc "^2.0.1" + node-releases@^2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"