Class: BSV::Script::PushDropTemplate

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/script/push_drop_template.rb

Overview

Wallet-integrated PushDrop template for any protocol.

Generalises the pattern used by Overlay::AdminTokenTemplate —derives a locking key from the wallet, optionally signs the data fields, and wraps everything in a PushDrop script backed by a P2PKH condition.

Note: P2PKH vs P2PK

This template uses P2PKH as the underlying spending condition. The Overlay::AdminTokenTemplate uses P2PK (matching the TS/Go SDKs’ overlay admin token convention). The two lock types are not interchangeable — tokens locked by one cannot be unlocked by the other.

PushDrop scripts embed arbitrary token data inline in a spendable output:

<field0> <field1> ... <fieldN> [OP_2DROP...] [OP_DROP?]
OP_DUP OP_HASH160 <hash160(derived_pubkey)> OP_EQUALVERIFY OP_CHECKSIG

When include_signature: true (the default), an ECDSA signature over the concatenation of all fields is appended as a final field. This authenticates the token at creation time using the same derived key.

Security note: counterparty: ‘anyone’ tokens

When counterparty is ‘anyone’, the locking key is derived from the secp256k1 generator point G (PrivateKey(1)). This is a publicly known scalar, meaning:

  1. The output is **publicly spendable** — any party can sign with PrivateKey(1) and spend the token. This is by design for overlay tokens where public revocability is desired.

  2. The field signature (+include_signature: true+) provides **no authenticity guarantee** — anyone can produce a valid signature with the known key. Rely on higher-level mechanisms (e.g. certificate keyrings from prove_certificate) for field-level integrity.

Examples:

Lock a token

wallet   = BSV::Wallet::ProtoWallet.new(private_key)
template = BSV::Script::PushDropTemplate.new(wallet:)
script   = template.lock(
  fields:       ['hello'.b, 'world'.b],
  protocol_id:  [1, 'my-protocol'],
  key_id:       '1',
  counterparty: 'self'
)
script.pushdrop? #=> true

Unlock a token

unlocker = template.unlock(
  protocol_id:  [1, 'my-protocol'],
  key_id:       '1',
  counterparty: 'self'
)
input.unlocking_script_template = unlocker

Defined Under Namespace

Classes: Unlocker

Constant Summary collapse

GENERATOR_PUBKEY_HEX =

Public key for PrivateKey(1) — the generator point G. Used when counterparty: ‘anyone’ so any party can verify.

'0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'

Instance Method Summary collapse

Constructor Details

#initialize(wallet:, originator: nil) ⇒ PushDropTemplate

Returns a new instance of PushDropTemplate.

Parameters:

  • wallet (#get_public_key, #create_signature)

    BRC-100 wallet interface

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

    optional FQDN of the originating application



138
139
140
141
# File 'lib/bsv/script/push_drop_template.rb', line 138

def initialize(wallet:, originator: nil)
  @wallet = wallet
  @originator = originator
end

Instance Method Details

#lock(fields:, protocol_id:, key_id:, counterparty:, include_signature: true, lock_position: :before) ⇒ BSV::Script::Script

Create a PushDrop locking script for the given data fields.

Derives a public key from the wallet using the supplied protocol parameters, then builds a P2PKH locking condition from it. When include_signature: true (the default), signs the concatenation of all fields and appends the DER signature as an additional field.

When counterparty is ‘anyone’, the generator point (PrivateKey(1) public key) is used directly as the locking key. This is the convention for tokens that any party can verify.

The lock_position parameter controls where the P2PKH locking condition is placed relative to the data fields. Defaults to :before (lock first, then fields and drops), matching the ts-sdk convention.

**Breaking change (v0.9):** the default changed from :after to :before. Callers that relied on the old layout must pass lock_position: :after.

Parameters:

  • fields (Array<String>)

    data payloads to embed (binary strings)

  • protocol_id (Array)

    two-element [security_level, protocol_name]

  • key_id (String)

    key identifier

  • counterparty (String)

    ‘self’, ‘anyone’, or a hex public key

  • include_signature (Boolean) (defaults to: true)

    whether to append an ECDSA field signature

  • lock_position (Symbol) (defaults to: :before)

    :before (default) or :after

Returns:

Raises:

  • (ArgumentError)

    if fields is empty



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/bsv/script/push_drop_template.rb', line 169

def lock(fields:, protocol_id:, key_id:, counterparty:, include_signature: true, lock_position: :before)
  raise ArgumentError, 'fields must not be empty' if fields.empty?

  # When counterparty is 'anyone', use the generator point directly as the
  # locking key — this is the "anyone can verify" convention.
  pubkey_bytes = if counterparty == 'anyone'
                   [GENERATOR_PUBKEY_HEX].pack('H*')
                 else
                   pub_args = { protocol_id: protocol_id, key_id: key_id, counterparty: counterparty }
                   orig_kw = @originator ? { originator: @originator } : {}
                   pub_result = @wallet.get_public_key(pub_args, **orig_kw)
                   [pub_result[:public_key]].pack('H*')
                 end

  # Build all fields, optionally appending a signature over their concatenation
  all_fields = fields.map(&:b)

  if include_signature
    data_to_sign = all_fields.reduce(''.b) { |acc, f| acc + f }.unpack('C*')
    sig_args = { data: data_to_sign, protocol_id: protocol_id, key_id: key_id, counterparty: counterparty }
    orig_kw = @originator ? { originator: @originator } : {}
    sig_result = @wallet.create_signature(sig_args, **orig_kw)
    all_fields << sig_result[:signature].pack('C*')
  end

  # Build P2PKH lock from the derived pubkey's Hash160
  pubkey_hash = BSV::Primitives::Digest.hash160(pubkey_bytes)
  p2pkh_lock = BSV::Script::Script.p2pkh_lock(pubkey_hash)

  BSV::Script::Script.pushdrop_lock(all_fields, p2pkh_lock, lock_position: lock_position)
end

#unlock(protocol_id:, key_id:, counterparty:) ⇒ Unlocker

Return an unlocker for spending a PushDrop token output.

The returned Unlocker follows the unlocking template interface and can be assigned to an input’s unlocking_script_template.

Parameters:

  • protocol_id (Array)

    two-element [security_level, protocol_name]

  • key_id (String)

    key identifier

  • counterparty (String)

    ‘self’, ‘anyone’, or a hex public key

Returns:

  • (Unlocker)

    object with #sign and #estimated_length



210
211
212
# File 'lib/bsv/script/push_drop_template.rb', line 210

def unlock(protocol_id:, key_id:, counterparty:)
  Unlocker.new(@wallet, protocol_id, key_id, counterparty, @originator)
end