[chore] more NoLLaMas proof-of-work tweaking (#4096)

- replaces the sha256 calculation with an alternative implementation that seems to use more uniform time-taken across different platforms
- goes back to the simpler difficulty calculation without a "partial" difficulty level

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4096
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim
2025-05-03 16:45:25 +00:00
committed by kim
parent c15002d76e
commit 211192c482
7 changed files with 184 additions and 113 deletions

View File

@@ -72,8 +72,7 @@ func NoLLaMas(
var nollamas nollamas
nollamas.seed = seed
nollamas.ttl = time.Hour
nollamas.diff1 = 4
nollamas.diff2 = '4'
nollamas.diff = 4
nollamas.getInstanceV1 = getInstanceV1
nollamas.policy = cookiePolicy
return nollamas.Serve
@@ -101,16 +100,9 @@ type nollamas struct {
ttl time.Duration
// algorithm difficulty knobs.
// diff1 determines the number of
// leading zeroes required, while
// diff2 checks the next byte at
// index is less than it.
//
// e.g. you look for say:
// - b[0:3] must be '0'
// - b[4] can be < '5'
diff1 uint8
diff2 uint8
// diff determines the number
// of leading zeroes required.
diff uint8
// extra fields required for
// our template rendering.
@@ -187,6 +179,12 @@ func (m *nollamas) Serve(c *gin.Context) {
return
}
// From here-on out, all
// possibilities are handled
// by us. Prevent further http
// handlers from being called.
c.Abort()
// Prepare new log entry.
l := log.WithContext(ctx).
WithField("userAgent", userAgent).
@@ -225,10 +223,6 @@ func (m *nollamas) Serve(c *gin.Context) {
l.Infof("challenge passed: %s", nonce)
// Don't pass to further
// handlers, we'll redirect.
c.Abort()
// Drop solution query and encode.
query.Del("nollamas_solution")
c.Request.URL.RawQuery = query.Encode()
@@ -240,11 +234,6 @@ func (m *nollamas) Serve(c *gin.Context) {
}
func (m *nollamas) renderChallenge(c *gin.Context, challenge string) {
// Don't pass to further
// handlers, they only get
// our challenge page.
c.Abort()
// Fetch current instance information for templating vars.
instance, errWithCode := m.getInstanceV1(c.Request.Context())
if errWithCode != nil {
@@ -264,11 +253,7 @@ func (m *nollamas) renderChallenge(c *gin.Context, challenge string) {
},
Extra: map[string]any{
"challenge": challenge,
"difficulty1": m.diff1,
// must be a str otherwise template
// renders uint8 as int, not char
"difficulty2": hexStrs[m.diff2],
"difficulty": m.diff,
},
Javascript: []apiutil.JavascriptEntry{
{
@@ -289,8 +274,7 @@ func (m *nollamas) token(hash *hashWithBufs, userAgent, clientIP string) string
// Include difficulty level in
// hash input data so if config
// changes then token invalidates.
hash.hash.Write([]byte{m.diff1})
hash.hash.Write([]byte{m.diff2})
hash.hash.Write([]byte{m.diff})
// Also seed the generated input with
// current time rounded to TTL, so our
@@ -326,40 +310,18 @@ func (m *nollamas) checkChallenge(hash *hashWithBufs, challenge, nonce string) b
hex.Encode(hash.ebuf, hash.hbuf)
solution := hash.ebuf
// Compiler bound-check-elimination hint.
if len(solution) < int(m.diff1+1) {
// Compiler bound-check hint.
if len(solution) < int(m.diff) {
panic(gtserror.New("BCE"))
}
// Check that the first 'diff'
// many chars are indeed zeroes.
for i := range m.diff1 {
for i := range m.diff {
if solution[i] != '0' {
return false
}
}
// Check that next char is < 'diff2'.
return solution[m.diff1] < m.diff2
}
// hexStrs is a quick lookup of ASCII hex
// bytes to their string equivalent.
var hexStrs = [...]string{
'0': "0",
'1': "1",
'2': "2",
'3': "3",
'4': "4",
'5': "5",
'6': "6",
'7': "7",
'8': "8",
'9': "9",
'a': "a",
'b': "b",
'c': "c",
'd': "d",
'e': "e",
'f': "f",
return true
}

View File

@@ -96,8 +96,7 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) {
}
var challenge string
var diff1 uint64
var diff2 uint8
var difficulty uint64
// Parse output body and find the challenge / difficulty.
for _, line := range strings.Split(string(b), "\n") {
@@ -107,22 +106,17 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) {
line = line[25:]
line = line[:len(line)-1]
challenge = line
case strings.HasPrefix(line, "data-nollamas-difficulty1=\""):
line = line[27:]
case strings.HasPrefix(line, "data-nollamas-difficulty=\""):
line = line[26:]
line = line[:len(line)-1]
var err error
diff1, err = strconv.ParseUint(line, 10, 8)
difficulty, err = strconv.ParseUint(line, 10, 8)
assert.NoError(t, err)
case strings.HasPrefix(line, "data-nollamas-difficulty2=\""):
line = line[27:]
line = line[:len(line)-1]
diff2 = line[0]
}
}
// Ensure valid posed challenge.
assert.NotZero(t, diff1)
assert.NotZero(t, diff2)
assert.NotZero(t, difficulty)
assert.NotEmpty(t, challenge)
// Prepare a test request for gin engine.
@@ -131,12 +125,11 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) {
rw = httptest.NewRecorder()
// Now compute and set solution query paramater.
solution := computeSolution(challenge, diff1, diff2)
solution := computeSolution(challenge, difficulty)
r.URL.RawQuery = "nollamas_solution=" + solution
t.Logf("challenge=%s", challenge)
t.Logf("diff1=%d", diff1)
t.Logf("diff2='%c'", diff2)
t.Logf("difficulty=%d", difficulty)
t.Logf("solution=%s", solution)
// Pass req through
@@ -159,21 +152,18 @@ func testNoLLaMasMiddleware(t *testing.T, e *gin.Engine, userAgent string) {
}
// computeSolution does the functional equivalent of our nollamas workerTask.js.
func computeSolution(challenge string, diff1 uint64, diff2 uint8) string {
func computeSolution(challenge string, diff uint64) string {
outer:
for i := 0; ; i++ {
solution := strconv.Itoa(i)
combined := challenge + solution
hash := sha256.Sum256(byteutil.S2B(combined))
encoded := hex.EncodeToString(hash[:])
for i := range diff1 {
for i := range diff {
if encoded[i] != '0' {
continue outer
}
}
if encoded[diff1] >= diff2 {
continue outer
}
return solution
}
}

View File

@@ -1,3 +1,4 @@
node_modules
prism.js
prism.css
nollamasworker/sha256.js

View File

@@ -19,7 +19,7 @@
const explanation = "Your browser is currently solving a proof-of-work challenge designed to deter \"ai\" scrapers. This should take no more than a few seconds...";
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener("DOMContentLoaded", function() {
// Get the nollamas section container.
const nollamas = document.querySelector(".nollamas");
@@ -44,20 +44,17 @@ document.addEventListener('DOMContentLoaded', function() {
// Read the challenge and difficulty from
// data attributes on the nollamas section.
const challenge = nollamas.dataset.nollamasChallenge;
const difficulty1 = nollamas.dataset.nollamasDifficulty1;
const difficulty2 = nollamas.dataset.nollamasDifficulty2;
const difficulty = nollamas.dataset.nollamasDifficulty;
console.log('challenge:', challenge); // eslint-disable-line no-console
console.log('difficulty1:', difficulty1); // eslint-disable-line no-console
console.log('difficulty2:', difficulty2); // eslint-disable-line no-console
console.log("challenge:", challenge); // eslint-disable-line no-console
console.log("difficulty:", difficulty); // eslint-disable-line no-console
// Prepare the worker with task function.
const worker = new Worker("/assets/dist/nollamasworker.js");
const startTime = performance.now();
worker.postMessage({
challenge: challenge,
difficulty1: difficulty1,
difficulty2: difficulty2,
difficulty: difficulty,
});
// Set the main worker function.
@@ -79,7 +76,8 @@ document.addEventListener('DOMContentLoaded', function() {
solutionWrapper.appendChild(tick);
const took = document.createElement("span");
took.appendChild(document.createTextNode(`Solved in ${duration.toString()}ms!`));
const solvedText = `Solved after ${e.data.nonce} iterations, in ${duration.toString()}ms!`;
took.appendChild(document.createTextNode(solvedText));
solutionWrapper.appendChild(took);
nollamas.appendChild(solutionWrapper);
@@ -89,7 +87,7 @@ document.addEventListener('DOMContentLoaded', function() {
// not so long that they have to wait unduly.
setTimeout(() => {
let url = new URL(window.location.href);
url.searchParams.set('nollamas_solution', e.data.nonce);
url.searchParams.set("nollamas_solution", e.data.nonce);
window.location.replace(url.toString());
}, 500);
}

View File

@@ -17,43 +17,51 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
onmessage = async function(e) {
console.log('worker started'); // eslint-disable-line no-console
import sha256 from "./sha256";
const challenge = e.data.challenge;
let compute = async function(challengeStr, diffStr) {
const textEncoder = new TextEncoder();
// Get difficulty and generate the expected
// zero ASCII prefix to check for in hashes.
const difficulty1Str = e.data.difficulty1;
const difficulty2Str = e.data.difficulty2;
const difficulty1 = parseInt(difficulty1Str, 10);
const zeroPrefix = '0'.repeat(difficulty1);
// Get difficulty1 as number and generate
// expected zero ASCII prefix to check for.
const diff1 = parseInt(diffStr, 10);
const zeros = "0".repeat(diff1);
// Calculate hex encoded prefix required to check solution, where we
// need diff1 no. chars in hex, and hex encoding doubles input length.
const prefixLen = diff1 / 2 + (diff1 % 2 != 0 ? 2 : 0);
let nonce = 0;
while (true) { // eslint-disable-line no-constant-condition
// Create possible solution string from challenge + nonce.
const solution = textEncoder.encode(challenge + nonce.toString());
// Create possible solution string from challenge string + nonce.
const solution = textEncoder.encode(challengeStr + nonce.toString());
// Generate SHA256 hashsum of solution string and hex encode the result.
const hashBuffer = await crypto.subtle.digest('SHA-256', solution);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// Generate SHA256 hashsum of solution string, and hex encode the
// necessary prefix length we need to check for a valid solution.
const prefixArray = Array.from(sha256(solution).slice(0, prefixLen));
const prefixHex = prefixArray.map(b => b.toString(16).padStart(2, "0")).join("");
// Check if the hex encoded hash has
// difficulty defined zeroes prefix.
if (hashHex.startsWith(zeroPrefix)) {
// Check if the next char after zero prefix
// is specifically less than difficulty2 char.
if (hashHex.charAt(difficulty1) < difficulty2Str) {
postMessage({ nonce: nonce, done: true });
break;
}
if (prefixHex.startsWith(zeros)) {
return nonce;
}
// Iter.
nonce++;
}
};
onmessage = async function(e) {
console.log('worker started'); // eslint-disable-line no-console
const challenge = e.data.challenge;
const difficulty = e.data.difficulty;
// Compute the nonce that produces solution with args.
let nonce = await compute(challenge, difficulty);
// Post the solution nonce back to caller.
postMessage({ nonce: nonce, done: true });
};

View File

@@ -0,0 +1,113 @@
/*
Copyright 2022 Andrea Griffini
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// sha256(data) returns the digest of an input piece of data.
// sha256(none) returns an object you can call .add(data), and .digest() at the end.
// the returned digest is a 32-byte Uint8Array instance with an added .hex() function.
// input should be string (that will be encoded as UTF-8) or an array-like with values 0..255.
// source: https://github.com/6502/sha256
export default function sha256(data) {
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a,
h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19,
tsz = 0, bp = 0;
const k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2],
rrot = (x, n) => (x >>> n) | (x << (32-n)),
w = new Uint32Array(64),
buf = new Uint8Array(64),
process = () => {
for (let j=0,r=0; j<16; j++,r+=4) {
w[j] = (buf[r]<<24) | (buf[r+1]<<16) | (buf[r+2]<<8) | buf[r+3];
}
for (let j=16; j<64; j++) {
let s0 = rrot(w[j-15], 7) ^ rrot(w[j-15], 18) ^ (w[j-15] >>> 3);
let s1 = rrot(w[j-2], 17) ^ rrot(w[j-2], 19) ^ (w[j-2] >>> 10);
w[j] = (w[j-16] + s0 + w[j-7] + s1) | 0;
}
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
for (let j=0; j<64; j++) {
let S1 = rrot(e, 6) ^ rrot(e, 11) ^ rrot(e, 25),
ch = (e & f) ^ ((~e) & g),
t1 = (h + S1 + ch + k[j] + w[j]) | 0,
S0 = rrot(a, 2) ^ rrot(a, 13) ^ rrot(a, 22),
maj = (a & b) ^ (a & c) ^ (b & c),
t2 = (S0 + maj) | 0;
h = g; g = f; f = e; e = (d + t1)|0; d = c; c = b; b = a; a = (t1 + t2)|0;
}
h0 = (h0 + a)|0; h1 = (h1 + b)|0; h2 = (h2 + c)|0; h3 = (h3 + d)|0;
h4 = (h4 + e)|0; h5 = (h5 + f)|0; h6 = (h6 + g)|0; h7 = (h7 + h)|0;
bp = 0;
},
add = data => {
if (typeof data === "string") {
data = typeof TextEncoder === "undefined" ? Buffer.from(data) : (new TextEncoder).encode(data);
}
for (let i=0; i<data.length; i++) {
buf[bp++] = data[i];
if (bp === 64) {process();}
}
tsz += data.length;
},
digest = () => {
buf[bp++] = 0x80; if (bp == 64) {process();}
if (bp + 8 > 64) {
while (bp < 64) {buf[bp++] = 0x00;}
process();
}
while (bp < 58) {buf[bp++] = 0x00;}
// Max number of bytes is 35,184,372,088,831
let L = tsz * 8;
buf[bp++] = (L / 1099511627776.) & 255;
buf[bp++] = (L / 4294967296.) & 255;
buf[bp++] = L >>> 24;
buf[bp++] = (L >>> 16) & 255;
buf[bp++] = (L >>> 8) & 255;
buf[bp++] = L & 255;
process();
let reply = new Uint8Array(32);
reply[ 0] = h0 >>> 24; reply[ 1] = (h0 >>> 16) & 255; reply[ 2] = (h0 >>> 8) & 255; reply[ 3] = h0 & 255;
reply[ 4] = h1 >>> 24; reply[ 5] = (h1 >>> 16) & 255; reply[ 6] = (h1 >>> 8) & 255; reply[ 7] = h1 & 255;
reply[ 8] = h2 >>> 24; reply[ 9] = (h2 >>> 16) & 255; reply[10] = (h2 >>> 8) & 255; reply[11] = h2 & 255;
reply[12] = h3 >>> 24; reply[13] = (h3 >>> 16) & 255; reply[14] = (h3 >>> 8) & 255; reply[15] = h3 & 255;
reply[16] = h4 >>> 24; reply[17] = (h4 >>> 16) & 255; reply[18] = (h4 >>> 8) & 255; reply[19] = h4 & 255;
reply[20] = h5 >>> 24; reply[21] = (h5 >>> 16) & 255; reply[22] = (h5 >>> 8) & 255; reply[23] = h5 & 255;
reply[24] = h6 >>> 24; reply[25] = (h6 >>> 16) & 255; reply[26] = (h6 >>> 8) & 255; reply[27] = h6 & 255;
reply[28] = h7 >>> 24; reply[29] = (h7 >>> 16) & 255; reply[30] = (h7 >>> 8) & 255; reply[31] = h7 & 255;
reply.hex = () => {
let res = "";
reply.forEach(x => res += ("0" + x.toString(16)).slice(-2)); // eslint-disable-line no-return-assign
return res;
};
return reply;
};
if (data === undefined) {return {add, digest};}
add(data);
return digest();
}

View File

@@ -21,8 +21,7 @@
<main>
<section class="nollamas"
data-nollamas-challenge="{{ .challenge }}"
data-nollamas-difficulty1="{{ .difficulty1 }}"
data-nollamas-difficulty2="{{ .difficulty2 }}"
data-nollamas-difficulty="{{ .difficulty }}"
>
<h1>Checking you're not a creepy crawler...</h1>
<noscript>