Skip to main content

Proof-of-concept + proof-of-work - consensus = easycoin.

Project description

EasyCoin

This is a toy created for research and experimentation. Maybe it will become something useful, but until then it will at least be amusing to build.

This project is meant to be a functional demonstration of the UTXO/coin model of Bitcoin and the earlier "Making a Mint" paper published by the NSA, using the 2010 proposal by Red in a post titled "Not a Suggestion" as inspiration. (Place the emphasis on the "fun" part of "functional demonstration".)

❌ Bitcoin uses proof-of-work consensus.

❌ Ethereum uses proof-of-stake consensus.

✅ EasyCoin uses proof-of-concept consensus.

Status

  • Core system
  • CUI with Textual
  • Single-player/offline mode
  • Create/activate a GameSet with Txns, Coins, Inputs, and Outputs
  • Restore local db from backups made prior to activating a GameSet
  • Multi-player/online mode (network protocol)
  • TrustNet functionality
  • Headless relay mode

Issues for the project can be found here.

Historical changes can be found in the changelog.

Installation and Quick Start

To use this package, install with pip/uv and include the optional cui dependency:

pip install easycoin[cui]

Then, launch the Textual CUI with the --interactive CLI parameter:

easycoin --interactive [--debug]

Or launch the headless relay:

easycoin --daemon [--debug]

(Requires Python 3.10+. Networking may not currently work in Windows due to UDP multicast issues.)

Basic Exploration

When you launch the CUI, there will be a welcome message and help info available on every screen. The CUI exposes a lot of information and functionality, but I took care to organize it in a way that makes sense and should help build an intuition about the topic.

Playing a Game

To begin solving puzzles from a published GameSet, download the GameSet zip file, launch the CUI in that directory, create a wallet and an address, go to the Settings screen, and click the "Activate Gameset" button. It will require inputting the 72-char hexadecimal checksum (sha256 of the file + first 4 bytes of the double-hash). If the checksum is valid and matches the file, you will be able to activate the GameSet and replace the tables for txns, coins, inputs, and outputs. Then go to the Coins screen to view the coins and click the "Associate with Wallet" for any coin you want to claim by solving its puzzle.

From there, it is just a matter of constructing transactions to move coins to an address in your active wallet. Explore, experiment, poke around in the REPL, and have fun. And don't forget your wallet password, since it is only kept unlocked in-memory; the database record is encrypted.

Conceptual Overview

Coins are conceptually chains of digital signatures, but we will use tapescript to make it a bit more dynamic (much like how Satoshi wrote a script language in Bitcoin without mentioning it in the whitepaper). For deterministic serialization of data, we will use packify. Instead of storing the full data for every transaction and coin forever, we store only the STXO (spend txn outputs i.e. input hash) and UTXO (unspent txn output hash) sets and the coins held by our wallet. Since input and output hashes commit to all coin data, we can validate by requiring the coin data for all inputs and outputs of a transaction, ensure that the hashes match the provided data, validate that the inputs are part of the UTXO set (coins were properly created and have not yet been spent), and that the witness data of the transaction validates against the locks on the input coins.

The native unit of account is the Inverse Energy Credit (EC⁻¹).

The Inverse Energy Credit is a receipt proving that you burned energy doing cryptographic computations. When you want others to also spend some energy doing cryptographic computations to verify your transactions, you relinquish an amount of EC⁻¹ in the transaction. The purpose of this is to prevent someone from spamming the network and DoSing nodes. In blockchain systems, this is sometimes referred to as "burning gas fees".

To generate a fresh coin, the node solves a tapehash PoW challenge specifying a lock, an amount, a Unix epoch timestamp, and a nonce. The difficulty target will be (coin amount + txn fee overhead) divided by 1000 + a minimum difficulty parameter (currently 128). This fresh coin is then broadcast to the network; each receiving node validates 1) that the timestamp is not more than a small threshold in the past, 2) the PoW meets the threshold, and 3) that the coin has not already been spent; and the hash of the coin (its ID) is added to the set of UTXOs. Once the trust net features are added, nodes will create and broadcast an attestation/countersignature claiming that the txn is valid, and the trust net will periodically bundle the sets of inputs and outputs into checkpoints.

To spend a coin, the owner of the coin creates new coins with the same format: lock, amount, timestamp, and nonce. However, since the new coins were not made with PoW, they must be bundled in a transaction with witness data that satisfies the funding coins' locks and commits to the new output hashes. This transaction is then broadcast to the network, and the receiving nodes run the following validation: 1) if the inputs are not members of the UTXO set, fail; 2) if the sum of the output amounts is greater than the sum of the inputs + the required EC⁻¹ burn, fail; 3) if any of the input locks are not satisfied by the witness data, fail; 4) if any stamp constraints (see below) are not satisified by the witness data, fail. Once the transaction is verified, the funding coins are removed from the UTXO set, and the new coins are added to the UTXO set. (Transaction fees are burned and will be set at 1 per byte of txn data.)

Preventing double spending will be accomplished by maintaining a log of input hashes (i.e. spent coin IDs) and simply adding an input hash whenever a txn is received and validated in the absence of a TrustNet configuration. With a TrustNet configured, that TrustNet's consensus mechanism for validating transactions and checkpointing the STXO and UTXO sets will be used.

The details of a transaction can be discarded after verification by all but the participants in the transaction, since the outputs cannot be spent without this data, but the transaction data itself is of no use to anyone else -- the other participants of a TrustNet need only track the inputs and outputs to prevent double spending.

EasyCoins can have data and code stamped onto them.

After mining a new output, the EC⁻¹ can be spent/burned to create a stamped coin. Stamps are created by adding the following data to the Coin.details dict of the funded output:

coin.details = {
    'n': "Stamp note/name/number/nonce", # str|int|bytes
    # all the rest are optional
    'd': {'data': 'dictionary'}, # dict[str, str|int|bool|bytes]
    'L': tapescript.Script.from_src("<tapescript lock for new Stamps">).bytes,
    '_': tapescript.Script.from_src("<tapescript coin lock prefix">).bytes,
    '$': tapescript.Script.from_src("<tapescript coin lock postfix">).bytes,
}

Additionally, there are two derived values:

  • coin.stamp_id: 32 bytes sha256 of all details; equivalent to "so_det" in runtime
  • coin.dsh: 32 bytes sha256 of data (details['d']) and scripts ('L', '_', and '$')

This can be done with the Coin.stamp method:

details = {
    'd': {'type': 'token', 'name': '$HIT coin'}
}
stamp = Coin.stamp(lock, amount, n, details, net_id, net_state)

Stamped details carry covenants.

The lock postfix in coin.details['$'] can be used to enforce a covenant. All covenants must use _VERIFY ops to abort witness validation: since a valid witness script will cause the lock to evalute to a single true value on the stack, covenants must be constructed such that the result of executing the witness and lock is not altered if the covenant constraints are followed.

When the stamped coin is sent in a transaction, validation will call tapescript.run_auth_scripts([prefix, witness, lock, postfix], ...), which will cause validation to fail if the witness does not satisfy the lock or if the postfix raises a verification exception (e.g. calling OP_VERIFY on anything but a True value).

By default, Stamps can only be transferred whole.

In the basic case in which the '$' script is not present, each Stamp can be held by only one output, and this is done by evaluating a default tapescript lock postfix during validation with the following form:

get_value s"so_len" dup push d1 swap2 leq verify
if ( push d1 eq ) {
    get_value s"so_det" get_value s"ii_det" equal_verify
}

This pulls the "so_len" value from the tapescript runtime onto the stack, which is the integer count of stamped outputs in the transaction. It then duplicates it, pushes the integer 1 onto the stack, swaps the order, and runs OP_LESS_OR_EQUAL and VERIFY, which fails if "so_len" is not less than or equal to 1. This is a covenant that requires that there can be at most one stamped output in the transaction that sends this Stamp. (Note that this allows destroying std stamps.)

It then compares the "so_len" value to 1 and executes a further check if they are equal: it pulls the "so_det" value from the tapescript runtime onto the stack, which is the sha256 hash of the output_coin.details serialized with packify.pack (i.e. output_coin.stamp_id); pushes the sha256 of the input_coin.details serialized with packify.pack onto the stack (i.e. input_coin.stamp_id); then runs OP_EQUAL_VERIFY. This is a covenant that requires that the Stamp details be copied without alteration from the input coin to the new output coin.

Stamps can be created in a series identified by the data-script-hash.

It is possible to create fungible stamps in a series, which is where the sha256 of the data and embedded scripts is the same for all such stamps. To use these with an integer amount that must be conserved or burnt in transactions (except for special stamp issuance transactions that validate against the 'L' script), the covenant should ensure 1) that all stamped inputs have the same data-script-hash, and 2) that the sum of the 'n' values of stamped outputs is less than or equal to the sum of the 'n' values of stamped inputs.

Thus, the '$' script should have the following form:

# set some varibables #
get_value s"si_len" @= il 1
get_value s"ii_dsh" @= s 1
get_value s"so_len" @= ol 1

# ensure all stamped inputs are from the same series #
get_value s"si_dsh"
@il loop {
    push d-1 add_ints d2 @= i
    @s equal_verify
    @i
} pop0

# ensure all stamped outputs are from the same series #
get_value s"so_dsh"
@ol loop {
    push d-1 add_ints d2 @= i
    @s equal_verify
    @i
} pop0

# calculate the sum of stamped inputs #
get_value s"si_n" push d0
@il loop {
    push d-1 add_ints d2 @= i
    add_ints d2
    @i
} pop0

# calculate the sum of stamped outputs #
get_value s"so_n" push d0
@ol loop {
    push d-1 add_ints d2 @= i
    add_ints d2
    @i
} pop0

# ensure stamped output sum <= stamped input sum #
leq verify

The CUI interface supports creation of unique stamps and fungible stamp series, and it allows experimentation with custom covenants, mint locks, and prefix scripts.

NB: In practice, the loops are abstracted into functions defined in the prefix script to shave a few bytes in most use cases. Examine the Stamp Templates screen for the most up-to-date version of the standard scripts.

NB2: This covenant will become significantly more efficient after the tapescript 0.8.0 update changes how OP_ADD_INTS functions (final 2 loops will be replaced with @il add_ints and @ol add_ints).

Stamp series can also have constraints for initial stamping.

If a stamp's details include 'L', validation of the mint transaction will execute that tapescript lock. This will occur only when there are no input stamps with the same dsh. Note that the result of executing the 'L' script should result in a single true value on the stack for valid stamping and anything else for an invalid stamping.

The tapescript runtime cache will have the following serialized values:

  • "i_len": int number of inputs
  • "i_a": input amounts (in EC⁻¹)
  • "si_len": int number of stamped inputs
  • "si_det": stamp id for every stamped input_coin
  • "ii_det": stamp id for current input_coin
  • "si_dsh": sha256 of data and scripts in input_coin.details for every input_coin
  • "ii_dsh": sha256 of data and scripts in input_coin.details for current input_coin
  • "si_n": input_coin.details['n'] for every stamped input_coin
  • "ii_n": input_coin.details['n'] for current input_coin
  • "o_len": int number of outputs
  • "o_a": output amounts (in EC⁻¹)
  • "so_len": int number of stamped outputs
  • "so_det": stamp id for every stamped output_coin
  • "so_dsh": sha256 of data and scripts output_coin.details for every stamped output_coin
  • "so_n": output_coin.details['n'] for every stamped output_coin
  • "sigfield1": auth scope (i.e. b'Txn')
  • "sigfield2": the current coin hash (input coin or output stamp)
  • "sigfield3": sha256 of all input hashes, sorted and concatenated
  • "sigfield4": sha256 of all output hashes, sorted and concatenated
  • "timestamp": int Unix epoch timestamp (automatically added by tapescript)

The current input_coin is the coin whose lock is being evaluated, which may be a newly stamped output if it has an 'L' script.

The wallet implementation is hierarchical, deterministic, and password-protected.

The Wallet.seed will be derived from a seed phrase and encrypted before being saved. Encryption and decryption will be done with a key derived from a password, and the hash of the decrypted seed will be saved as a checksum to verify during wallet unlocking.

Future Development/Experimentation Plans

Future developments/experiments include the following:

  1. Explicit trust nets
  2. Overlay network with SpeedyMurmurs for routing between overlapping networks
  3. Novel consensus mechanisms
  4. Attacks against all of the above systems

Statement Concerning Generative AI Use

Though initially attempted, no vibes were achieved in the creation of this code. The use of LLMs was limited to primarily commit messages, and secondarily review and Textual CUI scaffolding. At least 99% of the logic created by AIs was wrong and had to be replaced; the UI had to be reorganized and pixel-pushed; many hallucinations had to be excised. The AGENTS.md file in the repo is the scar that bears witness to the many issues that occurred before I found a balance where it was somewhat useful.

All core logic is artisanal, free-range, organic, hand-crafted code. Less than 1% of the code in the underlying libraries that I maintain came from AI, and all of that was rewritten before being published anyway.

Testing

This project contains 129 tests, a mix of unit and integration tests. The underlying packages I maintain have many hundreds more.

To set up, clone the repository and run the following:

python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Then run the tests with find tests -name test_*.py -print -exec python {} \;.

Tests across LANs/the Internet are done manually.

ISC License

Copyright (c) 2026 Jonathan Voss (k98kurz)

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

easycoin-0.0.2.tar.gz (871.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

easycoin-0.0.2-py3-none-any.whl (183.1 kB view details)

Uploaded Python 3

File details

Details for the file easycoin-0.0.2.tar.gz.

File metadata

  • Download URL: easycoin-0.0.2.tar.gz
  • Upload date:
  • Size: 871.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for easycoin-0.0.2.tar.gz
Algorithm Hash digest
SHA256 31731892910a3a71bbf079e6c34bfa3ef5edbc35ec3aaa75518707b5be8e900a
MD5 7e081d0a3b631a5663a77c8840cc8ffd
BLAKE2b-256 60aa5ce295a7a529cdb03ebf43f0f5a57cc47133a6ed9763e46d86056a910c6b

See more details on using hashes here.

File details

Details for the file easycoin-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: easycoin-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 183.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for easycoin-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 b9ac416bcafdd2c80b07eb3a0556a75662607d597d29d258808834fbcdf21e1a
MD5 6acd0ef529ec1226a063b98b51d4786d
BLAKE2b-256 54266c4b567394616a1db0be2abac4df42a0ebb7d77536768f0e8727b43e999e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page