You may have heard of NFTs (“Non-Fungible Tokens”). The general idea of an NFT is that it is a piece of artwork stored on a decentralized, distributed ledger (the “blockchain”). If you bought an NFT, you could use this blockchain to incontrovertibly prove that you “own” a digital asset. Unfortunately, NFTs have a few flaws.

First, the blockchain only stores a link to artwork, rather than the artwork itself. Despite being on a “decentralized blockchain,” that link is controlled by a single, centralized entity: the company that registered (or “minted”) the NFT. NFTs also consume an obscene amount of energy in a misguided attempt to impose scarcity on a digital medium where art is inherently distributable.

On March 8, 2021, game designer Zach Gage released a piece of non-NFT artwork called Scarcity. The work was released alongside a statement on Zach Gage’s digital portfolio. I’d recommend reading that statement for more insight into the themes Gage wanted to explore with Scarcity, but one of the themes the work touches on is “art ownership,” an idea which NFTs claim to radically redefine. Scarcity states:

ART OWNERSHIP IS NOT ABOUT BEING THE OWNER

[…]

ART OWNERSHIP IS ABOUT COMMITMENT TO PROCESS

THE PROCESS OF SEEING ART EVERY DAY

A unique aspect of Scarcity is that it is a transient website. Each day, Scarcity’s URL may change. When the URL changes, Scarcity’s new URL is computed as a function of all its previous URLs, as well as the date of the most recent URL change.

Scarcity’s gimmick is well-chosen to suit its themes; in practical terms, the only way to retain access to Scarcity is to check it every day to see whether the URL has changed. To reliably maintain “ownership” of Scarcity, one must see the art every day, or risk losing access permanently.

I knew there would eventually come a day where I would be unable to check the URL. Although it might be a subversion of Scarcity’s themes, I decided to automate this process to ensure continued access to the work.

Design Requirements

Here is the process for retaining access to Scarcity, as described by the work:

Each day the url of this work may change.

If it does, you may compute its new address with this formula:

MD5 Hash( [the first characters of all previous subdirectory urls] + [The date the site last moved, represented as MMDDYYYY] )

Although it’s not mentioned in these instructions, you have to prepend http://stfj.net/scarcity/ to this new MD5 hash to obtain the new link. It is also mandatory to maintain a ledger of all the previous URLs to Scarcity so that the calculation can be performed. Scarcity has a link to a sample PHP script which computes the next URL from the ledger.

We’d like to come up with a small program that does the following:

  1. Checks whether the current link is still valid
  2. If the link is no longer valid, compute the new link
  3. Add this new link to the ledger, then save the ledger and current link

Also, we’d like to be able to guarantee that this program runs every day at roughly the same time. This situation is a perfect use case for AWS Lambda, which lets us avoid procuring a server. I’m already familiar with the AWS SDK for Node.js, so we’ll choose Javascript as our programming language for this project.

Code

First, we’ll write a function to determine whether the current link is still valid. Invalid links show a boilerplate “Not Found” page:

Not Found page - shown on an outdated or invalid Scarcity link

We’d normally expect a 404 HTML response for a “Not Found” page, but on this website these pages return a 200 HTML response, the same as a valid link. Since we can’t check the response code, we have to resort to checking the text of the page.

const fetch = require('node-fetch');

/**
 * Determines whether or not a given Scarcity URL is valid
 * @param {*} scarcityLink The Scarcity URL to check
 * @returns Whether or not the given link is valid
 */
async function isScarcityLinkValid(scarcityLink) {
    let scarcityLinkResponse = await fetch(scarcityLink);
    let scarcityLinkText = await scarcityLinkResponse.text();
    return (scarcityLinkText.includes("Scarcity"));
}

Second, we’ll write a function that takes the ledger and the current date, and then computes the MD5 hash for the next Scarcity link:

const crypto = require('crypto');

