Discussion on best practices for building anyone-can-spend automata contracts and covenants

Introduction

BitcoinScript and the Bitcoin VM enables a powerful set of stack-based logic to control how (or when) value is spent. In addition, higher-level scripting languages (CashScript & spedn) lower the technical bar for writing contracts that transpile to BitcoinScript. However, the reach of this technology remains relatively limited to a small set of blockchain application developers in the UTXO space, and while it makes sense to develop a complex contract (or set of contracts) for a large application or service (Anyhedge/Jedex), there is a large swath of bespoke financial transactions people typically conduct with a single trusted party (themselves, people they give money to), with zero or very limited & arguments at the time of execution.

Some of this core-traditional market demand is addressed by Electron-cash plugins. (Such as the Last Will Plugin by @Licho , or the Hodl Plugin by Pat.)

However, this approach leads to a number of UX challenges:

  • To create or claim the script, users must install both electron-cash and the plugin.
  • The relevant user(s) must remember the contract existence and execute the unlocking script.
  • In addition to seed phrases, the plugin must store, or derive, the data necessary to unlock the contract, or this data must be transmitted to the receiving parties.

In addition to these issues for simple cases, the actual way people “hard-code” the spending of value with soft-finance contracts and wills is usually a bit more complex than simply hodling, or willing everything to one heir.

Entities often divide value in shares or lays. Parents want to divide in equal fractions. People want equal payments over time. Or, they want to gift money in powerful ways far into the future.

People want to “mine” bitcoin in the browser, and someone will always want free bitcoin for doing little or nothing.

So rather than create a dozen plugins, or large bespoke contracts for everyone, perhaps there’s a better way, today…

Re: Introspection; Hello, Automata!

Since the introduction of introspection, unlocking script has access to both total value and individual input/output values in BitcoinScript. So some of the previous pain points can be resolved by:

  • Publishing the parameters of a “well known” script
  • Accounting for a small fee available to the party (anybody) that executes the script.

The idea being, if the ability to create the unlocking script is well known and incentivized, an economy can develop for the execution of a set of scripts by anyone, for a fee.

  • Users receiving funds may not actually have to call the script.
  • Users can publish the unlocking script, and someone may want to execute it.
  • Since some scripts may not require access to private keys, the security requirements for the user are greatly reduced, as most of the risk is off-loaded to the contract. Which is to say, it may be appropriate to use in a sketchy context, like a web-browser.

In the example of a time lock script, if a time locking script were written to leave some small fee (say 1500 sats) as extra output, then someone could call that script to claim the fee. The funding transaction, or any transaction, could publish the serialized parameters referring to the script in an OP_RETURN, and some party could be paid to track and execute it in a timely manner. Failing that, the original user would have a record (on-chain) and could call the contract over a web page, service or cli, without installing desktop software or plugins (because it doesn’t need keys).

With these simple contracts, more complex functionality could be emulated by chaining the output of contracts into others. In the case of a Will contract, it’s quite possible that someone has more than one heir. It also isn’t always appropriate to give people of a certain age an entire inheritance, or to give an inheritance all at once.

A simpler example: just divide input(s) in half*

For a concrete example of an anyone-can-spend contract, suppose we implement the above contract to divide any input across two addresses, mostly equally.

First, lets account for both the mining fee and the executor fee with an executorAllowance. This is the total amount outputs are reduced by, and it is presumed that the executor has accounted for the required miner fees out of their own end. Without loops, it’s expedient to have the divisor hardcoded as input to the contract. The function execute() takes no inputs and simply assures the receipts are correct, and the amounts exceed half the initial total, minus the executorAllowance.

pragma cashscript >= 0.7.0;

  contract Divide(
      // allowance for party executing the contract
      int executorAllowance,
      // number of outputs receiving payout
      int divisor,

      // for each beneficiary, take the LockingBytecode as input
      bytes r0LockingBytecode,
      bytes r1LockingBytecode
  ) {
      function execute() {  // no args!

        // distributes to each output in order
        require(tx.outputs[0].lockingBytecode == r0LockingBytecode);
        require(tx.outputs[1].lockingBytecode == r1LockingBytecode);

        // Get the total value of inputs
        int currentValue = tx.inputs[this.activeInputIndex].value;

        // Total value paid to beneficiaries, minus executor allowance
        int distributedValue = currentValue - executorAllowance;

        // Value paid to each beneficiary
        int distribution = distributedValue / divisor;

        // each output must be greater or equal to the distribution amount
        require(tx.outputs[0].value >= distribution);
        require(tx.outputs[1].value >= distribution);
      }
  }

