lbryio / proposals

Discussion of large projects
1 stars 0 forks source link

[Proof of Purchase] New buy/sell ops #6

Open eukreign opened 5 years ago

eukreign commented 5 years ago

Initial issue and discussion: https://github.com/lbryio/lbrycrd/issues/184

Problem Statement

One of the core features of LBRY is buying and selling content. There needs to be first-class support for this activity at the blockchain level so that anyone building services that stream paid content, provide digital rights management or otherwise make purchsed conent available to users can quickly and efficiently verify that a specific user (public key) has access to a specific piece of content (claim_id and optionally claim_version).

Requirements

Use Cases

Solution No. 1

Introduce four new opcodes to the lbrycrd scripting language which together address all of the requirements:

OP_SELL_CLAIM = 0xb8 Meta data opcode representing a claim being available for sale. OP_BUY_CLAIM = 0xb9 Meta data opcode, when accepted into blockchain, represents a proof of purchase. OP_PRICE_PAID = 0xb0 New value opcode which pops the amount paid onto the stack. OP_CLAIM_INTEGER_VALUE = 0xb1 New value opcode which pops the payload of a referenced claim onto the stack.

The two new metada opcodes each form new transaction types following the same structural pattern as the existing CLAIM, UPDATE and SUPPORT transaction types:

META_OP_CODE <value_1> <value_n>... OP_HASH160 <redeem_script_hash> OP_EQUAL

The new OP_PRICE_PAID opcode works in conjunction with a comparison opcode to verify that the price paid is correct, eg:

OP_PRICE_PAID 1000000 OP_GREATERTHANOREQUAL OP_VERIFY

Will check that the buyer provided at least 1000000 dewies in the purchase output.

The new OP_CLAIM_INTEGER_VALUE opcode works as a multiplier for the base price, eg:

OP_PRICE_PAID <claim_id> OP_CLAIM_INTEGER_VALUE 10 OP_MUL OP_GREATERTHANOREQUAL OP_VERIFY

Will replace <claim_id> OP_CLAIM_INTEGER_VALUE with the integer payload of the referenced claim and then multiply that by 10 to provide the publisher's dynamic LBC price.

Variables & Terms Used

Term Definition
<claim_id> Hash of the txid:nout of the first TX which created the claim. When used in OP_CLAIM_INTEGER_VALUE it points to a claim containing an integer as the payload.
<claim_version> For a claim with no updates this will equal the claim_id, for subsequent claim updates it will be a hash of the txid:nout of the TX containing the specific claim update.
<sell_script> Lbrycrd script defining the sales contract requiremnets a buyer must meet in order for the proof of purchase to be recorded on the blockchain.
<receive_script> Standard bitcoin pay-to-script-hash script which is hash160'd and becomes the payment address, in this proposal the receive_script is specifically the script to which purchase payments are supposed to go (can be made optional by seller).
<sell_id> Hash of the txid:nout of the sell script.
negotiation_signature (optional) Signature of the buyer transaction by the seller, verified by the <sell_script>.
<owner_pubkey_hash> When presenting the proof of purchase to download content, the content gate keeper will verify ownership using this public key hash.
<price> Price in dewies attached to the SELL script and used by OP_PRICECHECK opcode.
<redeem_script_hash> Ignore for the purpose of this proposal, it's just the standard pay-to-script-hash stuff which allow spending the various meta UTXOs (aka "abandoning").

OP_PRICE_PAID

OP_PRICE_PAID replaces the op code with the value of the BUY output, allowing the SELL script to verify that the buyer has provided enough LBC to complete the purchase.

OP_CLAIM_INTEGER_VALUE

OP_CLAIM_INTEGER_VALUE requires a single argument, a <claim_id>, which must point at a claim where the payload of the claim is a simple integer. This supports using a claim as an exchange rate provider.

OP_SELL_CLAIM

SELL transactions contain a <claim_id> being sold and a <sell_script> which programmatically defines the requirements for a BUY to be accepted and also the <receive_script> in order to be able to validate that purchases are sent to the correct scripthash (address).

When the content creator wants to sell something, they might create the following output script:

OP_SELL_CLAIM <claim_id> <sell_script> <receive_script> OP_2DROP OP_2DROP OP_HASH160 <redeem_script_hash> OP_EQUAL

The embedded <sell_script> could be the following serialized script:

OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL OP_VERIFY

