ripemd160mb

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 31, 2026 License: MIT Imports: 5 Imported by: 0

README

ripemd160mb

Go Reference CI Go Report Card License: MIT Free & Open Source

ripemd160mb is a high-performance RIPEMD-160 implementation for Go, built for Bitcoin-style HASH160 pipelines that need to process many independent 32-byte SHA-256 digests at once.

The library combines a portable pure-Go scalar implementation with a real Go-assembly SIMD backend. On arm64, the hand-tuned 4-lane NEON kernel is the default and is about 3x faster than scalar on Apple M3 while keeping the hot path zero-allocation.

This is free and open-source software released under the permissive MIT license.

src := make([]byte, n*32) // n SHA-256 digests, contiguous
dst := make([]byte, n*ripemd160mb.Size)

ripemd160mb.Hash32(dst, src, n)
fmt.Println(ripemd160mb.Backend(), ripemd160mb.Lanes())

Why This Exists

Bitcoin addresses and scripts frequently rely on HASH160:

HASH160(x) = RIPEMD160(SHA256(x))

Most Go code computes RIPEMD-160 one message at a time. That is simple, but it leaves throughput on the table when the workload is naturally batched: public key analysis, wallet tooling, address derivation experiments, indexers, test generators, benchmark suites, and cryptography education.

ripemd160mb focuses on that exact hot path: many fixed 32-byte inputs, contiguous memory, no per-message allocations, runtime backend reporting, and correctness checked against the independent golang.org/x/crypto/ripemd160 oracle.

It is a performance library, not a shortcut around Bitcoin security. Faster hashing makes experiments and benchmarks better; it does not make brute force a business model.

Highlights

  • Batched RIPEMD-160 for Go through Hash32(dst, src, n).
  • Bitcoin HASH160 ready for RIPEMD160(SHA256(x)) pipelines.
  • arm64 NEON SIMD backend with 4 parallel lanes and runtime dispatch.
  • Portable scalar fallback for every supported Go architecture.
  • Zero allocations on the Hash32 hot path.
  • Concurrent-safe API with no package-level mutable hashing state.
  • Reference-compatible New, Sum, and HashEach APIs for general RIPEMD-160 use.
  • Differential, property, race, fuzz, and coverage tests enforced in CI.
  • Free and open source under the permissive MIT license.

Install

go get github.com/Asylian21/ripemd160-asm@v0.1.0

Import the root package for RIPEMD-160:

import "github.com/Asylian21/ripemd160-asm"

Import the optional HASH160 helper when you want SHA-256 plus RIPEMD-160 in one convenience call:

import "github.com/Asylian21/ripemd160-asm/hash160"

To build against a local checkout before a release tag is available, add a temporary replace directive to the consumer's go.mod:

require github.com/Asylian21/ripemd160-asm v0.0.0

replace github.com/Asylian21/ripemd160-asm => /path/to/RIPEMD-160-Assembly

Quick Start

Use Hash32 when you already have n SHA-256 digests laid out as one contiguous byte slice:

package main

import (
	"fmt"

	"github.com/Asylian21/ripemd160-asm"
)

func main() {
	const n = 1024

	src := make([]byte, n*32) // msg i: src[i*32:(i+1)*32]
	dst := make([]byte, n*ripemd160mb.Size)

	// Fill src with 32-byte SHA-256 digests, then hash the batch.
	ripemd160mb.Hash32(dst, src, n)

	fmt.Printf("backend=%s lanes=%d first=%x\n",
		ripemd160mb.Backend(),
		ripemd160mb.Lanes(),
		dst[:ripemd160mb.Size],
	)
}

Hash32 panics on programmer errors (n < 0, short input, or short output). For n == 0, it is a no-op and accepts nil buffers.

HASH160 Helper

The hash160 subpackage computes Bitcoin-style RIPEMD160(SHA256(message)) for batches of fixed-width inputs, such as 33-byte compressed public keys:

import (
	"github.com/Asylian21/ripemd160-asm"
	"github.com/Asylian21/ripemd160-asm/hash160"
)

const (
	n     = 1000
	width = 33 // compressed public keys
)

src := make([]byte, n*width)
dst := make([]byte, n*ripemd160mb.Size)

hash160.Hash160_32(dst, src, n, width)

Hash160_32 is intentionally a convenience wrapper. It allocates one intermediate n*32 buffer and uses the standard library crypto/sha256. For maximum throughput, compute SHA-256 into a reusable n*32 buffer, then call ripemd160mb.Hash32 directly.

