Module: Tapyrus::PSTT

Defined in:
lib/tapyrus/pstt.rb,
lib/tapyrus/pstt/tx.rb,
lib/tapyrus/pstt/input.rb,
lib/tapyrus/pstt/output.rb,
lib/tapyrus/pstt/proprietary.rb,
lib/tapyrus/pstt/key_origin_info.rb

Overview

Partially Signed Tapyrus Transaction (PSTT) defined by TIP-0174.

A PSTT holds a not-yet-signed Tapyrus transaction together with the metadata signers need. The data model follows BIP-370: the transaction is represented by per-input and per-output fields, so inputs and outputs can be added after creation.

Defined Under Namespace

Modules: GlobalTypes, InputTypes, OutputTypes, TxModifiable Classes: Error, Input, KeyOriginInfo, KeyValue, Output, Proprietary, Tx

Constant Summary collapse

MAGIC =

The magic bytes which begin every PSTT. ASCII "pstt" followed by the separator 0xFF.

"70737474ff".htb
VERSION =

The only version this TIP defines.

0
LOCKTIME_THRESHOLD =

The boundary between height-based and time-based locktime.

500_000_000
SIGHASH_BASE_TYPES =

The sighash types TIP-0174 defines: SIGHASH_ALL, SIGHASH_NONE and SIGHASH_SINGLE, each optionally combined with SIGHASH_ANYONECANPAY.

[SIGHASH_TYPE[:all], SIGHASH_TYPE[:none], SIGHASH_TYPE[:single]].freeze
SIGHASH_TYPES =
(SIGHASH_BASE_TYPES + SIGHASH_BASE_TYPES.map { |t| t | SIGHASH_TYPE[:anyonecanpay] }).freeze

Class Method Summary collapse

Class Method Details

.order_records(records, order) ⇒ Array[Tapyrus::PSTT::KeyValue]

Put records back into the order the map was parsed in, so that parsing a PSTT and serializing it again yields the same bytes. TIP-0174 prescribes no order - either order is a valid PSTT carrying the same records - but a byte-stable round trip lets a caller hash, cache or diff a PSTT without normalizing it first. A record which was not in the parsed map, such as a signature collected since, follows the records which were, in the order this implementation generates them. A map which was not parsed is emitted in ascending order of the complete key.

Parameters:

  • records (Array[Tapyrus::PSTT::KeyValue])

    the records of the map.

  • order (Array[String])

    the complete keys of the map this PSTT was parsed from.

Returns:



189
190
191
192
193
194
# File 'lib/tapyrus/pstt.rb', line 189

def order_records(records, order)
  return records.sort_by { |r| [r.type, r.keydata] } if order.nil? || order.empty?
  rank = {}
  order.each_with_index { |key, i| rank[key] ||= i }
  records.each_with_index.sort_by { |record, i| [rank.fetch(record.key, order.size + i), i] }.map(&:first)
end

.parse_field(name) ⇒ Object

Parse the value or the key data of a record with block, and report any failure as a Tapyrus::PSTT::Error. The records of a PSTT come from another party, so a malformed one must not surface as an exception of the parser the field happens to be built on.

Parameters:

  • name (String)

    the name of the field, used in the error message.

Raises:



267
268
269
270
271
272
273
# File 'lib/tapyrus/pstt.rb', line 267

def parse_field(name)
  yield
rescue Error
  raise
rescue StandardError => e
  raise Error, "#{name} is malformed. #{e.message}"
end

.parse_map(buf) ⇒ Array[Tapyrus::PSTT::KeyValue]

Parse a single map, which is a sequence of key-value records terminated by 0x00.

Parameters:

  • buf (StringIO)

    a buffer.

Returns:

Raises:



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/tapyrus/pstt.rb', line 152

def parse_map(buf)
  records = []
  keys = {}
  loop do
    raise Error, "PSTT map is not terminated." if buf.eof?
    key_len = read_compact_size(buf)
    break if key_len.zero? # the 0x00 separator
    key = read_bytes(buf, key_len)
    key_buf = StringIO.new(key)
    type = read_compact_size(key_buf)
    keydata = key_buf.read || ""
    raise Error, "PSTT map contains a duplicated key." if keys.key?(key)
    keys[key] = true
    value_len = read_compact_size(buf)
    records << KeyValue.new(type, keydata, read_bytes(buf, value_len))
  end
  records