Once this SELL output is on the blockchain, buyers can start making BUY outputs referencing this SELL.

OP_BUY_CLAIM

BUY transactions contain a <sell_id> referencing the SELL which will validate this BUY and then the various values forming the proof of purchase <claim_id>, <claim_version>, <owner_pubkey_hash> and <negotiation_signature>:

OP_BUY_CLAIM <sell_id> <claim_id> <claim_version> <owner_pubkey_hash> <negotiation_signature> OP_2DROP OP_2DROP OP_2DROP OP_HASH160 <receive_script_hash> OP_EQUAL

If lbrycrd recieves this TX and validates it to be correct (described later) it means the sale has been successful and now this BUY output is the proof of purchase which can be redeemed when attempting to download content (along with proof that the <owner_pubkey_hash> belongs to redeemer).

Validation & Buying

Lbrycrd would implement two processing stages, one for processing the initial SELL transaction and then each of the BUY transaction referencing that SELL.

Validating OP_SELL_CLAIM

  1. Find the associated claim. If not found, reject. (Can't sell claim which doesn't exist.)
  2. Check if the address holding the claim (or latest update of the claim) is also an address in one of the inputs to the current SELL transaction. If not, reject. (Can't sell claim that's not yours.)
  3. Hash the txid:nout of the SELL output and place it into a lookup of SELLs.
  4. Place transaction in mempool.