/**
 * Gets the MD5 hash that will be used for the next Scarcity link
 * @param {*} scarcityLedger Buffer containing the bytes of the ledger file
 * @param {*} currentDate Current date in MMDDYYYY format
 * @returns The MD5 hash for the next Scarcity link
 */
function getNextScarcityHash(scarcityLedger, currentDate) {
    let scarcityLedgerLines = scarcityLedger.toString().trim().split('\n');
    let hashInput = scarcityLedgerLines.map(line => line.charAt(0)).join('');
    hashInput += currentDate;

    return crypto.createHash('md5').update(hashInput).digest('hex');
}

To automate the process of verifying Scarcity, there are two main files we need to keep track of:

  1. The current link to Scarcity
  2. The ledger containing all previous Scarcity MD5 hashes and dates of site movement

We’d like to make both these files public so that anyone can download them. I’ve already got a public-facing S3 bucket set up to host my website, so we’ll use that.

We’ll write a separate module that lets us download and upload the link and ledger from my S3 bucket:

/**
 * Responsible for interacting with AWS S3
 * to download and upload the link and ledger.
 */

const AWS = require('aws-sdk');

const s3 = new AWS.S3({
    apiVersion: '2006-03-01',
});

const BUCKET_NAME = 'nickhoskins.com';
const SCARCITY_LINK_KEY = 'scarcity/link.txt';
const SCARCITY_LEDGER_KEY = 'scarcity/ledger.txt';

async function getScarcityLink() {
    let response = await s3.getObject({
        Bucket: BUCKET_NAME,
        Key: SCARCITY_LINK_KEY,
    }).promise();
    return response.Body;
}

async function getScarcityLedger() {
    let response = await s3.getObject({
        Bucket: BUCKET_NAME,
        Key: SCARCITY_LEDGER_KEY,
    }).promise();
    return response.Body;
}

async function uploadScarcityLink(scarcityLink) {
    return await s3.putObject({
        Body: scarcityLink,
        Bucket: BUCKET_NAME,
        Key: SCARCITY_LINK_KEY,
    }).promise();
}

async function uploadScarcityLedger(scarcityLedger) {
    return await s3.putObject({
        Body: scarcityLedger,
        Bucket: BUCKET_NAME,
        Key: SCARCITY_LEDGER_KEY,
    }).promise();
}

module.exports = {
    getScarcityLink,
    getScarcityLedger,
    uploadScarcityLink,
    uploadScarcityLedger,
}

This will be a new file in our project that we’ll save as s3.js.

Finally, let’s put together a handler function for our Lambda. This is the function that Lambda will call, and it calls all our previous functions: it will get the link and ledger from S3, check whether the current Scarcity link is valid, compute the new Scarcity link, and then save the updated link and ledger to S3.

const moment = require('moment');
const s3 = require('./s3');

const LINK_PREFIX = "http://www.stfj.net/scarcity/";
const DATE_FORMAT = "MMDDYYYY";

exports.handler = async function(event) {
    // check whether the current link is still valid; if so, we can exit
    let scarcityLink = await s3.getScarcityLink();
    if ((await isScarcityLinkValid(scarcityLink))) {
        console.log(`The current link is still valid.`);
        return;
    }
    let scarcityLedger = await s3.getScarcityLedger();

    // compute the next link
    let siteMovedDate = moment.utc().format(DATE_FORMAT);  // assume UTC timezone
    let nextScarcityHash = getNextScarcityHash(scarcityLedger, siteMovedDate);
    let nextScarcityLink = LINK_PREFIX + nextScarcityHash;
    
    // check whether the next link is valid; if not, exit with an error
    if (!(await isScarcityLinkValid(nextScarcityLink))) {
        console.error(`Next link is invalid. MD5 hash: ${nextScarcityHash}`);
        return;
    }

    // update the link and ledger files in S3
    scarcityLedger += `${nextScarcityHash} ${siteMovedDate}\n`;
    await Promise.all([  // upload files concurrently
        s3.uploadScarcityLink(nextScarcityLink),
        s3.uploadScarcityLedger(scarcityLedger),
    ]);
    console.log(`Successfully updated link and ledger. MD5 hash: ${nextScarcityHash}`);
}

