Skip to content

ShiningRay/bcs-ruby

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BCS - Binary Canonical Serialization for Ruby

Gem Version Build Status

This Ruby gem provides an implementation of Binary Canonical Serialization (BCS), a serialization format used in blockchain applications like Move language. BCS provides canonical (deterministic) serialization across different platforms and languages.

Installation

Add this line to your application's Gemfile:

gem 'bcs'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install bcs

Quick Start

require 'bcs'

# Define a simple struct
Coin = Bcs.struct("Coin", {
  id: Bcs.bytes(32),
  value: Bcs.u64
})

# Serialize data
coin_data = {
  id: Array.new(32, 0),
  value: 1000000
}

# Serialize to bytes
bytes = Coin.serialize(coin_data)
puts "Serialized to #{bytes.length} bytes"

# Deserialize back to original format
deserialized = Coin.deserialize(bytes)
puts deserialized.inspect
# => {:id=>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], :value=>1000000}

Basic Types

BCS supports several primitive types:

Unsigned Integers

# 8-bit, 16-bit, 32-bit, 64-bit, 128-bit, and 256-bit unsigned integers
u8_value = Bcs.u8.serialize(255)
u64_value = Bcs.u64.serialize(18446744073709551615)
u128_value = Bcs.u128.serialize(340282366920938463463374607431768211455)

# Deserialize
Bcs.u8.deserialize(u8_value)     # => 255
Bcs.u64.deserialize(u64_value)   # => 18446744073709551615

Boolean

bool_bytes = Bcs.bool.serialize(true)
Bcs.bool.deserialize(bool_bytes)  # => true

String (UTF-8)

str_bytes = Bcs.string.serialize("Hello, 世界")
Bcs.string.deserialize(str_bytes)  # => "Hello, 世界"

Fixed-Length Bytes

# Exactly 32 bytes
bytes_type = Bcs.bytes(32)
data = Array.new(32, 42)
bytes = bytes_type.serialize(data)
deserialized = bytes_type.deserialize(bytes)

ULEB128 (Unsigned Little Endian Base 128)

uleb_bytes = Bcs.uleb128.serialize(1000000)
Bcs.uleb128.deserialize(uleb_bytes)  # => 1000000

Complex Types

Vector (Dynamic Array)

# Vector of u8 values
vector_type = Bcs.vector(Bcs.u8)
data = [1, 2, 3, 4, 5]
bytes = vector_type.serialize(data)
deserialized = vector_type.deserialize(bytes)
# => [1, 2, 3, 4, 5]

# Vector of strings
string_vector = Bcs.vector(Bcs.string)
words = ["hello", "world", "BCS"]
bytes = string_vector.serialize(words)
deserialized = string_vector.deserialize(bytes)
# => ["hello", "world", "BCS"]

Fixed Array

# Fixed array of 4 u16 values
array_type = Bcs.fixed_array(4, Bcs.u16)
data = [1000, 2000, 3000, 4000]
bytes = array_type.serialize(data)
deserialized = array_type.deserialize(bytes)
# => [1000, 2000, 3000, 4000]

Option (Optional Value)

# Optional string
option_type = Bcs.option(Bcs.string)

# Some value
some_bytes = option_type.serialize("present")
option_type.deserialize(some_bytes)  # => "present"

# None value (nil)
none_bytes = option_type.serialize(nil)
option_type.deserialize(none_bytes)  # => nil

Struct

# Define a Person struct
Person = Bcs.struct("Person", {
  name: Bcs.string,
  age: Bcs.u8,
  email: Bcs.option(Bcs.string),
  addresses: Bcs.vector(Bcs.struct("Address", {
    street: Bcs.string,
    city: Bcs.string,
    zip_code: Bcs.string
  }))
})

person_data = {
  name: "Alice Johnson",
  age: 30,
  email: "alice@example.com",
  addresses: [
    {
      street: "123 Main St",
      city: "San Francisco",
      zip_code: "94105"
    },
    {
      street: "456 Oak Ave",
      city: "New York",
      zip_code: "10001"
    }
  ]
}

bytes = Person.serialize(person_data)
deserialized = Person.deserialize(bytes)
puts deserialized.inspect

Enum

# Define an enum with different variants
Status = Bcs.enum("Status", {
  pending: nil,                    # Unit variant
  approved: Bcs.string,            # Variant with data
  rejected: Bcs.struct("Reason", { # Variant with complex data
    code: Bcs.u16,
    message: Bcs.string
  })
})

# Different enum values
pending_status = { pending: true }
approved_status = { approved: "John Doe" }
rejected_status = { rejected: { code: 404, message: "Not Found" } }

# Serialize and deserialize
pending_bytes = Status.serialize(pending_status)
approved_bytes = Status.serialize(approved_status)
rejected_bytes = Status.serialize(rejected_status)

Status.deserialize(pending_bytes)   # => {:pending=>true}
Status.deserialize(approved_bytes)  # => {:approved=>"John Doe"}
Status.deserialize(rejected_bytes)  # => {:rejected=>{:code=>404, :message=>"Not Found"}}

Tuple

# Define a tuple type
tuple_type = Bcs.tuple([Bcs.u32, Bcs.string, Bcs.bool])
tuple_data = [42, "answer", true]