See [examples/hash160](examples/hash160/main.go) and the package examples for runnable versions of both patterns.

API

const (
	Size      = 20
	BlockSize = 64
)

func Hash32(dst, src []byte, n int)
func HashEach(dst [][Size]byte, src [][]byte)
func New() hash.Hash
func Sum(p []byte) [Size]byte
func Lanes() int
func Backend() string

Hash32 is the fast path. It reads n contiguous 32-byte messages (src[i*32:(i+1)*32]) and writes n contiguous 20-byte digests (dst[i*Size:(i+1)*Size]). It allocates nothing, keeps no state between calls, and is safe for concurrent use from many goroutines.

HashEach, New, and Sum cover general RIPEMD-160 usage for arbitrary message lengths and are byte-for-byte compatible with golang.org/x/crypto/ripemd160.

Backends

Runtime dispatch selects the fastest implemented backend once during package initialization. Backend() always reports the kernel that actually executes:

Backend GOARCH Lanes Status
neon arm64 4 implemented, default on arm64
scalar all 1 implemented, portable fallback

Set GORIPEMD160MB_FORCE=scalar or GORIPEMD160MB_FORCE=neon to pin a backend for testing or benchmarking. Unknown or unsupported values fall back to scalar.

amd64 SIMD kernels (SSE2, AVX2, AVX-512) are planned but not yet implemented, so amd64 currently runs scalar. The NEON generator in [internal/neongen](internal/neongen) is the reference template for adding new kernels, and every new backend must be verified bit-for-bit against the scalar oracle before it is wired into dispatch.

Performance

Latest local Apple M3 smoke benchmark:

GOMAXPROCS=1
n = 2048
Backend ns/op MB/s hashes/s allocs/op
scalar 470680 139.18 4,349,526 0
neon 155403 421.06 13,158,076 0

The arm64 NEON backend delivers about 3.0x the scalar throughput for this large-batch workload, with zero allocations on both paths.

Reproduce and compare results with:

GOMAXPROCS=1 GORIPEMD160MB_FORCE=scalar \
	go test -run '^$' -bench '^BenchmarkHash32$' -benchmem -count=10 ./ \
	| tee scalar.txt

GOMAXPROCS=1 GORIPEMD160MB_FORCE=neon \
	go test -run '^$' -bench '^BenchmarkHash32$' -benchmem -count=10 ./ \
	| tee neon.txt

benchstat scalar.txt neon.txt

The full methodology, profiling workflow, and release criteria live in [PERFORMANCE.md](PERFORMANCE.md).

Correctness and Security Posture

This repository treats performance claims as secondary to correctness:

  • Known vectors, the million-a vector, differential tests, and fuzzing compare against golang.org/x/crypto/ripemd160.
  • Every implemented backend must pass the same contract tests, including lane boundaries and scalar tail handling.
  • Hash32 is tested for zero allocations, bounds safety, and concurrent use.
  • CI runs go test, forced-scalar tests, race tests, vet, static analysis, coverage checks, cross-builds, and generated-assembly drift checks.

RIPEMD-160 remains a legacy hash used by Bitcoin HASH160 and compatibility workflows. For new protocol design, prefer modern primitives selected for that protocol's threat model. This package is for correct RIPEMD-160 compatibility and batched performance, not for inventing new cryptographic constructions.

Testing

go test ./...                                   # all packages, native backend
GORIPEMD160MB_FORCE=scalar go test ./...        # force the scalar oracle
go test -race ./...                             # data-race detector

Coverage of the importable library packages (root plus hash160):

go test -covermode=atomic -coverprofile=coverage.out ./ ./hash160
go tool cover -func=coverage.out | tail -1
go tool cover -html=coverage.out

CI enforces a 95% statement-coverage floor on these two packages; both are currently at 100%.

Fuzzing:

go test -run '^$' -fuzz '^FuzzSum$'        -fuzztime=30s .
go test -run '^$' -fuzz '^FuzzHash32$'     -fuzztime=30s .
go test -run '^$' -fuzz '^FuzzHash160_32$' -fuzztime=30s ./hash160

Contribution guidelines, generator rules, and the pull request checklist are in [CONTRIBUTING.md](CONTRIBUTING.md). The precise API contract is documented in [SPEC.md](SPEC.md).

GitHub SEO

Recommended repository description:

