Skip to content

Simulated NFT management client for the Kaguya Discord server's 2022 April Fools event.

Notifications You must be signed in to change notification settings

subject-f/nfgt-client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NFGT: Non-Fungible Git Tokens

This document serves to provide an overview of NFGTs.

Client

Getting Started

From Source

  1. Ensure that you have the Go toolchain installed
  2. Clone the repo
  3. Create a config.yaml based on config.example.yaml, replacing it with relevant values
  4. Run the source go run main.go
  5. Transact!

Beacon Repo Setup

The beacon chain should be set up as a repository with a default branch (eg. main, master, etc) that will serve as the epoch branch for new interaction chains. The epoch branch name should then be configured under config.git.epoch_branch in your config.yaml.

Deploy keys can be created with the following command:

ssh-keygen -t ed25519

Follow the interactive prompt to generate the private and public keypair, then navigate to Settings > Deploy keys in your repository to add the public key as a deploy key. Deploy keys with read-only access should be safe to share (they will power observer nodes), while deploy keys with write access should be protected.

Introduction

Why?

Modern NFTs are slow, energy-inefficient, and suffer from a poor PR problem. Many of these issues stem from the distributed consensus protocols that are inherent to cryptocurrency, despite many NFT implementations still suffering from centralization issues. For example, an NFT's metadata and referenced content may be hosted on a traditional server, resulting in a single point of failure from an availability standpoint.

Thus, we will introduce NFGTs, a new standard that serves to provide an NFT-like interface without the overhead of maintaining a cryptographically secure blockchain.

High-Level Overview

At a high level, an NFGT is a JSON record in a single JSON file stored in a git repo. This git repo will be considered the beacon chain that is the synchronization mechanism that nodes will subscribe to and communicate with. The repo acts as a broker for the distributed nodes, which allows nodes to run without exposing themselves to the public internet.

However, because anybody can run a node, the chain must provide a mechanism to validate transactions. Thus, nodes can operate in two modes: (1) as a validator, and (2) as an observer. Node types are differentiated by a mechanism called "proof of identity", which is faciliated by the deploy key they are given as configuration. A node with a read-write deploy key effectively becomes a validator, while a node with a read-only deploy key is an observer. Functionally, a validator and an observer differ only by their ability to write transactions to the chain.

In addition, nodes will provide an HTTP API that can be used to read from and transact on the chain. Some additional security aspects that will be discussed below.

Technical Details

JSON Record

Each commit to the chain is a JSON record that has, at a minimum, the following schema:

{
	"transaction_id": "string",
	"transaction_time": "number",
	"asset_id": "string",
	"owner_id": "string",
	"successor_hash": "string",
	"metadata": {
		...
	}
}

The transaction_id is a UUID generated by the node writing the transaction. This is required in order to provide external clients the ability to check whether their transactions have been written to the chain yet.

The tranaction_time is the time at which the node sees the transaction.

The asset_id is some string identifier for the given record. For example, it could be an image source URL or a UUID.

The owner_id is some string identifier for the owner of the given record. These can be arbitrary identifiers. At a minimum, stable IDs like user IDs should be encoded so that they aren't easily crawled by public search engines, but this is up to the caller to implement.

The successor_hash is a hash of the passphrase required to write a successor to the record.

Finally, the metadata is all metadata related to the record, and is up to the caller to define. Generally, one should expect an image source and owner information such as name and display picture.

The latest commit on a given interaction chain (ie. branch) will be considered the latest transaction, thus that owner_id is the owner of the asset.

Beacon Chain

The beacon chain is the synchronization mechanism between nodes; it is the centralized broker that coordinates distributed state. This differs from traditional cryptocurrencies that use a gossip protocol to communicate.

The chain itself is a git repo hosted on GitHub that creates a new branch for each interaction chain.

An interaction chain is a branch that refers to a specific resource.

Interaction Chains

In git terms, an interaction chain is simply a branch.

In order to coordinate and establish well-known metadata to be discovered, there are several reserved keywords that should not be used for branch naming:

  • well-known
  • common
  • metadata

Clients should also be careful about creating local refs that reference those remotes as they do not hold a guarantee that the history is preserved.

Each interaction chain reference should contain an object named metadata.json that fully describes that specific interaction's data.

Otherwise, interaction chains should be namespaced with the type then resource separated by a dash. For example, an nfgt with a resource ID of abcd should be on a branch named nfgt-abcd.

Committing a Transaction

A transaction is considered committed if the commit is tagged with the transaction ID. This means that a transaction can be written but not considered committed if the commit is in the git repo, but the commit hasn't been tagged yet or pushed to the repository. A transaction isn't considered finalized until a client synchronizes against the upstream.

