Module: BSV::Primitives::Base58

Defined in:
lib/bsv/primitives/base58.rb

Overview

Base58 and Base58Check encoding/decoding.

Implements the Base58 alphabet used throughout Bitcoin for addresses, WIF keys, and extended keys. Base58Check adds a 4-byte double-SHA-256 checksum for error detection.

Examples:

Encode and decode an address payload

encoded = BSV::Primitives::Base58.check_encode(payload)
decoded = BSV::Primitives::Base58.check_decode(encoded)

Defined Under Namespace

Classes: ChecksumError

Constant Summary collapse

ALPHABET =

The Base58 alphabet (no 0, O, I, l to avoid visual ambiguity).

'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
BASE =

The base (58).

ALPHABET.length
DECODE_MAP =

Reverse lookup table mapping ASCII byte values to Base58 digit indices.

Array.new(256, -1).tap do |map|
  ALPHABET.each_char.with_index { |c, i| map[c.ord] = i }
end.freeze

Class Method Summary collapse

Class Method Details

.check_decode(string, prefix_length: 0) ⇒ String, Hash

Decode a Base58Check string and verify its checksum.

When prefix_length is greater than zero, the decoded payload is split into a prefix and data portion. The returned value is then a Hash with :prefix and :data keys. When prefix_length is zero (default), the raw payload is returned unchanged for backwards compatibility.

Parameters:

  • string (String)

    Base58Check-encoded string

  • prefix_length (Integer) (defaults to: 0)

    number of leading bytes to treat as a prefix (default: 0)

Returns:

  • (String, Hash)

    decoded payload, or { prefix:, data: } when prefix_length > 0

Raises:

  • (ChecksumError)

    if the checksum does not match or input is too short



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/bsv/primitives/base58.rb', line 116

def check_decode(string, prefix_length: 0)
  data = decode(string)
  raise ChecksumError, 'input too short for checksum' if data.length < 4

  payload = data[0...-4]
  checksum = data[-4..]
  expected = Digest.sha256d(payload)[0, 4]
  raise ChecksumError, 'checksum mismatch' unless checksum == expected

  return payload if prefix_length.zero?

  raise ArgumentError, 'prefix_length exceeds payload' if prefix_length > payload.length

  { prefix: payload[0, prefix_length], data: payload[prefix_length..] }
end

.check_encode(payload, prefix: nil) ⇒ String

Encode binary data with a 4-byte double-SHA-256 checksum appended.

When prefix is given, it is prepended to the payload before checksumming. The checksum covers the full prefix payload+ concatenation.

Parameters:

  • payload (String)

    binary data to encode

  • prefix (String, nil) (defaults to: nil)

    optional version prefix to prepend (binary string)

Returns:

  • (String)

    Base58Check-encoded string



99
100
101
102
103
# File 'lib/bsv/primitives/base58.rb', line 99

def check_encode(payload, prefix: nil)
  full = (prefix || ''.b) + payload
  checksum = Digest.sha256d(full)[0, 4]
  encode(full + checksum)
end

.decode(string) ⇒ String

Decode a Base58 string to binary data.

Leading ‘1’ characters are decoded as zero bytes.

Parameters:

  • string (String)

    Base58-encoded string

Returns:

  • (String)

    decoded binary data

Raises:

  • (ArgumentError)

    if the string is empty or contains invalid Base58 characters



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/bsv/primitives/base58.rb', line 66

def decode(string)
  raise ArgumentError, 'cannot decode empty string' if string.empty?

  # Count leading '1' characters (representing zero bytes)
  leading_ones = 0
  string.each_char { |c| c == ALPHABET[0] ? leading_ones += 1 : break }

  # Convert from base58 to integer
  n = 0
  string.each_char do |c|
    digit = DECODE_MAP[c.ord]
    raise ArgumentError, "invalid Base58 character: #{c.inspect}" if digit == -1

    n = (n * BASE) + digit
  end

  # Convert integer to bytes — C-backed hex round-trip is the fastest pure-Ruby integer→bytes path.
  hex = n.zero? ? '' : n.to_s(16)
  hex = "0#{hex}" if hex.length.odd?
  result = [hex].pack('H*')

  # Prepend zero bytes for leading '1' characters
  (("\x00" * leading_ones) + result).b
end

.encode(bytes) ⇒ String

Encode binary data as a Base58 string.

Leading zero bytes are preserved as ‘1’ characters.

Parameters:

  • bytes (String)

    binary data to encode

Returns:

  • (String)

    Base58-encoded string



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/bsv/primitives/base58.rb', line 37

def encode(bytes)
  return '' if bytes.empty?

  # Count leading zero bytes
  leading_zeros = 0
  bytes.each_byte { |b| b.zero? ? leading_zeros += 1 : break }

  # Convert to big integer and repeatedly divide by 58
  # C-backed hex conversion — 10x faster than pure-Ruby byte shifting; not a porting artefact.
  n = bytes.unpack1('H*').to_i(16)
  result = +''
  while n.positive?
    n, remainder = n.divmod(BASE)
    result << ALPHABET[remainder]
  end

  # Preserve leading zeros as '1' characters
  result << (ALPHABET[0] * leading_zeros)
  result.reverse!
  result
end