Our code is complete! We can now zip up these files and deploy them to a new Lambda function in AWS.

Deployment

After saving this project and deploying it to a Lambda, the next step is to schedule the Lambda to run at periodic intervals. This goal can be accomplished by using CloudWatch Events Rule that triggers on a set schedule.

In particular, we will specify a cron expression that indicates when the function should be run. However, we are missing some crucial information:

  1. It’s not clear what time of day Scarcity moves, or if it always moves at the same time of day. Accordingly, we’ll need to choose a reasonable polling interval that minimizes the amount of time that our link can be outdated.
  2. It’s not clear which time zone Scarcity operates on. The “date the site last moved” is a crucial part of calculating the next link, and this date could be wrong depending on the time zone. For instance, Scarcity might move at 11:59 PM on 09032021, but in another time zone that would be 12:59 AM on 10032021. In the code above, I assumed the correct time zone was UTC.

I chose a 6-hour polling interval, so my Lambda will actually run 4 times throughout a single 24-hour day, and the link should never be outdated for more than 6 hours. I also needed to run the Lambda as close to the end of the day to recognize any changes that had been made on that day (and not the next day). To that end, I ensured that the schedule would trigger at 11:59 PM instead of 12:00 AM. In CloudWatch, this schedule can be expressed with the following cron expression:

59 5/6 ? * * *

After setting up the Lambda and the CloudWatch Events Rule, our automated system is complete.

Results and Troubleshooting

A few days after setting up the Lambda and CloudWatch Events Rule, there was still no change in the Scarcity webpage. I left the Lambda to its own devices for several months before the webpage moved and I was able to verify whether the code was working as intended.

I based my implementation on the sample PHP program provided by Zach Gage. Unfortunately, the sample implementation had a major flaw. Instead of using the date that Scarcity moved from its current URL in the calculations, it used the date of the previous move:

$ledger = fopen("ledger", "r") or die("");

$keystring = "";
$lastModifiedDate = "";

while(!feof($ledger)) {
  $line = fgets($ledger);
  if(strlen($line) > 0)
  {
	$keystring .= $line[0];
    $lastModifiedDate = split(" ", $line)[1];  // <-- this needs to be the current date!
  }
}
fclose($ledger);

$valid = hash("md5", $keystring.$lastModifiedDate, false);

echo "http://stfj.net/scarcity/".$valid;

Once Scarcity moved for the first time, I was able to identify and correct this bug. (The code in the previous section is the fixed version.)

At the time of writing, it has been 285 days since Scarcity was released, and it has moved only 3 times. After having seen the site move, it appears that the site is running on or near the UTC time zone.

The complete, up-to-date ledger and current link to Scarcity are continuously updated by the Lambda, and can be viewed at the following links:

Final Thoughts

I wasn’t the only one to automatically track Scarcity’s movements: @_arp wrote a Twitter bot in Python that tweets whenever the work moves. This bot is open-source, and it was helpful to be able to have another implementation that I could use to check my work.

I set out to make this project thinking that it would be a subversion of Scarcity’s themes, because the program circumvents the explicit need to view Scarcity every day to keep track of it. Now, I realize that thinking about this program over the course of several months has continuously reminded me about Scarcity, frequently prompting me to view the work again - and repeated viewings of Scarcity are very much aligned with its themes.

Another of Scarcity’s themes is that repeated viewings of a work over time cause one’s relationship with that work to change and grow. With this project, I’ve inadvertently done exactly that: Scarcity and its themes have really stuck with me, and I’m keen to see how my relationship with Scarcity will evolve in the future.

Acknowledgements

  • Laura McLean (for proofreading and editing)
  • Zach Gage (for making Scarcity)