Free and open-source high-performance batched RIPEMD-160 and Bitcoin HASH160 for Go, with arm64 NEON SIMD, zero-allocation Hash32, fuzz-tested correctness, and scalar fallback.

Recommended GitHub topics:

ripemd160, ripemd-160, hash160, bitcoin, bitcoin-address, cryptography, go, golang, open-source, foss, mit-license, simd, neon, arm64, assembly, hashing, benchmark, performance, fuzz-testing

Search keywords naturally covered by this repository:

RIPEMD-160 Go implementation, RIPEMD160 Golang, Bitcoin HASH160 Go, batched RIPEMD-160, arm64 NEON hash, Go assembly SIMD, zero allocation hashing, Bitcoin public key hash, RIPEMD-160 benchmark, Hash160 benchmark

Roadmap

  • Add amd64 SIMD backends only when real SSE2, AVX2, or AVX-512 kernels beat scalar and pass the same bit-for-bit verification suite.
  • Keep Hash32 zero-allocation and stable for high-throughput callers.
  • Improve benchmark coverage across more CPUs and Go releases.
  • Expand documentation around real-world HASH160 pipeline design.

Support This Project ₿

If this project helped you understand Bitcoin security, benchmark Go code, or explain why brute force is not a business model, you can support continued research here:

Bitcoin donation address:

bc1q9c5mmx9d3ajevjrvvw9yf52jclsre8x86qhnak

Every satoshi helps fund more experiments, better documentation, and fewer hand-wavy claims about cryptography.

License

ripemd160mb is free and open-source software under the MIT license. You may use, copy, modify, and redistribute it under the terms in LICENSE.

Documentation

Overview

Package ripemd160mb computes RIPEMD-160 hashes, with a multi-buffer fixed-32-byte fast path designed for Bitcoin-style Hash160 pipelines.

The package offers two layers of API:

  • General purpose: New returns a streaming hash.Hash, and Sum hashes a single message of any length. Both are byte-for-byte compatible with golang.org/x/crypto/ripemd160.
  • Fast path: Hash32 hashes many independent 32-byte messages in one call, and HashEach is a convenience wrapper for a slice of arbitrary messages.

Hash32 buffer layout

Hash32 reads n messages of exactly 32 bytes from a single contiguous source buffer and writes n digests of exactly Size (20) bytes to a single contiguous destination buffer. Message i occupies src[i*32:(i+1)*32] and its digest occupies dst[i*Size:(i+1)*Size]:

src: | msg 0 (32B) | msg 1 (32B) | ... | msg n-1 (32B) |
dst: | dig 0 (20B) | dig 1 (20B) | ... | dig n-1 (20B) |

Hash32 does not allocate, holds no state between calls, and is safe for concurrent use by multiple goroutines.

Panics

Hash32 panics if n is negative, if len(src) < n*32, or if len(dst) < n*Size. HashEach panics if len(dst) != len(src). These conditions indicate a caller bug rather than a runtime error, so they are not reported via error values.

Backend selection

At package initialization the implementation selects the fastest backend available in this build for the current architecture. The scalar backend is a pure-Go implementation that also serves as the correctness oracle for the vector backends. Backend reports the active backend name and Lanes reports how many messages it processes in parallel.

Two backends are implemented today: a hand-tuned 4-lane arm64 NEON kernel (the default on arm64, several times faster than scalar) and the portable scalar fallback (the default everywhere else). amd64 SIMD kernels are planned but not yet implemented, so amd64 currently runs the scalar backend.

The environment variable GORIPEMD160MB_FORCE may be set to scalar or neon to pin a specific backend. An unknown value, or a backend not implemented for the current architecture, falls back to scalar rather than failing; the value reported by Backend always names the kernel that actually runs.

Example (Hash160HotPath)

Example_hash160HotPath shows the recommended high-throughput HASH160 pattern, the one a Bitcoin-style brute-force pipeline should use: compute the SHA-256 digests yourself (here with crypto/sha256, but typically a vectorized SHA-256) into a single reusable n*32 buffer, then call Hash32 once. Hash32 allocates nothing and is safe to call from many worker goroutines, so the only work per batch is the hashing itself.

const (
	n     = 8
	width = 33 // compressed public keys
)
keys := make([]byte, n*width)
for i := range keys {
	keys[i] = byte(i)
}

// Reused across batches in a real worker; shown once here.
digests := make([]byte, n*32) // SHA-256 outputs, contiguous
out := make([]byte, n*ripemd160mb.Size)

