├── .gitignore ├── Dockerfile ├── LICENSE.md ├── PoW_Bot_Deterrent_API_Tokens └── README.md ├── README.md ├── build-docker.sh ├── example ├── index.html └── main.go ├── go.mod ├── go.sum ├── main.go ├── package.json ├── proofOfWorkerStub.js ├── readme ├── probability.png ├── screencast.gif ├── sequence.drawio └── sequence.png ├── static ├── pow-bot-deterrent.css ├── pow-bot-deterrent.js ├── proofOfWorker.js └── scrypt.wasm └── wasm_build └── build_wasm.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | wasm_build/node_modules 3 | wasm_build/scrypt-wasm 4 | PoW_Bot_Deterrent_API_Tokens/* 5 | !PoW_Bot_Deterrent_API_Tokens/README.md 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM golang:1.16-alpine as build 3 | ARG GOARCH= 4 | ARG GO_BUILD_ARGS= 5 | 6 | RUN mkdir /build 7 | WORKDIR /build 8 | RUN apk add --update --no-cache ca-certificates git 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | COPY main.go main.go 12 | RUN go get && go build -v $GO_BUILD_ARGS -o /build/pow-bot-deterrent . 13 | 14 | FROM alpine 15 | WORKDIR /app 16 | COPY --from=build /build/pow-bot-deterrent /app/pow-bot-deterrent 17 | COPY static /app/static 18 | COPY PoW_Bot_Deterrent_API_Tokens /app/PoW_Bot_Deterrent_API_Tokens 19 | RUN chmod +x /app/pow-bot-deterrent 20 | ENTRYPOINT ["/app/pow-bot-deterrent"] 21 | 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | ================== 3 | 4 | Version 3, 29 June 2007 5 | 6 | 7 | Preamble 8 | --------------------- 9 | 10 | The GNU General Public License is a free, copyleft license for software and other kinds of works. 11 | 12 | The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. 13 | 14 | When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. 15 | 16 | To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. 17 | 18 | For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. 19 | 20 | Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. 21 | 22 | For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. 23 | 24 | Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. 25 | 26 | Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. 27 | 28 | The precise terms and conditions for copying, distribution and modification follow. 29 | 30 | TERMS AND CONDITIONS 31 | ==================== 32 | 33 | ## 0. Definitions. 34 | -------------------------------- 35 | 36 | 37 | “This License” refers to version 3 of the GNU General Public License. 38 | 39 | “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. 40 | 41 | “The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. 42 | 43 | To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. 44 | 45 | A “covered work” means either the unmodified Program or a work based on the Program. 46 | 47 | To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. 48 | 49 | To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. 50 | 51 | An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 52 | 53 | ## 1. Source Code. 54 | -------------------------------- 55 | 56 | 57 | The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. 58 | 59 | A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. 60 | 61 | The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. 62 | 63 | The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. 64 | 65 | The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. 66 | 67 | The Corresponding Source for a work in source code form is that same work. 68 | 69 | ## 2. Basic Permissions. 70 | ---------------------------- 71 | 72 | 73 | All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. 74 | 75 | You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. 76 | 77 | Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 78 | 79 | ## 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 80 | -------------------------------- 81 | 82 | No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. 83 | 84 | When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 85 | 86 | ## 4. Conveying Verbatim Copies. 87 | -------------------------------- 88 | 89 | You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. 90 | 91 | You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 92 | 93 | ## 5. Conveying Modified Source Versions. 94 | -------------------------------- 95 | 96 | You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: 97 | 98 | - a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 99 | - b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. 100 | - c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. 101 | - d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. 102 | 103 | A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 104 | 105 | ## 6. Conveying Non-Source Forms. 106 | -------------------------------- 107 | 108 | You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: 109 | 110 | - a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. 111 | - b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. 112 | - c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. 113 | - d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. 114 | - e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. 115 | 116 | A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. 117 | 118 | A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. 119 | 120 | “Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. 121 | 122 | If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). 123 | 124 | The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. 125 | 126 | Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 127 | 128 | ## 7. Additional Terms. 129 | -------------------------------- 130 | 131 | “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. 132 | 133 | When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. 134 | 135 | Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: 136 | 137 | - a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 138 | - b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or 139 | - c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or 140 | - d) Limiting the use for publicity purposes of names of licensors or authors of the material; or 141 | - e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 142 | - f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. 143 | 144 | All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. 145 | 146 | If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. 147 | 148 | Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 149 | 150 | ## 8. Termination. 151 | -------------------------------- 152 | 153 | You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). 154 | 155 | However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. 156 | 157 | Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. 158 | 159 | Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 160 | 161 | ## 9. Acceptance Not Required for Having Copies. 162 | -------------------------------- 163 | 164 | You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 165 | 166 | 167 | ## 10. Automatic Licensing of Downstream Recipients. 168 | -------------------------------- 169 | 170 | Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. 171 | 172 | An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. 173 | 174 | You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 175 | 176 | ## 11. Patents. 177 | -------------------------------- 178 | 179 | A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. 180 | 181 | A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. 182 | 183 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. 184 | 185 | In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. 186 | 187 | If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. 188 | 189 | If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. 190 | 191 | A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. 192 | 193 | Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 194 | 195 | ## 12. No Surrender of Others' Freedom. 196 | -------------------------------- 197 | 198 | If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 199 | 200 | ## 13. Use with the GNU Affero General Public License. 201 | -------------------------------- 202 | 203 | Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 204 | 205 | 206 | ## 14. Revised Versions of this License. 207 | -------------------------------- 208 | 209 | The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 210 | 211 | Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. 212 | 213 | If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. 214 | 215 | Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 216 | 217 | ## 15. Disclaimer of Warranty. 218 | -------------------------------- 219 | 220 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 221 | 222 | 223 | ## 16. Limitation of Liability. 224 | -------------------------------- 225 | 226 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 227 | 228 | ## 17. Interpretation of Sections 15 and 16. 229 | -------------------------------- 230 | 231 | If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 232 | -------------------------------------------------------------------------------- /PoW_Bot_Deterrent_API_Tokens/README.md: -------------------------------------------------------------------------------- 1 | # PoW_Bot_Deterrent_API_Tokens folder 2 | 3 | 💥PoW! Bot Deterrent will store API tokens here. You may place this folder either in the current working directory from which the application is started, or you may place it next to the application binary. 4 | 5 | If you run 💥PoW! Bot Deterrent in a linux container, you will want to mount this folder to a persistent volume so you don't lose your API tokens when you upgrade the container to a new image! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💥PoW! Bot Deterrent 2 | 3 | A proof-of-work based bot deterrent. Lightweight, self-hosted and copyleft licensed. 4 | 5 | ![screencast](readme/screencast.gif) 6 | 7 | Compared to mainstream captchas like recaptcha, hcaptcha, friendlycaptcha, this one is better for a few reasons: 8 | 9 | - Free as in Freedom, and Free as in Free Beer 10 | - It is lightweight & all dependencies are included; total file size is about 68KB unminified / uncompressed, and 23kb gzipped. 11 | - It is self-hosted. It does not spy on you or your users; you can tell because you run it on your own server, you wholly own and control it. 12 | - If you wish to use the one that I host instead of running it yourself, just let me know. Maybe we can work something out. 13 | 14 | Compared to other proof of work bot deterrents like mCaptcha, I believe that this one is better because: 15 | 16 | - It uses a multi-threaded [WASM (Web Assembly)](https://webassembly.org/) [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) running the [Scrypt hash function](https://en.wikipedia.org/wiki/Scrypt) instead of [SHA256](https://en.wikipedia.org/wiki/SHA-2). Because of this, it's: 17 | - 1. Fundamentally harder to accelerate. 18 | - 1. More likely to stop bots; a basic headless browser with JS execution might not be enough. 19 | - It is optimized for production use; its API minimizes the number of requests and amount of latency that you have to add to your system. 20 | 21 | 22 | ### Table of Contents 23 | 24 | 1. [How it works](#how-it-works) 25 | 1. [What is Proof of Work?](#what-is-proof-of-work) 26 | 1. [Overview sequence diagram](#overview-sequence-diagram) 27 | 1. [Configuring](#configuring) 28 | 1. [HTTP Challenge API](#http-challenge-api) 29 | 1. [HTTP Admin API](#http-admin-api) 30 | 1. [HTML DOM API](#html-dom-api) 31 | 1. [Running the example app](#running-the-example-app) 32 | 1. [Implementation walkthrough via example app](#implementation-walkthrough-via-example-app) 33 | 1. [Implementation Details for Developers](#implementation-details-for-developers) 34 | 1. [What is Proof of Work? Extended Concrete Example](#what-is-proof-of-work-extended-concrete-example) 35 | 36 | # How it works 37 | 38 | This application was designed to be a drop-in replacement for ReCaptcha by Google. It works pretty much the same way; 39 | 40 | 1. Your web application requests a challenge (in this case, a batch of challenges) from the challenge HTTP API 41 | 2. Your web application displays an HTML page which includes a form, and passes the challenge data to the form 42 | 3. The HTML page includes the JavaScript part of the Bot Deterrent app, this JavaScript draws a progress bar on the page 43 | 4. When the Proof of Work is complete, its JavaScript will fire off a callback to your JavaScript 44 | 5. When the form is submitted, your web application submits the nonce (solution) to the challenge HTTP API for validation 45 | 46 | # What is Proof of Work? 47 | 48 | Proof of Work (PoW) is a scheme by which one computer can prove to another that it expended a certain amount of computational effort. 49 | 50 | PoW does not require any 3rd party or authority to enforce rules, it is based on mathematics and the nature of the universe. 51 | 52 | PoW works fairly well as a deterrent against spam, a PoW requirement makes sending high-volume spam computationally expensive. 53 | 54 | It is impossible to predict how long a given Proof of Work will take to calculate. It could take no time at all (got it on the first try 😎 ), or it could take an abnormally long time (got unlucky and took forever to find the right hash 😟 ). You can think of it like flipping coins until you get a certain # of heads in a row. This **DOES** matter in terms of user interface and usability, so you will want to make sure that the difficulty is low enough that users are extremely unlikely to be turned away by an unlucky "takes forever" challenge. 55 | 56 | The word ["Nonce"](https://en.wikipedia.org/wiki/Cryptographic_nonce#Hashing) in this document refers to "Number Used Once", in the context of hashing and proof of work. 57 | 58 | If you want to read more or see a concrete example, see [What is Proof of Work? Extended Concrete Example](#what-is-proof-of-work-extended-concrete-example) at the bottom of this file. 59 | 60 | # Overview sequence diagram 61 | 62 | ![sequence diagram](readme/sequence.png) 63 | 64 | This diagram was created with https://app.diagrams.net/. 65 | To edit it, download the diagram file and edit it with the https://app.diagrams.net/ web application, or you may run the application from [source](https://github.com/jgraph/drawio) if you wish. 66 | 67 | # Configuring 68 | 69 | 💥PoW! Bot Deterrent gets all of its configuration from environment variables. 70 | 71 | #### `POW_BOT_DETERRENT_ADMIN_API_TOKEN` 72 | 73 | ⚠️ **REQUIRED** 74 | 75 | This token allows control of the Admin API & allows the bearer to create, list, and revoke application tokens. 76 | 77 | ---- 78 | 79 | #### `POW_BOT_DETERRENT_BATCH_SIZE` 80 | 81 | 💬 *OPTIONAL* default value is 1000 82 | 83 | How many challenges to return at once. 84 | 85 | ---- 86 | 87 | #### `POW_BOT_DETERRENT_DEPRECATE_AFTER_BATCHES` 88 | 89 | 💬 *OPTIONAL* default value is 10 90 | 91 | How many "batches-old" challenges can be before being dropped from memory. 92 | 93 | ---- 94 | 95 | #### `POW_BOT_DETERRENT_LISTEN_PORT` 96 | 97 | 💬 *OPTIONAL* default value is 2730 98 | 99 | Which TCP port should the server listen on. 100 | 101 | ---- 102 | 103 | #### `POW_BOT_DETERRENT_SCRYPT_CPU_AND_MEMORY_COST` 104 | 105 | 💬 *OPTIONAL* default value is 4096 106 | 107 | Allows you to tweak how difficult each individual hash in the proof of work will be. 108 | 109 | ---- 110 | 111 | # HTTP Challenge API 112 | 113 | #### `POST /GetChallenges?difficultyLevel=` 114 | 115 | Required Header: `Authorization: Bearer ` 116 | 117 | Return type: `application/json` 118 | 119 | `GetChallenges` returns a JSON array of 1000 strings. The Bot Deterrent server will remember each one of these challeges until it is 120 | restarted, or until GetChallenges has been called 10 more times. Each challenge can only be used once. 121 | 122 | The difficultyLevel parameter specifies how many bits of difficulty the challenges should have. 123 | Each time you increase the difficultyLevel by 1, it doubles the amount of time the Proof of Work will take on average. 124 | The recommended value is 5. A difficulty of 5 will be solved quickly by a laptop or desktop computer, and solved within 60 seconds or so by a cell phone. 125 | 126 | 127 | #### `POST /Verify?challenge=&nonce=` 128 | 129 | Required Header: `Authorization: Bearer ` 130 | 131 | Return type: `text/plain` (error/status messages only) 132 | 133 | `Verify` returns HTTP 200 OK only if all of the following are true: 134 | 135 | - This challenge was returned by `GetChallenges`. 136 | - `GetChallenges` hasn't been called 10 or more times since this challenge was originally returned. 137 | - `Verify` has not been called on this challenge before. 138 | - The provided hexadecimal nonce solves the challenge. 139 | - (The winning nonce string will be passed to the function you specify in [data-pow-bot-deterrent-callback](#data-pow-bot-deterrent-callback). You just have to make sure to post it to your server so your server can include it when it calls `/Verify`) 140 | 141 | 142 | Otherwise it returns 404, 400, or 500. 143 | 144 | 145 | #### `GET /static/` 146 | 147 | Return type: depends on file 148 | 149 | Files: 150 | 151 | - pow-bot-deterrent.js 152 | - pow-bot-deterrent.css 153 | - proofOfWorker.js 154 | 155 | You only need to include `pow-bot-deterrent.js` in your page, it will pull in the other files automatically if they are not already present in the page. 156 | See below for a more detailed implementation walkthrough. 157 | 158 | # HTTP Admin API 159 | 160 | #### `GET /Tokens` 161 | 162 | Required Header: `Authorization: Bearer ` 163 | 164 | Return type: `text/plain` 165 | 166 | Lists all existing api tokens in CSV format, including the token itself, the name, and when it was created. 167 | 168 | #### `POST /Tokens/Create?name=` 169 | 170 | Required Header: `Authorization: Bearer ` 171 | 172 | Return type: `text/plain` 173 | 174 | Creates a new given API token with the given name and returns the token as a plain text hexadecimal string. 175 | 176 | #### `POST /Tokens/Revoke?token=` 177 | 178 | Required Header: `Authorization: Bearer ` 179 | 180 | Return type: `text/plain` (error/status messages only) 181 | 182 | Revokes an existing API token. 183 | 184 | 185 | # HTML DOM API 186 | 187 | In order to set up 💥PoW! Bot Deterrent on your page, you just need to load/include `pow-bot-deterrent.js` and one or more html elements 188 | with all 3 of the following properties: 189 | 190 | #### `data-pow-bot-deterrent-url` 191 | 192 | This is the base url from which `pow-bot-deterrent.js` will attempt to load additional resources `pow-bot-deterrent.css` and `proofOfWorker.js`. 193 | 194 | > 💬 *INFO* In our examples, we passed the Bot Deterrent server URL down to the HTML page and used it as the value for this property. 195 | However, that's not required. The HTML page doesn't need to talk to the Bot Deterrent server at all, it just needs to know where it can 196 | download the `pow-bot-deterrent.css` and `proofOfWorker.js` files. There is nothing stopping you from simply hosting those files on your own server or CDN and placing the corresponding URL into the `data-pow-bot-deterrent-url` property. 197 | 198 | #### `data-pow-bot-deterrent-challenge` 199 | 200 | Set this property to one of the challenge strings returned by `GetChallenges`. It must be unique, each challenge can only be used once. 201 | 202 | ⚠️ **NOTE** that the element with the 3 `pow-bot-deterrent-xyz` data properties **MUST** be placed **inside a form element**. This is required, to allow the bot deterrent to know which input elements it needs to trigger on. We only want it to trigger when the user actually intends to submit the form; otherwise we are wasting a lot of their CPU cycles for no reason! 203 | 204 | #### `data-pow-bot-deterrent-callback` 205 | 206 | This is the name of a function in the global namespace which will be called & passed the winning nonce once the Proof of Work 207 | is completed. So, for example, if you had: 208 | 209 | `
` 210 | 211 | Then you would provide your callback like so: 212 | 213 | ``` 214 | 219 | ``` 220 | 221 | > 💬 *INFO* You may also nest the callback inside object(s) if you wish: 222 | 223 | `
` 224 | 225 | ``` 226 | 233 | ``` 234 | 235 | When `pow-bot-deterrent.js` runs, if it finds an element with `data-pow-bot-deterrent-challenge` & `data-pow-bot-deterrent-callback`, but the callback function is not defined yet, it will print a warning message. If the callback is still not defined when the Proof of Work is completed, it will throw an error. 236 | 237 | > 💬 *INFO* the element with the `pow-bot-deterrent` data properties should probably be styled to have a very small font size. When I was designing the css for the bot deterrent element, I made everything scale based on the font size (by using `em`). But because the page I was testing it on had a small font by default, I accidentally made it huge when it is rendered on a default HTML page. So for now you will want to make the font size of the element which contains it fairly small, like `10px` or `11px`. 238 | 239 | #### `window.botBotDeterrentInit` 240 | 241 | The bot deterrent event listeners, elements, css, & webworkers **won't be loaded until this function is called**. 242 | 243 | **`pow-bot-deterrent.js` will call this function automatically** if there's at least one DOM element with `data-pow-bot-deterrent-challenge` already when `pow-bot-deterrent.js` loads. Otherwise, it is up to you to call this function after you render the DOM elements & add the `data-pow-bot-deterrent-challenge` property to them. 244 | 245 | This function will throw an error if it is called more than once without calling `window.powBotDeterrentReset()` in between. 246 | 247 | For example: 248 | 249 | ``` 250 | 253 | ``` 254 | 255 | #### `window.powBotDeterrentReset` 256 | 257 | Resets the bot deterrent(s), stops the webworkers, etc. Use this if you have updated the page and you need to call `window.botBotDeterrentInit` again. 258 | 259 | #### `window.botBotDeterrentInitDone` 260 | 261 | A boolean variable that `pow-bot-deterrent.js` uses internally, so it can know if it has already been initialized or not. 262 | 263 | ---- 264 | 265 | If you wanted to integrate 💥PoW! Bot Deterrent with a JavaScript driven front-end app, like a React-based app for example, you can install it via npm: 266 | 267 | `npm install git+https://git.sequentialread.com/forest/pow-bot-deterrent.git` 268 | 269 | and use it like this: 270 | 271 | ``` 272 | import {React, useEffect, useState} from 'react'; 273 | 274 | ... 275 | 276 | import '../node_modules/pow-bot-deterrent/static/pow-bot-deterrent.css' 277 | import '../node_modules/pow-bot-deterrent/static/pow-bot-deterrent.js' 278 | 279 | // assumes that this component gets passed the botDeterrentURL and challenge as props 280 | // these would be loaded/passed from the server somehow. Especially the challenge, it has to be unique each time. 281 | function MyComponent({botDeterrentURL, challenge}) { 282 | 283 | // When the component is created, set a unique string to be used as the callback in the global namespace (window) 284 | const [uniqueCallback] = useState(`pow-bot-deterrent-callback-${String(Math.random()).substring(6)}`); 285 | 286 | // when the nonce is calculated, we will call setNonce 287 | const [nonce, setNonce] = useState(""); 288 | 289 | // because this useEffect will cause the WebWorkers to be re-created each time, which could get expensive, 290 | // you will want to ensure that this component does not somehow flicker in and out of existence 291 | useEffect(() => { 292 | window[uniqueCallback] = (winningNonce) => { 293 | setNonce(winningNonce); 294 | } 295 | 296 | // Maybe less clear than the above, but JavaScript heads might enjoy this more: 297 | // window[uniqueCallback] = setNonce; 298 | 299 | if(window.botBotDeterrentInitDone) { 300 | window.powBotDeterrentReset(); 301 | } 302 | window.botBotDeterrentInit(); 303 | }, [uniqueCallback]); 304 | 305 | return ( 306 |
307 | ... 308 | 309 |
310 | 311 | 312 |
316 |
317 |
318 |
319 | ); 320 | } 321 | 322 | ``` 323 | 324 | 325 | 326 | # Running the example app 327 | 328 | The `example` folder in this repository contains an example app that demonstrates how to implement the 💥PoW! Bot Deterrent 329 | in as simple of a fashion as possible. 330 | 331 | If you wish to run the example app, you will have to run both the 💥PoW! Bot Deterrent server and the example app server. 332 | 333 | The easiest way to do this would probably be to open two separate terminal windows or tabs and run each app in its own terminal. 334 | 335 | #### `terminal 1` 336 | ``` 337 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ go run main.go 338 | 339 | panic: can't start the app, the POW_BOT_DETERRENT_ADMIN_API_TOKEN environment variable is required 340 | 341 | goroutine 1 [running]: 342 | main.main() 343 | /home/forest/Desktop/git/pow-bot-deterrent/main.go:84 +0xf45 344 | exit status 2 345 | ``` 346 | As you can see, the server requires an admin API token to be set. This is the token we will use authenticate and create 347 | individual tokens for different apps or different people who all might want to use the bot deterrent server. 348 | 349 | Once we provide this admin API token environment variable, it will run just fine: 350 | 351 | ``` 352 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ POW_BOT_DETERRENT_ADMIN_API_TOKEN="example_admin" go run main.go 353 | 2021/02/25 16:24:00 💥 PoW! Bot Deterrent server listening on port 2370 354 | ``` 355 | 356 | Now let's try to launch the example Todo List application: 357 | 358 | #### `terminal 2` 359 | ``` 360 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ cd example/ 361 | forest@thingpad:~/Desktop/git/pow-bot-deterrent/example$ go run main.go 362 | 363 | panic: can't start the app, the BOT_DETERRENT_API_TOKEN environment variable is required 364 | 365 | goroutine 1 [running]: 366 | main.main() 367 | /home/forest/Desktop/git/pow-bot-deterrent/example/main.go:40 +0x488 368 | exit status 2 369 | ``` 370 | 371 | It's a similar story for the example app, except this time we can't just make up any old token, we have to ask the Bot Deterrent server to generate a new API token for the example app. I will do this by manually sending it an http request with `curl`: 372 | 373 | ``` 374 | $ curl -X POST -H "Authorization: Bearer example_admin" http://localhost:2370/Tokens/Create 375 | 400 Bad Request: url param ?name= is required 376 | 377 | $ curl -X POST -H "Authorization: Bearer example_admin" http://localhost:2370/Tokens/Create?name=todo-list 378 | b804f221e8a9053b2e6e89de83c5d7a4 379 | ``` 380 | 381 | Now we can use this token to start the example Todo List app: 382 | 383 | ``` 384 | $ BOT_DETERRENT_API_TOKEN="b804f221e8a9053b2e6e89de83c5d7a4" go run main.go 385 | 2021/02/25 16:38:32 📋 Todo List example application listening on port 8080 386 | ``` 387 | 388 | Then, you should be able to visit the example Todo List application in the browser at http://localhost:8080. 389 | 390 | # Implementation walkthrough via example app 391 | 392 | Lets walk through how example app works and how it integrates the 💥PoW! Bot Deterrent. 393 | 394 | The Todo List app has three pieces of configuration related to the bot deterrent: the API token, the url, and the difficulty. 395 | Currently the url and difficulty are hardcoded into the Todo List app's code, while the API token is provideded via an environment variable. 396 | 397 | ``` 398 | // 5 bits of difficulty, 1 in 2^5 (1 in 32) tries will succeed on average. 399 | // 400 | // 7 bits of difficulty would be fine for apps that are never used on mobile phones, 5 is better suited for mobile apps 401 | // 402 | const difficultyLevel = 5 403 | 404 | ... 405 | 406 | apiToken := os.ExpandEnv("$BOT_DETERRENT_API_TOKEN") 407 | if apiToken == "" { 408 | panic(errors.New("can't start the app, the BOT_DETERRENT_API_TOKEN environment variable is required")) 409 | } 410 | 411 | powAPIURL, err = url.Parse("http://localhost:2370") 412 | ``` 413 | 414 | When the Todo List app starts, it has a few procedures it runs through to ensure it's ready to run, including 415 | retrieving a batch of challenges from the bot deterrent API: 416 | 417 | ``` 418 | func main() { 419 | 420 | ... 421 | 422 | err = loadChallenges() 423 | if err != nil { 424 | panic(errors.Wrap(err, "can't start the app because could not loadChallenges():")) 425 | } 426 | ``` 427 | 428 | `loadChallenges()` calls the `GetChallenges` API & sets the global variable `powChallenges`. 429 | 430 | It's a good idea to do this when your app starts, to ensure that it can talk to the Bot Deterrent server before it starts serving content to users. 431 | 432 | The Todo List app only has one route: `/`. 433 | 434 | This route displays a basic HTML page with a form, based on the template `index.html`. 435 | 436 | ``` 437 | http.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) { 438 | 439 | ... 440 | 441 | }) 442 | ``` 443 | 444 | This route does 4 things: 445 | 446 | 1. If it was a `POST` request, call the `Verify` endpoint to ensure that a valid challenge and nonce were posted. 447 | - see `validatePow` on line 202. 448 | 2. If it was a *valid* `POST` request, add the posted `item` string to the global list variable `items`. 449 | 3. Check if the global `powChallenges` list is running out, if it is, kick off a background process to grab more from the `GetChallenges` API. 450 | - see `loadChallenges` on line 155. 451 | 4. Consume one challenge string from the global `powChallenges` list variable and output an HTML page containing that challenge. 452 | 453 | The challenge API (`GetChallenges` and `Verify`) was designed this way to optimize the performance of your application; instead of calling something like *GetChallenge* for every single request, your application can load batches of challenges asychronously in the background, and always have a challenge loaded into local memory & ready to go. 454 | 455 | However, you have to make sure that you are using it right: 456 | 457 | - You must ensure that you only serve each challenge once, and 458 | - You must only call `GetChallenges` when necessary (when you are running out of challenges). If you call it too often you may accidentally expire otherwise-valid challenges before they can be verified. 459 | - Note that for high-traffic web sites where multiple requests can hit the server at once, you should probably use a [lock, mutex](https://git.sequentialread.com/forest/sequentialread-comments/src/af2f999134214412c1c6cf32c458e9b8a8c88289/main.go#L278), partitioning scheme, or other thread safe data structure to ensure that two concurrent requests don't end up trying to grab the same challenge from the list ([Software Race Condition](https://en.wikipedia.org/wiki/Race_condition#Software)). 460 | 461 | --- 462 | 463 | Anyways, lets get on with things & look at how the Todo List app renders its HTML page. 464 | There are two main important parts, the form and the javascript at the bottom: 465 | 466 | ``` 467 |
468 | 469 | 470 | 471 | 472 |
476 |
477 |
478 | 479 | ... 480 | 481 | 487 | 488 | ``` 489 | 490 | ⚠️ **NOTE** that the element with the `pow-bot-deterrent` data properties is placed **inside a form element**. This is required because the bot deterrent needs to know which input elements it should trigger on. We only want it to trigger when the user actually intends to submit the form; otherwise we are wasting a lot of their CPU cycles for no reason! 491 | 492 | > 💬 *INFO* The double curly brace elements like `{{ .Challenge }}` are Golang string template interpolations. They are specific to the example app & how it renders the page. 493 | 494 | When the page loads, the `pow-bot-deterrent.js` script will execute, querying the page for all elements with the `data-pow-bot-deterrent-challenge` 495 | property. It will then validate each element to make sure it also has the `data-pow-bot-deterrent-url` and `data-pow-bot-deterrent-callback` properties. For each element it found, it will locate the `
` parent/grandparent enclosing the element. If none are found, it will throw an error. Otherwise, it will set up an event listener on every input element inside that form, so that as soon as the user starts filling out the form, the bot deterrent display will pop up and the Proof of Work will begin. 496 | 497 | When the Proof of Work finishes, `pow-bot-deterrent.js` will call the function specified by `data-pow-bot-deterrent-callback`, passing the winning nonce as the first argument, or throw an error if that function is not defined. 498 | 499 | > 💬 *INFO* the element with the `pow-bot-deterrent` data properties also has a class that *WE* defined, called `bot-deterrent-container`. 500 | This class has a very small font size. When I was designing the css for the bot deterrent element, I made everything scale based on the font size (by using `em`). But because the page I was testing it on had a small font by default, I accidentally made it huge when it is rendered on a default HTML page. So for now you will want to make the font size of the element which contains it fairly small. 501 | 502 | ``` 503 | 512 | ``` 513 | 514 | I think that concludes the walkthrough! In the Todo App, as soon as `pow-bot-deterrent.js` calls `myPowCallback`, the form will be completely filled out and the submit button will be enabled. When the form is posted, the browser will make a `POST` request to the server, and the server logic we already discussed will take over, closing the loop. 515 | 516 | # Implementation Details for Developers 517 | 518 | 💥PoW! Bot Deterrent uses [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)s and [WebAssembly (WASM)](https://developer.mozilla.org/en-US/docs/WebAssembly) to calculate Proof of Work in the browser as efficiently as possible. WebWorkers allow the application to run code on multiple threads and take advantage of multi-core CPUs. WebAssembly gives us access to *actual integers* (😲) and more low-level memory operations that have been historically missing from JavaScript. 519 | 520 | I measured the performance of the application with and without WebWorker / WebAssembly on a variety of devices. 521 | 522 | I tried two different implementations of the scrypt hash function, one from the [Stanford Javascript Crypto Library (sjcl)](https://github.com/bitwiseshiftleft/sjcl) and the WASM one from [github.com/MyEtherWallet/scrypt-wasm](https://github.com/MyEtherWallet/scrypt-wasm). 523 | 524 | | hardware | scryptCPUAndMemoryCost | sjcl,single thread | sjcl,multi-thread | WASM,multi-thread | 525 | | :------------- | :------------- | :------------- | :----------: | -----------: | 526 | | Lenovo T480s | 4096 | 1-2 h/s | ~5 h/s | ~70 h/s | 527 | | Motorolla G7 | 4096 | not tested | not tested | ~12 h/s | 528 | | Macbook Air 2018 | 4096 | not tested | not tested | ~ 32h/s | 529 | | Google Pixel 3a | 4096 | not tested | not tested | ~ 24h/s | 530 | | Framework Laptop AMD 7640U | 4096 | not tested | not tested | ~ 243h/s | 531 | | Framework Laptop AMD 7640U | 16384 | not tested | not tested | ~ 57h/s | 532 | | Motorolla One 5G Ace | 16384 | not tested | not tested | ~ 35h/s | 533 | 534 | I had some trouble getting the WASM module loaded properly inside the WebWorkers. In my production environment, the web application server and the Bot Deterrent server are running on separate subdomains, so I was getting cross-origin security violation issues. 535 | 536 | I ended up embedding the WASM binary inside the WebWorker javascript `proofOfWorker.js` using a boutique binary encoding called [base32768](https://github.com/qntm/base32768). I set up a custom build process for this in the `wasm_build` folder. It even includes the scripts necessary to clone the github.com/MyEtherWallet/scrypt-wasm repo and install the Rust compiler! You are welcome! However, this script does assume that you are running on a Linux computer. I have not tested it outside of Linux. 537 | 538 | 539 | # What is Proof of Work? Extended Concrete Example 540 | 541 | 542 | When you calculate the hash of a file or a piece of data, you get this random string of characters: 543 | 544 | ``` 545 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md 546 | 119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md 547 | ``` 548 | 549 | Here, I have called the SHA256 hash function on the GPLv3 `LICENSE.md` file in this repo. The result is displayed as a hexidecimal string, that is, each character can have one of 16 possible values, 0-9 and a-f. You can think of it like rolling a whole bunch of 16-sided dice, however, it's not random like dice are, its *pseudorandom*, meaning that given the same input file, if we execute the same hash function multiple times, it will return the same output. All the dice will land the same way every time: 550 | 551 | ``` 552 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md 553 | 119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md 554 | 555 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md 556 | 119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md 557 | 558 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md 559 | 119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md 560 | ``` 561 | 562 | However, If I change the input, even if I only change it a tiny bit, say, append the letter `a` at the end of the file, it will completely change the way the result shakes out: 563 | 564 | ``` 565 | # append the letter a to the end of the file 566 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ echo 'a' >> LICENSE.md 567 | 568 | # calculate the SHA256 hash again 569 | forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md 570 | 67e0e2cc3429b799036bfa95e2bd7854a0e468939d6cb9d4a3e9d32c3b6615dc LICENSE.md 571 | ``` 572 | 573 | It's impossible to tell how the hash will be affected by changing the input... Well, unless you calculate the hash! 574 | This is related to the famous [Halting Problem](https://en.wikipedia.org/wiki/Halting_problem) from computer science. 575 | 576 | PoW is a game which exploits these interesting properties of hash functions. It works like this: I give you a file, and then you have to change the file (Add "`a`"s at the end, increment a number in the file, whatever you want to do) and recalculate the hash each time you change it, until you find a hash which ends in two zeros in a row. Or three zeros in a row, or four, whatever. Since there are 16 possible values for each character, each additional required zero divides your likelhood of finding the "winning" hash by 16. 577 | 578 | The number or string of "`a`"s, whatever it is you use to change the file before you hash it, is called the Nonce. 579 | 580 | This is exactly how Bitcoin mining works, Bitcoin requires miners to search for SHA256 hashes that end in a rediculously unlikely number of zeros, like flipping 100 coins and getting 100 heads in a row. 581 | 582 | 💥PoW! Bot Deterrent uses a different hash function called [Scrypt](https://en.wikipedia.org/wiki/Scrypt). Scrypt was designed to take an arbitrarily long amount of time to execute on a computer, and to be hard to optimize. 583 | 584 | A modified version of Scrypt is used by the crypto currency [Litecoin](https://en.wikipedia.org/wiki/Litecoin). 585 | 586 | Like I mentioned in the condensed "What is Proof of Work" section, because of this pseudorandom behaviour, we can't predict how long a given challenge will take to complete. The UI does have a "progress bar" but the behaviour of the bar is more related to probability than to progress. In fact, it displays the "probability that we should have found the answer already", which is related to the amount of work done so far, but it's not exactly a linear relationship. 587 | 588 | Here is a screenshot of a plot I generated using WolframAlpha while I was developing this progress bar, given the formula for the progress bar's width: 589 | 590 | ![wolfram alpha plot](readme/probability.png) 591 | 592 | This explains why the progress bar moves faster at the start & slows down once it starts approaching the end. 593 | -------------------------------------------------------------------------------- /build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | VERSION="0.0.13" 4 | 5 | rm -rf dockerbuild || true 6 | mkdir dockerbuild 7 | 8 | cp Dockerfile dockerbuild/Dockerfile-amd64 9 | cp Dockerfile dockerbuild/Dockerfile-arm 10 | cp Dockerfile dockerbuild/Dockerfile-arm64 11 | 12 | sed -E 's|FROM alpine|FROM amd64/alpine|' -i dockerbuild/Dockerfile-amd64 13 | sed -E 's|FROM alpine|FROM arm32v7/alpine|' -i dockerbuild/Dockerfile-arm 14 | sed -E 's|FROM alpine|FROM arm64v8/alpine|' -i dockerbuild/Dockerfile-arm64 15 | 16 | sed -E 's/GOARCH=/GOARCH=amd64/' -i dockerbuild/Dockerfile-amd64 17 | sed -E 's/GOARCH=/GOARCH=arm/' -i dockerbuild/Dockerfile-arm 18 | sed -E 's/GOARCH=/GOARCH=arm64/' -i dockerbuild/Dockerfile-arm64 19 | 20 | docker build -f dockerbuild/Dockerfile-amd64 -t sequentialread/pow-bot-deterrent:$VERSION-amd64 . 21 | docker build -f dockerbuild/Dockerfile-arm -t sequentialread/pow-bot-deterrent:$VERSION-arm . 22 | docker build -f dockerbuild/Dockerfile-arm64 -t sequentialread/pow-bot-deterrent:$VERSION-arm64 . 23 | 24 | docker push sequentialread/pow-bot-deterrent:$VERSION-amd64 25 | docker push sequentialread/pow-bot-deterrent:$VERSION-arm 26 | docker push sequentialread/pow-bot-deterrent:$VERSION-arm64 27 | 28 | export DOCKER_CLI_EXPERIMENTAL=enabled 29 | 30 | docker manifest create sequentialread/pow-bot-deterrent:$VERSION \ 31 | sequentialread/pow-bot-deterrent:$VERSION-amd64 \ 32 | sequentialread/pow-bot-deterrent:$VERSION-arm \ 33 | sequentialread/pow-bot-deterrent:$VERSION-arm64 34 | 35 | docker manifest annotate --arch amd64 sequentialread/pow-bot-deterrent:$VERSION sequentialread/pow-bot-deterrent:$VERSION-amd64 36 | docker manifest annotate --arch arm sequentialread/pow-bot-deterrent:$VERSION sequentialread/pow-bot-deterrent:$VERSION-arm 37 | docker manifest annotate --arch arm64 sequentialread/pow-bot-deterrent:$VERSION sequentialread/pow-bot-deterrent:$VERSION-arm64 38 | 39 | docker manifest push sequentialread/pow-bot-deterrent:$VERSION 40 | 41 | rm -rf dockerbuild || true -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 📋 Todo List 6 | 7 | 22 | 23 | 24 |

📋 Todo List

25 |
    26 | {{ range $index, $item := .Items }} 27 |
  1. {{ $item }}
  2. 28 | {{ end }} 29 |
  3. 30 | 31 | 32 | 33 | 34 | 35 |
    39 |
    40 |
  4. 41 | 42 | 43 |
44 | 50 | 51 | 76 | 77 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | "time" 17 | 18 | errors "git.sequentialread.com/forest/pkg-errors" 19 | ) 20 | 21 | var httpClient *http.Client 22 | var powAPIURL *url.URL 23 | var powChallenges []string 24 | 25 | var items []string 26 | 27 | // 5 bits of difficulty, 1 in 2^6 (1 in 32) tries will succeed on average. 28 | // 29 | // 7 bits of difficulty would be ok for apps that are never used on mobile phones, 5 is better suited for mobile apps 30 | const powDifficultyLevel = 7 31 | 32 | func main() { 33 | 34 | httpClient = &http.Client{ 35 | Timeout: time.Second * time.Duration(5), 36 | } 37 | 38 | apiToken := os.ExpandEnv("$BOT_DETERRENT_API_TOKEN") 39 | if apiToken == "" { 40 | panic(errors.New("can't start the app, the BOT_DETERRENT_API_TOKEN environment variable is required")) 41 | } 42 | 43 | var err error 44 | powAPIURL, err = url.Parse("http://localhost:2370") 45 | if err != nil { 46 | panic(errors.New("can't start the app because can't parse powAPIURL")) 47 | } 48 | 49 | err = loadChallenges(apiToken) 50 | if err != nil { 51 | panic(errors.Wrap(err, "can't start the app because could not loadChallenges():")) 52 | } 53 | 54 | _, err = os.ReadFile("index.html") 55 | if err != nil { 56 | panic(errors.Wrap(err, "can't start the app because can't open the template file. Are you in the right directory? ")) 57 | } 58 | 59 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("../static/")))) 60 | 61 | http.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) { 62 | 63 | // The user submitted a POST request, attempting to add a new item to the list 64 | if request.Method == "POST" { 65 | 66 | // Ask the bot deterrent server if the user's proof of work result is legit, 67 | // and if not, return HTTP 400 Bad Request 68 | err := request.ParseForm() 69 | if err == nil { 70 | err = validatePow(apiToken, request.Form.Get("challenge"), request.Form.Get("nonce")) 71 | } 72 | 73 | if err != nil { 74 | responseWriter.WriteHeader(400) 75 | responseWriter.Write([]byte(fmt.Sprintf("400 bad request: %s", err))) 76 | return 77 | } 78 | 79 | // Validation passed, add the user's new item to the list 80 | items = append(items, request.Form.Get("item")) 81 | 82 | http.Redirect(responseWriter, request, "/", http.StatusFound) 83 | return 84 | } 85 | 86 | // if it looks like we will run out of challenges soon, then kick off a goroutine to go get more in the background 87 | // note that in a real application in production, you would want to use a lock or mutex to ensure that 88 | // this only happens once if lots of requests come in at the same time 89 | if len(powChallenges) > 0 && len(powChallenges) < 5 { 90 | go loadChallenges(apiToken) 91 | } 92 | 93 | // if we somehow completely ran out of challenges, load more synchronously 94 | if len(powChallenges) == 0 { 95 | err = loadChallenges(apiToken) 96 | if err != nil { 97 | log.Printf("loading bot deterrent challenges failed: %v", err) 98 | responseWriter.WriteHeader(500) 99 | responseWriter.Write([]byte("bot deterrent api error")) 100 | return 101 | } 102 | } 103 | 104 | // This gets & consumes the next challenge from the begining of the slice 105 | challenge := powChallenges[0] 106 | powChallenges = powChallenges[1:] 107 | 108 | // render the page HTML & output the result to the web browser 109 | htmlBytes, err := renderPageTemplate(challenge) 110 | if err != nil { 111 | log.Printf("renderPageTemplate(): %v", err) 112 | responseWriter.WriteHeader(500) 113 | responseWriter.Write([]byte("500 internal server error")) 114 | return 115 | } 116 | responseWriter.Write(htmlBytes) 117 | }) 118 | 119 | log.Println("📋 Todo List example application listening on port 8080") 120 | 121 | err = http.ListenAndServe(":8080", nil) 122 | 123 | // if got this far it means server crashed! 124 | panic(err) 125 | } 126 | 127 | func renderPageTemplate(challenge string) ([]byte, error) { 128 | 129 | // in a real application in production you would read the template file & parse it 1 time when the app starts 130 | // I'm doing it for each request here just to make it easier to hack on it while its running 😇 131 | indexHTMLTemplateString, err := ioutil.ReadFile("index.html") 132 | if err != nil { 133 | return nil, errors.Wrap(err, "can't open the template file. Are you in the right directory? ") 134 | } 135 | pageTemplate, err := template.New("master").Parse(string(indexHTMLTemplateString)) 136 | if err != nil { 137 | return nil, errors.Wrap(err, "can't parse the template file: ") 138 | } 139 | 140 | // constructing an instance of an anonymous struct type to contain all the data 141 | // that we need to pass to the template 142 | pageData := struct { 143 | Challenge string 144 | Items []string 145 | PowAPIURL string 146 | }{ 147 | Challenge: challenge, 148 | Items: items, 149 | PowAPIURL: powAPIURL.String(), 150 | } 151 | var outputBuffer bytes.Buffer 152 | err = pageTemplate.Execute(&outputBuffer, pageData) 153 | if err != nil { 154 | return nil, errors.Wrap(err, "rendering page template failed: ") 155 | } 156 | 157 | return outputBuffer.Bytes(), nil 158 | } 159 | 160 | func loadChallenges(apiToken string) error { 161 | 162 | query := url.Values{} 163 | query.Add("difficultyLevel", strconv.Itoa(powDifficultyLevel)) 164 | 165 | loadURL := url.URL{ 166 | Scheme: powAPIURL.Scheme, 167 | Host: powAPIURL.Host, 168 | Path: filepath.Join(powAPIURL.Path, "GetChallenges"), 169 | RawQuery: query.Encode(), 170 | } 171 | 172 | request, err := http.NewRequest("POST", loadURL.String(), nil) 173 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | response, err := httpClient.Do(request) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | responseBytes, err := ioutil.ReadAll(response.Body) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | if response.StatusCode != 200 { 189 | return fmt.Errorf( 190 | "load proof of work bot deterrent challenges api returned http %d: %s", 191 | response.StatusCode, string(responseBytes), 192 | ) 193 | } 194 | 195 | err = json.Unmarshal(responseBytes, &powChallenges) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | if len(powChallenges) == 0 { 201 | return errors.New("proof of work bot deterrent challenges api returned empty array") 202 | } 203 | 204 | return nil 205 | } 206 | 207 | func validatePow(apiToken, challenge, nonce string) error { 208 | query := url.Values{} 209 | query.Add("challenge", challenge) 210 | query.Add("nonce", nonce) 211 | 212 | verifyURL := url.URL{ 213 | Scheme: powAPIURL.Scheme, 214 | Host: powAPIURL.Host, 215 | Path: filepath.Join(powAPIURL.Path, "Verify"), 216 | RawQuery: query.Encode(), 217 | } 218 | 219 | request, err := http.NewRequest("POST", verifyURL.String(), nil) 220 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | response, err := httpClient.Do(request) 226 | if err != nil { 227 | return err 228 | } 229 | 230 | if response.StatusCode != 200 { 231 | bodyString := "http read error" 232 | bytez, err := io.ReadAll(response.Body) 233 | if err == nil { 234 | bodyString = string(bytez) 235 | } 236 | log.Printf("validation failed: HTTP %d: %s\n", response.StatusCode, bodyString) 237 | return errors.New("PoW bot deterrent validation failed") 238 | } 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module git.sequentialread.com/forest/pow-bot-deterrent 2 | 3 | go 1.16 4 | 5 | require ( 6 | git.sequentialread.com/forest/pkg-errors v0.9.2 // indirect 7 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | git.sequentialread.com/forest/pkg-errors v0.9.2 h1:j6pwbL6E+TmE7TD0tqRtGwuoCbCfO6ZR26Nv5nest9g= 2 | git.sequentialread.com/forest/pkg-errors v0.9.2/go.mod h1:8TkJ/f8xLWFIAid20aoqgDZcCj9QQt+FU+rk415XO1w= 3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 4 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= 5 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 7 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 8 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 10 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "math" 12 | "net/http" 13 | "os" 14 | "os/exec" 15 | "path" 16 | "path/filepath" 17 | "regexp" 18 | "strconv" 19 | "strings" 20 | "time" 21 | 22 | errors "git.sequentialread.com/forest/pkg-errors" 23 | "golang.org/x/crypto/scrypt" 24 | ) 25 | 26 | // https://en.wikipedia.org/wiki/Scrypt 27 | type ScryptParameters struct { 28 | CPUAndMemoryCost int `json:"N"` 29 | BlockSize int `json:"r"` 30 | Paralellization int `json:"p"` 31 | KeyLength int `json:"klen"` 32 | } 33 | 34 | type Challenge struct { 35 | ScryptParameters 36 | Preimage string `json:"i"` 37 | Difficulty string `json:"d"` 38 | DifficultyLevel int `json:"dl"` 39 | } 40 | 41 | var currentChallengesGeneration = map[string]int{} 42 | var challenges = map[string]map[string]int{} 43 | 44 | func main() { 45 | 46 | var err error 47 | 48 | batchSize := 1000 49 | deprecateAfterBatches := 10 50 | portNumber := 2370 51 | scryptCPUAndMemoryCost := 16384 52 | batchSizeEnv := os.ExpandEnv("$POW_BOT_DETERRENT_BATCH_SIZE") 53 | deprecateAfterBatchesEnv := os.ExpandEnv("$POW_BOT_DETERRENT_DEPRECATE_AFTER_BATCHES") 54 | portNumberEnv := os.ExpandEnv("$POW_BOT_DETERRENT_LISTEN_PORT") 55 | scryptCPUAndMemoryCostEnv := os.ExpandEnv("$POW_BOT_DETERRENT_SCRYPT_CPU_AND_MEMORY_COST") 56 | if batchSizeEnv != "" { 57 | batchSize, err = strconv.Atoi(batchSizeEnv) 58 | if err != nil { 59 | panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_BATCH_SIZE '%s' can't be converted to an integer", batchSizeEnv)) 60 | } 61 | } 62 | if deprecateAfterBatchesEnv != "" { 63 | deprecateAfterBatches, err = strconv.Atoi(deprecateAfterBatchesEnv) 64 | if err != nil { 65 | panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_DEPRECATE_AFTER_BATCHES '%s' can't be converted to an integer", deprecateAfterBatchesEnv)) 66 | } 67 | } 68 | if portNumberEnv != "" { 69 | portNumber, err = strconv.Atoi(portNumberEnv) 70 | if err != nil { 71 | panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_LISTEN_PORT '%s' can't be converted to an integer", portNumberEnv)) 72 | } 73 | } 74 | if scryptCPUAndMemoryCostEnv != "" { 75 | scryptCPUAndMemoryCost, err = strconv.Atoi(scryptCPUAndMemoryCostEnv) 76 | if err != nil { 77 | panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_SCRYPT_CPU_AND_MEMORY_COST '%s' can't be converted to an integer", scryptCPUAndMemoryCostEnv)) 78 | } 79 | } 80 | 81 | apiTokensFolder := locateAPITokensFolder() 82 | adminAPIToken := os.ExpandEnv("$POW_BOT_DETERRENT_ADMIN_API_TOKEN") 83 | if adminAPIToken == "" { 84 | panic(errors.New("can't start the app, the POW_BOT_DETERRENT_ADMIN_API_TOKEN environment variable is required")) 85 | } 86 | 87 | scryptParameters := ScryptParameters{ 88 | CPUAndMemoryCost: scryptCPUAndMemoryCost, 89 | BlockSize: 8, 90 | Paralellization: 1, 91 | KeyLength: 16, 92 | } 93 | 94 | requireMethod := func(method string) func(http.ResponseWriter, *http.Request) bool { 95 | return func(responseWriter http.ResponseWriter, request *http.Request) bool { 96 | if request.Method != method { 97 | responseWriter.Header().Set("Allow", method) 98 | http.Error(responseWriter, fmt.Sprintf("405 Method Not Allowed, try %s", method), http.StatusMethodNotAllowed) 99 | return true 100 | } 101 | return false 102 | } 103 | } 104 | 105 | requireAdmin := func(responseWriter http.ResponseWriter, request *http.Request) bool { 106 | if request.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", adminAPIToken) { 107 | http.Error(responseWriter, "401 Unauthorized", http.StatusUnauthorized) 108 | return true 109 | } 110 | return false 111 | } 112 | 113 | requireToken := func(responseWriter http.ResponseWriter, request *http.Request) bool { 114 | authorizationHeader := request.Header.Get("Authorization") 115 | if !strings.HasPrefix(authorizationHeader, "Bearer ") { 116 | http.Error(responseWriter, "401 Unauthorized: Authorization header is required and must start with 'Bearer '", http.StatusUnauthorized) 117 | return true 118 | } 119 | token := strings.TrimPrefix(authorizationHeader, "Bearer ") 120 | if token == "" { 121 | http.Error(responseWriter, "401 Unauthorized: Authorization Bearer token is required", http.StatusUnauthorized) 122 | return true 123 | } 124 | if !regexp.MustCompile("^[0-9a-f]{32}$").MatchString(token) { 125 | errorMsg := fmt.Sprintf("401 Unauthorized: Authorization Bearer token '%s' must be a 32 character hex string", token) 126 | http.Error(responseWriter, errorMsg, http.StatusUnauthorized) 127 | return true 128 | } 129 | fileInfos, err := ioutil.ReadDir(apiTokensFolder) 130 | if err != nil { 131 | log.Printf("failed to list the apiTokensFolder (%s): %v", apiTokensFolder, err) 132 | http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError) 133 | return true 134 | } 135 | foundToken := false 136 | for _, fileInfo := range fileInfos { 137 | if strings.HasPrefix(fileInfo.Name(), token) { 138 | foundToken = true 139 | break 140 | } 141 | } 142 | if !foundToken { 143 | errorMsg := fmt.Sprintf("401 Unauthorized: Authorization Bearer token '%s' was in the right format, but it was unrecognized", token) 144 | http.Error(responseWriter, errorMsg, http.StatusUnauthorized) 145 | return true 146 | } 147 | return false 148 | } 149 | 150 | myHTTPHandleFunc("/Tokens", requireMethod("GET"), requireAdmin, func(responseWriter http.ResponseWriter, request *http.Request) bool { 151 | fileInfos, err := ioutil.ReadDir(apiTokensFolder) 152 | if err != nil { 153 | log.Printf("failed to list the apiTokensFolder (%s): %v", apiTokensFolder, err) 154 | http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError) 155 | return true 156 | } 157 | 158 | output := []string{} 159 | 160 | for _, fileInfo := range fileInfos { 161 | filenameSplit := strings.Split(fileInfo.Name(), "_") 162 | if len(filenameSplit) == 2 { 163 | filepath := path.Join(apiTokensFolder, fileInfo.Name()) 164 | content, err := ioutil.ReadFile(filepath) 165 | if err != nil { 166 | log.Printf("failed to read the token file (%s): %v", filepath, err) 167 | http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError) 168 | return true 169 | } 170 | contentInt64, err := strconv.ParseInt(string(content), 10, 64) 171 | timestampString := time.Unix(contentInt64, 0).UTC().Format(time.RFC3339) 172 | output = append(output, fmt.Sprintf("%s,%s,%d,%s", filenameSplit[0], filenameSplit[1], contentInt64, timestampString)) 173 | } 174 | 175 | } 176 | 177 | responseWriter.Header().Set("Content-Type", "text/plain") 178 | responseWriter.Write([]byte(strings.Join(output, "\n"))) 179 | 180 | return true 181 | }) 182 | 183 | myHTTPHandleFunc("/Tokens/Create", requireMethod("POST"), requireAdmin, func(responseWriter http.ResponseWriter, request *http.Request) bool { 184 | name := request.URL.Query().Get("name") 185 | if name == "" { 186 | http.Error(responseWriter, "400 Bad Request: url param ?name= is required", http.StatusBadRequest) 187 | return true 188 | } 189 | // we use underscore as a syntax character in the filename, so we have to remove it from the user-inputted name 190 | name = strings.ReplaceAll(name, "_", "-") 191 | // let's also remove any sort of funky or path-related characters 192 | name = strings.ReplaceAll(name, "*", "") 193 | name = strings.ReplaceAll(name, "?", "") 194 | name = strings.ReplaceAll(name, "/", "-") 195 | name = strings.ReplaceAll(name, "\\", "-") 196 | name = strings.ReplaceAll(name, ".", "-") 197 | 198 | tokenBytes := make([]byte, 16) 199 | rand.Read(tokenBytes) 200 | 201 | ioutil.WriteFile( 202 | path.Join(apiTokensFolder, fmt.Sprintf("%x_%s", tokenBytes, name)), 203 | []byte(fmt.Sprintf("%d", time.Now().Unix())), 204 | 0644, 205 | ) 206 | 207 | fmt.Fprintf(responseWriter, "%x", tokenBytes) 208 | 209 | return true 210 | }) 211 | 212 | myHTTPHandleFunc("/Tokens/Revoke", requireMethod("POST"), requireAdmin, func(responseWriter http.ResponseWriter, request *http.Request) bool { 213 | token := request.URL.Query().Get("token") 214 | if token == "" { 215 | http.Error(responseWriter, "400 Bad Request: url param ?token= is required", http.StatusBadRequest) 216 | return true 217 | } 218 | if !regexp.MustCompile("^[0-9a-f]{32}$").MatchString(token) { 219 | errorMsg := fmt.Sprintf("400 Bad Request: url param ?token=%s must be a 32 character hex string", token) 220 | http.Error(responseWriter, errorMsg, http.StatusBadRequest) 221 | return true 222 | } 223 | 224 | fileInfos, err := ioutil.ReadDir(apiTokensFolder) 225 | if err != nil { 226 | log.Printf("failed to list the apiTokensFolder (%s): %v", apiTokensFolder, err) 227 | http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError) 228 | return true 229 | } 230 | for _, fileInfo := range fileInfos { 231 | if strings.HasPrefix(fileInfo.Name(), token) { 232 | os.Remove(path.Join(apiTokensFolder, fileInfo.Name())) 233 | } 234 | } 235 | 236 | responseWriter.Write([]byte("Revoked")) 237 | return true 238 | }) 239 | 240 | myHTTPHandleFunc("/GetChallenges", requireMethod("POST"), requireToken, func(responseWriter http.ResponseWriter, request *http.Request) bool { 241 | 242 | // requireToken already validated the API Token, so we can just do this: 243 | token := strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer ") 244 | 245 | if _, has := currentChallengesGeneration[token]; !has { 246 | currentChallengesGeneration[token] = 0 247 | } 248 | if _, has := challenges[token]; !has { 249 | challenges[token] = map[string]int{} 250 | } 251 | currentChallengesGeneration[token]++ 252 | 253 | requestQuery := request.URL.Query() 254 | difficultyLevelString := requestQuery.Get("difficultyLevel") 255 | difficultyLevel, err := strconv.Atoi(difficultyLevelString) 256 | if err != nil { 257 | errorMessage := fmt.Sprintf( 258 | "400 url param ?difficultyLevel=%s value could not be converted to an integer", 259 | difficultyLevelString, 260 | ) 261 | http.Error(responseWriter, errorMessage, http.StatusBadRequest) 262 | return true 263 | } 264 | 265 | toReturn := make([]string, batchSize) 266 | for i := 0; i < batchSize; i++ { 267 | preimageBytes := make([]byte, 8) 268 | _, err := rand.Read(preimageBytes) 269 | if err != nil { 270 | log.Printf("read random bytes failed: %v", err) 271 | http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError) 272 | return true 273 | } 274 | preimage := base64.StdEncoding.EncodeToString(preimageBytes) 275 | difficultyBytes := make([]byte, int(math.Ceil(float64(difficultyLevel)/float64(8)))) 276 | 277 | for j := 0; j < len(difficultyBytes); j++ { 278 | difficultyByte := byte(0) 279 | for k := 0; k < 8; k++ { 280 | currentBitIndex := (j*8 + (7 - k)) 281 | if currentBitIndex+1 > difficultyLevel { 282 | difficultyByte = difficultyByte | 1< challenge.Difficulty { 407 | errorMessage := fmt.Sprintf( 408 | "400 bad request: nonce given by url param ?nonce=%s did not result in a hash that meets the required difficulty", 409 | nonceHex, 410 | ) 411 | http.Error(responseWriter, errorMessage, http.StatusBadRequest) 412 | return true 413 | } 414 | 415 | responseWriter.WriteHeader(200) 416 | responseWriter.Write([]byte("OK")) 417 | return true 418 | }) 419 | 420 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) 421 | 422 | log.Printf("💥 PoW! Bot Deterrent server listening on port %d", portNumber) 423 | 424 | err = http.ListenAndServe(fmt.Sprintf(":%d", portNumber), nil) 425 | 426 | // if got this far it means server crashed! 427 | panic(err) 428 | } 429 | 430 | func myHTTPHandleFunc(path string, stack ...func(http.ResponseWriter, *http.Request) bool) { 431 | http.HandleFunc(path, func(responseWriter http.ResponseWriter, request *http.Request) { 432 | for _, handler := range stack { 433 | if handler(responseWriter, request) { 434 | break 435 | } 436 | } 437 | }) 438 | } 439 | 440 | func locateAPITokensFolder() string { 441 | workingDirectory, err := os.Getwd() 442 | if err != nil { 443 | log.Fatalf("locateAPITokensFolder(): can't os.Getwd(): %v", err) 444 | } 445 | executableDirectory, err := getCurrentExecDir() 446 | if err != nil { 447 | log.Fatalf("locateAPITokensFolder(): can't getCurrentExecDir(): %v", err) 448 | } 449 | 450 | nextToExecutable := filepath.Join(executableDirectory, "PoW_Bot_Deterrent_API_Tokens") 451 | inWorkingDirectory := filepath.Join(workingDirectory, "PoW_Bot_Deterrent_API_Tokens") 452 | 453 | nextToExecutableStat, err := os.Stat(nextToExecutable) 454 | foundKeysNextToExecutable := err == nil && nextToExecutableStat.IsDir() 455 | inWorkingDirectoryStat, err := os.Stat(inWorkingDirectory) 456 | foundKeysInWorkingDirectory := err == nil && inWorkingDirectoryStat.IsDir() 457 | if foundKeysNextToExecutable && foundKeysInWorkingDirectory && workingDirectory != executableDirectory { 458 | log.Fatalf(`locateAPITokensFolder(): Something went wrong with your installation, 459 | I found two PoW_Bot_Deterrent_API_Tokens folders and I'm not sure which one to use. 460 | One of them is located at %s 461 | and the other is at %s`, inWorkingDirectory, nextToExecutable) 462 | } 463 | if foundKeysInWorkingDirectory { 464 | return inWorkingDirectory 465 | } else if foundKeysNextToExecutable { 466 | return nextToExecutable 467 | } 468 | 469 | log.Fatalf(`locateAPITokensFolder(): I didn't find a PoW_Bot_Deterrent_API_Tokens folder 470 | in the current working directory (in %s) or next to the executable (in %s)`, workingDirectory, executableDirectory) 471 | 472 | return "" 473 | } 474 | 475 | func getCurrentExecDir() (dir string, err error) { 476 | path, err := exec.LookPath(os.Args[0]) 477 | if err != nil { 478 | fmt.Printf("exec.LookPath(%s) returned %s\n", os.Args[0], err) 479 | return "", err 480 | } 481 | 482 | absPath, err := filepath.Abs(path) 483 | if err != nil { 484 | fmt.Printf("filepath.Abs(%s) returned %s\n", path, err) 485 | return "", err 486 | } 487 | 488 | dir = filepath.Dir(absPath) 489 | 490 | return dir, nil 491 | } 492 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pow-bot-deterrent", 3 | "version": "0.0.1", 4 | "description": "A proof of work based bot deterrent. lightweight, selfhosted, and copyleft licensed", 5 | "main": "static/bot-deterrent.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://git.sequentialread.com/forest/pow-bot-deterrent.git" 9 | }, 10 | "author": "forest johnson ", 11 | "license": "GPL-3.0-or-later", 12 | "bugs": { 13 | "url": "https://git.sequentialread.com/forest/pow-bot-deterrent/issues" 14 | }, 15 | "scripts": { 16 | "wasm-build": "cd wasm_build && ./build_wasm.sh", 17 | "wasm_build": "cd wasm_build && ./build_wasm.sh", 18 | "wasmbuild": "cd wasm_build && ./build_wasm.sh" 19 | }, 20 | "homepage": "https://git.sequentialread.com/forest/pow-bot-deterrent", 21 | "keywords": [ 22 | "bot-deterrent", 23 | "proof of work", 24 | "scrypt", 25 | "webworker" 26 | ], 27 | "files": [ 28 | "static" 29 | ], 30 | "directories": { 31 | "static": "static" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /proofOfWorkerStub.js: -------------------------------------------------------------------------------- 1 | // IN ORDER FOR CHANGES TO THIS FILE TO "TAKE" AND BE USED IN THE APP, THE BUILD IN wasm_build HAS TO BE RE-RUN 2 | // scrypt and scryptPromise will be filled out by js code that gets appended below this script by the wasm_build process 3 | 4 | // --- snip --- 5 | 6 | 7 | 8 | let scrypt; 9 | let scryptPromise; 10 | 11 | let working = false; 12 | const batchSize = 8; 13 | 14 | onmessage = function(e) { 15 | if(e.data.stop) { 16 | working = false; 17 | return; 18 | } 19 | 20 | const challengeBase64 = e.data.challenge; 21 | const workerId = e.data.workerId; 22 | if(!challengeBase64) { 23 | postMessage({ 24 | type: "error", 25 | challenge: challengeBase64, 26 | message: `challenge was not provided` 27 | }); 28 | } 29 | working = true; 30 | let challengeJSON = null; 31 | let challenge = null; 32 | try { 33 | challengeJSON = atob(challengeBase64); 34 | } catch (err) { 35 | postMessage({ 36 | type: "error", 37 | challenge: challengeBase64, 38 | message: `couldn't decode challenge '${challengeBase64}' as base64: ${err}` 39 | }); 40 | } 41 | try { 42 | challenge = JSON.parse(challengeJSON); 43 | } catch (err) { 44 | postMessage({ 45 | type: "error", 46 | challenge: challengeBase64, 47 | message: `couldn't parse challenge '${challengeJSON}' as json: ${err}` 48 | }); 49 | } 50 | 51 | challenge = { 52 | cpuAndMemoryCost: challenge.N, 53 | blockSize: challenge.r, 54 | paralellization: challenge.p, 55 | keyLength: challenge.klen, 56 | preimage: challenge.i, 57 | difficulty: challenge.d, 58 | difficultyLevel: challenge.dl 59 | } 60 | 61 | const probabilityOfFailurePerAttempt = 1-(1/Math.pow(2, challenge.difficultyLevel)); 62 | 63 | let i = workerId * Math.pow(2, challenge.difficultyLevel) * 1000; 64 | const hexPreimage = base64ToHex(challenge.preimage); 65 | let smallestHash = challenge.difficulty.split("").map(x => "f").join(""); 66 | 67 | postMessage({ 68 | type: "progress", 69 | challenge: challengeBase64, 70 | attempts: 0, 71 | smallestHash: smallestHash, 72 | difficulty: challenge.difficulty, 73 | probabilityOfFailurePerAttempt: probabilityOfFailurePerAttempt 74 | }); 75 | 76 | const doWork = () => { 77 | 78 | var j = 0; 79 | while(j < batchSize) { 80 | j++; 81 | i++; 82 | 83 | let nonceHex = i.toString(16); 84 | if((nonceHex.length % 2) == 1) { 85 | nonceHex = `0${nonceHex}`; 86 | } 87 | const hashHex = scrypt( 88 | nonceHex, 89 | hexPreimage, 90 | challenge.cpuAndMemoryCost, 91 | challenge.blockSize, 92 | challenge.paralellization, 93 | challenge.keyLength 94 | ); 95 | 96 | //console.log(i.toString(16), hashHex); 97 | 98 | const endOfHash = hashHex.substring(hashHex.length-challenge.difficulty.length); 99 | if(endOfHash < smallestHash) { 100 | smallestHash = endOfHash 101 | } 102 | if(endOfHash <= challenge.difficulty) { 103 | postMessage({ 104 | type: "success", 105 | challenge: challengeBase64, 106 | nonce: nonceHex, 107 | smallestHash: endOfHash, 108 | difficulty: challenge.difficulty 109 | }); 110 | break 111 | } 112 | } 113 | 114 | postMessage({ 115 | type: "progress", 116 | challenge: challengeBase64, 117 | attempts: batchSize, 118 | smallestHash: smallestHash, 119 | difficulty: challenge.difficulty, 120 | probabilityOfFailurePerAttempt: probabilityOfFailurePerAttempt 121 | }); 122 | 123 | if(working) { 124 | this.setTimeout(doWork, 1); 125 | } 126 | }; 127 | 128 | if(scrypt) { 129 | doWork(); 130 | } else { 131 | scryptPromise.then(() => { 132 | doWork(); 133 | }); 134 | } 135 | } 136 | 137 | // https://stackoverflow.com/questions/39460182/decode-base64-to-hexadecimal-string-with-javascript 138 | function base64ToHex(str) { 139 | const raw = atob(str); 140 | let result = ''; 141 | for (let i = 0; i < raw.length; i++) { 142 | const hex = raw.charCodeAt(i).toString(16); 143 | result += (hex.length === 2 ? hex : '0' + hex); 144 | } 145 | return result; 146 | } 147 | -------------------------------------------------------------------------------- /readme/probability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sequentialread/pow-bot-deterrent/86a1d903dc47fdd491a973a759d94c9df805a701/readme/probability.png -------------------------------------------------------------------------------- /readme/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sequentialread/pow-bot-deterrent/86a1d903dc47fdd491a973a759d94c9df805a701/readme/screencast.gif -------------------------------------------------------------------------------- /readme/sequence.drawio: -------------------------------------------------------------------------------- 1 | 5VzbduI2FP0aHsPyHXgMkEy7pmmmpe2s9qVL2PKlERaRZSDz9ZV8xZYDxLGNgTywzLF9ZGtrn6OzJTJQZ6vdFwLW7hO2IBookrUbqPOBokwmMvvkhrfYYEz02OAQz4pNcm5YeD9gYpQSa+hZMChcSDFG1FsXjSb2fWjSgg0QgrfFy2yMiq2ugQMFw8IESLR+9yzqxtaxLuX2n6DnuGnLspScWYH04sQQuMDC2z2T+jBQZwRjGh+tdjOIeN+l/RLf9/jO2ezBCPTpKTf8u3tYPH9V1Kf7hbn7e76ZPi3/uUu8bAAKkxdOHpa+pT0AfeuedyT75mOfGacWCFzIvcrsi0tXKDmMb4WW0KP5I8rZi7MBA/EKUvLGLtnmXZv2rLvXq6mNQASotym6BwnCTuYua+Eb9ljDipQMRlVN/CRjUZ5IRRcBDokJk7v2u/KYI7nkiALiQCo4Ygd7r52bIqSqUXN8af56t/jZf/nT/01+Hk+Wzw/1UBOAEnARsDsXULJ2pH9PBcowzguU0jy9eozapNjZ2rgmamVHXaOm3hJq2qiE2qgmamVHctlRy6hpt4SaIetF1OqmsrKjrrlmCKiZYE1NF3DkINlAIqBI4Y4W0QoowS9whhEmObS2h1DJBJDn+OyryVBljtUpc089Ntm7T06sPMvizUy3rkfhYg1M3uaWzWyZjeDQt6LBIh0aIdwn3B0cI8lZ3Sj2fRov94ZQNp3cH0OK9P5wKeDzUTBGAhhbuOQNrdeIdRP1sH+9aGhKz9AYC2iEjBEBf6QIlSWvbq6ZH7IiHUVE6RKRyYdSjIlAEHjmZSWWchZXpIYSi+Co7RpJqgDr8QukMxcgBH0HBiJ4DINfwBKiIman04LAwPsBlpE/ToI1f7voffXpQJ9X4n9wpJW5kgkdSSODfS2hikN30lDX1XEBirQs+Ww5PR7qo0rHqQ9s2wFsB92PlcAXSUWBQeW4VndmLjhqm4oVZTDjg2K8hlz8mg6H+fFAYW1Ih84xiz7vMXXl9/JcHe4qmqYVKSY3MrbuZGM4Njpj68cq6otka1m0ULSm2Fp21DZbxUL6d/gawoA/IxgkkjnrVt5ybLExWfWZkenwa4SRqlqApxk+VjvtgJn69TNTIFRTuqTgqG1mimJJzDzWkawf2WhOp7Z9JmM64j5NRjaznaitZMex2hn/RMmF9UIQrniEpS7/9Hk5r0ihz6p/qwiyZBO8yi5EXkAF4D8vBiBo089JATb2abKyO2lIqxmVpsZ6hTQg6WJIaU0akEW15uoCaTn+qXWX5coRWXDUdiAVhRwLb32EgZWxKZeh/+uzUJCNuz4rBaVV2O5kgpTuBVYaiMczy9uwQ4dGEREQGkGOzJC/qO/ENWh04ZKk16UW9ih7d1c4XBOMbeYi+thi8hLPkUvBG3ApN8BcU4+P44EXRpquFD0Ut/LQHD8RDml2VZz4T33CxpMCiYNLv7JCJs+nYUUTs8L4QHhsPCkoN6BSCbFcbWh2LThqe6+GqFIlOWDY6wzwLk96pTdpo+Gos6pWuUG9SWtqMiY4apt3VRs3xJSKg+rkdzQZJzpVfOc3/J19/op9lpfqZs8eUb9JYautyd9oxKKKlP/JlY10EBRuUOrS6i4ZCfuLOl4yUkSpi/n5CxLPfuszIRsTty5/3bZyv7k4x4r3slhwAxFe93w3SwWsJ2xuT27ISo8z7C+qxKKxeHhBO/7rFifCNsn2ipNKrCrD4R+MBj4rTR5nBAJ6RtH/5HBXHBrvj8qzzFOMYWkVrruw2Nx2lv5yUZhSNLVALjhqmYsV21mizW3S89froKB8OJFd4raU6vdsTCboL+sEspTFzrrynOCobdaJMsGVsU49G+tUbaiN90p0SeuMgjcwCTXk0bA0e6xLQlVVjrlqm4biTFSSFRUsTWvI92peBROPzkHbY2J3U87GfszQX+YJ5d8F/LaxWjY5aR17hn3bc0ICo98IxVvCPrGKTXl5yd/RtzgUIS81+y6ZNxoGJkfDQCt7QWXmuJCNi4q5XHJYJ0Swr/k/nogvz/97h/rwPw== -------------------------------------------------------------------------------- /readme/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sequentialread/pow-bot-deterrent/86a1d903dc47fdd491a973a759d94c9df805a701/readme/sequence.png -------------------------------------------------------------------------------- /static/pow-bot-deterrent.css: -------------------------------------------------------------------------------- 1 | .pow-bot-deterrent { 2 | background-color: #ddd; 3 | border: 1px solid #9359fa; 4 | border-radius: 1em; 5 | font-size: 1.2em; 6 | padding: 1em; 7 | padding-top: 0.5em; 8 | border-bottom: 2px solid #452775; 9 | margin-bottom: 2em; 10 | min-width: 37em; 11 | } 12 | 13 | .pow-bot-deterrent-link { 14 | color: #452775; 15 | font-weight: bold; 16 | text-decoration: underline; 17 | font-size: 1.4em; 18 | font-family: monospace; 19 | } 20 | 21 | @media screen and (max-width: 410px) { 22 | .pow-bot-deterrent { 23 | min-width: 25em; 24 | } 25 | .pow-bot-deterrent-icon { 26 | height: 3em; 27 | } 28 | } 29 | 30 | @media screen and (max-width: 380px) { 31 | .pow-bot-deterrent-link span { 32 | display: none; 33 | } 34 | } 35 | 36 | .pow-bot-deterrent-link:hover, 37 | .pow-bot-deterrent-link:active, 38 | .pow-bot-deterrent-link:visited { 39 | color: #452775; 40 | } 41 | 42 | 43 | .pow-bot-deterrent-row { 44 | display: inline-flex; 45 | flex-direction: row; 46 | align-content: center; 47 | width: 100%; 48 | justify-content: space-between; 49 | } 50 | 51 | .pow-bot-deterrent-icon-container { 52 | margin-left: 1.5em; 53 | margin-top: 0.2em; 54 | margin-bottom: -2em; 55 | margin-right: 0.2em; 56 | } 57 | 58 | .pow-bot-deterrent-best-hash { 59 | font-family: monospace; 60 | background: #585a29; 61 | color: #f6ff72; 62 | transition: background 0.5s ease-in-out, color 0.5s ease-in-out; 63 | padding: 0.2em 0.8em; 64 | padding-bottom: 0.3em; 65 | margin-left: -0.5em; 66 | border-radius: 0.5em; 67 | font-size: 0.8em; 68 | font-weight: bolder; 69 | display: block; 70 | float: right; 71 | } 72 | 73 | .pow-bot-deterrent-best-hash-done { 74 | background: #3b6262; 75 | color: #53f65d; 76 | } 77 | 78 | .pow-bot-deterrent-description { 79 | margin-top: 1em; 80 | font-size: 1em; 81 | } 82 | 83 | .pow-bot-deterrent-progress-bar-container { 84 | border-radius: 1em; 85 | background: #444; 86 | height: 1em; 87 | margin-top: 1em; 88 | border: 1px solid #727630; 89 | box-sizing: content-box; 90 | 91 | } 92 | 93 | .pow-bot-deterrent-progress-bar { 94 | background: #f6ff72; 95 | height: 1em; 96 | width: 0; 97 | border-radius: 1em; 98 | transition: width 0.5s ease-in-out; 99 | } 100 | 101 | .pow-bot-deterrent-icon { 102 | height: 4em; 103 | } 104 | 105 | .pow-bot-deterrent-hidden { 106 | display: none; 107 | } 108 | 109 | .pow-checkmark-icon-checkmark { 110 | fill:none; 111 | stroke: #31bd82; 112 | stroke-width: 6em; 113 | stroke-dasharray: 60em; 114 | stroke-dashoffset: 74em; 115 | stroke-linecap: round; 116 | stroke-linejoin: round; 117 | animation: 0.8s normal forwards ease-in-out pow-draw-checkmark; 118 | animation-play-state: inherit; 119 | } 120 | 121 | .pow-checkmark-icon-border { 122 | fill:none; 123 | stroke: #aaa; 124 | stroke-width: 3em; 125 | stroke-dasharray: 110em; 126 | stroke-dashoffset: 110em; 127 | stroke-linecap: round; 128 | stroke-linejoin: round; 129 | animation: 0.8s normal forwards ease-in-out pow-draw-checkmark-border; 130 | animation-play-state: inherit; 131 | } 132 | 133 | .pow-gears-icon-gear-large { 134 | fill: #9359fa; 135 | animation: 4s linear infinite pow-spinning-gears-large; 136 | animation-play-state: running; 137 | } 138 | .pow-gears-icon-gear-small { 139 | fill: #9359fa; 140 | animation: 4s linear infinite pow-spinning-gears-small; 141 | animation-play-state: running; 142 | } 143 | 144 | 145 | @keyframes pow-draw-checkmark-border { 146 | 0% { 147 | stroke-dashoffset: 110em; 148 | } 149 | 100% { 150 | stroke-dashoffset: 10em; 151 | } 152 | } 153 | 154 | @keyframes pow-draw-checkmark { 155 | 0% { 156 | stroke-dashoffset: 74em; 157 | } 158 | 100% { 159 | stroke-dashoffset: 120em; 160 | } 161 | } 162 | 163 | @keyframes pow-spinning-gears-small { 164 | 0% { 165 | transform: translate(161px, 161px) rotate(0deg) translate(-161px,-161px); 166 | } 167 | 100% { 168 | transform: translate(161px, 161px) rotate(360deg) translate(-161px,-161px); 169 | } 170 | } 171 | 172 | @keyframes pow-spinning-gears-large { 173 | 0% { 174 | transform: translate(73px, 73px) rotate(360deg) translate(-73px,-73px); 175 | } 176 | 100% { 177 | transform: translate(73px, 73px) rotate(0deg) translate(-73px,-73px); 178 | } 179 | } -------------------------------------------------------------------------------- /static/pow-bot-deterrent.js: -------------------------------------------------------------------------------- 1 | (function(window, document, undefined){ 2 | 3 | const numberOfWebWorkersToCreate = 4; 4 | 5 | window.powBotDeterrentReset = () => { 6 | window.botBotDeterrentInitDone = false; 7 | }; 8 | 9 | window.botBotDeterrentInit = () => { 10 | if(window.botBotDeterrentInitDone) { 11 | console.error("botBotDeterrentInit was called twice!"); 12 | return 13 | } 14 | window.botBotDeterrentInitDone = true; 15 | 16 | const challenges = Array.from(document.querySelectorAll("[data-pow-bot-deterrent-challenge]")); 17 | const challengesMap = {}; 18 | let url = null; 19 | let proofOfWorker = { postMessage: () => console.error("error: proofOfWorker was never loaded. ") }; 20 | 21 | challenges.forEach(element => { 22 | 23 | if(!url) { 24 | if(!element.dataset.powBotDeterrentUrl) { 25 | console.error("error: element with data-pow-bot-deterrent-challenge property is missing the data-pow-bot-deterrent-url property"); 26 | } 27 | url = element.dataset.powBotDeterrentUrl; 28 | if(url.endsWith("/")) { 29 | url = url.substring(0, url.length-1) 30 | } 31 | } 32 | 33 | if(!element.dataset.powBotDeterrentCallback) { 34 | console.error("error: element with data-pow-bot-deterrent-challenge property is missing the data-pow-bot-deterrent-callback property"); 35 | return 36 | } 37 | 38 | if(typeof element.dataset.powBotDeterrentCallback != "string") { 39 | console.error("error: data-pow-bot-deterrent-callback property should be of type 'string'"); 40 | return 41 | } 42 | 43 | const callback = getCallbackFromGlobalNamespace(element.dataset.powBotDeterrentCallback); 44 | if(!callback) { 45 | console.warn(`warning: data-pow-bot-deterrent-callback '${element.dataset.powBotDeterrentCallback}' ` 46 | + "is not defined in the global namespace yet. It had better be defined by the time it's called!"); 47 | } 48 | 49 | 50 | let form = null; 51 | let parent = element.parentElement; 52 | let sanity = 1000; 53 | while(parent && !form && sanity > 0) { 54 | sanity--; 55 | if(parent.tagName.toLowerCase() == "form") { 56 | form = parent 57 | } 58 | parent = parent.parentElement 59 | } 60 | if(!form) { 61 | console.error("error: element with data-pow-bot-deterrent-challenge property was not inside a form element"); 62 | //todo 63 | } 64 | 65 | let cssIsAlreadyLoaded = document.querySelector(`link[href='${url}/static/pow-bot-deterrent.css']`); 66 | 67 | cssIsAlreadyLoaded = cssIsAlreadyLoaded || Array.from(document.styleSheets).some(x => { 68 | try { 69 | return Array.from(x.rules).some(x => x.selectorText == ".pow-bot-deterrent") 70 | } catch (err) { 71 | return false 72 | } 73 | }); 74 | 75 | if(!cssIsAlreadyLoaded) { 76 | const stylesheet = createElement(document.head, "link", { 77 | "rel": "stylesheet", 78 | "charset": "utf8", 79 | }); 80 | stylesheet.onload = () => renderProgressInfo(element); 81 | stylesheet.setAttribute("href", `${url}/static/pow-bot-deterrent.css`); 82 | } else { 83 | renderProgressInfo(element); 84 | } 85 | 86 | window.powBotDeterrentTrigger = () => { 87 | 88 | const challenge = element.dataset.powBotDeterrentChallenge; 89 | if(!challengesMap[challenge]) { 90 | challengesMap[challenge] = { 91 | element: element, 92 | attempts: 0, 93 | startTime: new Date().getTime(), 94 | }; 95 | const progressBarContainer = element.querySelector(".pow-bot-deterrent-progress-bar-container"); 96 | progressBarContainer.style.display = "block"; 97 | const mainElement = element.querySelector(".pow-bot-deterrent"); 98 | mainElement.style.display = "inline-block"; 99 | const gears = element.querySelector(".pow-gears-icon"); 100 | gears.style.display = "block"; 101 | 102 | challengesMap[challenge].updateProgressInterval = setInterval(() => { 103 | // calculate the probability of finding a valid nonce after n tries 104 | if(challengesMap[challenge].probabilityOfFailurePerAttempt && !challengesMap[challenge].done) { 105 | const probabilityOfSuccessSoFar = 1-Math.pow( 106 | challengesMap[challenge].probabilityOfFailurePerAttempt, 107 | challengesMap[challenge].attempts 108 | ); 109 | const element = challengesMap[challenge].element; 110 | const progressBar = element.querySelector(".pow-bot-deterrent-progress-bar"); 111 | const bestHashElement = element.querySelector(".pow-bot-deterrent-best-hash"); 112 | bestHashElement.textContent = getHashProgressText(challengesMap[challenge]); 113 | progressBar.style.width = `${probabilityOfSuccessSoFar*100}%`; 114 | } 115 | }, 500); 116 | 117 | 118 | proofOfWorker.postMessage({challenge: challenge}); 119 | } 120 | }; 121 | 122 | const inputElements = Array.from(form.querySelectorAll("input")) 123 | .concat(Array.from(form.querySelectorAll("textarea"))); 124 | 125 | inputElements.forEach(inputElement => { 126 | inputElement.onchange = () => window.powBotDeterrentTrigger(); 127 | inputElement.onkeydown = () => window.powBotDeterrentTrigger(); 128 | }); 129 | }); 130 | 131 | if (!window.Worker) { 132 | console.error("error: webworker is not support"); 133 | //todo 134 | } 135 | 136 | if(url) { 137 | 138 | // // https://stackoverflow.com/questions/21913673/execute-web-worker-from-different-origin/62914052#62914052 139 | // const webWorkerUrlWhichIsProbablyCrossOrigin = `${url}/static/proofOfWorker.js`; 140 | 141 | // const webWorkerPointerDataURL = URL.createObjectURL( 142 | // new Blob( 143 | // [ `importScripts( "${ webWorkerUrlWhichIsProbablyCrossOrigin }" );` ], 144 | // { type: "text/javascript" } 145 | // ) 146 | // ); 147 | 148 | // return 149 | let webWorkers; 150 | webWorkers = [...Array(numberOfWebWorkersToCreate)].map((_, i) => { 151 | const webWorker = new Worker('/static/proofOfWorker.js'); 152 | webWorker.onmessage = function(e) { 153 | const challengeState = challengesMap[e.data.challenge] 154 | if(!challengeState) { 155 | console.error(`error: webworker sent message with unknown challenge '${e.data.challenge}'`); 156 | } 157 | if(e.data.type == "progress") { 158 | challengeState.difficulty = e.data.difficulty; 159 | challengeState.probabilityOfFailurePerAttempt = e.data.probabilityOfFailurePerAttempt; 160 | if(!challengeState.smallestHash || challengeState.smallestHash > e.data.smallestHash) { 161 | challengeState.smallestHash = e.data.smallestHash; 162 | } 163 | challengeState.attempts += e.data.attempts; 164 | } else if(e.data.type == "success") { 165 | if(!challengeState.done) { 166 | challengeState.done = true; 167 | clearInterval(challengeState.updateProgressInterval); 168 | 169 | const element = challengeState.element; 170 | const progressBar = element.querySelector(".pow-bot-deterrent-progress-bar"); 171 | const checkmark = element.querySelector(".pow-checkmark-icon"); 172 | const gears = element.querySelector(".pow-gears-icon"); 173 | const bestHashElement = element.querySelector(".pow-bot-deterrent-best-hash"); 174 | const description = element.querySelector(".pow-bot-deterrent-description"); 175 | challengeState.smallestHash = e.data.smallestHash; 176 | bestHashElement.textContent = getHashProgressText(challengeState); 177 | bestHashElement.classList.add("pow-bot-deterrent-best-hash-done"); 178 | checkmark.style.display = "block"; 179 | checkmark.style.animationPlayState = "running"; 180 | gears.style.display = "none"; 181 | progressBar.style.width = "100%"; 182 | 183 | description.innerHTML = ""; 184 | createElement( 185 | description, 186 | "a", 187 | {"href": "https://en.wikipedia.org/wiki/Proof_of_work"}, 188 | "PoW" 189 | ); 190 | appendFragment(description, " complete, you may continue."); 191 | createElement(description, "br"); 192 | appendFragment(description, "Privacy-respecting anti-spam measure."); 193 | 194 | webWorkers.forEach(x => x.postMessage({stop: "STOP"})); 195 | 196 | const callback = getCallbackFromGlobalNamespace(element.dataset.powBotDeterrentCallback); 197 | if(!callback) { 198 | console.error(`error: data-pow-bot-deterrent-callback '${element.dataset.powBotDeterrentCallback}' ` 199 | + "is not defined in the global namespace!"); 200 | } else { 201 | console.log(`firing callback for challenge ${e.data.challenge} w/ nonce ${e.data.nonce}, smallestHash: ${e.data.smallestHash}, difficulty: ${e.data.difficulty}`); 202 | callback(e.data.nonce); 203 | } 204 | } else { 205 | console.log("success recieved twice"); 206 | } 207 | } else if(e.data.type == "error") { 208 | console.error(`error: webworker errored out: '${e.data.message}'`); 209 | } else { 210 | console.error(`error: webworker sent message with unknown type '${e.data.type}'`); 211 | } 212 | }; 213 | return webWorker; 214 | }); 215 | 216 | // URL.revokeObjectURL(webWorkerPointerDataURL); 217 | 218 | proofOfWorker = { 219 | postMessage: arg => webWorkers.forEach((x, i) => { 220 | x.postMessage({ ...arg, workerId: i }) 221 | }) 222 | }; 223 | 224 | window.powBotDeterrentReset = () => { 225 | window.botBotDeterrentInitDone = false; 226 | webWorkers.forEach(x => x.terminate()); 227 | }; 228 | } 229 | }; 230 | 231 | const challenges = Array.from(document.querySelectorAll("[data-pow-bot-deterrent-challenge]")); 232 | if(challenges.length) { 233 | window.botBotDeterrentInit(); 234 | } 235 | 236 | function getCallbackFromGlobalNamespace(callbackString) { 237 | const callbackPath = callbackString.split("."); 238 | let context = window; 239 | callbackPath.forEach(pathElement => { 240 | if(!context[pathElement]) { 241 | return null; 242 | } else { 243 | context = context[pathElement]; 244 | } 245 | }); 246 | 247 | return context; 248 | } 249 | 250 | function getHashProgressText(challengeState) { 251 | const durationSeconds = ((new Date().getTime()) - challengeState.startTime)/1000; 252 | let hashesPerSecond = '[...]'; 253 | if (durationSeconds > 1) { 254 | hashesPerSecondFloat = challengeState.attempts / durationSeconds; 255 | hashesPerSecond = `[${leftPad(Math.round(hashesPerSecondFloat), 3)}h/s]`; 256 | } 257 | 258 | return `${hashesPerSecond} ${challengeState.smallestHash} < ${challengeState.difficulty}`; 259 | } 260 | 261 | function leftPad (str, max) { 262 | str = str.toString(); 263 | return str.length < max ? leftPad(" " + str, max) : str; 264 | } 265 | 266 | function renderProgressInfo(parent) { 267 | const svgXMLNS = "http://www.w3.org/2000/svg"; 268 | const xmlnsXMLNS = 'http://www.w3.org/2000/xmlns/'; 269 | const xmlSpaceXMLNS = 'http://www.w3.org/XML/1998/namespace'; 270 | 271 | parent.innerHTML = ""; 272 | 273 | const main = createElement(parent, "div", {"class": "pow-bot-deterrent pow-bot-deterrent-hidden"}); 274 | const mainRow = createElement(main, "div", {"class": "pow-bot-deterrent-row"}); 275 | const mainColumn = createElement(mainRow, "div"); 276 | const headerLink = createElement( 277 | mainColumn, 278 | "a", 279 | { 280 | "class": "pow-bot-deterrent-link", 281 | "href": "https://git.sequentialread.com/forest/pow-bot-deterrent", 282 | "target": "_blank" 283 | }, 284 | "💥PoW! " 285 | ); 286 | createElement(headerLink, "span", null, "Bot Deterrent"); 287 | const description = createElement(mainColumn, "div", {"class": "pow-bot-deterrent-description"}); 288 | appendFragment(description, "Creating "); 289 | createElement( 290 | description, 291 | "a", 292 | { "href": "https://en.wikipedia.org/wiki/Proof_of_work", "target": "_blank" }, 293 | "Proof of Work" 294 | ); 295 | appendFragment(description, ". "); 296 | createElement(description, "br"); 297 | appendFragment(description, "Privacy-respecting anti-spam measure."); 298 | const bestHashContainer = createElement(mainRow, "div"); 299 | createElement(bestHashContainer, "div", {"class": "pow-bot-deterrent-best-hash"}, "loading..."); 300 | const progressBarContainer = createElement(main, "div", { 301 | "class": "pow-bot-deterrent-progress-bar-container pow-bot-deterrent-hidden" 302 | }); 303 | createElement(progressBarContainer, "div", {"class": "pow-bot-deterrent-progress-bar"}); 304 | const iconContainer = createElement(mainRow, "div", {"class": "pow-bot-deterrent-icon-container"}); 305 | 306 | 307 | const checkmarkIcon = createElementNS(iconContainer, svgXMLNS, "svg", { 308 | "xmlns": [xmlnsXMLNS, svgXMLNS], 309 | "xml:space": [xmlSpaceXMLNS, 'preserve'], 310 | "version": "1.1", 311 | "viewBox": "0 0 512 512", 312 | "class": "pow-checkmark-icon pow-bot-deterrent-icon pow-bot-deterrent-hidden" 313 | }); 314 | createElementNS(checkmarkIcon, svgXMLNS, "polyline", { 315 | "class": "pow-checkmark-icon-checkmark", 316 | "points": "444,110 206,343 120,252" 317 | }); 318 | createElementNS(checkmarkIcon, svgXMLNS, "polyline", { 319 | "class": "pow-checkmark-icon-border", 320 | "points": "240,130 30,130 30,470 370,470 370,350" 321 | }); 322 | 323 | const gearsIcon = createElementNS(iconContainer, svgXMLNS, "svg", { 324 | "xmlns": [xmlnsXMLNS, svgXMLNS], 325 | "xml:space": [xmlSpaceXMLNS, 'preserve'], 326 | "version": "1.1", 327 | "viewBox": "-30 -5 250 223", 328 | "class": "pow-gears-icon pow-bot-deterrent-icon pow-bot-deterrent-hidden" 329 | }); 330 | createElementNS(gearsIcon, svgXMLNS, "path", { 331 | "class": "pow-gears-icon-gear-large", 332 | "d": "M113.595,133.642l-5.932-13.169c5.655-4.151,10.512-9.315,14.307-15.209l13.507,5.118c2.583,0.979,5.469-0.322,6.447-2.904 l4.964-13.103c0.47-1.24,0.428-2.616-0.117-3.825c-0.545-1.209-1.547-2.152-2.788-2.622l-13.507-5.118 c1.064-6.93,0.848-14.014-0.637-20.871l13.169-5.932c1.209-0.545,2.152-1.547,2.622-2.788c0.47-1.24,0.428-2.616-0.117-3.825 l-5.755-12.775c-1.134-2.518-4.096-3.638-6.612-2.505l-13.169,5.932c-4.151-5.655-9.315-10.512-15.209-14.307l5.118-13.507 c0.978-2.582-0.322-5.469-2.904-6.447L93.88,0.82c-1.239-0.469-2.615-0.428-3.825,0.117c-1.209,0.545-2.152,1.547-2.622,2.788 l-5.117,13.506c-6.937-1.07-14.033-0.849-20.872,0.636L55.513,4.699c-0.545-1.209-1.547-2.152-2.788-2.622 c-1.239-0.469-2.616-0.428-3.825,0.117L36.124,7.949c-2.518,1.134-3.639,4.094-2.505,6.612l5.932,13.169 c-5.655,4.151-10.512,9.315-14.307,15.209l-13.507-5.118c-1.239-0.469-2.615-0.427-3.825,0.117 c-1.209,0.545-2.152,1.547-2.622,2.788L0.326,53.828c-0.978,2.582,0.322,5.469,2.904,6.447l13.507,5.118 c-1.064,6.929-0.848,14.015,0.637,20.871L4.204,92.196c-1.209,0.545-2.152,1.547-2.622,2.788c-0.47,1.24-0.428,2.616,0.117,3.825 l5.755,12.775c0.544,1.209,1.547,2.152,2.787,2.622c1.241,0.47,2.616,0.429,3.825-0.117l13.169-5.932 c4.151,5.656,9.314,10.512,15.209,14.307l-5.118,13.507c-0.978,2.582,0.322,5.469,2.904,6.447l13.103,4.964 c0.571,0.216,1.172,0.324,1.771,0.324c0.701,0,1.402-0.147,2.054-0.441c1.209-0.545,2.152-1.547,2.622-2.788l5.117-13.506 c6.937,1.069,14.034,0.849,20.872-0.636l5.931,13.168c0.545,1.209,1.547,2.152,2.788,2.622c1.24,0.47,2.617,0.429,3.825-0.117 l12.775-5.754C113.607,139.12,114.729,136.16,113.595,133.642z M105.309,86.113c-4.963,13.1-17.706,21.901-31.709,21.901 c-4.096,0-8.135-0.744-12.005-2.21c-8.468-3.208-15.18-9.522-18.899-17.779c-3.719-8.256-4-17.467-0.792-25.935 c4.963-13.1,17.706-21.901,31.709-21.901c4.096,0,8.135,0.744,12.005,2.21c8.468,3.208,15.18,9.522,18.899,17.778 C108.237,68.434,108.518,77.645,105.309,86.113z" 333 | }); 334 | createElementNS(gearsIcon, svgXMLNS, "path", { 335 | "class": "pow-gears-icon-gear-small", 336 | "d": "M216.478,154.389c-0.896-0.977-2.145-1.558-3.469-1.615l-9.418-0.404 c-0.867-4.445-2.433-8.736-4.633-12.697l6.945-6.374c2.035-1.867,2.17-5.03,0.303-7.064l-6.896-7.514 c-0.896-0.977-2.145-1.558-3.47-1.615c-1.322-0.049-2.618,0.416-3.595,1.312l-6.944,6.374c-3.759-2.531-7.9-4.458-12.254-5.702 l0.404-9.418c0.118-2.759-2.023-5.091-4.782-5.209l-10.189-0.437c-2.745-0.104-5.091,2.023-5.209,4.781l-0.404,9.418 c-4.444,0.867-8.735,2.433-12.697,4.632l-6.374-6.945c-0.896-0.977-2.145-1.558-3.469-1.615c-1.324-0.054-2.618,0.416-3.595,1.312 l-7.514,6.896c-2.035,1.867-2.17,5.03-0.303,7.064l6.374,6.945c-2.531,3.759-4.458,7.899-5.702,12.254l-9.417-0.404 c-2.747-0.111-5.092,2.022-5.21,4.781l-0.437,10.189c-0.057,1.325,0.415,2.618,1.312,3.595c0.896,0.977,2.145,1.558,3.47,1.615 l9.417,0.403c0.867,4.445,2.433,8.736,4.632,12.698l-6.944,6.374c-0.977,0.896-1.558,2.145-1.615,3.469 c-0.057,1.325,0.415,2.618,1.312,3.595l6.896,7.514c0.896,0.977,2.145,1.558,3.47,1.615c1.319,0.053,2.618-0.416,3.595-1.312 l6.944-6.374c3.759,2.531,7.9,4.458,12.254,5.702l-0.404,9.418c-0.118,2.759,2.022,5.091,4.781,5.209l10.189,0.437 c0.072,0.003,0.143,0.004,0.214,0.004c1.25,0,2.457-0.468,3.381-1.316c0.977-0.896,1.558-2.145,1.615-3.469l0.404-9.418 c4.444-0.867,8.735-2.433,12.697-4.632l6.374,6.945c0.896,0.977,2.145,1.558,3.469,1.615c1.33,0.058,2.619-0.416,3.595-1.312 l7.514-6.896c2.035-1.867,2.17-5.03,0.303-7.064l-6.374-6.945c2.531-3.759,4.458-7.899,5.702-12.254l9.417,0.404 c2.756,0.106,5.091-2.022,5.21-4.781l0.437-10.189C217.847,156.659,217.375,155.366,216.478,154.389z M160.157,183.953 c-12.844-0.55-22.846-11.448-22.295-24.292c0.536-12.514,10.759-22.317,23.273-22.317c0.338,0,0.678,0.007,1.019,0.022 c12.844,0.551,22.846,11.448,22.295,24.292C183.898,174.511,173.106,184.497,160.157,183.953z" 337 | }); 338 | } 339 | 340 | function createElementNS(parent, ns, tag, attr) { 341 | const element = document.createElementNS(ns, tag); 342 | if(attr) { 343 | Object.entries(attr).forEach(kv => { 344 | const value = kv[1]; 345 | if((typeof value) == "string") { 346 | element.setAttributeNS(null, kv[0], kv[1]) 347 | } else { 348 | element.setAttributeNS(value[0], kv[0], value[1]) 349 | } 350 | 351 | }); 352 | } 353 | parent.appendChild(element); 354 | return element; 355 | } 356 | 357 | function createElement(parent, tag, attr, textContent) { 358 | const element = document.createElement(tag); 359 | if(attr) { 360 | Object.entries(attr).forEach(kv => element.setAttribute(kv[0], kv[1])); 361 | } 362 | if(textContent) { 363 | element.textContent = textContent; 364 | } 365 | parent.appendChild(element); 366 | return element; 367 | } 368 | 369 | function appendFragment(parent, textContent) { 370 | const fragment = document.createDocumentFragment() 371 | fragment.textContent = textContent 372 | parent.appendChild(fragment) 373 | } 374 | 375 | })(window, document); -------------------------------------------------------------------------------- /static/proofOfWorker.js: -------------------------------------------------------------------------------- 1 | 2 | // THIS FILE IS GENERATED AUTOMATICALLY 3 | // Dont edit this file by hand. 4 | // Either edit proofOfWorkerStub.js or edit the build located in the wasm_build folder. 5 | 6 | 7 | 8 | let scrypt; 9 | let scryptPromise; 10 | 11 | let working = false; 12 | const batchSize = 4; 13 | 14 | onmessage = function(e) { 15 | if(e.data.stop) { 16 | working = false; 17 | return; 18 | } 19 | 20 | const challengeBase64 = e.data.challenge; 21 | const workerId = e.data.workerId; 22 | if(!challengeBase64) { 23 | postMessage({ 24 | type: "error", 25 | challenge: challengeBase64, 26 | message: `challenge was not provided` 27 | }); 28 | } 29 | working = true; 30 | let challengeJSON = null; 31 | let challenge = null; 32 | try { 33 | challengeJSON = atob(challengeBase64); 34 | } catch (err) { 35 | postMessage({ 36 | type: "error", 37 | challenge: challengeBase64, 38 | message: `couldn't decode challenge '${challengeBase64}' as base64: ${err}` 39 | }); 40 | } 41 | try { 42 | challenge = JSON.parse(challengeJSON); 43 | } catch (err) { 44 | postMessage({ 45 | type: "error", 46 | challenge: challengeBase64, 47 | message: `couldn't parse challenge '${challengeJSON}' as json: ${err}` 48 | }); 49 | } 50 | 51 | challenge = { 52 | cpuAndMemoryCost: challenge.N, 53 | blockSize: challenge.r, 54 | paralellization: challenge.p, 55 | keyLength: challenge.klen, 56 | preimage: challenge.i, 57 | difficulty: challenge.d, 58 | difficultyLevel: challenge.dl 59 | } 60 | 61 | const probabilityOfFailurePerAttempt = 1-(1/Math.pow(2, challenge.difficultyLevel)); 62 | 63 | let i = workerId * Math.pow(2, challenge.difficultyLevel) * 1000; 64 | const hexPreimage = base64ToHex(challenge.preimage); 65 | let smallestHash = challenge.difficulty.split("").map(x => "f").join(""); 66 | 67 | postMessage({ 68 | type: "progress", 69 | challenge: challengeBase64, 70 | attempts: 0, 71 | smallestHash: smallestHash, 72 | difficulty: challenge.difficulty, 73 | probabilityOfFailurePerAttempt: probabilityOfFailurePerAttempt 74 | }); 75 | 76 | const doWork = () => { 77 | 78 | var j = 0; 79 | while(j < batchSize) { 80 | j++; 81 | i++; 82 | 83 | let nonceHex = i.toString(16); 84 | if((nonceHex.length % 2) == 1) { 85 | nonceHex = `0${nonceHex}`; 86 | } 87 | const hashHex = scrypt( 88 | nonceHex, 89 | hexPreimage, 90 | challenge.cpuAndMemoryCost, 91 | challenge.blockSize, 92 | challenge.paralellization, 93 | challenge.keyLength 94 | ); 95 | 96 | const endOfHash = hashHex.substring(hashHex.length-challenge.difficulty.length); 97 | if(endOfHash < smallestHash) { 98 | smallestHash = endOfHash 99 | } 100 | if(endOfHash <= challenge.difficulty) { 101 | postMessage({ 102 | type: "success", 103 | challenge: challengeBase64, 104 | nonce: nonceHex, 105 | smallestHash: endOfHash, 106 | difficulty: challenge.difficulty 107 | }); 108 | break 109 | } 110 | } 111 | 112 | postMessage({ 113 | type: "progress", 114 | challenge: challengeBase64, 115 | attempts: batchSize, 116 | smallestHash: smallestHash, 117 | difficulty: challenge.difficulty, 118 | probabilityOfFailurePerAttempt: probabilityOfFailurePerAttempt 119 | }); 120 | 121 | if(working) { 122 | this.setTimeout(doWork, 1); 123 | } 124 | }; 125 | 126 | if(scrypt) { 127 | doWork(); 128 | } else { 129 | scryptPromise.then(() => { 130 | doWork(); 131 | }); 132 | } 133 | } 134 | 135 | // https://stackoverflow.com/questions/39460182/decode-base64-to-hexadecimal-string-with-javascript 136 | function base64ToHex(str) { 137 | const raw = atob(str); 138 | let result = ''; 139 | for (let i = 0; i < raw.length; i++) { 140 | const hex = raw.charCodeAt(i).toString(16); 141 | result += (hex.length === 2 ? hex : '0' + hex); 142 | } 143 | return result; 144 | } 145 | let wasm_bindgen; 146 | (function() { 147 | const __exports = {}; 148 | let script_src; 149 | if (typeof document !== 'undefined' && document.currentScript !== null) { 150 | script_src = new URL(document.currentScript.src, location.href).toString(); 151 | } 152 | let wasm = undefined; 153 | 154 | let WASM_VECTOR_LEN = 0; 155 | 156 | let cachedUint8ArrayMemory0 = null; 157 | 158 | function getUint8ArrayMemory0() { 159 | if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { 160 | cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); 161 | } 162 | return cachedUint8ArrayMemory0; 163 | } 164 | 165 | const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); 166 | 167 | const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' 168 | ? function (arg, view) { 169 | return cachedTextEncoder.encodeInto(arg, view); 170 | } 171 | : function (arg, view) { 172 | const buf = cachedTextEncoder.encode(arg); 173 | view.set(buf); 174 | return { 175 | read: arg.length, 176 | written: buf.length 177 | }; 178 | }); 179 | 180 | function passStringToWasm0(arg, malloc, realloc) { 181 | 182 | if (realloc === undefined) { 183 | const buf = cachedTextEncoder.encode(arg); 184 | const ptr = malloc(buf.length, 1) >>> 0; 185 | getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); 186 | WASM_VECTOR_LEN = buf.length; 187 | return ptr; 188 | } 189 | 190 | let len = arg.length; 191 | let ptr = malloc(len, 1) >>> 0; 192 | 193 | const mem = getUint8ArrayMemory0(); 194 | 195 | let offset = 0; 196 | 197 | for (; offset < len; offset++) { 198 | const code = arg.charCodeAt(offset); 199 | if (code > 0x7F) break; 200 | mem[ptr + offset] = code; 201 | } 202 | 203 | if (offset !== len) { 204 | if (offset !== 0) { 205 | arg = arg.slice(offset); 206 | } 207 | ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; 208 | const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); 209 | const ret = encodeString(arg, view); 210 | 211 | offset += ret.written; 212 | ptr = realloc(ptr, len, offset, 1) >>> 0; 213 | } 214 | 215 | WASM_VECTOR_LEN = offset; 216 | return ptr; 217 | } 218 | 219 | const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); 220 | 221 | if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; 222 | 223 | function getStringFromWasm0(ptr, len) { 224 | ptr = ptr >>> 0; 225 | return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); 226 | } 227 | /** 228 | * @param {string} password 229 | * @param {string} salt 230 | * @param {number} n 231 | * @param {number} r 232 | * @param {number} p 233 | * @param {number} dklen 234 | * @returns {string} 235 | */ 236 | __exports.scrypt = function(password, salt, n, r, p, dklen) { 237 | let deferred3_0; 238 | let deferred3_1; 239 | try { 240 | const ptr0 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 241 | const len0 = WASM_VECTOR_LEN; 242 | const ptr1 = passStringToWasm0(salt, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 243 | const len1 = WASM_VECTOR_LEN; 244 | const ret = wasm.scrypt(ptr0, len0, ptr1, len1, n, r, p, dklen); 245 | deferred3_0 = ret[0]; 246 | deferred3_1 = ret[1]; 247 | return getStringFromWasm0(ret[0], ret[1]); 248 | } finally { 249 | wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); 250 | } 251 | }; 252 | 253 | async function __wbg_load(module, imports) { 254 | if (typeof Response === 'function' && module instanceof Response) { 255 | if (typeof WebAssembly.instantiateStreaming === 'function') { 256 | try { 257 | return await WebAssembly.instantiateStreaming(module, imports); 258 | 259 | } catch (e) { 260 | if (module.headers.get('Content-Type') != 'application/wasm') { 261 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); 262 | 263 | } else { 264 | throw e; 265 | } 266 | } 267 | } 268 | 269 | const bytes = await module.arrayBuffer(); 270 | return await WebAssembly.instantiate(bytes, imports); 271 | 272 | } else { 273 | const instance = await WebAssembly.instantiate(module, imports); 274 | 275 | if (instance instanceof WebAssembly.Instance) { 276 | return { instance, module }; 277 | 278 | } else { 279 | return instance; 280 | } 281 | } 282 | } 283 | 284 | function __wbg_get_imports() { 285 | const imports = {}; 286 | imports.wbg = {}; 287 | imports.wbg.__wbindgen_init_externref_table = function() { 288 | const table = wasm.__wbindgen_export_0; 289 | const offset = table.grow(4); 290 | table.set(0, undefined); 291 | table.set(offset + 0, undefined); 292 | table.set(offset + 1, null); 293 | table.set(offset + 2, true); 294 | table.set(offset + 3, false); 295 | ; 296 | }; 297 | 298 | return imports; 299 | } 300 | 301 | function __wbg_init_memory(imports, memory) { 302 | 303 | } 304 | 305 | function __wbg_finalize_init(instance, module) { 306 | wasm = instance.exports; 307 | __wbg_init.__wbindgen_wasm_module = module; 308 | cachedUint8ArrayMemory0 = null; 309 | 310 | 311 | wasm.__wbindgen_start(); 312 | return wasm; 313 | } 314 | 315 | function initSync(module) { 316 | if (wasm !== undefined) return wasm; 317 | 318 | 319 | if (typeof module !== 'undefined') { 320 | if (Object.getPrototypeOf(module) === Object.prototype) { 321 | ({module} = module) 322 | } else { 323 | console.warn('using deprecated parameters for `initSync()`; pass a single object instead') 324 | } 325 | } 326 | 327 | const imports = __wbg_get_imports(); 328 | 329 | __wbg_init_memory(imports); 330 | 331 | if (!(module instanceof WebAssembly.Module)) { 332 | module = new WebAssembly.Module(module); 333 | } 334 | 335 | const instance = new WebAssembly.Instance(module, imports); 336 | 337 | return __wbg_finalize_init(instance, module); 338 | } 339 | 340 | async function __wbg_init(module_or_path) { 341 | if (wasm !== undefined) return wasm; 342 | 343 | 344 | if (typeof module_or_path !== 'undefined') { 345 | if (Object.getPrototypeOf(module_or_path) === Object.prototype) { 346 | ({module_or_path} = module_or_path) 347 | } else { 348 | console.warn('using deprecated parameters for the initialization function; pass a single object instead') 349 | } 350 | } 351 | 352 | if (typeof module_or_path === 'undefined' && typeof script_src !== 'undefined') { 353 | module_or_path = script_src.replace(/\.js$/, '_bg.wasm'); 354 | } 355 | const imports = __wbg_get_imports(); 356 | 357 | if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { 358 | module_or_path = fetch(module_or_path); 359 | } 360 | 361 | __wbg_init_memory(imports); 362 | 363 | const { instance, module } = await __wbg_load(await module_or_path, imports); 364 | 365 | return __wbg_finalize_init(instance, module); 366 | } 367 | 368 | wasm_bindgen = Object.assign(__wbg_init, { initSync }, __exports); 369 | 370 | })(); 371 | 372 | scrypt = wasm_bindgen.scrypt; 373 | scryptPromise = wasm_bindgen({module_or_path: "/static/scrypt.wasm"}); 374 | 375 | 376 | -------------------------------------------------------------------------------- /static/scrypt.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sequentialread/pow-bot-deterrent/86a1d903dc47fdd491a973a759d94c9df805a701/static/scrypt.wasm -------------------------------------------------------------------------------- /wasm_build/build_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -f build_wasm.sh ]; then 4 | printf "Please run this script from the wasm_build folder.\n" 5 | fi 6 | 7 | if [ ! -d scrypt-wasm ]; then 8 | printf "Cloning https://github.com/MyEtherWallet/scrypt-wasm... \n" 9 | git clone https://github.com/MyEtherWallet/scrypt-wasm 10 | fi 11 | 12 | cd scrypt-wasm 13 | 14 | rust_is_installed="$(which rustc | wc -l)" 15 | 16 | if [ "$rust_is_installed" == "0" ]; then 17 | printf "rust language compilers & tools will need to be installed." 18 | printf "using rustup.rs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh \n" 19 | read -p "is this ok? [y] " -n 1 -r 20 | printf "\n" 21 | if [[ $REPLY =~ ^[Yy]$ ]] 22 | then 23 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 24 | else 25 | printf "exiting due to no rust compiler" 26 | exit 1 27 | fi 28 | fi 29 | 30 | if [ ! -d pkg ]; then 31 | printf "running Makefile for MyEtherWallet/scrypt-wasm... \n" 32 | rustup target add wasm32-unknown-unknown 33 | cargo install wasm-pack --force 34 | wasm-pack build --target no-modules 35 | fi 36 | 37 | cd ../ 38 | 39 | cp scrypt-wasm/pkg/scrypt_wasm_bg.wasm ../static/ 40 | 41 | echo ' 42 | // THIS FILE IS GENERATED AUTOMATICALLY 43 | // Dont edit this file by hand. 44 | // Either edit proofOfWorkerStub.js or edit the build located in the wasm_build folder. 45 | ' > ../static/proofOfWorker.js 46 | 47 | cat ../proofOfWorkerStub.js | tail -n +6 >> ../static/proofOfWorker.js 48 | 49 | cat scrypt-wasm/pkg/scrypt_wasm.js >> ../static/proofOfWorker.js 50 | 51 | # see: https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html 52 | echo ' 53 | scrypt = wasm_bindgen.scrypt; 54 | scryptPromise = wasm_bindgen({module_or_path: "/static/scrypt.wasm"}); 55 | 56 | ' >> ../static/proofOfWorker.js 57 | 58 | echo "Build successful!" --------------------------------------------------------------------------------