Packages

Proof of work resource creation, generation and verification

Current section

4 Versions

Jump to

Compare versions

6 files changed
+162 additions
-66 deletions
  @@ -1,4 +1,4 @@
1 - [![Build Status](https://travis-ci.org/danj3/elixir-hashcash.svg?branch=master)](https://travis-ci.org/danj3/elixir-hashcash)
1 + [![Build Status](https://travis-ci.com/danj3/elixir-hashcash.svg?branch=master)](https://travis-ci.com/danj3/elixir-hashcash)
2 2
3 3 # Hashcash
4 4
  @@ -31,14 +31,14 @@ end
31 31
32 32 ## Installation
33 33
34 - This is not applicable, not available on Hex, but will be shortly.
34 + Available [in Hex](https://hex.pm/packages/hashcash).
35 35
36 - If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
36 + The package can be added to your project:
37 37
38 - 1. Add `hashcash` to your list of dependencies in `mix.exs`:
38 + 1. Add `hashcash` to your list of dependencies in `mix.exs`:
39 39
40 40 ```elixir
41 41 def deps do
42 - [{:hashcash, "~> 0.1.0"}]
42 + [{:hashcash, "~> 1.0"}]
43 43 end
44 44 ```
  @@ -3,9 +3,11 @@
3 3 {<<"description">>,
4 4 <<"Proof of work resource creation, generation and verification">>}.
5 5 {<<"elixir">>,<<"~> 1.6">>}.
6 - {<<"files">>,[<<"lib">>,<<"lib/hashcash.ex">>,<<"mix.exs">>,<<"README.md">>]}.
7 - {<<"licenses">>,[<<"Apache 2.0">>]}.
6 + {<<"files">>,
7 + [<<"lib/hashcash.ex">>,<<"mix.exs">>,<<"README.md">>,<<"js/hashcash.js">>,
8 + <<"js/README.md">>]}.
9 + {<<"licenses">>,[<<"Apache-2.0">>]}.
8 10 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/danj3/elixir-hashcash">>}]}.
9 11 {<<"name">>,<<"hashcash">>}.
10 12 {<<"requirements">>,[]}.
11 - {<<"version">>,<<"1.0.0">>}.
13 + {<<"version">>,<<"1.1.0">>}.
  @@ -0,0 +1,38 @@
1 + # Here are some test resources
2 +
3 + * 1:16:220902:testcase-resource::3hVEhwDtcJtpd6q0UIwhJCeLZH6ykoG
4 + * 1:16:220902:testcase-resource::/t5MiwxP9p4Aebl7o0c9C7LebKUbiVi
5 + * 1:16:220902:testcase-resource::eH1s4fyhThkkn5STOvbkvw7Oe1Rpc8J
6 + * 1:16:220902:testcase-resource::H05w4Lo6WS2/0BkEmwJcHTNtlq2iPLv
7 + * 1:16:220902:testcase-resource::CheQe4xfXH56n6gmkyxwdhn/zcRQ6GD
8 +
9 + # A JS console session
10 +
11 + <pasted in hashcash.js>
12 +
13 + ```
14 + >> await hashcash.generate("1:16:220902:testcase-resource::3hVEhwDtcJtpd6q0UIwhJCeLZH6ykoG")
15 + "1:16:220902:testcase-resource::3hVEhwDtcJtpd6q0UIwhJCeLZH6ykoG:14891"
16 +
17 + >> await hashcash.generate("1:16:220902:testcase-resource::/t5MiwxP9p4Aebl7o0c9C7LebKUbiVi")
18 + "1:16:220902:testcase-resource::/t5MiwxP9p4Aebl7o0c9C7LebKUbiVi:40079"
19 +
20 + >> await hashcash.generate("1:16:220902:testcase-resource::eH1s4fyhThkkn5STOvbkvw7Oe1Rpc8J")
21 + "1:16:220902:testcase-resource::eH1s4fyhThkkn5STOvbkvw7Oe1Rpc8J:34379"
22 +
23 + >> await hashcash.generate("1:16:220902:testcase-resource::H05w4Lo6WS2/0BkEmwJcHTNtlq2iPLv")
24 + "1:16:220902:testcase-resource::H05w4Lo6WS2/0BkEmwJcHTNtlq2iPLv:37040"
25 +
26 + >> await hashcash.generate("1:16:220902:testcase-resource::CheQe4xfXH56n6gmkyxwdhn/zcRQ6GD")
27 + "1:16:220902:testcase-resource::CheQe4xfXH56n6gmkyxwdhn/zcRQ6GD:738"
28 + ```
29 +
30 + # hash results
31 +
32 + ```
33 + printf "1:16:220902:testcase-resource::3hVEhwDtcJtpd6q0UIwhJCeLZH6ykoG:14891" | shasum
34 + 0000244505915b721ebf81e9d385a59dc495ca2c -
35 +
36 + printf "1:16:220902:testcase-resource::/t5MiwxP9p4Aebl7o0c9C7LebKUbiVi:40079" | shasum
37 + 0000621d7840e7b7417828d8e6c5c13c989a8349 -
38 + ```
\ No newline at end of file
  @@ -0,0 +1,35 @@
1 + const hashcash = {
2 + generate: function (stamp) {
3 + const [version, bits ] = stamp.split(':');
4 + return hashcash.solution(stamp, 0, bits);
5 + },
6 +
7 + solution: async function (stamp, count, bits) {
8 + const stamp_with_count = stamp + ':' + count;
9 + const digits = await hashcash.sha(stamp_with_count);
10 + if (hashcash.count_zeros(digits) >= bits) {
11 + return stamp_with_count;
12 + } else {
13 + return hashcash.solution(stamp, count+1, bits);
14 + }
15 + },
16 +
17 + digest_to_binary: function (digest) {
18 + return Array.from(new Uint8Array(digest))
19 + .map(b => b.toString(2).padStart(8,'0')).join('');
20 + },
21 +
22 + sha: function (string) {
23 + const encoder = new TextEncoder();
24 + const data = encoder.encode(string);
25 + return crypto.subtle.digest("SHA-1",data)
26 + .then(hashcash.digest_to_binary);
27 + },
28 +
29 + count_zeros: function (base2_string) {
30 + for(var i=0; i<base2_string.length; i++) {
31 + if (base2_string[i]=="1") { return i; }
32 + }
33 + return i;
34 + }
35 + };
  @@ -1,6 +1,6 @@
1 1 defmodule Hashcash do
2 2 require Logger
3 -
3 +
4 4 defstruct version: 1,
5 5 bits: 20,
6 6 date: nil,
  @@ -10,12 +10,17 @@ defmodule Hashcash do
10 10 counter: 0,
11 11 stamp_string: nil,
12 12 stamp_base: nil
13 -
13 +
14 + @type t :: %Hashcash{}
15 +
14 16 @stamp_version "1"
15 -
16 - # Create a %Hashcash stamp from a stamp string. Use this to turn a string
17 - # that may be passed between parties to a form that can be used with the
18 - # rest of the functions here.
17 +
18 + @doc """
19 + Create a %Hashcash stamp from a stamp string. Use this to turn a string
20 + that may be passed between parties to a form that can be used with the
21 + rest of the functions here.
22 + """
23 +
19 24 def stamp(stamp_string) do
20 25 [@stamp_version,
21 26 bits,
  @@ -32,34 +37,36 @@ defmodule Hashcash do
32 37 resource: resource,
33 38 ext: ext,
34 39 rand: rand,
35 - counter: counter,
40 + counter: String.to_integer(counter),
36 41 stamp_string: stamp_string,
37 42 }
38 43 end
39 44
40 - # Create a new %Hashcash with the resource_string and specified
41 - # date Keyword list
42 - @spec resource(resource_string :: String.t, date :: Keyword.t) :: %Hashcash{}
45 + @doc """
46 + Create a new %Hashcash with the resource_string and specified
47 + date Keyword list
48 + """
49 + @spec resource(resource_string :: String.t, date :: Keyword.t) :: t
43 50 def resource(resource_string, date = [year: _y, month: _m, day: _d]),
44 51 do: %Hashcash{resource: resource_string,
45 52 rand: rand_generate(),
46 53 date: date}
47 54
48 - # Create a new %Hashcash with resource_string and today as the date.
49 - @spec resource(resource_string :: String.t) :: %Hashcash{}
55 + @doc "Create a new %Hashcash with resource_string and today as the date."
56 + @spec resource(resource_string :: String.t) :: t
50 57 def resource(resource_string),
51 58 do: %Hashcash{resource: resource_string,
52 59 rand: rand_generate(),
53 60 date: date_now()}
54 61
55 - # Modify the bits required of a %Hashcash
56 - @spec resource_bits(hcash :: %Hashcash{}, bits :: integer) :: %Hashcash{}
62 + @doc "Modify the bits required of a %Hashcash"
63 + @spec resource_bits(hcash :: t, bits :: integer) :: t
57 64 def resource_bits(hcash = %Hashcash{},bits) when is_integer(bits),
58 65 do: %Hashcash{hcash | bits: bits}
59 66
60 - # Modify the date of a %Hashcash
61 - @spec resource_date(hcash :: %Hashcash{},
62 - y :: integer, m :: integer, d :: integer) :: %Hashcash{}
67 + @doc "Modify the date of a %Hashcash"
68 + @spec resource_date(hcash :: t,
69 + y :: integer, m :: integer, d :: integer) :: t
63 70
64 71 def resource_date(hcash, y, m, d) when is_integer(y) and is_integer(m) and is_integer(d) do
65 72 %Hashcash{hcash | date: %{year: y, month: m, day: d}}
  @@ -71,60 +78,64 @@ defmodule Hashcash do
71 78 <<core::binary-size(slen), _rest::binary>> = string
72 79 core
73 80 end
74 - # Generate the rand field using a crypto.strong_rand_bytes
81 +
82 + @doc "Generate the rand field using a crypto.strong_rand_bytes"
75 83 @spec rand_generate :: String.t
76 84 def rand_generate do
77 - 12
85 + 24
78 86 |> :crypto.strong_rand_bytes
79 87 |> Base.encode64
80 88 |> strip_trailing_char # remove the trailing =
81 89 end
82 90
83 - # Set rand field of %Hashcash to newly generated string
84 - @spec resource_rand(stamp :: %Hashcash{}) :: %Hashcash{}
91 + @doc "Set rand field of %Hashcash to newly generated string"
92 + @spec resource_rand(stamp :: t) :: t
85 93 def resource_rand(hcash) do
86 94 %Hashcash{hcash | rand: rand_generate()}
87 95 end
88 96
89 - # Generate date string section from date keywords list
97 + @doc "Generate date string section from date keywords list"
90 98 @spec date_format(date_keywords :: Keyword.t) :: String.t
91 99 def date_format(date_keywords) do
92 100 [y,m,d] = Keyword.values(date_keywords)
93 101 to_string(:io_lib.format("~2..0B~2..0B~2..0B", [rem(y,100),m,d]))
94 102 end
95 103
96 - # Generate date keywords list of now
104 + @doc "Generate date keywords list of now"
97 105 @spec date_now :: Keyword.t
98 106 def date_now do
99 107 %{day: day, month: month, year: year} = DateTime.utc_now
100 108 [year: year, month: month, day: day]
101 109 end
102 110
103 - # Generate or return hcash.base from properies
104 - # This excludes the count field so that the generator can use this base
105 - # with successive iterations of new counts by appending just the count.
106 - @spec resource_format_base(hcash :: %Hashcash{}) :: String.t
111 + @doc """
112 + Generate or return hcash.base from properies
113 + This excludes the count field so that the generator can use this base
114 + with successive iterations of new counts by appending just the count.
115 + """
116 +
117 + @spec resource_format_base(hcash :: t) :: String.t
107 118 def resource_format_base(hcash) do
108 119 if base = hcash.stamp_base do
109 120 base
110 121 else
111 - Enum.join([hcash.version,
112 - hcash.bits,
113 - date_format(hcash.date),
114 - hcash.resource,
115 - hcash.ext,
116 - hcash.rand,
117 - ],":")
122 + Enum.join([hcash.version,
123 + hcash.bits,
124 + date_format(hcash.date),
125 + hcash.resource,
126 + hcash.ext,
127 + hcash.rand,
128 + ],":")
118 129 end
119 130 end
120 131
121 - # Append the counter to the base to make a full stamp string
132 + @doc "Append the counter to the base to make a full stamp string"
122 133 @spec resource_format_string(base :: String.t ,counter :: integer) :: String.t
123 134 def resource_format_string(base,counter) do
124 135 base <> ":#{counter}"
125 136 end
126 137
127 - # Count leading zero bits in a bitstring
138 + @doc "Count leading zero bits in a bitstring"
128 139 @spec count_lead_zeros_in_bitstring(bs :: String.t, count :: integer) :: integer
129 140 def count_lead_zeros_in_bitstring(bs, count \\ 0) do
130 141 <<bh::size(1), rest::bitstring>> = bs
  @@ -135,44 +146,44 @@ defmodule Hashcash do
135 146 end
136 147 end
137 148
138 - # Count leading zero bits in SHA1 hash of stamp.stamp_string
139 - @spec zero_bits_count(hcash :: %Hashcash{}) :: integer
149 + @doc "Count leading zero bits in SHA1 hash of stamp.stamp_string"
150 + @spec zero_bits_count(hcash :: t) :: integer
140 151 def zero_bits_count(hcash) do
141 152 count_lead_zeros_in_bitstring(:crypto.hash(:sha,hcash.stamp_string))
142 153 end
143 154
144 - # Return %Hashcash with stamp_base and stamp_string set.
145 - @spec resource_format(hcash :: %Hashcash{}) :: %Hashcash{}
155 + @doc "Return %Hashcash with stamp_base and stamp_string set."
156 + @spec resource_format(hcash :: t) :: t
146 157 def resource_format(hcash) do
147 158 base = resource_format_base(hcash)
148 159 %Hashcash{hcash | stamp_base: base,
149 160 stamp_string: resource_format_string(base,hcash.counter)}
150 161 end
151 162
152 - # Validate the stamp string proof-of-work only. Use verify for full check
153 - @spec validate(hcash :: %Hashcash{}) :: tuple
163 + @doc "Validate the stamp string proof-of-work only. Use verify for full check"
164 + @spec validate(hcash :: t) :: tuple
154 165 def validate(hcash) do
155 - if zero_bits_count(hcash) == hcash.bits do
166 + if zero_bits_count(hcash) >= hcash.bits do
156 167 {:ok}
157 168 else
158 169 {:error, :unproven}
159 170 end
160 171 end
161 172
162 - # Generate a full stamp, doing the work
163 - @spec generate(hcash :: %Hashcash{}) :: %Hashcash{}
173 + @doc "Generate a full stamp, doing the work"
174 + @spec generate(hcash :: t) :: t
164 175 def generate(hcash) do
165 176 generate(hcash,System.os_time(:millisecond))
166 177 end
167 178
168 - @spec generate(hcash :: %Hashcash{}, began :: integer) :: %Hashcash{}
179 + @spec generate(hcash :: t, began :: integer) :: t
169 180 defp generate(hcash,began) do
170 181 hcash = resource_format(hcash)
171 182 case validate(hcash) do
172 183 {:ok} ->
173 - hcash
184 + hcash
174 185 {:error, :unproven} ->
175 - generate(%Hashcash{hcash | counter: hcash.counter+1},began)
186 + generate(%Hashcash{hcash | counter: hcash.counter+1},began)
176 187 end
177 188 end
178 189
  @@ -182,7 +193,7 @@ defmodule Hashcash do
182 193 (:calendar.date_to_gregorian_days(y,m,d) - @calendar_base) * 86400
183 194 end
184 195
185 - # Verify stamp resource is a valid resource
196 + @doc "Verify stamp resource is a valid resource"
186 197 @spec verify_resource(resource :: String.t, valid_resources :: list) :: tuple
187 198 def verify_resource(resource,valid_resources) do
188 199 if resource in valid_resources do
  @@ -192,7 +203,7 @@ defmodule Hashcash do
192 203 end
193 204 end
194 205
195 - # Verify date keyword list is within 2 days
206 + @doc "Verify date keyword list is within 2 days"
196 207 @spec verify_time(date :: Keyword.t) :: tuple
197 208 def verify_time(date) do
198 209 if System.os_time(:second) - date_seconds(date) < 86400*2 do
  @@ -202,16 +213,18 @@ defmodule Hashcash do
202 213 end
203 214 end
204 215
205 - # Verfiy all attributes and proof of work against a list of acceptable resources
206 - @spec verify(hcash :: %Hashcash{}, valid_resources :: list) :: tuple
216 + @doc """
217 + Verfiy all attributes and proof of work against a list
218 + of acceptable resources or a single resource string
219 + """
220 + @spec verify(hcash :: t, valid_resources :: list) :: tuple
207 221 def verify(hcash = %Hashcash{},valid_resources = [_h|_t]) do
208 222 with {:ok} <- verify_resource(hcash.resource,valid_resources),
209 223 {:ok} <- verify_time(hcash.date),
210 224 {:ok} <- validate(hcash), do: {:ok, :verified}
211 225 end
212 226
213 - # Verfiy all attributes and proof of work against a single resources
214 - @spec verify(hcash :: %Hashcash{}, single_resource :: String.t) :: tuple
227 + @spec verify(hcash :: t, single_resource :: String.t) :: tuple
215 228 def verify(hcash = %Hashcash{},single_resource) do
216 229 verify(hcash,[single_resource])
217 230 end
Loading more files…