add highly efficient hashing functions

This commit is contained in:
kno10
2025-01-16 21:14:26 +01:00
committed by the-real-herowl
parent 289816a7b4
commit 2d61cc1178
3 changed files with 100 additions and 0 deletions
mods/CORE/mcl_util

22
mods/CORE/mcl_util/API.md Normal file

@ -0,0 +1,22 @@
# VoxeLibre utility functions
The documentation is currently incomplete.
## Hashing
While Luanti core provides access to *secure hashing* functions via `core.sha1` and `core.sha256`, perfect for password validation,
this often is overkill when there are no cryptographic requirements. This package provides some low-complexity alternatives.
- `mcl_util.djb2_hash(str)` is a simple string hashing function attributed to DJ Bernstein.
- `mcl_util.bitmix32(a, b)` is a simple bit mixer that combines two integers `a` and `b` into an integer hash value.
- `mcl_util.hash_pos(x, y, z, seed)` is a simple hash function of a coordinate vector,
suitable for deterministic low-security hashing, such as map generation.
A good choice if performance is more important than cryptographic security.
In contrast to the misnamed Luanti `core.hash_node_position` (which is `x << 32 + y << 16 + z`,
a reversible map of the position into a 48 bit integer), this is a *mixing* function,
such that nearby nodes are expected to produce very different hash code.
For *cryptographic* uses, please continue to use `core.sha256`.

@ -0,0 +1,77 @@
---- Hashing related functions
-- The bitop is avilable both in luaJIT and in non-JIT Luanti since 5.5, so safe to use
local tobit = bit.tobit
local band = bit.band
local lshift = bit.lshift
local rshift = bit.rshift
-- u32 multiplication in luas double data types, via 16 bit multiplications
local function u32_mul(a, b)
return (band(a, 0xffff) * b) + lshift(band(rshift(a, 16) * b, 0xffff), 16)
end
--- Simple and cheap bit mixing operation
--- to use with a seed, do bitmix32(bitmix32(seed, a), b)
--- @param a number: first value
--- @param b number: second value
--- @return number: combined hash code in signed int32 range
function mcl_util.bitmix32(a, b)
return tobit(u32_mul(u32_mul(tobit(a), 0x85ebca6b) + tobit(b), 0xc2b2ae35))
end
local bitmix32 = mcl_util.bitmix32
--- Simple position hash function.
--- Use this with a custom seed to avoid coincidences with other mods.
--- @param x number: X coordinate (only integer part is used)
--- @param y number: Y coordinate (only integer part is used)
--- @param z number: Z coordinate (only integer part is used)
--- @param seed number: Seed value
--- @return number: combined hash code (signed int32)
function mcl_util.hash_pos(x, y, z, seed)
if not seed then return bitmix32(bitmix32(x, y), z) end
return bitmix32(bitmix32(bitmix32(seed, x), y), z)
end
local hash_pos = mcl_util.hash_pos
---- Some simple assertions on the hash function.
--- Count the number of bits, unfortunately not part of bitops library
local function popcnt(x)
local b = 0
while x ~= 0 do
if band(x,1) == 1 then b = b + 1 end
x = rshift(x, 1)
end
return b
end
assert(hash_pos(0, 0, 0, 0) ~= hash_pos(0, 0, 0, 1))
-- Expected difference is always 16 bit for an "optimal" hash function
local function assert_hash_difference(x,y,z,x2,y2,z2,seed)
local p = popcnt(bit.bxor(hash_pos(x,y,z,seed),hash_pos(x2,y2,z2,seed)))
-- core.log("action", "hash test "..x..","..y..","..z..": "..bit.tohex(hash_pos(x,y,z,seed)).." "..x2..","..y2..","..z2..": "..bit.tohex(hash_pos(x2,y2,z2,seed)).." difference "..p)
-- On introduction of this function, we would observe 13 to 19 bits of difference in the tests, which is fine
assert(p >= 10 and p <= 22, "hash codes similar, but could be coincidence: "..x..","..y..","..z.." and "..x2..","..y2..","..z2)
end
assert_hash_difference(0,0,0, 0,0,-1, 0)
assert_hash_difference(0,0,0, 0,0,1, 0)
assert_hash_difference(0,0,0, 0,1,0, 0)
assert_hash_difference(0,0,0, 1,0,0, 0)
assert_hash_difference(0,0,1, 0,1,0, 0)
assert_hash_difference(0,0,1, 1,0,0, 0)
assert_hash_difference(0,1,0, 1,0,0, 0)
assert_hash_difference(0,0,0, 1,1,1, 0)
--- DJ Bernstein hash function known as djb2.
--- @param str string: Input
--- @return number: combined hash code
function mcl_util.djb2_hash(str)
str = tostring(str)
local hash = 5381
for i = 1, #str do
-- we don't do the h<<5+h trick here, as lua only supports doubles!
hash = band(hash * 33 + str:byte(i), 0xffffffff)
end
-- by default, the values would be signed, we want u32
return hash >= 0 and hash or (0x100000000 + hash)
end
assert(mcl_util.djb2_hash("VoxeLibre") == 2331368085, "djb2 hash does not agree with expected hash code from a Python implementation")

@ -5,6 +5,7 @@ local modpath = core.get_modpath(modname)
dofile(modpath.."/roman_numerals.lua")
dofile(modpath.."/nodes.lua")
dofile(modpath.."/table.lua")
dofile(modpath.."/hashing.lua")
local LOGGING_ON = minetest.settings:get_bool("mcl_logging_default", false)
local LOG_MODULE = "[MCL2]"