bytes = tuple_type.serialize(tuple_data)
deserialized = tuple_type.deserialize(bytes)
# => [42, "answer", true]

Map

# Map from strings to u64 values
balance_type = Bcs.map(Bcs.string, Bcs.u64)
balances = {
  "BTC" => 1000000,
  "ETH" => 2000000000000000000,
  "USDC" => 500000000
}

bytes = balance_type.serialize(balances)
deserialized = balance_type.deserialize(bytes)
# => {"BTC"=>1000000, "ETH"=>2000000000000000000, "USDC"=>500000000}

Type Transformations

You can transform types to handle different input/output formats:

# Transform hex string to 32-byte array
Address = Bcs.bytes(32).transform(
  input_proc: ->(hex_str) { [hex_str.gsub(/^0x/, '')].pack('H*') },
  output_proc: ->(bytes) { "0x#{bytes.unpack('H*').first}" }
)

# Use hex strings directly
hex_address = "0x" + "a" * 64
bytes = Address.serialize(hex_address)
deserialized = Address.deserialize(bytes)
puts deserialized  # => "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

# Transform timestamps between seconds and milliseconds
TimestampMs = Bcs.u64.transform(
  input_proc: ->(seconds) { seconds * 1000 },
  output_proc: ->(milliseconds) { milliseconds / 1000 }
)

timestamp_seconds = 1640995200  # Unix timestamp
bytes = TimestampMs.serialize(timestamp_seconds)
deserialized = TimestampMs.deserialize(bytes)
puts deserialized  # => 1640995200

Encoding Formats

BCS supports multiple output formats:

Coin = Bcs.struct("Coin", {
  id: Bcs.bytes(32),
  value: Bcs.u64
})

coin_data = {
  id: Array.new(32, 0),
  value: 1000000
}

# Raw bytes
bytes = Coin.serialize(coin_data)

# Hex string
hex_string = Coin.to_hex(coin_data)
puts hex_string  # => "000000000000000000000000000000000000000000000000000000000000000040420f0000000000"

# Base64 string
base64_string = Coin.to_base64(coin_data)
puts base64_string  # => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEECDgAAAAA="

# Decode from different formats
from_hex = Coin.from_hex(hex_string)
from_base64 = Coin.from_base64(base64_string)

Blockchain Example

Here's a more complex example for a cryptocurrency transaction:

# Address type (32 bytes as hex string)
Address = Bcs.bytes(32).transform(
  input_proc: ->(hex_str) { [hex_str.gsub(/^0x/, '')].pack('H*') },
  output_proc: ->(bytes) { "0x#{bytes.unpack('H*').first}" }
)

# Coin type
Coin = Bcs.struct("Coin", {
  id: Bcs.bytes(32),
  value: Bcs.u64
})

# Transaction type
Transaction = Bcs.struct("Transaction", {
  sender: Address,
  receiver: Address,
  amount: Bcs.u64,
  fee: Bcs.u64,
  coins: Bcs.vector(Coin),
  timestamp: Bcs.u64
})

# Create a transaction
transaction = {
  sender: "0x" + "1" * 64,
  receiver: "0x" + "2" * 64,
  amount: 1000000000,
  fee: 1000000,
  coins: [
    { id: ["0x" + "3" * 64].pack('H*'), value: 500000000 },
    { id: ["0x" + "4" * 64].pack('H*'), value: 500000001 }
  ],
  timestamp: Time.now.to_i
}

# Serialize transaction
tx_bytes = Transaction.serialize(transaction)
puts "Transaction size: #{tx_bytes.length} bytes"

# Deserialize transaction
deserialized_tx = Transaction.deserialize(tx_bytes)
puts deserialized_tx.inspect

Error Handling

BCS provides comprehensive error handling:

begin
  Bcs.u8.serialize(256)  # Too large for u8
rescue Bcs::ValidationError => e
  puts "Validation error: #{e.message}"
end

begin
  Bcs.u32.deserialize("short")  # Not enough bytes
rescue Bcs::DeserializationError => e
  puts "Deserialization error: #{e.message}"
end

begin
  Bcs.bool.serialize("not_boolean")  # Wrong type
rescue Bcs::ValidationError => e
  puts "Validation error: #{e.message}"
end

Performance

BCS is designed for high performance:

# Large data handling
large_array = Array.new(10000) { |i| i % 256 }
vector_type = Bcs.vector(Bcs.u8)

start_time = Time.now
bytes = vector_type.serialize(large_array)
serialize_time = Time.now - start_time

start_time = Time.now
deserialized = vector_type.deserialize(bytes)
deserialize_time = Time.now - start_time

puts "Serialize time: #{serialize_time}s"
puts "Deserialize time: #{deserialize_time}s"
puts "Data size: #{bytes.length} bytes"

Comparison with TypeScript BCS

This Ruby implementation is compatible with the TypeScript BCS library. Data serialized with one can be deserialized with the other, ensuring cross-platform compatibility.

Feature TypeScript BCS Ruby BCS
Basic Types
Complex Types
Transformations
Type Inference ⚠️ (Dynamic typing)
Hex/Base64 Support
Error Handling

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a Pull Request

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

License

This gem is available as open source under the terms of the Apache-2.0 License.

Acknowledgments

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages