Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -1465,8 +1465,33 @@ AM_CONDITIONAL([EMBEDDED_LIBSECP256K1],[test x$system_libsecp256k1 = xno])
AC_SUBST(libsecp256k1_CFLAGS)
AC_SUBST(libsecp256k1_LIBS)

CPPFLAGS="-I/opt/homebrew/opt/lua/include/lua $CPPFLAGS"
LIBS="-L/opt/homebrew/opt/lua/lib -llua -lm $LIBS"
dnl Check for Lua using pkg-config
PKG_CHECK_MODULES([LUA], [lua5.4], [
CPPFLAGS="$LUA_CFLAGS $CPPFLAGS"
LIBS="$LUA_LIBS $LIBS"
], [
PKG_CHECK_MODULES([LUA], [lua5.3], [
CPPFLAGS="$LUA_CFLAGS $CPPFLAGS"
LIBS="$LUA_LIBS $LIBS"
], [
dnl Fallback: Try homebrew on macOS, system paths on Linux
case $host_os in
darwin*)
dnl macOS - try homebrew
CPPFLAGS="-I/opt/homebrew/opt/lua/include/lua $CPPFLAGS"
LIBS="-L/opt/homebrew/opt/lua/lib -llua -lm $LIBS"
;;
linux*)
dnl Linux - try common system paths
CPPFLAGS="-I/usr/include/lua5.4 $CPPFLAGS"
LIBS="-llua5.4 -lm $LIBS"
;;
*)
AC_MSG_ERROR([Lua not found. Please install lua5.4-dev or lua5.3-dev])
;;
esac
])
])

if test "$enable_wallet" != "no"; then
dnl Check for libdb_cxx only if wallet enabled
Expand Down
236 changes: 236 additions & 0 deletions share/scripts/opreturn_spam.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
-- Chained OP_RETURN Spam Detector for Bitcoin Knots
-- Detects ordiknots data embedding via chained OP_RETURN outputs
-- Compatible with Bitcoin Knots PR #119 modular filter system

-- Ordiknots "444" prefix
local ORDIKNOT_PREFIX = {0x34, 0x34, 0x34}

local function extract_opreturn_data(script_pubkey)
if #script_pubkey < 2 then
return nil
end

-- Check for OP_RETURN (0x6a)
local first_byte = string.byte(script_pubkey, 1)
if first_byte ~= 0x6a then
return nil
end

-- Skip OP_RETURN and PUSH opcodes to get to actual data
local i = 2
local opcode = string.byte(script_pubkey, i)

-- Handle PUSHDATA opcodes (OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4)
if opcode == 0x4c then
-- OP_PUSHDATA1: next byte is length
i = i + 2
elseif opcode == 0x4d then
-- OP_PUSHDATA2: next 2 bytes are length
i = i + 3
elseif opcode == 0x4e then
-- OP_PUSHDATA4: next 4 bytes are length
i = i + 5
elseif opcode > 0 and opcode <= 0x4b then
-- Direct push opcode (1-75 bytes)
i = i + 1
else
return nil
end

if i > #script_pubkey then
return nil
end

-- Extract the data portion
return string.sub(script_pubkey, i)
end

local function is_ordiknots_format(data)
-- Ordiknots format requires at least 5 bytes:
-- Bytes 0-2: "444" prefix (0x34 0x34 0x34)
-- Byte 3: chunk_index
-- Byte 4: total_chunks
-- Optional bytes 5-6: file_size (u16 little-endian) if chunk_index == 0
-- Remaining bytes: actual data

if not data or #data < 5 then
return false
end

-- Check for "444" prefix
local b1 = string.byte(data, 1)
local b2 = string.byte(data, 2)
local b3 = string.byte(data, 3)

if b1 ~= ORDIKNOT_PREFIX[1] or b2 ~= ORDIKNOT_PREFIX[2] or b3 ~= ORDIKNOT_PREFIX[3] then
return false
end

-- Validate chunk metadata
local chunk_index = string.byte(data, 4)
local total_chunks = string.byte(data, 5)

-- Sanity checks on chunk metadata
-- ordiknots MAX_CHAINED_TRANSACTIONS = 25, so total_chunks must be 1-25
if total_chunks == 0 or total_chunks > 25 then
return false
end

-- chunk_index must be less than total_chunks
if chunk_index >= total_chunks then
return false
end

-- If this is the first chunk (index 0), it should have file_size metadata
if chunk_index == 0 and #data >= 7 then
-- First chunk format: "444" + index + total + file_size (2 bytes) + data
return true
elseif chunk_index > 0 and #data >= 5 then
-- Subsequent chunk format: "444" + index + total + data
return true
end

return false
end

local function get_chunk_info(data)
if not data or #data < 5 then
return nil
end

-- Ordiknots format (Lua uses 1-indexed strings):
-- Bytes 1-3: "444" prefix
-- Byte 4: chunk_index
-- Byte 5: total_chunks
return {
chunk_index = string.byte(data, 4),
total_chunks = string.byte(data, 5)
}
end

-- Main filter function called by Bitcoin Knots
function validate(tx, ctx)
local outputs = tx:get_outputs()
local opreturn_outputs = {}
local has_magic = false
local has_chain_pattern = false
local largest_opreturn = 0
local details = {}

-- Find all OP_RETURN outputs
for i, output in ipairs(outputs) do
if output:is_op_return() then
table.insert(opreturn_outputs, {
index = i - 1,
output = output,
script = output:get_script_pubkey()
})
end
end

-- No OP_RETURN outputs
if #opreturn_outputs == 0 then
return {
accept = true,
score = 0,
reason = "No OP_RETURN outputs"
}
end

-- Analyze each OP_RETURN output
for _, op_out in ipairs(opreturn_outputs) do
local data = extract_opreturn_data(op_out.script)

if data then
local data_size = #data
if data_size > largest_opreturn then
largest_opreturn = data_size
end

-- Check for ordiknots format
if is_ordiknots_format(data) then
has_magic = true
details.ordiknots_format = true

local chunk_info = get_chunk_info(data)
if chunk_info then
details.chunk_index = chunk_info.chunk_index
details.total_chunks = chunk_info.total_chunks

if ctx and ctx.log_warn then
ctx:log_warn(string.format(
"Ordiknots spam detected: chunk %d/%d",
chunk_info.chunk_index,
chunk_info.total_chunks
))
end
end
end
end
end

-- Check for continuation output pattern (small value output for chaining)
local has_continuation = false
for _, output in ipairs(outputs) do
local value = output:get_value()
if value > 0 and value < 15000 then
has_continuation = true
break
end
end

if has_continuation and #opreturn_outputs > 0 then
has_chain_pattern = true
details.has_continuation = true
end

-- Detect ordiknots spam format (high confidence)
if has_magic then
return {
accept = false,
score = 95,
reason = "Ordiknots spam format detected in OP_RETURN data"
}
end

-- Detect chained OP_RETURN pattern (medium confidence)
if has_chain_pattern then
return {
accept = false,
score = 60,
reason = "Chained OP_RETURN pattern with continuation output"
}
end

-- Detect oversized OP_RETURN (lower confidence)
if largest_opreturn > 80 then
return {
accept = false,
score = 40,
reason = string.format("OP_RETURN exceeds standard size (%d bytes)", largest_opreturn)
}
end

-- No suspicious patterns
return {
accept = true,
score = 0,
reason = "No suspicious OP_RETURN patterns"
}
end

-- Optional: Lifecycle hooks
local function on_load()
return true
end

local function on_unload()
return true
end

-- Export module functions
return {
validate = validate,
on_load = on_load,
on_unload = on_unload
}
Loading