for i := 0; i < n; i++ {
	sum := sha256.Sum256(keys[i*width : (i+1)*width])
	copy(digests[i*32:], sum[:])
}
ripemd160mb.Hash32(out, digests, n)

ok := true
for i := 0; i < n; i++ {
	sum := sha256.Sum256(keys[i*width : (i+1)*width])
	want := ripemd160mb.Sum(sum[:])
	if !bytes.Equal(out[i*ripemd160mb.Size:(i+1)*ripemd160mb.Size], want[:]) {
		ok = false
	}
}
fmt.Println(ok)
Output:
true

Index

Examples

Constants

View Source
const (
	// Size is the size, in bytes, of a RIPEMD-160 digest.
	Size = 20

	// BlockSize is RIPEMD-160's internal block size in bytes.
	BlockSize = 64
)

Variables

This section is empty.

Functions

func Backend

func Backend() string

Backend returns the active backend name.

Example

ExampleBackend reports which kernel is active and how many messages it hashes per call. A diagnostic banner like this lets a caller confirm at startup whether it is running the SIMD or the scalar path.

// In real code: log.Printf("ripemd160mb backend=%s lanes=%d", ...)
_ = ripemd160mb.Backend() // e.g. "neon" on arm64, "scalar" elsewhere
lanes := ripemd160mb.Lanes()
fmt.Println(lanes >= 1)
Output:
true

func Hash32

func Hash32(dst, src []byte, n int)

Hash32 hashes n independent 32-byte messages from src into dst.

The buffers are contiguous: message i is src[i*32:(i+1)*32] and its digest is written to dst[i*Size:(i+1)*Size]. src must contain at least n*32 bytes and dst at least n*Size bytes. Hash32 does not allocate and is safe for concurrent use by multiple goroutines.

Hash32 panics if n is negative, if len(src) < n*32, or if len(dst) < n*Size. A count of zero is a no-op and tolerates nil buffers.

Example

ExampleHash32 shows the multi-buffer fast path: several 32-byte messages are laid out contiguously and hashed in a single call. Each 20-byte output lane equals the single-message Sum of the matching input slice.

const n = 3
src := make([]byte, n*32)
for i := range src {
	src[i] = byte(i)
}

dst := make([]byte, n*ripemd160mb.Size)
ripemd160mb.Hash32(dst, src, n)

consistent := true
for i := 0; i < n; i++ {
	want := ripemd160mb.Sum(src[i*32 : (i+1)*32])
	if !bytes.Equal(dst[i*ripemd160mb.Size:(i+1)*ripemd160mb.Size], want[:]) {
		consistent = false
	}
}
fmt.Println(consistent)
Output:
true

func HashEach

func HashEach(dst [][Size]byte, src [][]byte)

HashEach hashes each source message independently into the corresponding dst element, where dst[i] = Sum(src[i]). Unlike Hash32, the messages may have arbitrary lengths.

HashEach panics if len(dst) != len(src).

func Lanes

func Lanes() int

Lanes returns the number of messages processed in parallel by the active backend. It returns 1 for the scalar backend.

func New

func New() hash.Hash

New returns a new RIPEMD-160 hash.Hash.

Example

ExampleNew uses the streaming hash.Hash interface, which accepts arbitrary messages written in any number of chunks.

h := ripemd160mb.New()
io.WriteString(h, "ab")
io.WriteString(h, "c")
fmt.Printf("%x\n", h.Sum(nil))
Output:
8eb208f7e05d987a9b044a8e98c6b087f15a0bfc

func Sum

func Sum(p []byte) [Size]byte

Sum returns the RIPEMD-160 digest of p.

Example

ExampleSum hashes a single message with the one-shot helper.

sum := ripemd160mb.Sum([]byte("abc"))
fmt.Printf("%x\n", sum)
Output:
8eb208f7e05d987a9b044a8e98c6b087f15a0bfc

Types

This section is empty.

Directories

Path Synopsis
examples
hash160 command
Package hash160 combines SHA-256 and ripemd160mb.Hash32 for Bitcoin-style HASH160 pipelines.
Package hash160 combines SHA-256 and ripemd160mb.Hash32 for Bitcoin-style HASH160 pipelines.
internal
neongen command
Command neongen emits the arm64 NEON kernel block_arm64.s.
Command neongen emits the arm64 NEON kernel block_arm64.s.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL