tapehash 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ */__pycache__
2
+ *.swp
3
+ venv/
4
+ temp/
5
+ dist/
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: tapehash
3
+ Version: 0.1.0
4
+ Summary: Simple proof-of-work system that uses many hash algorithms.
5
+ Project-URL: Homepage, https://github.com/k98kurz/tapehash
6
+ Project-URL: Repository, https://github.com/k98kurz/tapehash
7
+ Project-URL: Bug Tracker, https://github.com/k98kurz/tapehash/issues
8
+ Author-email: k98kurz <k98kurz@gmail.com>
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security :: Cryptography
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # tapehash
19
+
20
+ Tapehash is a slapdash proof-of-work system that uses a lot of different hash
21
+ algorithms and an opcode execution system for ASIC-resistance. This is meant to
22
+ be a good-enough solution for proof-of-concept systems.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install tapehash
28
+ ```
29
+
30
+ An overview follows, but full documentation can be found
31
+ [here](https://github.com/k98kurz/tapehash/blob/master/dox.md), generated
32
+ automatically by [autodox](https://pypi.org/project/autodox).
33
+
34
+ ## Functions
35
+
36
+ This package contains three tuneable hashing functions:
37
+
38
+ - `tapehash1(preimage: bytes, code_size: int = 20) -> bytes:`
39
+ - `tapehash2(preimage: bytes, tape_size_multiplier: int = 2) -> bytes:`
40
+ - `tapehash3(preimage: bytes, code_size: int = 64, tape_size_multiplier: int = 2) -> bytes:`
41
+
42
+ All three have defaul parameters tuned to require about 0.25-0.3 ms per hash
43
+ (roughly 250-300x as long as sha256 on the reference hardware).
44
+
45
+ It also includes the following proof-of-work functions:
46
+
47
+ - `work(state: HasNonceProtocol, serialize: Callable, difficulty: int, hash_algo: Callable) -> HasNonceProtocol:`
48
+ - `calculate_difficulty(val: bytes) -> int:`
49
+ - `calculate_target(difficulty: int) -> int:`
50
+ - `check_difficulty(val: bytes, difficulty: int) -> bool:`
51
+
52
+ Difficulty is calculated by dividing `2**256` by the big-endian int value of a
53
+ hash, which produces the expected average number of tries to find that hash or
54
+ better (smaller). It is a linear difficulty metric. A target for a given
55
+ difficulty level can then be calculated by dividing `2**256` by the difficulty.
56
+ The `work` function first calculates this target, then increments the `nonce`
57
+ property of `state` until the `hash_algo(serialize(state))` is less than the
58
+ calculated target (unsigned int comparison).
59
+
60
+ ## Use
61
+
62
+ To use for proof-of-work/hashcash, create a class that follows the
63
+ `HasNonceProtocol` contract (must have a settable `nonce` property) and a
64
+ serialization function, then invoke `work` to find a nonce that meets the
65
+ difficulty threshold. To check a received value, serialize it, hash it, and then
66
+ call `check_difficulty`.
67
+
68
+ For example, it could be used to filter out spam as in the original hashcash proposal:
69
+
70
+ ```python
71
+ from dataclasses import dataclass, field
72
+ from tapehash import tapehash3, work, check_difficulty, calculate_difficulty
73
+ from packify import pack, unpack
74
+
75
+ _default_diff_threshold = 128 # average number of attempts to find a nonce
76
+
77
+ @dataclass
78
+ class Message:
79
+ message: str = field()
80
+ nonce: int = field(default=0)
81
+ def pack(self) -> bytes:
82
+ return pack((self.message, self.nonce))
83
+ @classmethod
84
+ def unpack(cls, data: bytes) -> 'Message':
85
+ return cls(*unpack(data))
86
+ def find_nonce(self, difficulty: int = _default_diff_threshold):
87
+ work(self, lambda m: m.pack(), difficulty, tapehash3)
88
+ def hash(self) -> bytes:
89
+ return tapehash3(self.pack())
90
+ def check(self, difficulty: int = _default_diff_threshold) -> bool:
91
+ return check_difficulty(self.hash(), difficulty)
92
+ def difficulty(self) -> int:
93
+ return calculate_difficulty(self.hash())
94
+ @classmethod
95
+ def make(cls, msg: str, difficulty: int = _default_diff_threshold) -> 'Message':
96
+ m = cls(msg)
97
+ m.find_nonce(difficulty)
98
+ return m
99
+
100
+ def filter(messages: list[Message], difficulty: int = _default_diff_threshold) -> list[Message]:
101
+ """Filter out spam messages that do not reach the difficulty threshold."""
102
+ return [m for m in messages if m.check(difficulty)]
103
+ ```
104
+
105
+ Then, a message that has the required difficulty can be generated by calling
106
+ `Message.make`, and a batch of received messages can be verified and filtered
107
+ using the `filter` function.
108
+
109
+ This example uses my serialization package [packify](https://pypi.org/project/packify)
110
+ for convenience and brevity. Any serialization would do, and a real system would
111
+ benefit from an optimized serialization function rather than a generalized one.
112
+
113
+ ## CLI
114
+
115
+ Installing this package provides three CLI commands:
116
+
117
+ - tapehash1
118
+ - tapehash2
119
+ - tapehash3
120
+
121
+ These three commands function in the same way, with only the algorithm tuning
122
+ parameters being somewhat different:
123
+
124
+ - All 3 accept a `--help` or `-h` flag to print help text
125
+ - All 3 accept a preimage from stdin, through a `--preimage {string}` parameter,
126
+ or through a `--file {path}` parameter
127
+ - NB: `cat file.txt | tapehashX` is equivalent to `tapehashX --file file.txt`
128
+ - All 3 accept a `--from:hex` flag that will parse the preimage as hexadecimal
129
+ - All 3 accept these optional output flags (only 1 at a time):
130
+ - `--to:raw` will output the digest as raw bytes
131
+ - `--difficulty` will output the calculated difficulty score of the hash digest
132
+ - `--check {int}` will output 1 if the digest meets the difficulty threshold
133
+ {int} or 0 (and an exit code of 2) if it did not
134
+ - Default behavior is to print the hexadecimal digest
135
+ - tapehash1 and tapehash3 accept `--code_size {int}` or `-cs {int}` tuning
136
+ parameter (default=20 for tapehash1 and 64 for tapehash3)
137
+ - tapehash2 and tapehash3 accept `--tape_size_multiplier {int}` or `-tsm {int}`
138
+ tuning parameter (default=2)
139
+
140
+ An improper invocation will result in an exit value of 1. A proper invocation
141
+ will result in an exit code of 0 in all cases except when `--check {int}` exits
142
+ with 2.
143
+
144
+ ## Testing
145
+
146
+ Testing has thus far been done manually. I may add a unit test suite at some
147
+ point, but it seems unnecessary for now.
148
+
149
+ ## License
150
+
151
+ ISC License
152
+
153
+ Copyleft (c) 2025 Jonathan Voss (k98kurz)
154
+
155
+ Permission to use, copy, modify, and/or distribute this software
156
+ for any purpose with or without fee is hereby granted, provided
157
+ that the above copyleft notice and this permission notice appear in
158
+ all copies.
159
+
160
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
161
+ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
162
+ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
163
+ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
164
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
165
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
166
+ NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
167
+ CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
168
+
tapehash-0.1.0/dox.md ADDED
@@ -0,0 +1,71 @@
1
+ # tapehash
2
+
3
+ ## Classes
4
+
5
+ ### `HasNonceProtocol(Protocol)`
6
+
7
+ The HasNonceProtocol requires that an implementation has a settable nonce
8
+ property.
9
+
10
+ #### Properties
11
+
12
+ - nonce
13
+
14
+ ## Functions
15
+
16
+ ### `tapehash1(preimage: bytes, code_size: int = 20) -> bytes:`
17
+
18
+ Runs the tapehash 1 algorithm on the preimage and returns a 32-byte hash.
19
+ Computational complexity is tuneable via the `code_size` parameter.
20
+
21
+ ### `tapehash2(preimage: bytes, tape_size_multiplier: int = 2) -> bytes:`
22
+
23
+ Runs the tapehash2 algorithm on the preimage and returns a 32-byte hash. Memory
24
+ complexity can be tuned via the `tape_size_multiplier` parameter.
25
+
26
+ ### `tapehash3(preimage: bytes, code_size: int = 64, tape_size_multiplier: int = 2) -> bytes:`
27
+
28
+ Runs the tapehash3 algorithm on the preimage and returns a 32-byte hash.
29
+ Computational complexity is tuneable via the `code_size` parameter. Memory
30
+ complexity is tuneable via the `tape_size_multiplier` parameter.
31
+
32
+ ### `license():`
33
+
34
+ Copyright (c) 2025 Jonathan Voss (k98kurz) Permission to use, copy, modify,
35
+ and/or distribute this software for any purpose with or without fee is hereby
36
+ granted, provided that the above copyright notice and this permission notice
37
+ appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS
38
+ ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
39
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
40
+ SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
41
+ RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
42
+ NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
43
+ USE OR PERFORMANCE OF THIS SOFTWARE.
44
+
45
+ ### `work(state: HasNonceProtocol, serialize: Callable, difficulty: int, hash_algo: Callable) -> HasNonceProtocol:`
46
+
47
+ Continually increments `state.nonce` until the difficulty score of
48
+ `hash_algo(serialize(state))` >= target, then returns the updated state.
49
+
50
+ ### `calculate_difficulty(val: bytes) -> int:`
51
+
52
+ Calculates the difficulty of a hash by dividing 2**256 (max int) by the supplied
53
+ val interpreted as a big-endian unsigned int. This provides a linear metric that
54
+ represents the expected amount of work (hashes) that have to be computed on
55
+ average to reach the given hash val or better (lower).
56
+
57
+ ### `calculate_target(difficulty: int) -> int:`
58
+
59
+ Calculates the target value that a hash must be below to meet the difficulty
60
+ threshold.
61
+
62
+ ### `check_difficulty(val: bytes, difficulty: int) -> bool:`
63
+
64
+ Returns True if the val has a difficulty score greater than or equal to the
65
+ supplied difficulty, otherwise False.
66
+
67
+ ### `version() -> str:`
68
+
69
+ Returns the current library version.
70
+
71
+
tapehash-0.1.0/license ADDED
@@ -0,0 +1,18 @@
1
+ ISC License
2
+
3
+ Copyleft (c) 2025 Jonathan Voss (k98kurz)
4
+
5
+ Permission to use, copy, modify, and/or distribute this software
6
+ for any purpose with or without fee is hereby granted, provided
7
+ that the above copyleft notice and this permission notice appear in
8
+ all copies.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
11
+ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
12
+ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
13
+ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
14
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
15
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
16
+ NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
17
+ CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18
+
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tapehash"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="k98kurz", email="k98kurz@gmail.com" },
10
+ ]
11
+ description = "Simple proof-of-work system that uses many hash algorithms."
12
+ readme = "readme.md"
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: ISC License (ISCL)",
18
+ "Operating System :: OS Independent",
19
+ "Intended Audience :: Developers",
20
+ "Topic :: Security :: Cryptography",
21
+ ]
22
+
23
+ [project.urls]
24
+ "Homepage" = "https://github.com/k98kurz/tapehash"
25
+ "Repository" = "https://github.com/k98kurz/tapehash"
26
+ "Bug Tracker" = "https://github.com/k98kurz/tapehash/issues"
27
+
28
+ [project.scripts]
29
+ tapehash1 = "tapehash.cli:run_tapehash1"
30
+ tapehash2 = "tapehash.cli:run_tapehash2"
31
+ tapehash3 = "tapehash.cli:run_tapehash3"
32
+
@@ -0,0 +1,151 @@
1
+ # tapehash
2
+
3
+ Tapehash is a slapdash proof-of-work system that uses a lot of different hash
4
+ algorithms and an opcode execution system for ASIC-resistance. This is meant to
5
+ be a good-enough solution for proof-of-concept systems.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install tapehash
11
+ ```
12
+
13
+ An overview follows, but full documentation can be found
14
+ [here](https://github.com/k98kurz/tapehash/blob/master/dox.md), generated
15
+ automatically by [autodox](https://pypi.org/project/autodox).
16
+
17
+ ## Functions
18
+
19
+ This package contains three tuneable hashing functions:
20
+
21
+ - `tapehash1(preimage: bytes, code_size: int = 20) -> bytes:`
22
+ - `tapehash2(preimage: bytes, tape_size_multiplier: int = 2) -> bytes:`
23
+ - `tapehash3(preimage: bytes, code_size: int = 64, tape_size_multiplier: int = 2) -> bytes:`
24
+
25
+ All three have defaul parameters tuned to require about 0.25-0.3 ms per hash
26
+ (roughly 250-300x as long as sha256 on the reference hardware).
27
+
28
+ It also includes the following proof-of-work functions:
29
+
30
+ - `work(state: HasNonceProtocol, serialize: Callable, difficulty: int, hash_algo: Callable) -> HasNonceProtocol:`
31
+ - `calculate_difficulty(val: bytes) -> int:`
32
+ - `calculate_target(difficulty: int) -> int:`
33
+ - `check_difficulty(val: bytes, difficulty: int) -> bool:`
34
+
35
+ Difficulty is calculated by dividing `2**256` by the big-endian int value of a
36
+ hash, which produces the expected average number of tries to find that hash or
37
+ better (smaller). It is a linear difficulty metric. A target for a given
38
+ difficulty level can then be calculated by dividing `2**256` by the difficulty.
39
+ The `work` function first calculates this target, then increments the `nonce`
40
+ property of `state` until the `hash_algo(serialize(state))` is less than the
41
+ calculated target (unsigned int comparison).
42
+
43
+ ## Use
44
+
45
+ To use for proof-of-work/hashcash, create a class that follows the
46
+ `HasNonceProtocol` contract (must have a settable `nonce` property) and a
47
+ serialization function, then invoke `work` to find a nonce that meets the
48
+ difficulty threshold. To check a received value, serialize it, hash it, and then
49
+ call `check_difficulty`.
50
+
51
+ For example, it could be used to filter out spam as in the original hashcash proposal:
52
+
53
+ ```python
54
+ from dataclasses import dataclass, field
55
+ from tapehash import tapehash3, work, check_difficulty, calculate_difficulty
56
+ from packify import pack, unpack
57
+
58
+ _default_diff_threshold = 128 # average number of attempts to find a nonce
59
+
60
+ @dataclass
61
+ class Message:
62
+ message: str = field()
63
+ nonce: int = field(default=0)
64
+ def pack(self) -> bytes:
65
+ return pack((self.message, self.nonce))
66
+ @classmethod
67
+ def unpack(cls, data: bytes) -> 'Message':
68
+ return cls(*unpack(data))
69
+ def find_nonce(self, difficulty: int = _default_diff_threshold):
70
+ work(self, lambda m: m.pack(), difficulty, tapehash3)
71
+ def hash(self) -> bytes:
72
+ return tapehash3(self.pack())
73
+ def check(self, difficulty: int = _default_diff_threshold) -> bool:
74
+ return check_difficulty(self.hash(), difficulty)
75
+ def difficulty(self) -> int:
76
+ return calculate_difficulty(self.hash())
77
+ @classmethod
78
+ def make(cls, msg: str, difficulty: int = _default_diff_threshold) -> 'Message':
79
+ m = cls(msg)
80
+ m.find_nonce(difficulty)
81
+ return m
82
+
83
+ def filter(messages: list[Message], difficulty: int = _default_diff_threshold) -> list[Message]:
84
+ """Filter out spam messages that do not reach the difficulty threshold."""
85
+ return [m for m in messages if m.check(difficulty)]
86
+ ```
87
+
88
+ Then, a message that has the required difficulty can be generated by calling
89
+ `Message.make`, and a batch of received messages can be verified and filtered
90
+ using the `filter` function.
91
+
92
+ This example uses my serialization package [packify](https://pypi.org/project/packify)
93
+ for convenience and brevity. Any serialization would do, and a real system would
94
+ benefit from an optimized serialization function rather than a generalized one.
95
+
96
+ ## CLI
97
+
98
+ Installing this package provides three CLI commands:
99
+
100
+ - tapehash1
101
+ - tapehash2
102
+ - tapehash3
103
+
104
+ These three commands function in the same way, with only the algorithm tuning
105
+ parameters being somewhat different:
106
+
107
+ - All 3 accept a `--help` or `-h` flag to print help text
108
+ - All 3 accept a preimage from stdin, through a `--preimage {string}` parameter,
109
+ or through a `--file {path}` parameter
110
+ - NB: `cat file.txt | tapehashX` is equivalent to `tapehashX --file file.txt`
111
+ - All 3 accept a `--from:hex` flag that will parse the preimage as hexadecimal
112
+ - All 3 accept these optional output flags (only 1 at a time):
113
+ - `--to:raw` will output the digest as raw bytes
114
+ - `--difficulty` will output the calculated difficulty score of the hash digest
115
+ - `--check {int}` will output 1 if the digest meets the difficulty threshold
116
+ {int} or 0 (and an exit code of 2) if it did not
117
+ - Default behavior is to print the hexadecimal digest
118
+ - tapehash1 and tapehash3 accept `--code_size {int}` or `-cs {int}` tuning
119
+ parameter (default=20 for tapehash1 and 64 for tapehash3)
120
+ - tapehash2 and tapehash3 accept `--tape_size_multiplier {int}` or `-tsm {int}`
121
+ tuning parameter (default=2)
122
+
123
+ An improper invocation will result in an exit value of 1. A proper invocation
124
+ will result in an exit code of 0 in all cases except when `--check {int}` exits
125
+ with 2.
126
+
127
+ ## Testing
128
+
129
+ Testing has thus far been done manually. I may add a unit test suite at some
130
+ point, but it seems unnecessary for now.
131
+
132
+ ## License
133
+
134
+ ISC License
135
+
136
+ Copyleft (c) 2025 Jonathan Voss (k98kurz)
137
+
138
+ Permission to use, copy, modify, and/or distribute this software
139
+ for any purpose with or without fee is hereby granted, provided
140
+ that the above copyleft notice and this permission notice appear in
141
+ all copies.
142
+
143
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
144
+ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
145
+ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
146
+ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
147
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
148
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
149
+ NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
150
+ CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
151
+
@@ -0,0 +1,16 @@
1
+ from .algos import tapehash1, tapehash2, tapehash3, license
2
+ from .work import (
3
+ HasNonceProtocol,
4
+ calculate_difficulty,
5
+ calculate_target,
6
+ check_difficulty,
7
+ work
8
+ )
9
+ del algos
10
+
11
+ __version__ = '0.1.0'
12
+
13
+ def version() -> str:
14
+ """Returns the current library version."""
15
+ return __version__
16
+
@@ -0,0 +1,184 @@
1
+ from hashlib import (
2
+ sha256, md5,
3
+ shake_128, shake_256,
4
+ sha3_256, sha3_512,
5
+ blake2b, blake2s
6
+ )
7
+
8
+
9
+ """New randomized proof-of-work hashing algorithms inspired roughly by
10
+ XMR's RandomX but much simpler and shoddier.
11
+ From https://pastebin.com/xY3B3K8S
12
+ """
13
+
14
+ def tapehash1(preimage: bytes, code_size: int = 20) -> bytes:
15
+ """Runs the tapehash 1 algorithm on the preimage and returns a
16
+ 32-byte hash. Computational complexity is tuneable via the
17
+ `code_size` parameter.
18
+ """
19
+ if type(preimage) is not bytes:
20
+ raise TypeError('preimage must be bytes')
21
+ if type(code_size) is not int:
22
+ raise TypeError('code_size must be an int between 1 and 65,536')
23
+ if code_size <= 0 or code_size > 65_536:
24
+ raise ValueError('code_size must be an int between 1 and 65,536')
25
+
26
+ # generate the code and the tape
27
+ code = shake_256(preimage).digest(code_size)
28
+ tape = bytearray(blake2b(preimage).digest())
29
+
30
+ # run the program
31
+ for i in range(0, len(code)):
32
+ opcode = code[i] >> 4
33
+ #pointer = ((code[i] << 4) % 256) >> 4
34
+ pointer = code[i] & 0b00001111
35
+ double_pointer = int.from_bytes(code[i:i+2], 'big') % 2
36
+
37
+ tape = execute_opcode(opcode, pointer, tape)
38
+ tape = execute_opcode(opcode, pointer + 16, tape)
39
+ tape = execute_opcode(opcode, pointer + 32, tape)
40
+ tape = execute_opcode(opcode, pointer + 48, tape)
41
+
42
+ return sha256(tape).digest()
43
+
44
+
45
+ def tapehash2(preimage: bytes, tape_size_multiplier: int = 2) -> bytes:
46
+ """Runs the tapehash2 algorithm on the preimage and returns a
47
+ 32-byte hash. Memory complexity can be tuned via the
48
+ `tape_size_multiplier` parameter.
49
+ """
50
+ if type(preimage) is not bytes:
51
+ raise TypeError('preimage must be bytes')
52
+ if type(tape_size_multiplier) is not int:
53
+ raise TypeError('tape_size_multiplier must be an int between 1 and 65,536')
54
+ if tape_size_multiplier <= 0 or tape_size_multiplier > 65_536:
55
+ raise ValueError('tape_size_multiplier must be an int between 1 and 65,536')
56
+
57
+ # generate the code and the tape
58
+ code = blake2b(preimage).digest()
59
+ tape = bytearray(shake_256(preimage).digest(tape_size_multiplier * 32))
60
+
61
+ # run the program
62
+ for i in range(0, len(code), 2):
63
+ opcode = code[i] >> 4
64
+ #pointer = ((code[i] << 4) % 256) >> 4
65
+ pointer = code[i] & 0b00001111
66
+ double_pointer = int.from_bytes(code[i:i+2], 'big') % tape_size_multiplier
67
+
68
+ tape = execute_opcode(opcode, pointer + double_pointer * 32, tape)
69
+ tape = execute_opcode(opcode, pointer + 16 + double_pointer * 32, tape)
70
+
71
+ return sha256(tape).digest()
72
+
73
+
74
+ def tapehash3(
75
+ preimage: bytes, code_size: int = 64, tape_size_multiplier: int = 2
76
+ ) -> bytes:
77
+ """Runs the tapehash3 algorithm on the preimage and returns a
78
+ 32-byte hash. Computational complexity is tuneable via the
79
+ `code_size` parameter. Memory complexity is tuneable via the
80
+ `tape_size_multiplier` parameter.
81
+ """
82
+ if type(preimage) is not bytes:
83
+ raise TypeError('preimage must be bytes')
84
+ if type(code_size) is not int:
85
+ raise TypeError('code_size must be an int between 1 and 65,536')
86
+ if code_size <= 0 or code_size > 65_536:
87
+ raise ValueError('code_size must be an int between 1 and 65,536')
88
+ if type(tape_size_multiplier) is not int:
89
+ raise TypeError('tape_size_multiplier must be an int between 1 and 65,536')
90
+ if tape_size_multiplier <= 0 or tape_size_multiplier > 65_536:
91
+ raise ValueError('tape_size_multiplier must be an int between 1 and 65,536')
92
+
93
+ # generate the code and the tape
94
+ code = shake_256(preimage).digest(code_size)
95
+ tape = bytearray(shake_256(preimage).digest(tape_size_multiplier * 32))
96
+
97
+ # run the program
98
+ for i in range(0, len(code), 2):
99
+ opcode = code[i] >> 4
100
+ #pointer = ((code[i] << 4) % 256) >> 4
101
+ pointer = code[i] & 0b00001111
102
+ double_pointer = int.from_bytes(code[i:i+2], 'big') % tape_size_multiplier
103
+
104
+ tape = execute_opcode(opcode, pointer + double_pointer * 32, tape)
105
+ tape = execute_opcode(opcode, pointer + 16 + double_pointer * 32, tape)
106
+
107
+ return sha256(tape).digest()
108
+
109
+
110
+
111
+ def rotate_tape(tape: bytearray, pointer: int) -> bytes:
112
+ """Rotates the tape so that the pointer-indexed byte is first."""
113
+ if not isinstance(tape, bytearray):
114
+ raise TypeError('tape must be bytearray')
115
+ if not isinstance(pointer, int):
116
+ raise TypeError('pointer must be int')
117
+ if len(tape) < 1:
118
+ raise ValueError('tape must not be empty')
119
+ if pointer < 0 or pointer >= len(tape):
120
+ raise ValueError('pointer must be a valid index of tape')
121
+
122
+ new_tape = tape[pointer:]
123
+ new_tape.extend(tape[:pointer])
124
+ return bytes(new_tape)
125
+
126
+
127
+ def execute_opcode(opcode: int, pointer: int, tape: bytearray) -> bytearray:
128
+ """Execute a single opcode."""
129
+ if type(opcode) is not int:
130
+ raise TypeError('opcode must be an int')
131
+ if type(pointer) is not int:
132
+ raise TypeError('pointer must be an int')
133
+ if type(tape) is not bytearray:
134
+ raise TypeError('tape must be bytearray')
135
+ if opcode < 0 or opcode > 15:
136
+ raise ValueError('opcode must be between 0 and 15')
137
+ if pointer < 0 or pointer >= len(tape):
138
+ raise ValueError('pointer must be a valid index of tape')
139
+
140
+ operations = {
141
+ 0: lambda data: data, # no op
142
+ 1: lambda data: (data + 1) % 256,
143
+ 2: lambda data: data - 1 if data > 0 else 255,
144
+ 3: lambda data: data >> 1,
145
+ 4: lambda data: (data << 1) % 256,
146
+ 5: lambda data: data ^ 255,
147
+ 6: lambda data: (data * 2) % 256,
148
+ 7: lambda data: (data ** 2) % 256,
149
+ 8: lambda data: (data // 2) % 256,
150
+ 9: lambda data: ((data << 4) % 256) | (data >> 4),
151
+ 10: lambda data: sha256(data).digest()[data[0] % 32],
152
+ 11: lambda data: md5(data).digest()[data[0] % 16],
153
+ 12: lambda data: shake_128(data).digest(data[0] + 1)[data[0]],
154
+ 13: lambda data: sha3_256(data).digest()[data[0] % 32],
155
+ 14: lambda data: sha3_512(data).digest()[data[0] % 64],
156
+ 15: lambda data: blake2s(data).digest()[data[0] % 32]
157
+ }
158
+
159
+ if opcode < 10:
160
+ tape[pointer] = operations[opcode](tape[pointer])
161
+ else:
162
+ tape[pointer] = operations[opcode](rotate_tape(tape, pointer))
163
+ return tape
164
+
165
+
166
+ def license():
167
+ """Copyright (c) 2025 Jonathan Voss (k98kurz)
168
+
169
+ Permission to use, copy, modify, and/or distribute this software
170
+ for any purpose with or without fee is hereby granted, provided
171
+ that the above copyright notice and this permission notice appear in
172
+ all copies.
173
+
174
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
175
+ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
176
+ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
177
+ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
178
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
179
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
180
+ NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
181
+ CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
182
+ """
183
+ return license.__doc__
184
+
@@ -0,0 +1,270 @@
1
+ from sys import argv, stdin, stdout
2
+ from .algos import tapehash1, tapehash2, tapehash3
3
+ from .work import work, check_difficulty, calculate_difficulty
4
+
5
+
6
+ def help_tapehash1():
7
+ print('Use: command | tapehash1 OR tapehash1 --preimage {str} OR tapehash1 --file path/to/file')
8
+ print('\t--from:hex - parse preimage as hexadecimal')
9
+ print('\t--code_size {int} - set the code_size parameter (default=1024)')
10
+ print('\t-cs {int} - set the code_size parameter (default=1024)')
11
+ print('\t--to:raw - write the raw hash digest bytes to stdout')
12
+ print('\t--difficulty - output the difficulty of the hash digest')
13
+ print('\t--check {int} - outputs 1 if the hash digest meets the ' +
14
+ 'difficulty threshold; outputs 0 and exits with error code 2 '+
15
+ 'if it does not'
16
+ )
17
+ print('\nOnly 1 of --to:raw --difficulty --check can be used at a time')
18
+
19
+ def run_tapehash1() -> None:
20
+ preimage = b''
21
+ code_size = 20
22
+
23
+ if not stdin.isatty():
24
+ preimage = stdin.buffer.read()
25
+
26
+ if len(argv) > 1:
27
+ if '--help' in argv or '-h' in argv:
28
+ return help_tapehash1()
29
+
30
+ if '--preimage' in argv:
31
+ idx = argv.index('--preimage')
32
+ if len(argv) < idx + 2:
33
+ print("must supply data after --preimage")
34
+ exit(1)
35
+ preimage = argv[idx+1].encode('utf-8')
36
+
37
+ if '--file' in argv:
38
+ idx = argv.index('--file')
39
+ if len(argv) < idx + 2:
40
+ print("must supply path after --file")
41
+ exit(1)
42
+ with open(argv[idx+1], 'rb') as f:
43
+ preimage = f.read()
44
+
45
+ if '--from:hex' in argv:
46
+ preimage = bytes.fromhex(preimage.decode('utf-8'))
47
+
48
+ if '--code_size' in argv:
49
+ idx = argv.index('--code_size')
50
+ if len(argv) < idx + 2:
51
+ print("must supply integer after --code_size")
52
+ exit(1)
53
+ code_size = int(argv[idx+1])
54
+
55
+ if '-cs' in argv:
56
+ idx = argv.index('-cs')
57
+ if len(argv) < idx + 2:
58
+ print("must supply integer after -cs")
59
+ exit(1)
60
+ code_size = int(argv[idx+1])
61
+
62
+ if '--to:raw' in argv:
63
+ stdout.buffer.write(tapehash1(preimage, code_size))
64
+ return
65
+
66
+ if '--difficulty' in argv:
67
+ diff = calculate_difficulty(tapehash1(preimage, code_size))
68
+ print(diff)
69
+ return
70
+
71
+ if '--check' in argv:
72
+ idx = argv.index('--check')
73
+ if len(argv) < idx + 2:
74
+ print("must supply integer after --check")
75
+ exit(1)
76
+ target = int(argv[idx+1])
77
+ digest = tapehash1(preimage, code_size)
78
+ if check_difficulty(digest, target):
79
+ print(1)
80
+ return
81
+ else:
82
+ print(0)
83
+ exit(2)
84
+
85
+ print(tapehash1(preimage, code_size).hex())
86
+
87
+ def help_tapehash2():
88
+ print('Use: command | tapehash1 OR tapehash1 --preimage {str} OR tapehash2 --file path/to/file')
89
+ print('\t--from:hex - parse preimage as hexadecimal')
90
+ print('\t--tape_size_multiplier {int} - set the tape_size_multiplier' +
91
+ ' parameter (default=1024)'
92
+ )
93
+ print('\t-tsm {int} - set the tape_size_multiplier parameter (default=1024)')
94
+ print('\t--to:raw - write the raw hash digest bytes to stdout')
95
+ print('\t--difficulty - output the difficulty of the hash digest')
96
+ print('\t--check {int} - outputs 1 if the hash digest meets the ' +
97
+ 'difficulty threshold; outputs 0 and exits with error code 2 '+
98
+ 'if it does not'
99
+ )
100
+ print('\nOnly 1 of --to:raw --difficulty --check can be used at a time')
101
+
102
+ def run_tapehash2() -> None:
103
+ preimage = b''
104
+ tape_size_multiplier = 2
105
+
106
+ if not stdin.isatty():
107
+ preimage = stdin.buffer.read()
108
+
109
+ if len(argv) > 1:
110
+ if '--help' in argv or '-h' in argv:
111
+ return help_tapehash2()
112
+
113
+ if '--preimage' in argv:
114
+ idx = argv.index('--preimage')
115
+ if len(argv) < idx + 2:
116
+ print("must supply data after --preimage")
117
+ exit(1)
118
+ preimage = argv[idx+1].encode('utf-8')
119
+
120
+ if '--file' in argv:
121
+ idx = argv.index('--file')
122
+ if len(argv) < idx + 2:
123
+ print("must supply path after --file")
124
+ exit(1)
125
+ with open(argv[idx+1], 'rb') as f:
126
+ preimage = f.read()
127
+
128
+ if '--from:hex' in argv:
129
+ preimage = bytes.fromhex(preimage.decode('utf-8'))
130
+
131
+ if '--tape_size_multiplier' in argv:
132
+ idx = argv.index('--tape_size_multiplier')
133
+ if len(argv) < idx + 2:
134
+ print("must supply integer after --tape_size_multiplier")
135
+ exit(1)
136
+ tape_size_multiplier = int(argv[idx+1])
137
+
138
+ if '-tsm' in argv:
139
+ idx = argv.index('-tsm')
140
+ if len(argv) < idx + 2:
141
+ print("must supply integer after -tsm")
142
+ exit(1)
143
+ tape_size_multiplier = int(argv[idx+1])
144
+
145
+ if '--to:raw' in argv:
146
+ stdout.buffer.write(tapehash2(preimage, tape_size_multiplier))
147
+ return
148
+
149
+ if '--difficulty' in argv:
150
+ diff = calculate_difficulty(tapehash2(preimage, tape_size_multiplier))
151
+ print(diff)
152
+ return
153
+
154
+ if '--check' in argv:
155
+ idx = argv.index('--check')
156
+ if len(argv) < idx + 2:
157
+ print("must supply integer after --check")
158
+ exit(1)
159
+ target = int(argv[idx+1])
160
+ digest = tapehash2(preimage, tape_size_multiplier)
161
+ if check_difficulty(digest, target):
162
+ print(1)
163
+ return
164
+ else:
165
+ print(0)
166
+ exit(2)
167
+
168
+ print(tapehash2(preimage, tape_size_multiplier).hex())
169
+
170
+ def help_tapehash3():
171
+ print('Use: command | tapehash1 OR tapehash1 --preimage {str} OR tapehash3 --file path/to/file')
172
+ print('\t--from:hex - parse preimage as hexadecimal')
173
+ print('\t--code_size {int} - set the code_size parameter (default=1024)')
174
+ print('\t-cs {int} - set the code_size parameter (default=1024)')
175
+ print('\t--tape_size_multiplier {int} - set the tape_size_multiplier' +
176
+ ' parameter (default=1024)'
177
+ )
178
+ print('\t-tsm {int} - set the tape_size_multiplier parameter (default=1024)')
179
+ print('\t--to:raw - write the raw hash digest bytes to stdout')
180
+ print('\t--difficulty - output the difficulty of the hash digest')
181
+ print('\t--check {int} - outputs 1 if the hash digest meets the ' +
182
+ 'difficulty threshold; outputs 0 and exits with error code 2 '+
183
+ 'if it does not'
184
+ )
185
+ print('\nOnly 1 of --to:raw --difficulty --check can be used at a time')
186
+
187
+
188
+ def run_tapehash3() -> None:
189
+ preimage = b''
190
+ code_size = 64
191
+ tape_size_multiplier = 2
192
+
193
+ if not stdin.isatty():
194
+ preimage = stdin.buffer.read()
195
+
196
+ if len(argv) > 1:
197
+ if '--help' in argv or '-h' in argv:
198
+ return help_tapehash2()
199
+
200
+ if '--preimage' in argv:
201
+ idx = argv.index('--preimage')
202
+ if len(argv) < idx + 2:
203
+ print("must supply data after --preimage")
204
+ exit(1)
205
+ preimage = argv[idx+1].encode('utf-8')
206
+
207
+ if '--file' in argv:
208
+ idx = argv.index('--file')
209
+ if len(argv) < idx + 2:
210
+ print("must supply path after --file")
211
+ exit(1)
212
+ with open(argv[idx+1], 'rb') as f:
213
+ preimage = f.read()
214
+
215
+ if '--from:hex' in argv:
216
+ preimage = bytes.fromhex(preimage.decode('utf-8'))
217
+
218
+ if '--code_size' in argv:
219
+ idx = argv.index('--code_size')
220
+ if len(argv) < idx + 2:
221
+ print("must supply integer after --code_size")
222
+ exit(1)
223
+ code_size = int(argv[idx+1])
224
+
225
+ if '-cs' in argv:
226
+ idx = argv.index('-cs')
227
+ if len(argv) < idx + 2:
228
+ print("must supply integer after -cs")
229
+ exit(1)
230
+ code_size = int(argv[idx+1])
231
+
232
+ if '--tape_size_multiplier' in argv:
233
+ idx = argv.index('--tape_size_multiplier')
234
+ if len(argv) < idx + 2:
235
+ print("must supply integer after --tape_size_multiplier")
236
+ exit(1)
237
+ tape_size_multiplier = int(argv[idx+1])
238
+
239
+ if '-tsm' in argv:
240
+ idx = argv.index('-tsm')
241
+ if len(argv) < idx + 2:
242
+ print("must supply integer after -tsm")
243
+ exit(1)
244
+ tape_size_multiplier = int(argv[idx+1])
245
+
246
+ if '--to:raw' in argv:
247
+ stdout.buffer.write(tapehash3(preimage, code_size, tape_size_multiplier))
248
+ return
249
+
250
+ if '--difficulty' in argv:
251
+ diff = calculate_difficulty(tapehash3(preimage, code_size, tape_size_multiplier))
252
+ print(diff)
253
+ return
254
+
255
+ if '--check' in argv:
256
+ idx = argv.index('--check')
257
+ if len(argv) < idx + 2:
258
+ print("must supply integer after --check")
259
+ exit(1)
260
+ target = int(argv[idx+1])
261
+ digest = tapehash1(preimage, code_size, tape_size_multiplier)
262
+ if check_difficulty(digest, target):
263
+ print(1)
264
+ return
265
+ else:
266
+ print(0)
267
+ exit(2)
268
+
269
+ print(tapehash3(preimage, code_size, tape_size_multiplier).hex())
270
+
@@ -0,0 +1,52 @@
1
+ from typing import Protocol, Callable
2
+ import math
3
+
4
+
5
+ class HasNonceProtocol(Protocol):
6
+ """The HasNonceProtocol requires that an implementation has a settable
7
+ nonce property.
8
+ """
9
+ @property
10
+ def nonce(self) -> int:
11
+ ...
12
+ @nonce.setter
13
+ def nonce(self, val: int):
14
+ ...
15
+
16
+ def calculate_difficulty(val: bytes) -> int:
17
+ """Calculates the difficulty of a hash by dividing 2**256 (max int)
18
+ by the supplied val interpreted as a big-endian unsigned int.
19
+ This provides a linear metric that represents the expected
20
+ amount of work (hashes) that have to be computed on average to
21
+ reach the given hash val or better (lower).
22
+ """
23
+ val = int.from_bytes(val, 'big')
24
+ if not val:
25
+ return 2**256
26
+ return 2**256 // val
27
+
28
+ def calculate_target(difficulty: int) -> int:
29
+ """Calculates the target value that a hash must be below to meet
30
+ the difficulty threshold.
31
+ """
32
+ return 2**256 if not difficulty else 2**256 // difficulty
33
+
34
+ def check_difficulty(val: bytes, difficulty: int) -> bool:
35
+ """Returns True if the val has a difficulty score greater than or
36
+ equal to the supplied difficulty, otherwise False.
37
+ """
38
+ return calculate_difficulty(val) >= difficulty
39
+
40
+ def work(
41
+ state: HasNonceProtocol, serialize: Callable[[HasNonceProtocol], bytes],
42
+ difficulty: int, hash_algo: Callable[[bytes], bytes]
43
+ ) -> HasNonceProtocol:
44
+ """Continually increments `state.nonce` until the difficulty score of
45
+ `hash_algo(serialize(state))` >= target, then returns the updated
46
+ state.
47
+ """
48
+ target = calculate_target(difficulty)
49
+ while int.from_bytes(hash_algo(serialize(state)), 'big') > target:
50
+ state.nonce += 1
51
+ return state
52
+