Why don't we just use the commit?

A commit doesn't have any additional reference to it, so if the local repository creates a commit but is rejected (somebody else transacted on that branch, for example), then the local repository cannot know if the transaction was truly committed without some other action (such as delete that commit object locally).

Thus, consistency is maintained through tag references. Each time a transaction is pushed on a branch, the refspec pushes that commit to the remote reference of that branch, as well as to a tag of its transaction ID. Since we're pushing atomically, if somebody else transacted on that branch between the last sync then we should expect the entire push to be rejected (so the tag is also rejected).

During the next sync, the tag won't be retrieved from the remote and the client will not be able to confirm the transaction and update its cached state.

Immutability

Immutability will be enforced by requiring linear history on GitHub, and disabling force pushes. This does not prevent leaked deploy keys from wreaking havoc on the chain state, so it's important to keep deploy keys safe.

Proof-of-Identity

Proof-of-identity is facilitated by deploy keys offered by GitHub. As described in the high-level overview, read-write deploy keys allow nodes to act as validators, while read-only deploy keys allow nodes to be observers.

HTTP API

The API has the following routes:

  • POST /api/transaction/create - create a transaction. Takes a json with an owner_id, asset_id, passphrase, and metadata

  • GET /api/transaction/status/:transactionId - checks the status of a transaction. This should be performed after the creation to poll the status

  • GET /api/transaction/detail/:transactionId - gets transaction details

  • GET /api/query/spot/owner/:ownerId - the owner's current assets

  • GET /api/query/spot/asset/:assetId - the asset's current metadata (ie. its latest transaction)

  • GET /api/query/history/owner/:ownerId/:depth - the owner's transaction history up to :depth

  • GET /api/query/history/asset/:assetId/:depth - the asset's transaction history up to :depth

Response codes are standardized -- 400 means you did something wrong, 500 means the server had an issue and that you should try again.

Introspection

These endpoints shouldn't be used on a validator node, and should really only be used every once in a while (especially since there's no pagination).

  • GET /api/introspection/transactions - returns a list of verified transactions
  • GET /api/introspection/owners - returns a mapping from all owner IDs to a list of their asset IDs
  • GET /api/introspection/assets - returns a mapping from an asset ID to its owner ID

Integrations

Integration with AiBot

AiBot's host will likely run a validator that will process the majority of transactions. It will provide an interface that allows users to mint NFGTs, as well as the ability to automatically manager successor passphrases to facilitate trading. Note that the concept of GuyaCoin is unknown to the NFGT technical universe, so AiBot should manage these transactions in a layer-2 fashion to the base git layer.

Transactions between AiBot and users (such as for Guyacha) can be facilitated by transacting between the bot's user ID and a user's user ID. When a user rolls for an NFT, AiBot should check its inventory with the node and trigger a transaction if AiBot owns the NFT. Otherwise, a soft error path should be provided to reroll a given result.

Integration with Guya.moe

The chains will have a stable URL through the format of https://raw.githubusercontent.com/${user}/${repo}/${reference}/${file}, which also returns proper CORS headers. Thus, the reader can simply generate the URL to check for a given image's metadata, returning and rendering its ownership (if any).

Stale Data

By default, the GitHub CDN caches raw contents and ignores query parameters (ie. so we can't add ?cache_buster=123 to the URL) for a relatively long duration. This means that users won't be able to see their ownership reflected on Guya.moe until the cache is reset.

However, GitHub respects revision selections (nb. to an extent; it depends on your reference name) in their URL resolution. This means that we can use ancestry selectors like ~ and ^ to navigate from a particular reference, both of which can be chained together. For example, if you have a branch named some-branch and you want the commit before its HEAD then you can use some-branch~1 as ${reference} in the URL above. In addition, some-branch and some-branch~0 point to the same reference.

Since we have 2 ancestry selectors to work with, we can encode our cache buster (which normally would be some random number) as its binary string instead. Putting this altogether, this means that we can map 0 to ^0 and 1 to ~0 in that binary string and append that to the end of the reference in the URL.

For example:

...
const cacheBuster = Math.random()
	.toString(2)
	.split(".")[1]
	.split("")
	.map(e => {"0": "^0", "1": "~0"})
	.join("");

const freshGithubUrl = `https://raw.githubusercontent.com/${user}/${repo}/${reference}${cacheBuster}/${file}`

The cardinality of such strings is large enough that the majority of clients will be able to bust the cache.

About

Simulated NFT management client for the Kaguya Discord server's 2022 April Fools event.

Resources

Stars

Watchers

Forks

Languages