Skip to content

Instantly share code, notes, and snippets.

@tgangwani
Created September 28, 2016 20:38
Show Gist options
  • Save tgangwani/d6bd7b3c04dc4a3b68e0b83dbb46359d to your computer and use it in GitHub Desktop.
Save tgangwani/d6bd7b3c04dc4a3b68e0b83dbb46359d to your computer and use it in GitHub Desktop.
PPV Smart Contract
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Pay-Per-View Smart Contract"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Contract Description\n",
"Pay-Per-View (PPV) is a type of service where content makers and distributors can publish their feeds, and subscribers have an option to pay selectively, as per their liking. Publishers decide the price for each feed and subscribers can pay the publishers only for the feeds they want access to.\n",
"\n",
"The following Ethereum smart contact implements a version of the aforementioned distribution paradigm. There are two types of contracts - *publisher* and *subscriber*. As the names suggest, the former is created by a publisher or content creator, while the latter is created by subscribers or users.\n",
"\n",
"## 2. Publisher Contract\n",
"The publisher uses her contract to publish a list of feeds, which we refer to as *stumps*. In this contract, we only deal with article feeds. As such, stumps can be thought of as the first few lines (or teasers) of a new article that the publisher has written. The stumps sit in the contract storage, along with an identification number and the price. Users can query the available stumps and the corresponding purchase price from contract storage. Following are the available functions for this contract:\n",
"* publishStump()\n",
"* purchase()\n",
"* reclaim()\n",
"* completeRequest()\n",
"* getRequestsSerialized()\n",
"* getStumpData()\n",
"* getStumpPrice()\n",
"* getStumpIds()\n",
"* getNumPending()\n",
"\n",
"\n",
"## 3. Subscriber Contract\n",
"Each user creates a subscriber contract. The contract acts a safe-house for all the articles purchased by this user. The publisher contract communicates only with the subscriber contract account and not with the subscriber user account. As explained later, the data stored in this contract is small (just links, not full articles) and encrypted, alleviating concerns regarding blockchain size and privacy, respectively. Following are the available functions for this contract:\n",
"* getNumberLinks()\n",
"* getLink()\n",
"\n",
"## 4. Purchasing an article\n",
"The user queries the publisher contract to read the available stumps. To purchase an article, she sends a transaction with the identification number (stump-id) to the publisher contract. The appropriate price for the article has to be included. The user sends two additional pieces of information in the function call - a public key part of a RSA key pair and her subscriber contract address. The request is then time-stamped and added (conditional on available space) in an ordered circular buffer in the publisher contract. For now, the money sits in the publisher contract. At some later time, the publisher queries the circular buffer for pending requests. She services the requests in order. Servicing a request includes fetching *a link* (from local disk, not the blockchain) to the full article related to the requested stump-id, encrypting the link with the public key provided by the requesting user and sending a transaction to the publisher contract with the encrypted data. The publisher contract verifies if the in-order servicing rule was respected by the publisher. It then relays the encrypted data to the subscriber contract and releases the payment to the publisher account. The subscriber can query her contract to fetch the encrypted link, decrypt using her private key and enjoy the full content! \n",
"\n",
"## 5. Protecting the subscriber\n",
"1. The money deposited by the subscriber is not directly transferred to the publisher account, but is instead kept in the publisher contract until the request is completed by the publisher. A refund mechanism is implemented such that a subscriber can initiate a refund if she doesn't get the encrypted link to the full article in her subscriber contract by a fixed (pre-agreed) time amount. Though the full article link sits on the blockchain and is visible to all, the RSA encryption ensures that it's useful only to the subscriber with the private key. \n",
"\n",
"2. A possible attack by a publisher could be denial of service - it could decide not to complete the request for a particular subscriber. Although the subscriber can always claim a refund (after a timeout), the enforced ordering of the circular buffer further dis-incentivizes the publisher from doing so. Specifically, the publisher contract would not accept a complete request *R* (and release funds) from the publisher unless all previous pending requests in the buffer have been completed. Also, if the refund process is successful for request in the buffer, all the previous requests in the buffer are automatically (without a trigger from the corresponding subscriber) refunded. \n",
"\n",
"\n",
"## 6. Protecting the publisher\n",
"The prices for all stumps are stored in the publisher contract and any attempt by a user to deposit its request in the circular buffer is thwarted if the necessary pricing demands are not met. The size of the circular buffer is specified by the publisher when she creates the contract. If the buffer if full, new incoming requests from users are not accepted, preventing any sort of request flooding.\n",
"\n",
"## 7. Open Issues\n",
"1. The publisher is not defended against the subscriber making the link to the full article public on her channel after honestly purchasing it. This is an issue with PPV today as well. For example, Alice can purchase a Sky-Sports pass for the Silverstone Formula 1 race and then start an illegal stream for non-paying users. \n",
"\n",
"2. Although the publisher contract acts as a mediator by a) holding payment until data is served b) enforcing in-order request serving and c) facilitating refunds, it does not provide any guarantees for the quality of data served by the publisher as it only sees an encrypted link. Some sort of publisher rating could be built in the smart contract to increase subscriber confidence.\n",
"\n",
"\n",
"## 8. Code \n",
"The file pubsub.sol contains the publisher and subscriber contracts written in Solidity programming language. The python file pubsub.py provides a test execution of the contracts on the Pyethereum simulator. The execution is for **1 subscriber** and **1 publisher**, but could easily be generalized to a multi-party scenario. We show two executions - a normal execution and a faulty execution where the publisher doesn't service the requests in time.\n",
"\n",
"### 8.1 Normal Execution\n",
"In main(), the publisher and subscriber contracts are initialized. The publisher provides the buffer capacity and request expiration time as constructor parameters of the publisher contract. Both the contracts, as well as the user accounts start with 100 ethers."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"-----\n",
"Initial balance:\n",
"Publisher: 100.00 ethers\n",
"Publisher-contract: 100.00 ethers\n",
"Subscriber: 100.00 ethers\n",
"Subscriber-contract: 100.00 ethers\n",
"-----\n",
"\n",
"\n"
]
}
],
"source": [
"main()\n",
"state.mine(1) # mine a block\n",
"showBalance('Initial')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The publisher releases some stumps along with the price. These can be queried using getStumps(). The stumps and the prices are printed."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"('Stumps from publisher ', 'ad7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5')\n",
"Snapchat unveils $130 spectales and ... Price: 1.00 ethers\n",
"Messi ruled out for ... Price: 7.00 ethers\n",
"Jack the Ripper's horrifying murders ... Price: 33.00 ethers\n"
]
}
],
"source": [
"publishStump()\n",
"getStumps()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Next, the subscriber decides to purchase the first stump (with id '0'). The purchase() sends a transaction to the publisher contract with all the necessary information like the subscriber contract address and public key for encryption. The amount sits in the publisher contract until the request is serviced by the publisher."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"-----\n",
"Intermediate balance:\n",
"Publisher: 100.00 ethers\n",
"Publisher-contract: 101.00 ethers\n",
"Subscriber: 99.00 ethers\n",
"Subscriber-contract: 100.00 ethers\n",
"-----\n",
"\n",
"\n"
]
}
],
"source": [
"purchase(0)\n",
"state.mine(1) # mine a block\n",
"showBalance('Intermediate')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The publisher comes along, fetches the requests from contract storage and calls handleRequests(). This function sends a transaction to the publisher contract with the encrypted link to the full article. The balance is now released to the publisher account. The metadata for the request served by the publisher is also shown below."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Publisher contract has 1 pending requests\n",
"Publisher retrieved request : stump id(45), request time(1), contract address(b7c8368491d39cd3e5a9e9455e3bcc184533b95d) pbkey(-----BEGIN RSA PUBLIC KEY-----\n",
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4VQcw2RxyKkzIDjDQu//68VlH\n",
"2eO8AqvPKb5woEHYv+G6BXHPdPBKwR/dq+Q/Ts2pjLbGzBRcARQqgPATm7wyLFt/\n",
"Y/nXzU4Uh1KffTPDp7uD6cQBbnTJb8+9C1v2NfLVOpFKk/erJQM3idxUFuvFnTbo\n",
"TX8xmmb7EvFABc9GoQIDAQAB\n",
"-----END RSA PUBLIC KEY-----)\n",
"\n",
"\n",
"-----\n",
"Intermediate balance:\n",
"Publisher: 101.00 ethers\n",
"Publisher-contract: 100.00 ethers\n",
"Subscriber: 99.00 ethers\n",
"Subscriber-contract: 100.00 ethers\n",
"-----\n",
"\n",
"\n"
]
}
],
"source": [
"requests = getRequests() \n",
"handleRequests(requests) \n",
"showBalance('Intermediate')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Following similar pattern, the subscriber purchases two more stumps from the publisher. The final balance of each account is shown below."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Publisher contract has 2 pending requests\n",
"Publisher retrieved request : stump id(198), request time(2), contract address(b7c8368491d39cd3e5a9e9455e3bcc184533b95d) pbkey(-----BEGIN RSA PUBLIC KEY-----\n",
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4VQcw2RxyKkzIDjDQu//68VlH\n",
"2eO8AqvPKb5woEHYv+G6BXHPdPBKwR/dq+Q/Ts2pjLbGzBRcARQqgPATm7wyLFt/\n",
"Y/nXzU4Uh1KffTPDp7uD6cQBbnTJb8+9C1v2NfLVOpFKk/erJQM3idxUFuvFnTbo\n",
"TX8xmmb7EvFABc9GoQIDAQAB\n",
"-----END RSA PUBLIC KEY-----)\n",
"Publisher retrieved request : stump id(244), request time(2), contract address(b7c8368491d39cd3e5a9e9455e3bcc184533b95d) pbkey(-----BEGIN RSA PUBLIC KEY-----\n",
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4VQcw2RxyKkzIDjDQu//68VlH\n",
"2eO8AqvPKb5woEHYv+G6BXHPdPBKwR/dq+Q/Ts2pjLbGzBRcARQqgPATm7wyLFt/\n",
"Y/nXzU4Uh1KffTPDp7uD6cQBbnTJb8+9C1v2NfLVOpFKk/erJQM3idxUFuvFnTbo\n",
"TX8xmmb7EvFABc9GoQIDAQAB\n",
"-----END RSA PUBLIC KEY-----)\n",
"\n",
"\n",
"-----\n",
"Final balance:\n",
"Publisher: 141.00 ethers\n",
"Publisher-contract: 100.00 ethers\n",
"Subscriber: 59.00 ethers\n",
"Subscriber-contract: 100.00 ethers\n",
"-----\n",
"\n",
"\n"
]
}
],
"source": [
"purchase(1) \n",
"purchase(2) \n",
"requests = getRequests() \n",
"handleRequests(requests)\n",
"showBalance('Final')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The subscriber finally calls readLinks() to read the encrypted links from contract storage, and decrypts them to access the full content."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Subscriber has 3 links from publisher ad7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5\n",
"('Full link from publisher: ', 'Snapchat unveils $130 spectales and rebrands as Snap, Inc.')\n",
"('Full link from publisher: ', 'Messi ruled out for three weeks with groin injury.')\n",
"('Full link from publisher: ', \"Jack the Ripper's horrifying murders terrorize London.\")\n"
]
}
],
"source": [
"readLinks()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 8.1 Faulty Execution\n",
"As before, we assume that the subscriber makes a couple of purchases by depositing the amount in the publisher contract. The expiration time in the publisher contract is set to 20 blocks (~4 min as per Ethereum block mining rate). "
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"-----\n",
"Intermediate balance:\n",
"Publisher: 141.00 ethers\n",
"Publisher-contract: 108.00 ethers\n",
"Subscriber: 51.00 ethers\n",
"Subscriber-contract: 100.00 ethers\n",
"-----\n",
"\n",
"\n"
]
}
],
"source": [
"purchase(0)\n",
"purchase(1)\n",
"state.mine(30) # mine blocks\n",
"showBalance('Intermediate')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We mine 30 blocks which is greater than the expiration time, allowing the subscriber to ask for a refund. She does it by calling reclaim() with the stump-id. This function sends a transaction to the publisher contract which does the necessary checks before releasing funds. Since the request for stump-id '0' sits before the request for stump id '1' in the circular buffer, both get refunded although the subscriber just asked for the latter. This preserves the circular buffer semantics. The refunded amount is deposited in the subscriber contract. "
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"-----\n",
"Final balance:\n",
"Publisher: 141.00 ethers\n",
"Publisher-contract: 100.00 ethers\n",
"Subscriber: 51.00 ethers\n",
"Subscriber-contract: 108.00 ethers\n",
"-----\n",
"\n",
"\n"
]
}
],
"source": [
"reclaim(1)\n",
"showBalance('Final')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Later on, when the publisher wakes up from her slumber, she would not find any pending requests to serve."
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Publisher contract has 0 pending requests\n"
]
}
],
"source": [
"requests = getRequests()\n",
"handleRequests(requests)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 1
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment