It's a very literal take on Lawrence Lessig's argument that "code is law", a libertarian attempt to reduce the costs and uncertainty of having to trust human beings and interpret ambiguous human language, or possibly a dystopian replacement of rights and safeguards with binary logic.
Smart contracts can be used to implement smart property, physical goods governed by computer code, and Distributed Autonomous Organizations, which replace written constitutions with code running on Ethereum's Blockchain.
This series of articles will look at applications of Ethereum to the production, exhibition, critique and institutions of art. Starting with digital art as smart property.
The sample code is written in Serpent, the high level Ethereum programming language, and is current as of Ethereum POC 5. It will be revised and tested for the release version of Ethereum.
You can learn more about Ethereum here and here. You can learn more about Serpent here and here.
Storing and Identifying Digital Art In Ethereum
There are three cases of digital art as smart property. The first is the conceptual or code art case, where the code of the contract itself is or contains the artwork. The second is the Ethereum storage case, where a small digital artwork is stored in the Ethereum datastore. The third is the stored identifier case, where only an identifier or proxy for the artwork is stored with the contract.
Conceptual and Code ArtContracts that are themselves art are a simple case. They should store their owner's Ethereum address and ensure that transactions initiating actions that only the owner should be able to perform come from that address.
Conceptual ArtAs art is defined by its inutility, a contract that does nothing must be art. ;-)
Hot/ColdA contract that does something, but nothing useful.
init: contract.storage = "hot" contract.storage = "cold" code: // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 100: // If not, stop stop // Swap temp = contract.storage contract.storage = contract.storage contract.storage = temp
Numbered WorksA simple generative work that creates a new, original piece for each request.
init: ARTWORK.NUMBER.INDEX = 1001 contract.storage[ARTWORK.NUMBER.INDEX] = 1 code: ARTWORK = "Work #" ARTWORK.NUMBER.INDEX = 1001 // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 400: // If not, stop stop // Get the number of the work to produce number = contract.storage[ARTWORK.NUMBER.INDEX] // Store the number to use for the next work contract.storage[ARTWORK.NUMBER.INDEX] = contract.storage[ARTWORK.NUMBER.INDEX] + 1 // Return the work return([ARTWORK, number], 2)
Data VisualizationA simple customised generative work / data visualization. The output looks something like:
HEX = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] ARTWORK = ["<svg><rect x="23" y="23" ", "height="123" width="123" ", "style="fill:none;stroke:#", ";stroke-width:32" /></svg>"] ARTWORK.LENGTH = 10 ARTWORK.INSERT.END = 8 // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 500: // If not, stop stop // If there are enough arguments // and the command to create the work is being given if msg.datasize == 1 and msg.data == "create": artwork = array(ARTWORK.LENGTH) artwork = ARTWORK artwork = ARTWORK artwork = ARTWORK artwork = ARTWORK // Copy the most significant hex bytes of the key as an html colour index = 0 hash.bytes = msg.sender while index < 3: current.byte = hash.bytes % 256 hash.bytes = hash.bytes / 256 hi = HEX[current.byte / 16] lo = HEX[current.byte % 16] artwork[ARTWORK.INSERT.END - (index * 2)] = lo artwork[ARTWORK.INSERT.END - (index * 2) + 1] = hi index = index + 1 return(artwork, ARTWORK.LENGTH) // Otherwise else: // Logical false for failure return(0)
Digital art can be stored in a contract's bytecodes or permanent storage rather than generated by the contract's code.
A Stored Image
ARTWORK = ['P1n32 32n00000000000000010000000', '00000000000000000000000010000000', '000000000000000n0000000001000000', '00000000000000000000000001000000', '0000000000000000000000n001100000', '00000000000000000000001100001010', '00001100001000000000100000000n00', '01011000010000000000100000000000', '01111000100000000001000000000001', '1111n000100000000001000000000011', '11110110000000000010000000001111', '11100010000n00000000000000011111', '11000000000000000010000001111111', '110000001000000000n1000001111111', '11000000000000000000000000111111', '1110000001000000000000000n000011', '11111000000000000000100000000001', '11111100000100000000000000000111', 'n1111110000000000000010000000011', '11111100000100000000010000001111', '1111000n000010000000000000000111', '11110000000000000000000100001111', '10000000000100n00000000000001111', '10000000000010000000000001111100', '000000000001000000000n0000011100', '00000000000100000000000000101000', '0000000000000000000000000000n010', '10000000010100000000000000000000', '01010010100000000000000000000000', '000n0110000000000000000000000000', '00000100000000000000000000000000', '0000010000n000000000000000000000', '00000010000000000000000'] ARTWORK.LENGTH = 33 // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 1000: // If not, stop stop // If there are enough arguments // and the command to show the work is being given if msg.datasize == 1 and msg.data == "exhibit": // Just return the artwork return(ARTWORK, ARTWORK.LENGTH) // Otherwise else: // Logical false for failure return(0)
Identifiers For Digital ArtWhere a work of digital art will not fit in the blockchain a more compact identifier must be stored in the contract and used to refer to the work instead.
Ideally a method for identifying unique instances of a digital artwork will be:
- Stable. The identifier should be usable for decades. Web URLs for example can change or become inaccessible as services change how they serve content or go out of business.
- Verifiable. The identifier should be usable as a way of verifying that a resource is the one it refers to. Cryptographic hashes for example will not work with digital images that have been resized or GPS co-ordinates that differ even fractionally.
- Amendable. Where stability fails or a change in ownership requires a change in identifier, it should be possible to update the identifier in a trusted and verifiable way.
- Sufficient. The information required to identify the resource should be usable directly from the contract rather than requiring external information to complete it.
- Private. An identifier should leak as little information about the owner of the resource as possible. For example GPS co-ordinates or street addresses, while stable, do locate the resource and possibly its owner. Storing only the cryptographic hash of an identifier can mitigate this.
Some of these criteria clash and therefore any given method of identification must trade them off against each other. For example being private and verifiable, or stable and amendable.
For artworks to interact with smart contracts we need a way of identifying them in those contracts.Where a digital artwork is too large or complex to keep in the contract's code or storage, a proxy or compact identifier that refers to the must be used.
The following identifiers have various strengths and weaknesses. We'll use some of them in the next section.
URLA URL, such as a web site address, is a clear public identifier. It lacks privacy and is only as stable as the service hosting it, but has the advantage of being unique. To add a veneer of privacy, only the cryptographic hash of the URL can be stored by the contract and this can be checked against the hash of a URL by anyone who wishes to check whether it is the instance of the work referred to by the contract.
For example the url:
has the SHA256 hash:
Never use a link shortening service or a consumer third party hosting service for work represented as URLs, such services are likely to go out of business or change their URL structure, rendering identifiers using them useless. When using your own site for hosting work make sure both to both keep your domain name registered and the server running and to make provisions for them to be maintained when you are no longer able or willing to do so.
File HashProducing a cryptographic hash of a work contained in or represented by a file is simple and uniquely identifies the data contained by the file (although any copies of the file will have the same hash). It is better to hash the contents of the file rather than the file itself: an image that has the same pixel values as a PNG or a GIF will have a very different structure on disk in each of those formats. Likewise the full-size or full-quality version of the contents of the file should be hashed rather than a thumbnail or a lossy version.
Git Repository Commit HashModern decentralised version control systems use cryptographic hashes to identify commits. Hashes can identify version of works in a series within a version control repository, although they are best accompanied by a URL or other identifier for the repository.
Serial Number or UUIDA serial number or unique identifier embedded in the work's filename or metadata can be used to identify it. Visible watermarks are the mark of the amateur, and steganographic watermarks are easily defeated.
Cryptographic SigningWhen producing editions of a digital work, each can be signed by the artist to identify it as authorised.
NameWhen all else fails, a unique name and description for a work is a useful identifier.
Art As Smart Propery
A Simple Owned Work
OWNER = 0x7c8999dc9a822c1f0df42023113edb4fdd543266 // Get the owner Ethereum address return(OWNER)
A Simple Owned Work That Confirms Ownership
OWNER = 0x7c8999dc9a822c1f0df42023113edb4fdd543266 // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 100: // If not, stop stop // If the Ethereum address sent matches the owner if msg.data == OWNER: // Return true return(1) // Otherwise else: // Return false return(0)
A Simple Owned Stored Work
OWNER = 0x8802b7f0bfa5e9f5825f2fc708e1ad00d2c2b5d6 // Artist initially WORK = "The art happens here." // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 200: // If not, stop stop // If there are enough arguments // and the command to return the owner address is given if msg.datasize == 1 and msg.data == "owner": // Return the owning Ethereum address return(OWNER) // If there are enough arguments // and the command to return the artwork is given if msg.datasize == 1 and msg.data == "work": // Return the work return(WORK) // Otherwise else: // Return logical failure return(0)
Simple Transferable Stored Work
init: ARTIST = 0x8802b7f0bfa5e9f5825f2fc708e1ad00d2c2b5d6 OWNER = 1001 // Initialize the owner to be the artist contract.storage[OWNER] = ARTIST code: OWNER = 1001 ARTWORK = "The art happens here." // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 200: // If not, stop stop // If the message is from the current owner // and there are enough arguments // and the command to transfer is being given if msg.sender == contract.storage[OWNER] and msg.datasize == 2 and msg.data == "transfer": // Transfer it to a new owner contract.storage[OWNER] = msg.data return(1) // If there are enough arguments // and the command to show the work is being given else if (msg.datasize == 1): // Just return the artwork return(ARTWORK) // Otherwise else: // Logical false for failure return(0)
An Ownership Registry For Digital Art
if tx.value < tx.basefee * 200: // If not enough gas, stop stop // If data was provided, it won't overwrite the code, and the artwork is unregistered if msg.datasize == 1 and msg.data > 1000 and contract.storage[msg.data] == 0: // Set the owner to be the sender contract.storage[msg.data] = msg.sender return(1) else: // Do nothing return(0)
A Hash-based Ownership Registry For Specific Instances Digital ArtThis is a registry for ownership of artworks at specific urls. Artworks are identified by the hash of their file contents and by the hash of their url. Owners are identified by Ethereum address.
if tx.value < tx.basefee * 200: // If not, stop stop // If registration is being requested if msg.datasize == 3 and msg.data == "register": // If the url/work combination has not been claimed if ! contract.storage[msg.data]: // Set the owner to be the provided Ethereum address contract.storage[msg.data] = msg.sender // Store the artwork hash next to the url ownership information contract.storage[msg.data + 1] = msg.data // return success return(1) // If the sender was trying to overwrite a work they do not own else: // They cannot set it, so return failure return(0) // If ownership confirmation is being requested // Confirm that the work and url hashes match else if msg.datasize == 4 and msg.data == "confirm": // Check the provided hashes against the stored work and url hashes return((contract.storage[msg.data] == msg.data) and (contract.storage[msg.data + 1] == msg.data)) // If no action was specified else: // Otherwise do nothing return(0)
Authenticating Art In Ethereum
Authentication, like ownership, is related to identity and contract law.
A Simple Certificate Of Authenticity For Digital Art
ARTIST = 0x8802b7f0bfa5e9f5825f2fc708e1ad00d2c2b5d6 ARTWORK.HASH = 0x76bba376ea574e63ab357b2374d1cee5aa77d24db38115e3824c5cc4f443d5f7 return((msg.data == ARTIST) and (msg.data == ARTWORK.HASH))
ARTIST = 0x8802b7f0bfa5e9f5825f2fc708e1ad00d2c2b5d6 WORK.HASH = 0x76bba376ea574e63ab357b2374d1cee5aa77d24db38115e3824c5cc4f443d5f7 URL.HASH = 0xa005b1625af0b6ee080dafb904c4505ad285764071ee45a8786159bd1a282634 // If there are enough arguments if msg.datasize == 2: // Check the provided hashes against the stored work and url hashes return((msg.data == WORK.HASH) and (msg.data == URL.HASH)) // Otherwise else: // Do nothing stop
Catalogue Raisonné For Digital Artists
ARTIST = 0x8802b7f0bfa5e9f5825f2fc708e1ad00d2c2b5d6 // Make sure we have enough gas to run the contact if tx.value < tx.basefee * 200: // If not, stop stop // If the message is from the artist if msg.datasize == 1 and msg.sender == ARTIST: // Add the work to the catalog contract.storage[msg.data] = 1 return(1) // check inclusion else if msg.datasize == 1: // Check whether the artwork is in the catalog return(contract.storage[msg.data]) // Otherwise else: return(0)