Validating OP_BUY_CLAIM

  1. Lookup the <sell_id> in lookup table. If not found, reject. (Can't buy something that's not for sale.)
  2. Extract the <claim_id>, <sell_script> and <receive_script> from the SELL output.
  3. Concatenate the various script parts and variables into a final validation script (described later in Executing Sale).
  4. Execute script in lbrycrd script interpreter. If output is false, reject. (Buyer is not paying to the correct address, doesn't have the correct amount or some other reason.)
  5. Place transaction in mempool.

Executing Sale

In order to validate and add the proof of purchase to the blockchain, lbrycrd has to gather both the SELL script and the BUY script and concatenate them in a specific way, then execute the script and finally if the result is true allow the TX and if the result is false reject it as invalid.

The standard SELL and BUY transactions do not vary in structure and the rest of this proposal uses the structure defined earlier. The <sell_script> does change and is up to the merchant to define the requirements for a successful sale, the rest of this section uses the following <sell_script>:

OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL OP_VERIFY

With that out of the way let's construct the proof of purchase script:

Step Part
Add the <claim_id> from both BUY and SELL scripts and an equality check. <claim_id> <claim_id> OP_EQUALVERIFY
From the BUY script, skip <sell_id> and <claim_id> (already added) and add all of the other values. <claim_version> <owner_pubkey_hash> <negotiation_signature>
Add the <receive_script> from SELL. <receive_script>
Entire BUY script. OP_BUY_CLAIM <sell_id> <claim_id> <claim_version> <owner_pubkey_hash> <negotiation_signature> OP_2DROP OP_2DROP OP_2DROP OP_HASH160 <receive_script_hash> OP_EQUAL
Deserialize and add the <sell_script> OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL OP_VERIFY

All of the above parts concatenated together results in the following final script:

<claim_id> <claim_id> OP_EQUALVERIFY <claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script> OP_BUY_CLAIM <sell_id> <claim_id> <claim_version> <owner_pubkey_hash> <negotiation_signature> OP_2DROP OP_2DROP OP_2DROP OP_HASH160 <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL

Lbrycrd script interpreter would now execute the script as follows (assume the value of the output is 1 LBC and the price of the claim is also 1 LBC):

Stack Script
[empty] <claim_id> <claim_id> OP_EQUALVERIFY <claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script> OP_BUY_CLAIM <sell_id> <claim_id> <claim_version> <owner_pubkey_hash> <negotiation_signature> OP_2DROP OP_2DROP OP_2DROP OP_HASH160 <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 1 Push the buy and sell claim_ids onto the stack.
<claim_id> <claim_id> OP_EQUALVERIFY <claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script> OP_BUY_CLAIM <sell_id> <claim_id> <claim_version> <owner_pubkey_hash> <negotiation_signature> OP_2DROP OP_2DROP OP_2DROP OP_HASH160 <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 2 Compare them, then fail if not equal, pop both values off stack if equal.
[empty] <claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script> OP_BUY_CLAIM <sell_id> <claim_id> <claim_version> <owner_pubkey_hash> <negotiation_signature> OP_2DROP OP_2DROP OP_2DROP OP_HASH160 <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 3 Push next values onto stack.
<claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script> OP_BUY_CLAIM <sell_id> <claim_id> <claim_version> <owner_pubkey_hash> <negotiation_signature> OP_2DROP OP_2DROP OP_2DROP OP_HASH160 <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 4 Perform all of the drops.
<claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script> OP_HASH160 <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 5 Hash160 the <receive_script> -> <receive_script_hash> (converts it in-place).
<claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script_hash> <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 6 Push the other <receive_script_hash> onto stack for comparison.
<claim_version> <owner_pubkey_hash> <negotiation_signature> <receive_script_hash> <receive_script_hash> OP_EQUAL OP_VERIFY OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 7 Compare the two hashes with OP_EQUAL and then drop them if they are equal with OP_VERIFY. This tells us that the BUY payment went to the correct scripthash.
<claim_version> <owner_pubkey_hash> <negotiation_signature> OP_DROP OP_DROP OP_DROP OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 8 This is where a merchant can check the <claim_version> or the <owner_pubkey_hash> if they only want to sell a specific version of the claim or if they only want to sell to specific pub key holder. If they are doing offchain negotiation they can do something with the <negotiation_signature> to validate it further. For this example we just ignore/drop all three values.
[empty] OP_PRICE_PAID <price> OP_GREATERTHANOREQUAL
step 9 Push the price of the output onto the stack.
100000000 <price> OP_GREATERTHANOREQUAL
step 10 Push <price> onto the stack.
100000000 <price> OP_GREATERTHANOREQUAL
step 11 Price paid is 1 LBC and price expected is 1 LBC therefore the greater than or equal to check passes.
true [empty]

If the final outcome is true then the BUY is recorded onto the blockchain and is now a valid proof of purchase.

Adding exchange rate support via OP_CLAIM_INTEGER_VALUE is also straightforward; to avoid too much repetition we'll re-use the previous execution steps but pick up after step 8 with a sale script that includes the OP_CLAIM_INTEGER_VALUE op code. For this script, the buy value is 220000000, the <claim_id> points to a claim with payload of 110000000 and the <price> is 2.

Stack Script
[empty] OP_PRICE_PAID <claim_id> OP_CLAIM_INTEGER_VALUE <price> OP_MUL OP_GREATERTHANOREQUAL
step 9 Push the price of the output onto the stack.
220000000 <claim_id> OP_CLAIM_INTEGER_VALUE <price> OP_MUL OP_GREATERTHANOREQUAL
step 10 Push the payload of the claim pointed to by <claim_id>.
220000000 110000000 <price> OP_MUL OP_GREATERTHANOREQUAL
step 11 Push <price> onto the stack.
220000000 110000000 2 OP_MUL OP_GREATERTHANOREQUAL
step 12 Multiply the exchange rate by the sell price.
220000000 220000000 OP_GREATERTHANOREQUAL
step 12 Price paid is 2.20 LBC and price expected is 2.20 LBC therefore the greater than or equal to check passes.
true [empty]
BrannonKing commented 5 years ago

We had a discussion on this topic today. Notes: Additional Requirements:

  1. A user wants to watch any of their purchased content (or a list of it) at any time/location, on any device, multiple times (or not).
  2. A user wants to see what of their content was purchased and the price paid for it.
  3. A user cannot argue that they bought/sold something that they did not buy/sell.
  4. Nice to have: dynamic pricing. The seller can have sale prices, temporary prices, or per-customer prices, etc.
  5. Sale prices need to support arbitrary currencies.
  6. Purchases need to be very differentiable from support, tips.

Open Questions

  1. Is adding the ClaimID and purchase type and price to the current purchase transactions sufficient, or do we really need/want new OP codes for buying and selling?
  2. Do we want payment information in the block chain at all? 3rd-party services exist for this.
  3. Will our blockchain be able to keep up with the number of payments coming in? (It has limited space in blocks and a fixed timeframe on blocks).
  4. Is there any value in supporting "claimid versions"? It's in the proposal above, but it may also mean that we have to keep all versions of some content in our peer network, which could get quite expansive.
  5. What are reasonable embedded script max-sizes?
  6. Can we run a similar scheme without embedded scripts and get 90% of the functionality? The embedded scripts complicate script validation code.
  7. Does the above proposal keep the buyer/seller secure and private?
BrannonKing commented 5 years ago

I wanted to add to this discussion the BIP70 requirements:

  1. Human-readable, secure payment destinations-- customers will be asked to authorize payment to "example.com" instead of an inscrutable, 34-character bitcoin address.
  2. Secure proof of payment, which the customer can use in case of a dispute with the merchant.
  3. Resistance from man-in-the-middle attacks that replace a merchant's bitcoin address with an attacker's address before a transaction is authorized with a hardware wallet.
  4. Payment received messages, so the customer knows immediately that the merchant has received, and has processed (or is processing) their payment.
  5. Refund addresses, automatically given to the merchant by the customer's wallet software, so merchants do not have to contact customers before refunding overpayments or orders that cannot be fulfilled for some reason.

And additional requirements from feedback on that:

  1. Allow users to verify identity of the receiving party.
  2. Allow users to pay without internet access.
BrannonKing commented 5 years ago

In your above proposal, you suggest that claims providing exchange rates represent a "single integer". I was picturing a table, something like this: LBC/USD: 0.0001 LBC/EUR: 0.0002 ... I think real numbers are necessary, and I'm not sure what I gain by having each currency base pair in its own claim. Well, I suppose what you gain is the ability to store the data on the blockchain without the currency identifier (e.g. "USD"). I think someone viewing the blockchain would want the currency ID in visible, though.

eukreign commented 5 years ago

The reason for the single integer per claim is to keep it simple in the lbrycrd implementation. If you have tables then this means a lot of extra work in validating it and figuring out how to deal with edge cases. If you have one claim per exchange rate and the payload is an integer then you don't have to parse anything and validation is super simple.

Table approach would require adding a new data format specification that needs to be documented and implemented and tested, etc. Using an integer as payload doesn't require any of that.

Another problem with table is that now you have to reference not only the claim containing the table but also which exchange rate in that table should be used.

BrannonKing commented 5 years ago

26 bytes per buy OP puts a maximum of 80k buys per 2MB block (leaving a little extra for other OPs). If blocks continue at one every 2.5 minutes, that gives us 500-ish buy transactions per second. We have a few different payment models:

  1. Channel subscription (pay once per time period)
  2. Content ownership (pay once per piece of content)
  3. Content stream (pay once per view)

If you had 100M users (40M less than Netflix) and they each streamed one new piece of content per day, that would lead to 1100 new streams per second. Hence, option 3 is not feasible with this plan.

eukreign commented 5 years ago
  1. Can we increase block size?
  2. Can we reduce time between blocks?
  3. Can you paste the TX example that you used to come up with the 26 bytes?
  4. Would it be worth dropping some features to reduce size? For example, dropping the <negotiation_signature> from the buy transaction?
BrannonKing commented 5 years ago
1. Can we increase block size?

Yes, but there are negative consequences.

  1. Can we reduce time between blocks? Not much.
  2. Can you paste the TX example that you used to come up with the 26 bytes? Looking at it again I realized it was 26 bytes for OP_PRICE_PAID, not OP_BUY_CLAIM. The latter seems to take 150+ bytes?
  3. Would it be worth dropping some features to reduce size? For example, dropping the <negotiation_signature> from the buy transaction? Each sig or hash we can drop would definitely increase the throughput.
kauffj commented 5 years ago

This seems largely reasonable to me.

I think this comment by @BrannonKing is one of the biggest problems/downsides, but this problem would presumably be roughly the same under the current model. So while this is a very important problem, unless the additional space required for this proposal is a significant difference, I would say it's not a reason to consider a solution along these lines.

I also think that since any price negotiation of even moderate complexity will have to happen off-chain, we should not be particularly concerned with sell/receive scripts. If these add any significant complexity to the design vs. simply allowing fixed prices, I'd propose excluding these from the first iteration.

jsigwart commented 5 years ago

I don't think the purchase price/transaction is as important as recording the right to access the content.

What about something like this:

tzarebczan commented 5 years ago

It's Craig Wright, but relevant “Storing IP on the Blockchain” by Craig Wright (Bitcoin SV is the original Bitcoin.) https://link.medium.com/qfr4a3mtPT

For this to work as you spec'd it, each publish would need to be unique for every user, which doesn't sound feasible.

Sharing your wallet or decryption key could still lead to sharing/stealing content.