end

.read_bytes(buf, size) ⇒ String

Read exactly size bytes from buf.

Parameters:

  • buf (StringIO)

    a buffer.

  • size (Integer)

    the byte size to be read.

Returns:

  • (String)

    the read data with binary format.

Raises:



142
143
144
145
146
# File 'lib/tapyrus/pstt.rb', line 142

def read_bytes(buf, size)
  data = buf.read(size)
  raise Error, "PSTT payload is truncated." if data.nil? || data.bytesize != size
  data
end

.read_compact_size(buf) ⇒ Integer

Read a compact size unsigned integer from buf.

Every compact size integer of the format must be minimally encoded. TIP-0174 states the requirement for , and holding the whole container to it is what makes the byte representation of a PSTT unique: two encodings of the same length would otherwise produce two different byte strings carrying the same records. It also removes the need to treat the map separator specially, since a non-minimally encoded 0 is rejected here.

Parameters:

  • buf (StringIO)

    a buffer.

Returns:

Raises:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/tapyrus/pstt.rb', line 119

def read_compact_size(buf)
  prefix = read_bytes(buf, 1).unpack1("C")
  case prefix
  when 0xfd
    value = read_bytes(buf, 2).unpack1("v")
    raise Error, "compact size is not minimally encoded." if value < 0xfd
  when 0xfe
    value = read_bytes(buf, 4).unpack1("V")
    raise Error, "compact size is not minimally encoded." if value <= 0xffff
  when 0xff
    value = read_bytes(buf, 8).unpack1("Q<")
    raise Error, "compact size is not minimally encoded." if value <= 0xffffffff
  else
    value = prefix
  end
  value
end

.read_count(record) ⇒ Object

Read a compact size unsigned integer from the value of record.

Raises:



255
256
257
258
259
260
# File 'lib/tapyrus/pstt.rb', line 255

def read_count(record)
  buf = StringIO.new(record.value)
  count = read_compact_size(buf)
  raise Error, format("value for type 0x%02x is not a compact size uint.", record.type) unless buf.eof?
  count
end

.read_i32(record) ⇒ Object

Read a 32-bit little endian signed integer from the value of record.



237
238
239
240
# File 'lib/tapyrus/pstt.rb', line 237

def read_i32(record)
  validate_value_size!(record, 4)
  record.value.unpack1("l<")
end

.read_i64(record) ⇒ Object

Read a 64-bit little endian signed integer from the value of record.



243
244
245
246
# File 'lib/tapyrus/pstt.rb', line 243

def read_i64(record)
  validate_value_size!(record, 8)
  record.value.unpack1("q<")
end

.read_u32(record) ⇒ Object

Read a 32-bit little endian unsigned integer from the value of record.



231
232
233
234
# File 'lib/tapyrus/pstt.rb', line 231

def read_u32(record)
  validate_value_size!(record, 4)
  record.value.unpack1("V")
end

.read_u8(record) ⇒ Object

Read an 8-bit unsigned integer from the value of record.



249
250
251
252
# File 'lib/tapyrus/pstt.rb', line 249

def read_u8(record)
  validate_value_size!(record, 1)
  record.value.unpack1("C")
end

.serialize_map(records, order = nil) ⇒ String

Serialize records as a map.

Parameters:

  • records (Array[Tapyrus::PSTT::KeyValue])

    the records of the map.

  • order (Array[String]) (defaults to: nil)

    the complete keys of the map this PSTT was parsed from.

Returns:

  • (String)

    the serialized map with binary format.



175
176
177
# File 'lib/tapyrus/pstt.rb', line 175

def serialize_map(records, order = nil)
  order_records(records, order).map(&:to_payload).join + "\x00".b
end

.validate_amount!(amount) ⇒ Object

Check that amount is within the range Tapyrus allows.

Parameters:

  • amount (Integer)

    an amount of tapy or of tokens.

Raises:



287
288
289
290
291
# File 'lib/tapyrus/pstt.rb', line 287

def validate_amount!(amount)
  unless amount.is_a?(Integer) && amount >= 0 && amount <= MAX_MONEY
    raise Error, "PSTT_OUT_AMOUNT must be between 0 and #{MAX_MONEY}, but #{amount}."
  end
end

.validate_empty_keydata!(record) ⇒ Object

Check that record has no key data, as its field definition requires.

Parameters:

Raises:



199
200
201
# File 'lib/tapyrus/pstt.rb', line 199

def validate_empty_keydata!(record)
  raise Error, format("keydata for type 0x%02x must be empty.", record.type) unless record.keydata.empty?
end

.validate_features!(features) ⇒ Object

Check that features is a positive value. Tapyrus currently defines only 1, but a transaction feature which a later version adds is accepted so that this implementation can carry it through.

Parameters:

  • features (Integer)

    the value of PSTT_GLOBAL_TX_FEATURES.

Raises:



298
299
300
# File 'lib/tapyrus/pstt.rb', line 298

def validate_features!(features)
  raise Error, "PSTT_GLOBAL_TX_FEATURES must be greater than 0, but #{features}." unless features.positive?
end

.validate_height_locktime!(value) ⇒ Object

Check that value is a valid PSTT_IN_REQUIRED_HEIGHT_LOCKTIME.

Raises:



328
329
330
331
332
# File 'lib/tapyrus/pstt.rb', line 328

def validate_height_locktime!(value)
  if value.zero? || value >= LOCKTIME_THRESHOLD
    raise Error, "PSTT_IN_REQUIRED_HEIGHT_LOCKTIME must be greater than 0 and less than #{LOCKTIME_THRESHOLD}."
  end
end

.validate_pubkey_keydata!(record) ⇒ Object

Check that the key data of record is a public key.

Parameters:

Raises:



206
207
208
209
210
211
212
# File 'lib/tapyrus/pstt.rb', line 206

def validate_pubkey_keydata!(record)
  size = record.keydata.bytesize
  unless [Tapyrus::Key::COMPRESSED_PUBLIC_KEY_SIZE, Tapyrus::Key::PUBLIC_KEY_SIZE].include?(size)
    raise Error,
          format("keydata for type 0x%02x must be a 33- or 65-byte public key, but %d bytes.", record.type, size)
  end
end

.validate_sighash_type!(hash_type) ⇒ Object

Check that hash_type is one of the sighash types TIP-0174 defines.

Parameters:

  • hash_type (Integer)

    a sighash type.

Raises:



278
279
280
281
282
# File 'lib/tapyrus/pstt.rb', line 278

def validate_sighash_type!(hash_type)
  unless SIGHASH_TYPES.include?(hash_type)
    raise Error, format("Sighash type 0x%x is not defined by TIP-0174.", hash_type)
  end
end

.validate_time_locktime!(value) ⇒ Object

Check that value is a valid PSTT_IN_REQUIRED_TIME_LOCKTIME.

Raises:



320
321
322
323
324
# File 'lib/tapyrus/pstt.rb', line 320

def validate_time_locktime!(value)
  if value < LOCKTIME_THRESHOLD
    raise Error, "PSTT_IN_REQUIRED_TIME_LOCKTIME must be greater than or equal to #{LOCKTIME_THRESHOLD}."
  end
end

.validate_tx_modifiable!(flags) ⇒ Object

Check that flags sets no reserved bit of PSTT_GLOBAL_TX_MODIFIABLE.

Parameters:

  • flags (Integer)

    the bitfield.

Raises:



305
306
307
308
309
# File 'lib/tapyrus/pstt.rb', line 305

def validate_tx_modifiable!(flags)
  unless (flags & ~TxModifiable::ALL).zero?
    raise Error, "The reserved bits of PSTT_GLOBAL_TX_MODIFIABLE must be 0."
  end
end

.validate_value_size!(record, size) ⇒ Object

Check that the value of record has size bytes.

Parameters:

Raises:



218
219
220
221
222
223
224
225
226
227
228
# File 'lib/tapyrus/pstt.rb', line 218

def validate_value_size!(record, size)
  unless record.value.bytesize == size
    raise Error,
          format(
            "value for type 0x%02x must be %d bytes, but %d bytes.",
            record.type,
            size,
            record.value.bytesize
          )
  end
end

.validate_version!(version) ⇒ Object

Check that version is a version this implementation supports.

Parameters:

  • version (Integer)

    the value of PSTT_GLOBAL_VERSION.

Raises:



314
315
316
# File 'lib/tapyrus/pstt.rb', line 314

def validate_version!(version)
  raise Error, "PSTT version #{version} is not supported." if version > VERSION
end