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



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

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



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

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



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

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
  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
# 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
  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