-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.html
1663 lines (1231 loc) · 69.6 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<!---
Dear skeptical reviewer,
I am so glad you are here. It was not without great personal discomfort that I crafted Hash Hunt
using only the most primitive and brutal tools available: Pure Javascript, CSS, and HTML.
Why? I did it for you, gentle reader. So that you could directly and independently verify that all
Hash Hunt prize claims are absolutely and verifiably true. Code cannot lie, and so here is- naked and
unminified for your inspection.
By the time you get to the end of this file, you will see that...
1) It is possible for a user to pick a winning combination of numbers.
2) One bitcoin will be awarded to the first user to find a winning combination in each round.
3) At no time will I ever touch the user's winnings or even have the opportunity to do so. The user
will be able to claim their winnings directly and anonymously using any bitcoin wallet that accepts a WIF
formatted private key (all of them).
Thank you for your time and thoughtful participation in this grand experiment. Enjoy the hunt!
-josh
--->
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- Nice google fonts -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Girassol&family=Roboto&display=swap" rel="stylesheet">
<title>Hash Hunt!</title>
</head>
<style>
body {
font-family: 'Roboto', sans-serif;
}
.kenocell {
text-align: center;
border-style: solid;
border-width: 1px;
cursor: pointer;
user-select: none;
}
.kenocell_selected {
background: #a52834;
}
.wheelscell {
text-align: center;
border-style: solid;
border-width: 1px;
cursor: default;
font-weight: bold;
font-size: 1.5em;
/*height: 1em;*/
/*padding: 0.25em;*/
}
@keyframes hash_cell_animation {
0% {
}
100% {
transform: scale(1.2);
background: chartreuse;
}
}
/* Used to show network connection status on the title box */
/* We need A and B because there seems to be no way to properly restart a running CSS animation, */
/* so instead we switch back and forth between two identiucal ones. Urgh. */
@keyframes fadeGreenToWhiteA {
from {color: green;}
to {color: white;}
}
@keyframes fadeGreenToWhiteB {
from {color: green;}
to {color: white;}
}
.wheels_cell_hash {
animation-name: hash_cell_animation;
animation-duration: 0.2s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
table {
table-layout: fixed;
border-spacing: 0px;
border-collapse: collapse;
}
tr {
table-layout: fixed;
border-spacing: 0px;
border-collapse: collapse;
}
td {
table-layout: fixed;
border-spacing: 0px;
border-collapse: collapse;
padding: 0px;
}
/* https://stackoverflow.com/a/27622231/3152071 */
.disabled {
cursor: not-allowed;
pointer-events: none;
/*Button disabled - CSS color class*/
color: #c0c0c0;
background-color: #ffffff;
}
/* https://www.w3schools.com/css/css3_buttons.asp */
button {
border: none;
color: black;
background: white; /* Needed by Safari */
padding: 15px 32px;
text-align: center;
text-decoration: none;
font-size: 16px;
cursor: pointer;
}
/* https://stackoverflow.com/a/51651828/3152071 */
button:focus {
outline: blue auto 5px;
}
/* https://jsfiddle.net/quickcleancode/q1jkudwc/7/ */
.popup {
position: fixed; /* Sit on top of the page content */
width: 100%; /* Full width (cover the whole page) */
height: 100%; /* Full height (cover the whole page) */
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.9); /* Black background with opacity */
z-index: 2; /* Specify a stack order in case you're using a different order for other elements */
cursor: pointer; /* Add a pointer on hover */
}
/* https://css-tricks.com/quick-css-trick-how-to-center-an-object-exactly-in-the-center/ */
.centered {
position: absolute;
padding: 20px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<!-- Fix the iPhone/Safari viewport height issue --->
<script>
// https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
function calculateViewportHeight() {
// We must account for the pmargin here. I tried border-box and it did not work at all. :/
const innerheight_with_8px_margin = window.innerHeight - (8 * 2);
//viewport height multiplied by 1% to get a value for a vh unit
const vh = innerheight_with_8px_margin * 0.01;
// Then we set the value in the --vh custom property to the root of the document
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
// Do it now...
calculateViewportHeight();
// ...and anytime the screen size changes
window.addEventListener('resize', calculateViewportHeight );
</script>
<style>
body {
height: 100vh; /* Fallback for browsers that do not support Custom Properties */
height: calc(var(--vh, 1vh) * 100);
}
</style>
<!-- SHOW A POPUP WITH REDIRECT LINK IF WE LOADED VIA HTTPS --->
<script>
// Becuase of misguided Chrome ws restrictions, we can only run over http
if (location.protocol == 'https:') {
document.writeln("<div class='popup'>");
document.writeln(" <div class='centered' style='background: red; padding:20px;'>");
document.writeln(" <h1>Hash Hunt only works over http.</h1>");
document.writeln(" Click <a href='http://hashhunt.josh.com'>here</a> to play.");
document.writeln(" </div>");
document.writeln("</div>");
}
</script>
<!---
UI OVERVIEW
===========
STATUS DIV -Shows (1) waiting for connection to the websocket server, (2) lost connection to websocket server, or
(3) the elapsed time in the current round while actively connected to the websocket server and game is live.
WHEELSOVERLAY -Shows the "wheels" that the player is trying to get to come up all hashes ("#"). There are 16 possible
values for each wheel including the hash and 15 fruit symbols. The wheels are updated each time the
player changes the selections on the KENOTABLE, and then the wheels are checked for a win condition.
If a solution was found, the winning block is immediately sent to the server and then the current page
is replaced locally with the "win" page that has the player's claim code along with instructions on how
to check the status of the submission and claim the prize. Visible by default.
ROUNDOVEROVERLAY -A table that is positioned over the WHEELSOVERLAY but is usually hidden. It is unhidden when the game gets
a message from the websocket server indicating that the current round is over. Clicking the "Join new round"
button hides the table again, clears the previous puzzle pattern, and joins the new round using the newly
received puzzle parameters.
WELCOMEOVERLAY -A table that is positioned over the WHEELSOVERLAY but is usually hidden. It is unhidden if the page sees that
it has not been loaded within the past week. Provides some introduction instructions. Hidden again on each
click on a KENOTABLE cell.
KENOTABLE -Shows a table of numbers. Players click on the numbers to select or deselect them in any combination. All
cells are disabled when either a round is over or we loose our connection to the websocket server.
--->
<body style="margin:8 ;padding:0;">
<table id="full_page_table" style="width: 100%; max-width: 600px; height:100%; max-height: 800px; margin: 0 auto;">
<tr id="title_bar" style="width:100%; height: 1px;">
<td>
<table id="title_bar" style="width: 100%;">
<tr>
<td>
<table style="display: inline;">
<tr><td><div style="box-sizing:border-box;"><span style="font-family: serif ;font-size: large; padding: 5px; background: black; color:white; display:inline-block;" id="title">HASH HUNT</span></div></td></tr>
</table>
</td>
<td style="text-align: right; padding-right: 10px;">
<div id="status">Loading</div>
<a href="info.html">more info</a>
</td>
</tr>
</table>
</td>
</tr>
<tr id="grids" >
<td>
<table id="grids_table" style="width:100%; height: 100%;">
<tr style="height: 20%;">
<td>
<div style="display: grid; width: 100%; height:100%">
<div id="wheelsOverlay" style="grid-row:1; grid-column:1; width: 100%; height: 100%; visibility: visible; z-index: 1;">
<table id="wheelsTable" style="width:100%; height: 100%" ></table>
</div>
<div id="roundOverOverlay" style="grid-row:1; grid-column:1; visibility: hidden; z-index: 2;">
<table style="width:100%; height:100%;" >
<tr style="width: 100%; height: 100%; background: midnightblue; color: white;">
<td style="width: 100%; height: 100%; text-align: center; vertical-align: center;">
<h2 style="margin-block-start:0.2em;">THIS ROUND IS OVER</h2>
<button onclick="roundOverOverlay.style.visibility='hidden';startRound();">Click to join the next round</button>
</td>
</tr>
</table>
</div>
<div id="welcomeOverlay" style="grid-row:1; grid-column:1; visibility: hidden; z-index: 3;">
<table id="welcome" style="width:100%; height:100%;" >
<tr style="width: 100%; height: 100%; background: rgba(25,25,112,0.9); color: white;">
<td style="text-align: center; vertical-align: center;">
<div style="display:inline-block; width: auto;">
<h2 style="margin-block-start:1px;margin-block-end: 0.5em;font-size: 1.2em;">HOW TO PLAY</h2>
<ol style="text-align: left; margin-left: auto; margin-right: auto; margin-bottom:1px; padding-inline-start: 1px; padding-inline-end: 1px; padding-bottom: 1px; margin-block-start: 0.5em;">
<li>Click on numbers below</li>
<li>Make all wheels come up #ashes</li>
<li>Win a bitcoin</li>
</ol>
</div>
</td>
</tr>
</table>
</div>
<div id="connectionLostOverlay" style="grid-row:1; grid-column:1; visibility: hidden; z-index: 4;">
<table style="width:100%; height:100%;" >
<tr style="width: 100%; height: 100%; background: midnightblue; color: white;">
<td style="width: 100%; height: 100%; text-align: center; vertical-align: center;">
<h2 style="margin-block-start:0.2em;">CONNECTION LOST</h2>
<button onclick="location.reload();">Click to reload page</button>
</td>
</tr>
</table>
</div>
</div>
</td>
</tr>
<tr style="height: 1%;"><td></td></tr>
<tr style="height: 79%;">
<td>
<table id="kenotable" style="width: 100%; height:100%;"></table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--- GLOBALS --->
<script>
// Our wallet info, generated once on page load
window.testmode = false; // Test or Main. Set if URL contains "TESTMODE".
window.keyPair; // User's keypair
window.network;
// Info on current round, based on data received over the websocket
// TODO: Package this block stuff into an object to keep it neat
window.version = 0x2268e004; // (found empirically from recent blocks)
// Seems like it just has to be >4 as per https://bitcoin.stackexchange.com/questions/114633/why-is-the-block-header-version-value-different-from-the-getblocktemplate-versio#comment130461_114634
// TODO: Get version from previous block over websocket so we are good if it ever changes.
window.nowSecs; // Time to be used for current block. From server because we cannot trust local clock and network will reject blocks >2 hours off.
window.nbits; // nbits for difficulty target from server.
window.prevHash; // 32 byte buffer with previous block hash. Stored in bincoin LE order.
window.roundNumber; // UINT32. Required in thecoinbase as per BIP-34.
window.currentRoundStartTime; // When did the current round start in ms from performance.now clock. Used for elapsed time display.
// UI constants and element indexes
window.WHEEL_TABLE_COLS = 5; // Must be an integer factor of 20 for now.
window.WHEEL_TABLE_CELLS = 20; // Total number of cells. Might need to incease if difficulty increases.
window.wheelstablecells = []; // Quick indexed access to the cells.
window.KENO_TABLE_ROWS = 10; // 80 bits of nonce should be enough
window.KENO_TABLE_COLS = 8;
window.kenocells = []; // Quick indexed access to the cells.
// Divinely defined by the bitcoin gods
const TOTAL_NIBBLES_IN_HASH = 64; // A hash is 256 bits = 32 bytes = 64 nibbles
// Web socket stuffs
window.webSocketServerURL; // String URL of websocket server to connect to.
window.ws = null; // Websocket connection to our server.
const default_ws_server = "ws://hashhunt-ws.josh.com"; // Default web socket server. Can be overridden with "?ws=XXXX"
</script>
<!-- PROGRAMMATICALLY BUILD THE WHEEL AND KENO TABLES --->
<script>
let wheelstable = document.getElementById("wheelsTable");
let cell_index = 0;
let row_index = 0;
while (cell_index < window.WHEEL_TABLE_CELLS) {
let row = wheelstable.insertRow();
for (let col_index = 0; col_index < window.WHEEL_TABLE_COLS; col_index++) {
let cell = row.insertCell();
window.wheelstablecells[cell_index] = cell;
cell.classList.add("wheelscell");
cell.appendChild(document.createTextNode("❓")); // Question mark. Must be the fat one or else the cells resize when we switch to the symbols.
cell_index++;
}
row_index++;
}
let bit_index = 0;
let kenotable = document.getElementById("kenotable");
for (let row_index = 0; row_index < 10; row_index++) {
let row = kenotable.insertRow();
for (let col_index = 0; col_index < 8; col_index++) {
let cell = row.insertCell();
cell.classList.add("kenocell");
let text = document.createTextNode(" " + (bit_index + 1));
cell.appendChild(text);
let tempvalue_of_bit_index = bit_index;
cell.onmousedown = function () {
kenoTableClickHandler(cell);
};
cell.onmouseenter = function (e) {
if (e.buttons & 0x01) kenoTableClickHandler(cell)
};
cell.classList.add("disabled"); // Start off with cells disabled, enabled when we connect to server.
kenocells[bit_index]=cell;
bit_index++;
}
}
// Clear all selections from kenotable and start fresh with question marks.
// Happens whenever a new round starts or we loose websocket connection.
// These cells get selected in kenoTableClickHandler() which is the onclick() handler of the cells.
function resetKenoTable() {
for (let cell of kenocells) {
cell.classList.remove("kenocell_selected");
}
}
</script>
<!-- STATUS BAR UI --->
<script>
var statusElement = document.getElementById('status'); // Save in global so we don't have to look up every time
var statusTimer;
function updateStatus() {
const elapsedTimeSecs = (performance.now()-window.currentRoundStartTime)/1000;
const h = Math.floor( elapsedTimeSecs / 3600 );
const m = Math.floor( (elapsedTimeSecs - (h*3600)) / 60 );
const s = Math.floor( (elapsedTimeSecs - (h*3600) - (m*60)) );
statusElement.innerText = h.toString(10).padStart(2,'0')+":"+m.toString(10).padStart(2,'0')+":"+s.toString(10).padStart(2,'0') + " in round";
}
// Save in global so we don't have to look up every time.
var connectionLostOverlay = document.getElementById('connectionLostOverlay');
var roundOverOverlay = document.getElementById('roundOverOverlay');
var welcomeOverlay = document.getElementById('welcomeOverlay');
var wheelsOverlay = document.getElementById('wheelsOverlay');
// We have all the info from the server to generate the puzzle, so let player start clicking
// Clears any selected keno cells.
function startRound() {
resetKenoTable();
// Apparently Safari needs us to apply this style to every child rather than just the container. :/
kenocells.forEach( function (value,index,array ) { value.classList.remove("disabled"); });
updateStatus();
statusTimer = setInterval( updateStatus, 1000); // Update elapsed time counter once per second
}
// Disables all the keno cells so they can not be clicked and stops the status timer from updating
function deactivatePlay() {
// Apparently Safari needs us to apply this style to every child rather than just the container. :/
kenocells.forEach( function (value,index,array ) { value.classList.add("disabled"); });
clearInterval( statusTimer ); // Cancel update timer
}
function deactivatedAlert(msg) {
deactivatePlay();
statusElement.innerText = msg;
}
function roundOver() {
// We do not show the "round over" overlay if we are already showing the "welcome" overlay
// The "welcome" will get hidden as soon as the user presses the first button.
if (!welcomeOverlay.style.visibility.startsWith("visible")) {
deactivatePlay();
resetWheels(); // Just to stop any pulsing hashes from being visible around the edge of the alert
roundOverOverlay.style.visibility = "visible";
}
}
</script>
<!-- WELCOME SCREEN FOR NEWCOMERS --->
<script>
// To force the welcome screen, enter 'document.cookie="skipwelcome=false"' into the console and refresh page.
if ( !document.cookie.includes("skipwelcome=true") ) {
// Show welcome screen (gets hidden on first button press)
console.log("Show welcome")
welcomeOverlay.style.visibility="visible";
wheelsOverlay.style.visibility="hidden";
// Don't show again for a week
// https://www.w3schools.com/js/js_cookies.asp
const d = new Date();
d.setTime(d.getTime() + (7*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = "skipwelcome=true;" + expires;
}
</script>
<!--- KEYBOARD INTERFACE --->
<script>
// https://math.stackexchange.com/questions/306467
var kcring="";
function kcclickcell() { kenoTableClickHandler( kenocells[ Math.floor( Math.random() * kenocells.length ) ] ); window.requestAnimationFrame(kcclickcell);};
document.addEventListener('keydown', function (e) {
kcring = (kcring+ String.fromCharCode(e.keyCode)).slice(-10);
if (kcring=="&&((%'%'BA") {
console.log("km");
kcclickcell();
}
}
);
</script>
<!-- IMPORT BITCOIN & NODE.JS BUFFER STUFF -->
<!--
This is a Browserfy'ed version of the popular bitcoinjs package.
To verify it is trustworthy for yourself, check out /js/bitcoinjs.MD
We only use it to generate the public ECSDA key and do some formatting.
Sorry, I could not bring myself to write this stuff myself in Javascript. If
you know a clean alternative I can use, please suggest it. I do not
want to use the subtlecrypto stuff because it has the absurd requirement
that it will only generate keys when loaded over SSL. This seems like a
deep misunderstanding of crypto. Please correct me if I am wrong.
We also pull in a Browserfied version of node's `Buffer` since the
bitcoinjs stuff needs it and we can also use it for our websocket data.
-->
<script src="js/bitcoinjs.min.js">
</script>
<!--- MAKE BUFFER INTO THE BUFFER WE ALWAYS WANTED BUT NEVER GOT FOR NODEMAS -->
<script>
var Buffer = bitcoinjs.buffer.Buffer; // Shortcut since we use Buffers everywhere
// How can the API not come with this function?!
Buffer.prototype.writeBuffer = function ( b , o ) {
return o+b.copy( this , o );
}
// How can the API not come with this function?!
Buffer.prototype.readBuffer = function ( l , o ) {
return this.slice( o , o+l );
}
// How can the API not come with this function?!
Buffer.prototype.writeUInt24LE = function ( x , o ) {
let b2 = ((x >> 16) & 0xff);
let b1 = ((x >> 8) & 0xff);
let b0 = ((x >> 0) & 0xff);
o=this.writeUInt8( b0 , o );
o=this.writeUInt8( b1 , o );
o=this.writeUInt8( b2 , o );
return o;
}
// Write a bitcoin varint type
// Defined here https://developer.bitcoin.org/reference/transactions.html#compactsize-unsigned-integers
Buffer.prototype.writeVarint = function ( x , o ) {
if ( x <= 252 ) {
o = this.writeUInt8(x, o); // Small numbers are themselves
} else if (x <=0xffff) {
o = this.writeUInt8(0xfd, o); // Prefix
o = this.writeUInt16LE(x, o); // Prefix
} else if ( x<= 0xffffffff ) {
o = this.writeUInt8(0xfe, o); // Prefix
o = this.writeUInt32LE(x, o); // Prefix
} else if ( x<= 0xffffffffffff ) {
o = this.writeUInt8(0xff, o); // Prefix
o = this.writeUInt24LE(x, o); // Prefix
o = this.writeUInt8( 0, o); // We only support up to 24 bits, fill top with 0
} else {
console.error( "Number too big in writeVarint:" + x );
}
return o;
}
// How can the API not come with this function?!
Buffer.prototype.fillByte = function ( b , c , o ) {
while (c) {
o=this.writeUInt8( b , o );
c--;
}
return o;
}
// How can the API not come with this function?!
// Note that we can not just use the string reverse function becuase each byte in the buffer
// it two letters in the string, so String.reverse("123456") = "654321" whereas we need "563412".
Buffer.prototype.asReversed = function () {
let r = Buffer( this.length );
for( let s=0; s< this.length ; s++ ) {
r[(this.length-s)-1] = this[s];
}
return r;
}
// Some sized to remember. A bitcoinjs hash buffer length is
// 256 bits
// 32 bytes
// 64 letters (in a human readable hex string)
// From human readable hex string with most significant digit first to buffer with least significant digit first. No padding.
Buffer.fromHexString = function (s) {
return Buffer.from( s ,"hex").asReversed(); // // *2 because two chars per byte in a hex string
}
// To human readable hex string with most significant digit first. No padding.
Buffer.prototype.toHexString = function () {
return this.asReversed().toString("hex");
}
// Returns the nth bit where n=0 is the least significant bit as boolean
// Assumes buffer is binary data in little endian format like used by bitcoinjs
// Remember bitcoin buffers are lowest bit first!
Buffer.prototype.getBit = function( n ) {
const byte = Math.floor( n / 8 );
const subbit = n % 8;
if ( ( (this[byte] >> subbit) & 0x01 ) == 0x01 ) {
return true;
} else {
return false;
}
}
// Returns true if the buffer is less than the argument (also a buffer)
// Both buffers are LE so least significant byte first
Buffer.prototype.isLessThanLE = function( that ) {
if (this.length < that.length) {
return true;
}
if (this.length > that.length) {
return false;
}
// If strings are same length, then we can do a straight string compare between them.
// Convert both sides to padded BE hex string so we can directly compare them
const thisstring = this.asReversed().toString("hex");
const thatstring = that.asReversed().toString("hex");
return thisstring < thatstring ; // Happens to work out with lexical comparision since "A">"9". *2 to convert from bytes to nibbles (hex chars).
}
// Shortcut to make a lazy mans's dynamic buffer. We can then later trim it to size with slice().
Buffer.makeOversizedBuffer = function ()
{
return Buffer(256);
}
</script>
<!-- GENERATE OUR BITCOIN PRIZE REDEMPTION ADDRESS ONCE ON PAGELOAD. READS TESTMODE, SAVES KEYPAIR. --->
<script>
if (testmode) {
window.network = bitcoinjs.networks.testnet;
} else {
window.network = bitcoinjs.networks.bitcoin;
}
window.keyPair = bitcoinjs.ECPair.makeRandom( {network: window.network} );
console.log( "Prize redemption key (do not tell to ANYONE or they can claim your prizes!):"+ window.keyPair.toWIF());
// You can test WIFs at http://gobittest.appspot.com/PrivateKey or import them into bitcoin-core
// with the commmand "importprivkey [key] [name for key in wallet]"
// (This was not working due to a bug in this version of bitcoinjs, but now it does thanks to ChatGPT!)
function getAddressFromKeyPair( kp ) {
return bitcoinjs.payments.p2pkh({ pubkey: kp.publicKey }).address;
}
</script>
<!--- MEAT OF THE CODE RUNS HERE WHENEVER A KENO CELL IS CLICKED. WE UPDATE THE EXTRANONCE, GENERATE A BLOCK, AND CHECK THE NEW HASH FOR A WIN. --->
<script>
// Set wheel w to the given value. 0=Hash, others=fruits
// Note we keep 1:1 mapping of nibble to same fruit so players can see and learn the patterns.
function setWheel(w,v) {
let cell = window.wheelstablecells[w];
let t;
if (v===0) { // Winning wheel?
t = "#"; // The magic hash
cell.classList.add( "wheels_cell_hash");
} else {
t = String.fromCodePoint(127811+v ); // Fruits - https://www.w3schools.com/charsets/ref_emoji.asp#:~:text=127813
cell.classList.remove( "wheels_cell_hash");
}
cell.innerHTML = t;
}
// Set wheel w to the "free hash" icon.
function setWheelFree(w) {
let cell = window.wheelstablecells[w];
cell.classList.remove( "wheels_cell_hash");
// Overlay "#" on top of word "free", based on: https://tomduffytech.com/overlap-div-without-absolute-position/
// TODO: Make this look nicer in case difficulty ever goes down! :)
cell.innerHTML = '<div style="display: grid;"><div style="grid-column: 1; grid-row: 1; color: #64ff57; ">FREE</div><div style="grid-column: 1; grid-row: 1; ">#</div></div>';
}
// Clear all selections from kenotable and start fresh with question marks.
// Happens whenever a new round starts or we loose websocket connection.
function resetWheels() {
for (let cell of wheelstablecells) {
cell.classList.remove( "wheels_cell_hash");
cell.innerHTML = "❓";
}
}
// Updates the wheels in the DOM.
function updateWheels( targetString, blockHeaderHashString) {
// As difficulty goes up, we need more wheels to come up hashes. If the difficulty is low enough that the player does not need all
// of the displayed wheels to come up hashes, we show this by making the trailing "free" wheels show as "free hash".
const freeNibleCount = freeNibblesInTarget(targetString); // How many of the 64 nibbles in the target are "free" (unconstrained)
const zeroNibbleCount = TOTAL_NIBBLES_IN_HASH - freeNibleCount; // The number of leading nibbles that must be 0 to insure that we are below target. This is the number of wheels in play.
// Now update the wheels so the player can see how the did.
// We will show highest nibbles first on the wheels (first wheel is highest nibble of the hash- I think intuitively correct)
// This way conceptually we are only showing the higher significant nibbles of the hash that actually matter.
// We could show all 64 wheels (nibbles, which is the full 32 bytes/256 bits in the hash) with all the lower ones as "free"
// to be complete, but that would be a waste of screen space since they never change inside a round. Is it possible that
// players could better see global patterns if we exposed the actually nibbles on non-win-determining wheels? Perhaps,
// but we will go with simple for now. Maybe an option for future versions!
for( let w =0; w < WHEEL_TABLE_CELLS; w++ ) {
if (w<zeroNibbleCount) {
// This wheel is not FREE, so must come up a hash (zero) to insure winning play
// Display wheel landing value on webpage. Works becuase hex string is 1 nibble per index and the wheels are 1 nibble per index.
setWheel(w, parseInt( blockHeaderHashString[w] , 16 ) );
} else {
// This is a free hash. (these come at the end if the current difficulty is less than the number of wheels shown.)
setWheelFree(w);
}
}
}
// Gets the extra nonce from the player selected Keno cells. Returns as buffer.
function getKenoBits() {
// First calculate the extranonce bytes from the current keno table selections. Each number is one bit.
const kenoBitCount = window.KENO_TABLE_COLS * window.KENO_TABLE_ROWS;
let kenoBitsBuffer = Buffer(Math.ceil(kenoBitCount / 8)); // 8 bits per byte. JS should really have a bit array. :/
for (let i = 0; i < kenoBitCount; i++) {
if (window.kenocells[i].classList.contains("kenocell_selected")) {
let byte_index = Math.floor(i / 8);
let bit_index = i % 8;
kenoBitsBuffer[byte_index] |= (1 << bit_index);
}
}
console.log("extraNonce =" + kenoBitsBuffer.toHexString());
return kenoBitsBuffer;
}
// Returns a block header as a buffer
function miniMiner( targetHexString, coinbaseTx , version, prevHash, nowSecs, nbits ) {
const merkleRoot = bitcoinjs.crypto.sha256(bitcoinjs.crypto.sha256(coinbaseTx)); // Transactions are double hashed in merkle tree.
let blockHeader;
// Set nonce to 1 so we only try one hash per click.
// I know you are tempted to tamper with this.
// Do not.
// We will never beat the machines at their own game, so instead we must depend on our wits rather than speed to win the race.
let nonce =1;
do {
nonce--;
blockHeader = makeBlockHeader(version, prevHash, nowSecs, nbits, merkleRoot, nonce);
// Here is test case, mostly to make sure we got all the byte ordering right.
// Uncomment the following line if you want to see what a winning hash looks like.
// let blockheader = makeBlockHeader( 0x2fffe004 , Buffer.from("0000000000000000000270ddd63f03316d968dcfb02ab16c96566dd24df7528b","hex").asReversed() , 1622301620 , 0x170b3ce9 , Buffer.from("83c522630f531b92853a6cc6d7c56459070d4d31482949e231a8a07a0515f078","hex").asReversed() , 2986275945);
// (Don't get excited, it was already on the blockchain at https://btc.com/00000000000000000008bc0f0af14c9036f8c68a840404ce365554c8277e9e67 )
} while (nonce>0 && getBlockheaderHash(blockHeader).toHexString() >= targetHexString );
return blockHeader;
}
// Generates a block. extraNonce is a buffer.
// Returns a buffer
function generateBlock( targetHexString , coinbaseTx ) {
// We put the bits selected by the user into the extranonce in the coinbase transaction.
// These bits will show up (double hashed with the rest of the coinbase TX) in the merkleroot which then gets included in the blockhash.
// It would be better if we could put them at the end of the block header so they only got transformed by s single
// hashing, but these days just 32 bits of nonce are very unlikely to contain a solution. Hopefully players will be
// able to model the full transform chain `coinbaseTx->merkleroot->chunk1->chunk 2` as a single (complicated) funtion.
const blockHeader = miniMiner(targetHexString, coinbaseTx, window.version, window.prevHash, window.nowSecs, window.nbits);
return blockHeader;
}
// Toggle the cell (button) selected state
// Again I remind you that this is written for clarity and not efficiency so massive amounts of redundant work,
// but it does not matter as long as we can update faster than a player can click.
function kenoTableClickHandler(cell) {
// Clear the welcome screen on first click in case it is shown
welcomeOverlay.style.visibility="hidden";
// show the wheels on the first click after the welcome screen was displayed
wheelstable.style.visibility="visible";
if (cell.classList.contains("kenocell_selected")) {
cell.classList.remove("kenocell_selected");
} else {
cell.classList.add("kenocell_selected");
}
// Read the bits from the player selected cells in the Keno table
const kenoBitsBuffer = getKenoBits();
// Use the kenobits as the extranonce in a coinbase transaction
// This transaction pays 1 bitcoin to the player's private key generated when this page was loaded.
// We surpisingly must pass the hieght here becuase coinbaseTX needs it as per BIP34
const coinbaseTx = makeCoinbaseTX(kenoBitsBuffer, window.roundNumber, window.keyPair.publicKey);
const target = nbits2target(window.nbits); // Compute our target
const targetString = target.toHexString();
// Make a new block header with that coinbaseTX as the only transaction
const blockHeader = generateBlock(targetString, coinbaseTx );
const blockHeaderHash = getBlockheaderHash(blockHeader);
const blockHeaderHashString = blockHeaderHash.toHexString(); // Use the hex string because it is easier and it is in correct byte order (highest first)
// Show the player how they did.
updateWheels( targetString , blockHeaderHashString );
// Now check if we solved the puzzle
// Note that here we compare the actual user-produced hash to the actual target.
// This means we give the win to the player in cases where miss by one hash but
// by sheer luck the hash happens to be less than the target.
if (blockHeaderHashString < targetString ) {
// Possible winner, show win page and submit new block
onSolve( blockHeaderHashString , blockHeader , coinbaseTx );
} else {
console.log("target:"+targetString);
console.log("hash :"+blockHeaderHashString);
console.log("Sorry, too high. Please try again. ")
}
}
// Add a line to the status area at the bottom of the win page
// https://stackoverflow.com/a/20673977/3152071
function addSubmisionStatus(s) {
let statusupdatelist = document.getElementById('statusupdatelist');
const li = document.createElement("li");
const textNode = document.createTextNode( new Date().toLocaleTimeString() +": " + s );
li.appendChild(textNode);
statusupdatelist.appendChild(li);
}
// For a really hacky way to at least see this page, try entering "window.nbits=0x20010000" into the console.
function onSolve( blockHeaderHashHexString , blockHeader , coinbaseTx ) {
// Now create and show a winner info page. Do it locally to avoid any network problems or delays.
// Put our logo at the top, this will continue to flash green as long as we are connected just like the element of the same ID on the play page
// Note that the existing JS eventlistener on the websocket seems to keep running and will update this box with the id="title". I could not find any documentation on
// why this works or should not work, please LMK if you know of any.
// Luckliy it seems that we do not need to quote the font-family name https://stackoverflow.com/questions/7638775/do-i-need-to-wrap-quotes-around-font-family-names-in-css
document.writeln("<div style='box-sizing:border-box;'><span style='font-family: serif ;font-size: large; padding: 5px; background: black; color:white; display:inline-block;' id='title'>HASH HUNT</span></div>");
// https://www.w3schools.com/jsref/met_win_open.asp
document.writeln("<h1>Hash Hunt Solved Puzzle Page</h1>");
document.writeln("<p>Congradulations on solving the puzzle! What happens next?</p>");
document.writeln("<p>Your solution is being submitted to the network right now. If your solution was the first");
document.writeln(" one in this round then you will win the prize. </P>");
document.writeln("<h2>Your Hash Hunt prize claim code is:</h2>");
if (window.webSocketServerURL != default_ws_server) {
document.writeln("<div style='background: darkred; padding=2px;color: white'>SUSPECT SOLUTION! NON-STANDARD WEBSOCKET SERVER TAMPERING!</div>");
} else {
document.writeln("<div>");
}
document.writeln(" <pre>"+window.keyPair.toWIF()+"</pre>");
document.writeln("</div>");
document.writeln("<p>It is very important that you save your claim code someplace very");
document.writeln(" safe.</p>");
document.writeln(" <ul>");
document.writeln(" <li>Take a photo of it with your phone</li>");
document.writeln(" <li>Make a screen shot</li>");
document.writeln(" <li>Copy/paste it into an email and send to yourself</li>");
document.writeln(" <li>Print it on your printer</li>");
document.writeln(" <li>Write it down (carefully) on a peice of paper</li>");
document.writeln(" </ul>");
document.writeln(" <p>Do it <b>NOW</b>. If you loose this code then you will not be able to claim");
document.writeln(" your prize. Do not share it with anyone who you do not 100% trust since anyone");
document.writeln(" with this code can claim your prize. </p>");
document.writeln("<h2>Status check links</h2>");
document.writeln("<a target='_blank' href='https://explorer.btc.com/btc/search/"+blockHeaderHashHexString+"'>Check if solution was accepted by the network</a>");
document.writeln("<br>");
document.writeln("<a target='_blank' href='https://explorer.btc.com/btc/search/"+getAddressFromKeyPair(window.keyPair)+"'>Check if prize is available to be claimed</a>");
document.writeln("<h2>Live Submision Updates Here</h2>");