Current section
4 Versions
Jump to
Current section
4 Versions
Compare versions
6
files changed
+162
additions
-66
deletions
| @@ -1,4 +1,4 @@ | |
| 1 | - [](https://travis-ci.org/danj3/elixir-hashcash) |
| 1 | + [](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…