So the trick here is that outputs beyond the first two are unrestricted. So anyone who submitted a transaction could claim the 1200 sats, minus the miner fee. An executor could call the CashScript function above and pay themselves by tacking on their address and desired payment to the third output, like so:

    // ... instantiate the CashScript contract, from artifact, parameters, provider, 
    // ... estimate fee, calculate installment, etc. 
    // ...
    let fn = divideContract.functions["execute"]();
    let to = []
    to.push({ to: heirs[0].getDepositAddress(), amount: installment  })
    to.push({ to: heirs[1].getDepositAddress(), amount: installment  })
    
    to.push(
      { to: executor.getDepositAddress(), amount: exFee - estimatedMinerFee - 2 }
    )
    
    await fn
      .to(to)
      .withoutChange()
      .send();

Note the above example does not strictly require the outputs be equal, nor exactly the minimum share.

Serialized examples: divide by four

In the case of a Divide contract among four inputs instead of two, the contract may be serialized for a keystore database (in a string of delimited text) as follows:

D,     // Type (Divide)
1,     // version
1200,  // executor/miner fee
a914cb75efdfd51ba11b81f2c7986dca8c3b78174f2487, // receipt 1
a9144ea5ce167a9656a601790eabca1e1165b1de9f1487, // receipt 2
a91496e199d7ea23fb779f5764b97196824002ef811a87 ,// receipt 3
a91405b471c045278ddc670a19415a47005929eb37c987, // receipt 4
a9141538fb59b073fbad92490eae961a12d542872f9a87  // contract lockingBytecode

As an OP_RETURN, the same announced contract might look like so:

6a           // 106 OP_RETURN
04 7574786f  // random protocol identifier
01 44        // Divide
01 01        // Version 1
02 b004      // 1200 sats allowance
17 a914cb75efdfd51ba11b81f2c7986dca8c3b78174f2487 // r0
17 a9144ea5ce167a9656a601790eabca1e1165b1de9f1487 // r1
17 a91496e199d7ea23fb779f5764b97196824002ef811a87 // r2
17 a91405b471c045278ddc670a19415a47005929eb37c987 // r3
17 a9141538fb59b073fbad92490eae961a12d542872f9a87 // check, locking bytecode

An interested party could check the unlocking bytecode here and execute the contract by dividing inputs (if any are available) while paying themselves the executor fee.

Complex functionality by chaining simple contracts

As a typical example of what someone may want, suppose instead of one heir, there are two heirs (age 12 and 15) who may, or may not, be very good with money, (we can’t tell yet). While it should be possible to create a set of Locking and Will contracts to pay them out over time in the absence of the contract funder, that may get somewhat tedious.

Let’s approximate the functionality of a Will and some Trusts with BitcoinScript. Here’s what we use instead:

  • A Will contract, which:
    • pays to itself or the funder if signed by the funder signature in some timeframe
    • or pays to a divide contract.
  • The Divide contract splits any input equally (roughly) among two addresses,
  • One is a Lock contract for 8 years, the other Locks for 5 years.
  • Each Lock contract then pays a Perpetuity, (or Annuity), which distributes funds to an heirs address (at a later time) over some interval of about 15 years.

The same functionality could be approximated by using ~30 Will contracts paying ~30 Lock contracts, but it’s simpler to use 1 Will, 1 Divide, 2 Time Locks and 2 Time Distribution contracts.

In pseudo yml code, as follows:

Will:
    exFee: 3000                             // 3k fee, after expiration 
    period: 24000                           // six months
    signature: <pk, sig>                    // principal's keys
    payTo:  
        Divide:
            fee: 3000                       // 3k fee
            payTo:                          // dividing remainder
                - Lock:
                    fee: 3000               // 3k fee
                    period: 384000          // 8 years
                    payTo:
                        Annuity: 
                            fee: 2000       // 2k fee, for
                            period: 4000    // monthly payments
                            amount: 100000  // of 100k
                            payTo: lock1    // to heir 1
                - Lock: 
                    fee: 3000               // 3k fee, for
                    period: 240000          // Lock for 5 years
                    payTo:              
                        Annuity:
                            fee: 2000        // 2k fee, for
                            period: 4000     // monthly payments
                            amount: 100000:  // of 100k
                            payTo: lock2            // to heir 2

Considerations & Observations

With a set of small scripts using introspection (that are reviewed and thoroughly tested) a larger userbase could gain access to more complex functionality with more reliability than if they had to commission or implement a single large script and test it on their own dime/time.

However, there are some new considerations for the safety and generally tidiness that should probably have wider consideration here.

Collision Attacks

Due to the possibility of collision attacks discussed previously here:

at this juncture, it should be assumed 1) that contracts that accept parameters for unlocking inputs are vulnerable to collision attacks, 2) that the output unlocking bytecode will be public and long-lasting, as well as 3) all data to redeem the script.

Without something like CHIP-2022-05 Pay-to-Script-Hash-32 P2SH32, the functionality of these anyone-can-spend contracts is (and should be) deliberately limited for security reasons.

Therefore:

  • Variables passed to unlocking script should be minimized to the extent possible, ideally zero.
  • Spending conditions should be limited (instead) to the value, number and locking bytecode of transaction inputs and outputs.

Announcing Contracts

Bitcoin Cash sub-protocols have used a four-byte prefix in OP_RETURN identifiers in the past. So known sets of contracts/covenants can be developed to utilize specific OP_RETURN prefixes.

In addition, as a postfix to the OP_RETURN:

  • It may be very expedient for users calling various contracts, (both as a checksum and to check balances) to have the locking bytecode of the contract (P2SH) in the contract announcement. It allows the “would be” caller to determine:
    • If the contract is funded, without instantiating the contract.
    • If they instantiated a funded contract correctly on their side

As some controversy in the memo/member protocol(s) has shown, it may be best to assume that someone will extend the protocol in ways initial developers don’t wish to accommodate, so it might be best not to get married to a random four byte code.

On announcing contracts v. covenants:

  • A Contract with fixed parameters only needs to be announced once,
  • A Covenant with changing parameters, can be made self-announcing. However:
    • If a four byte OP_RETURN space for a protocol is “claimed”, self-announcing covenants shouldn’t share space with fixed contracts. Because covenants may be announced every block, where covenants announcements are used once and discarded.

P2PKH v. PSH v. LockingBytecode

  • If contracts are to be composable, they should always favor asserting inputs & outputs as lockingBytecode over a PublicKeyHash or PublicScriptHash.
  • i.e. If a contract (P2SH) address is passed as output to another contract which enforces or assumes P2PKH outputs, it bricks those inputs forever.

OP_Return length

  • Presently, OP_RETURN is limited to 223 bytes, so if lockingBytecodes are stored in the contract announcement, the number of addresses involved is limited to five (or four plus the contract lockingBytecode shortcut/checksum).
  • Current OP_RETURN size shouldn’t be a limitation with current signature sizes. I.e. If inputs should, be divided by 4, they can be divided to 64 in three more steps―it is not worth a consensus change.

Cleanup the dust as you go

  • If a contract is not configured to utilize fractional satoshis and an ever decreasing DUST_LIMIT, it should NOT leave unspendable outputs on the contracts.
  • Ephemeral contracts should be constructed with a dust collection mechanism and leave zero dust when funds are exhausted, even if those inputs are far below thresholds for the contract.

Dust attacks, selective inputs

  • It will be trivial with a small amount of bitcoin to dust a contract with tiny inputs such that the transaction fee required will exceed the allowance given, so software around protocols might consider enabling UTXO selection and dust collection by default, from launch.
  • Likewise, it should always be assumed, when writing contracts, that the executor may call either all inputs, individual inputs, or some mix of both.
    • As an example: a perpetuity paying daily into a two party divide contract may either be executed as inputs as a lump sum, or as 356 times on individual inputs for the year. If the daily input amount is low relative to the fee on the divide contract, the difference between calling individual inputs and one lump input could be significant.

Insecurity on the network

With the nature of an anyone-can-spend contract, or the unrestricted output component, any party receiving the unlocking transaction may choose to replace the anyone-can-spend output with their own. Alternatively, the mempool can be monitored for anyone-can-spend transactions and any attempt to claim it may be “raced” as a double spend.

In the latter case, a party who put in the effort to query, store and execute transactions in a timely manner could likely be thwarted by someone simply trying to double spend their small reward.

Finally, a miner who implemented software to spend all known anyone-can-outputs in blocks they mine would virtually eliminate any incentive for others to attempt to profit from submitting transactions (in blocks they mine with their hash power).

For the initial funder and ultimate recipient of the contract funds, it’s immaterial whether the reward for executing the contract goes to someone who diligently submitted it, a double-spend cheater, or a sat pinching miner. The end result, that the contract is executed, is the same regardless of the party taking a small fee.

Malleability

Similar to the above venerability to the executor outputs, it may be theoretically possible to generate outputs after the fact which are indistinguishable from the original using immutable checks using block headers, but are in fact not the original executor, rather a different address. However, at the present time, with such low fees, it seems unlikely that generating collisions would be profitable, and every spend of a UTXO would add to the cost of creating a spurious output and having it accepted on the network.

Conclusion

The above topic outlines one view of how to go about enabling complex financial transactions from a set of well-known anyone-can-spend contracts. However these contracts may be disruptive to the network, have unconsidered security considerations or long term maintenance overhead that hasn’t been considered.

So the general idea and some early pain-points are laid out to begin a broader discussion.

2 Likes

Some notes:

  • With introspection you can easily offload the variable part of a contract to a “sidecar” output, so that “main” address will be constant, I use that pattern here. Caveat is that the contract then becomes 2-output contract, which complicates interacting with it since software would have to be designed for that. CashTokens will further simplify this because they’ll let you drop some checks and ID+commitment will be proof enough that the sidecar code is executed against correct inputs/outputs.

  • Once activated CashTokens will make it possible to extract the variable part into the NFT “commitment” field, making it possible to separate contract state from contract code, while keeping it all in 1 output.

  • Using introspection it is possible to emulate OP_EVAL (example), make more complex multi-output contracts, and work around the per-input Script VM limits, but you still can’t do operations that require a bigger program “memory” (like, you still can’t compute a hash of a blob bigger than 520b, and we don’t have lower level hash functions that would let us continue hashing from some intermediate state)

3 Likes

I like the idea of hiding functions (like a Will replacement or refund) in a sidecar.

With the simple patterns above, (lock, divide, etc) I’m finding most “one job” KISS contracts fit nicely in current Script limits, but it’s actually the OP_RETURN limit that determines how complex a setup could be recorded on chain with all the predefined variables. And that may get worse, if P2SH32 locking bytecode is longer.

As is, without CT, I think there are a lot of powerful contracts that are really quite small.

1 Like

You can’t execute it if it lives in OP_RETURN. We don’t have the “real” OP_EVAL. To get executed it must come as “sidecar” P2SH where redeem script will be provided by a data push in unlocking script, which means your app needs to worry about preparing the output and then referencing it inside the TX which authenticates the input against the hash in the “main” contract.

For data storage, you can also use a side-car input and have a full 520b blob. Plenty of options: https://www.researchgate.net/publication/345644650_Data_Insertion_in_Bitcoin's_Blockchain

I’m beginning to feel this could have been an Applications Layer Informational CHIP.

It’s interesting to see all the ways people stored data in the early days, but I think OP_RETURN is the best (and polite) place for the case described above. I’m proposing it could be used to say “This is a time Lock Contract (version #) and it can be sent to this address after X blocks.” where that contract lives in application software external to the network that a user can submit transactions from.

However, since the thing we’re storing is the unlocking script, it’s actually really efficient to put that on the blockchain. It should also be possible to say, “use the real transaction X as an unlocking template, popping the old parameters and substituting these instead”, with an OP_CODE protocol. Then if someone wants to create a different set of automata contracts, it should be possible to step them forward from more generic software if everyone can see, “this is the template transaction, and these are the different parameters”.

1 Like