├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── README.org
├── doc
└── intro.md
├── img
└── millions.jpg
├── project.clj
├── src
└── io
│ └── johnwalker
│ ├── bitcoin_protocol.clj
│ └── bitcoin_protocol
│ └── gloss
│ ├── extension.clj
│ └── extension
│ └── codecs.clj
└── test
└── io
└── johnwalker
└── bitcoin_protocol_test.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein2
3 | jdk:
4 | - openjdk6
5 | - openjdk7
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Issues and pull requests are welcome! I'll respond between five minutes and a week.
4 |
5 | [Gloss](https://github.com/ztellman/gloss) is a foundation for this library. Any improvements there is also a really big help.
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4 |
5 | 1. DEFINITIONS
6 |
7 | "Contribution" means:
8 |
9 | a) in the case of the initial Contributor, the initial code and
10 | documentation distributed under this Agreement, and
11 |
12 | b) in the case of each subsequent Contributor:
13 |
14 | i) changes to the Program, and
15 |
16 | ii) additions to the Program;
17 |
18 | where such changes and/or additions to the Program originate from and are
19 | distributed by that particular Contributor. A Contribution 'originates' from
20 | a Contributor if it was added to the Program by such Contributor itself or
21 | anyone acting on such Contributor's behalf. Contributions do not include
22 | additions to the Program which: (i) are separate modules of software
23 | distributed in conjunction with the Program under their own license
24 | agreement, and (ii) are not derivative works of the Program.
25 |
26 | "Contributor" means any person or entity that distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which are
29 | necessarily infringed by the use or sale of its Contribution alone or when
30 | combined with the Program.
31 |
32 | "Program" means the Contributions distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement,
36 | including all Contributors.
37 |
38 | 2. GRANT OF RIGHTS
39 |
40 | a) Subject to the terms of this Agreement, each Contributor hereby grants
41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
42 | reproduce, prepare derivative works of, publicly display, publicly perform,
43 | distribute and sublicense the Contribution of such Contributor, if any, and
44 | such derivative works, in source code and object code form.
45 |
46 | b) Subject to the terms of this Agreement, each Contributor hereby grants
47 | Recipient a non-exclusive, worldwide, royalty-free patent license under
48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
49 | transfer the Contribution of such Contributor, if any, in source code and
50 | object code form. This patent license shall apply to the combination of the
51 | Contribution and the Program if, at the time the Contribution is added by the
52 | Contributor, such addition of the Contribution causes such combination to be
53 | covered by the Licensed Patents. The patent license shall not apply to any
54 | other combinations which include the Contribution. No hardware per se is
55 | licensed hereunder.
56 |
57 | c) Recipient understands that although each Contributor grants the licenses
58 | to its Contributions set forth herein, no assurances are provided by any
59 | Contributor that the Program does not infringe the patent or other
60 | intellectual property rights of any other entity. Each Contributor disclaims
61 | any liability to Recipient for claims brought by any other entity based on
62 | infringement of intellectual property rights or otherwise. As a condition to
63 | exercising the rights and licenses granted hereunder, each Recipient hereby
64 | assumes sole responsibility to secure any other intellectual property rights
65 | needed, if any. For example, if a third party patent license is required to
66 | allow Recipient to distribute the Program, it is Recipient's responsibility
67 | to acquire that license before distributing the Program.
68 |
69 | d) Each Contributor represents that to its knowledge it has sufficient
70 | copyright rights in its Contribution, if any, to grant the copyright license
71 | set forth in this Agreement.
72 |
73 | 3. REQUIREMENTS
74 |
75 | A Contributor may choose to distribute the Program in object code form under
76 | its own license agreement, provided that:
77 |
78 | a) it complies with the terms and conditions of this Agreement; and
79 |
80 | b) its license agreement:
81 |
82 | i) effectively disclaims on behalf of all Contributors all warranties and
83 | conditions, express and implied, including warranties or conditions of title
84 | and non-infringement, and implied warranties or conditions of merchantability
85 | and fitness for a particular purpose;
86 |
87 | ii) effectively excludes on behalf of all Contributors all liability for
88 | damages, including direct, indirect, special, incidental and consequential
89 | damages, such as lost profits;
90 |
91 | iii) states that any provisions which differ from this Agreement are offered
92 | by that Contributor alone and not by any other party; and
93 |
94 | iv) states that source code for the Program is available from such
95 | Contributor, and informs licensees how to obtain it in a reasonable manner on
96 | or through a medium customarily used for software exchange.
97 |
98 | When the Program is made available in source code form:
99 |
100 | a) it must be made available under this Agreement; and
101 |
102 | b) a copy of this Agreement must be included with each copy of the Program.
103 |
104 | Contributors may not remove or alter any copyright notices contained within
105 | the Program.
106 |
107 | Each Contributor must identify itself as the originator of its Contribution,
108 | if any, in a manner that reasonably allows subsequent Recipients to identify
109 | the originator of the Contribution.
110 |
111 | 4. COMMERCIAL DISTRIBUTION
112 |
113 | Commercial distributors of software may accept certain responsibilities with
114 | respect to end users, business partners and the like. While this license is
115 | intended to facilitate the commercial use of the Program, the Contributor who
116 | includes the Program in a commercial product offering should do so in a
117 | manner which does not create potential liability for other Contributors.
118 | Therefore, if a Contributor includes the Program in a commercial product
119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend
120 | and indemnify every other Contributor ("Indemnified Contributor") against any
121 | losses, damages and costs (collectively "Losses") arising from claims,
122 | lawsuits and other legal actions brought by a third party against the
123 | Indemnified Contributor to the extent caused by the acts or omissions of such
124 | Commercial Contributor in connection with its distribution of the Program in
125 | a commercial product offering. The obligations in this section do not apply
126 | to any claims or Losses relating to any actual or alleged intellectual
127 | property infringement. In order to qualify, an Indemnified Contributor must:
128 | a) promptly notify the Commercial Contributor in writing of such claim, and
129 | b) allow the Commercial Contributor tocontrol, and cooperate with the
130 | Commercial Contributor in, the defense and any related settlement
131 | negotiations. The Indemnified Contributor may participate in any such claim
132 | at its own expense.
133 |
134 | For example, a Contributor might include the Program in a commercial product
135 | offering, Product X. That Contributor is then a Commercial Contributor. If
136 | that Commercial Contributor then makes performance claims, or offers
137 | warranties related to Product X, those performance claims and warranties are
138 | such Commercial Contributor's responsibility alone. Under this section, the
139 | Commercial Contributor would have to defend claims against the other
140 | Contributors related to those performance claims and warranties, and if a
141 | court requires any other Contributor to pay any damages as a result, the
142 | Commercial Contributor must pay those damages.
143 |
144 | 5. NO WARRANTY
145 |
146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
151 | appropriateness of using and distributing the Program and assumes all risks
152 | associated with its exercise of rights under this Agreement , including but
153 | not limited to the risks and costs of program errors, compliance with
154 | applicable laws, damage to or loss of data, programs or equipment, and
155 | unavailability or interruption of operations.
156 |
157 | 6. DISCLAIMER OF LIABILITY
158 |
159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
166 | OF SUCH DAMAGES.
167 |
168 | 7. GENERAL
169 |
170 | If any provision of this Agreement is invalid or unenforceable under
171 | applicable law, it shall not affect the validity or enforceability of the
172 | remainder of the terms of this Agreement, and without further action by the
173 | parties hereto, such provision shall be reformed to the minimum extent
174 | necessary to make such provision valid and enforceable.
175 |
176 | If Recipient institutes patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
178 | (excluding combinations of the Program with other software or hardware)
179 | infringes such Recipient's patent(s), then such Recipient's rights granted
180 | under Section 2(b) shall terminate as of the date such litigation is filed.
181 |
182 | All Recipient's rights under this Agreement shall terminate if it fails to
183 | comply with any of the material terms or conditions of this Agreement and
184 | does not cure such failure in a reasonable period of time after becoming
185 | aware of such noncompliance. If all Recipient's rights under this Agreement
186 | terminate, Recipient agrees to cease use and distribution of the Program as
187 | soon as reasonably practicable. However, Recipient's obligations under this
188 | Agreement and any licenses granted by Recipient relating to the Program shall
189 | continue and survive.
190 |
191 | Everyone is permitted to copy and distribute copies of this Agreement, but in
192 | order to avoid inconsistency the Agreement is copyrighted and may only be
193 | modified in the following manner. The Agreement Steward reserves the right to
194 | publish new versions (including revisions) of this Agreement from time to
195 | time. No one other than the Agreement Steward has the right to modify this
196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
197 | Eclipse Foundation may assign the responsibility to serve as the Agreement
198 | Steward to a suitable separate entity. Each new version of the Agreement will
199 | be given a distinguishing version number. The Program (including
200 | Contributions) may always be distributed subject to the version of the
201 | Agreement under which it was received. In addition, after a new version of
202 | the Agreement is published, Contributor may elect to distribute the Program
203 | (including its Contributions) under the new version. Except as expressly
204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
205 | licenses to the intellectual property of any Contributor under this
206 | Agreement, whether expressly, by implication, estoppel or otherwise. All
207 | rights in the Program not expressly granted under this Agreement are
208 | reserved.
209 |
210 | This Agreement is governed by the laws of the State of Washington and the
211 | intellectual property laws of the United States of America. No party to this
212 | Agreement will bring a legal action under this Agreement more than one year
213 | after the cause of action arose. Each party waives its rights to a jury trial
214 | in any resulting litigation.
215 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bitcoin-protocol
2 |
3 | This is an implementation of the [Bitcoin networking protocol](https://en.bitcoin.it/wiki/Protocol_specification). It can
4 | be used to communicate with peers within Bitcoin networks. The
5 | library handles deterministic fields like checksums and lengths, but
6 | the programmer must specify non-deterministic fields like nonces.
7 |
8 | Documentation to come. Mostly untested goodness at the moment.
9 |
10 | ```clojure
11 | ;; Latest version on Clojars
12 | [io.johnwalker/bitcoin-protocol "0.17.4"]
13 | ```
14 |
15 | ## Usage with Aleph
16 |
17 | [Aleph](https://github.com/ztellman/aleph) has support for decoding of the wire automatically. Suppose
18 | you are connecting to a Bitcoin peer. Then you can communicate him
19 | in the usual manner with:
20 |
21 | ```clojure
22 | (require '[io.johnwalker/bitcoin-protocol :as bitcoin])
23 | (def ch
24 | (wait-for-result
25 | (tcp-client {:host "localhost",
26 | :port 10000,
27 | :frame bitcoin/network-protocol})))
28 | (enqueue ch {:command :getaddr
29 | :magic :main})
30 | (wait-for-message ch)
31 | ;; => {:magic :main
32 | ;; :command :addr
33 | ;; :checksum [...]
34 | ;; :addrs [blah blah blah]}
35 | ```
36 |
37 | ## Examples
38 |
39 | The two functions you care most about are `write-message` and
40 | `read-message.` `write-message` will convert hashmaps to
41 | Bytebuffers. `read-message` will bring Bytebuffers back to
42 | hashmaps. If you use aleph, then you don't have to care about
43 | either of these.
44 |
45 | ### Build a version message
46 |
47 | ```clojure
48 | (require '[bitcoin-protocol.messages :refer [write-message read-message]])
49 |
50 | (def my-version-messages (write-message
51 | {:command "version"
52 | :magic :main
53 | :version 60001
54 | :services 1
55 | :timestamp 0x50D0B211
56 | :addr-recv {:services 1 :ip "0.0.0.0" :port 0}
57 | :addr-from {:services 1 :ip "0.0.0.0" :port 0}
58 | :nonce 0x6517E68C5DB32E3B
59 | :user-agent "/Satoshi:0.7.2/"
60 | :start-height 212672
61 | :relay true}))
62 |
63 | ;; #
64 | ```
65 |
66 | ### Read the version message back
67 |
68 | `Read-message` works for other messages, like getaddr, ping, pong
69 | and so forth.
70 |
71 | ```clojure
72 | (def read-it-back (read-message my-version-message))
73 | ```
74 |
75 | ```clojure
76 | {:addr-from {:port 0 :ip "0.0.0.0" :services 1N}
77 | :user-agent "/Satoshi:0.7.2/"
78 | :command "version"
79 | :magic :main
80 | :start-height 212672
81 | :relay true
82 | :checksum [133 39 57 227]
83 | :length 101
84 | :version 60001
85 | :timestamp 1355854353
86 | :nonce 7284544412836900411N
87 | :services 1N
88 | :addr-recv {:port 0 :ip "0.0.0.0" :services 1N}
89 | ```
90 |
91 | ### Verify the insides
92 |
93 | Don't be shy when it comes to verifying the encoding. The raw bytes
94 | are easy to see (and there are better formatters in Clojure land).
95 |
96 | ```clojure
97 | (defn- str-bytes [h]
98 | (clojure.string/trimr
99 | (apply str
100 | (for [x (range (.remaining h))]
101 | (format "%02X " (.get h x))))))
102 |
103 | (def raw-bytes (str-bytes my-version-message)
104 | ```
105 |
106 | ### The Rest
107 |
108 | Everything else works just like the version message. Some of them
109 | haven't been tested yet, however. If you run into an error, please
110 | report it to me. I'll be especially happy to accept `test.check` or
111 | unit tests that demonstrate the failure.
112 |
113 | Presently, `read-message` and `write-message` is expected to work
114 | for
115 |
116 | ```
117 | version
118 | verack
119 | addr
120 | getaddr
121 | inv
122 | ping
123 | pong
124 | reject
125 | ```
126 |
127 | while these are around, but are untested.
128 |
129 | ```
130 | alert
131 | block
132 | getdata
133 | notfound
134 | getblocks
135 | getheaders
136 | tx
137 | ```
138 |
139 | # License
140 |
141 | Copyright © 2014 John Walker
142 |
143 | Distributed under the Eclipse Public License version 1.0.
144 |
--------------------------------------------------------------------------------
/README.org:
--------------------------------------------------------------------------------
1 | * bitcoin-protocol
2 |
3 | [[img/millions.jpg]]
4 |
5 | This is an implementation of the [[https://en.bitcoin.it/wiki/Protocol_specification][Bitcoin networking protocol]]. It can
6 | be used to communicate with peers within Bitcoin networks. The
7 | library handles deterministic fields like checksums and lengths, but
8 | the programmer must specify non-deterministic fields like nonces.
9 |
10 | Documentation to come. Mostly untested goodness at the moment.
11 |
12 | #+BEGIN_SRC clojure
13 | ;; Latest version on Clojars
14 | [io.johnwalker/bitcoin-protocol "0.17.4"]
15 | #+END_SRC
16 |
17 | ** Usage with Aleph
18 | [[https://github.com/ztellman/aleph][Aleph]] has support for decoding of the wire automatically. Suppose
19 | you are connecting to a Bitcoin peer. Then you can communicate him
20 | in the usual manner with:
21 |
22 | #+BEGIN_SRC clojure
23 | (require '[io.johnwalker/bitcoin-protocol :as bitcoin])
24 | (def ch
25 | (wait-for-result
26 | (tcp-client {:host "localhost",
27 | :port 10000,
28 | :frame bitcoin/network-protocol})))
29 | (enqueue ch {:command :getaddr
30 | :magic :main})
31 | (wait-for-message ch)
32 | ;; => {:magic :main
33 | ;; :command :addr
34 | ;; :checksum [...]
35 | ;; :addrs [blah blah blah]}
36 | #+END_SRC
37 |
38 | ** Examples
39 |
40 | The two functions you care most about are =write-message= and
41 | =read-message.= =write-message= will convert hashmaps to
42 | Bytebuffers. =read-message= will bring Bytebuffers back to
43 | hashmaps. If you use aleph, then you don't have to care about
44 | either of these.
45 |
46 | *** Build a version message
47 |
48 | #+BEGIN_SRC clojure
49 | (require '[bitcoin-protocol.messages :refer [write-message read-message]])
50 |
51 | (def my-version-messages (write-message
52 | {:command "version"
53 | :magic :main
54 | :version 60001
55 | :services 1
56 | :timestamp 0x50D0B211
57 | :addr-recv {:services 1 :ip "0.0.0.0" :port 0}
58 | :addr-from {:services 1 :ip "0.0.0.0" :port 0}
59 | :nonce 0x6517E68C5DB32E3B
60 | :user-agent "/Satoshi:0.7.2/"
61 | :start-height 212672
62 | :relay true}))
63 |
64 | ;; #
65 | #+END_SRC
66 |
67 | *** Read the version message back
68 | =Read-message= works for other messages, like getaddr, ping, pong
69 | and so forth.
70 |
71 | #+BEGIN_SRC clojure
72 | (def read-it-back (read-message my-version-message))
73 | #+END_SRC
74 | #+BEGIN_SRC clojure
75 | {:addr-from {:port 0 :ip "0.0.0.0" :services 1N}
76 | :user-agent "/Satoshi:0.7.2/"
77 | :command "version"
78 | :magic :main
79 | :start-height 212672
80 | :relay true
81 | :checksum [133 39 57 227]
82 | :length 101
83 | :version 60001
84 | :timestamp 1355854353
85 | :nonce 7284544412836900411N
86 | :services 1N
87 | :addr-recv {:port 0 :ip "0.0.0.0" :services 1N}
88 | #+END_SRC
89 |
90 | *** Verify the insides
91 | Don't be shy when it comes to verifying the encoding. The raw bytes
92 | are easy to see (and there are better formatters in Clojure land).
93 |
94 | #+BEGIN_SRC clojure
95 | (defn- str-bytes [h]
96 | (clojure.string/trimr
97 | (apply str
98 | (for [x (range (.remaining h))]
99 | (format "%02X " (.get h x))))))
100 |
101 | (def raw-bytes (str-bytes my-version-message)
102 | #+END_SRC
103 |
104 | *** The Rest
105 | Everything else works just like the version message. Some of them
106 | haven't been tested yet, however. If you run into an error, please
107 | report it to me. I'll be especially happy to accept =test.check= or
108 | unit tests that demonstrate the failure.
109 |
110 | Presently, =read-message= and =write-message= is expected to work
111 | for
112 |
113 | #+BEGIN_SRC
114 | version
115 | verack
116 | addr
117 | getaddr
118 | inv
119 | ping
120 | pong
121 | reject
122 | #+END_SRC
123 |
124 | while these are around, but are untested.
125 |
126 | #+BEGIN_SRC
127 | alert
128 | block
129 | getdata
130 | notfound
131 | getblocks
132 | getheaders
133 | tx
134 | #+END_SRC
135 |
136 | * License
137 |
138 | Copyright © 2014 John Walker
139 |
140 | Distributed under the Eclipse Public License version 1.0.
141 |
--------------------------------------------------------------------------------
/doc/intro.md:
--------------------------------------------------------------------------------
1 | # Introduction to bitcoin-protocol
2 |
3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/)
4 |
--------------------------------------------------------------------------------
/img/millions.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnwalker/bitcoin-protocol/4301aeb20f2b958c4cd399d951b67435dc74d459/img/millions.jpg
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject io.johnwalker/bitcoin-protocol "0.17.4"
2 | :description "An implementation of the Bitcoin networking protocol."
3 | :url "https://github.com/johnwalker/bitcoin-protocol"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/clojure "1.6.0"]
7 | [gloss "0.2.2"]]
8 | :profiles {:dev {:dependencies [[org.clojure/tools.namespace "0.2.5"]
9 | [org.clojure/test.check "0.5.9"]]}})
10 |
--------------------------------------------------------------------------------
/src/io/johnwalker/bitcoin_protocol.clj:
--------------------------------------------------------------------------------
1 | (ns io.johnwalker.bitcoin-protocol
2 | (:require [io.johnwalker.bitcoin-protocol.gloss.extension :as e]
3 | [clojure.string :as str]
4 | [gloss.core :refer :all]
5 | [gloss.io :refer :all]))
6 |
7 | (let [u16-le (compile-frame :uint16-le)
8 | u32-le (compile-frame :uint32-le)
9 | u64-le (compile-frame :uint64-le)]
10 | (defcodec varint (header :ubyte
11 | (fn [h]
12 | (case h
13 | 0xfd u16-le
14 | 0xfe u32-le
15 | 0xff u64-le
16 | (compile-frame nil-frame
17 | identity
18 | (constantly h))))
19 | (fn [b]
20 | (condp >= b
21 | 0xfc b
22 | 0xffff 0xfd
23 | 0xffffffff 0xfe
24 | 0xff)))))
25 |
26 |
27 | (defcodec varstr (compile-frame (finite-frame
28 | varint
29 | (string :ascii))
30 | identity
31 | (fn [s]
32 | (or s ""))))
33 |
34 |
35 | (defcodec ip-addr (compile-frame (repeat 16 :ubyte)
36 | (fn [s]
37 | (concat (repeat 10 0)
38 | (repeat 2 0xFF)
39 | (map
40 | #(Integer/parseInt %)
41 | (str/split s #"\."))))
42 | (fn [b]
43 | (apply str (interpose "." (take-last 4 b))))))
44 |
45 | (defcodec net-addr (ordered-map
46 | :services :uint64-le
47 | :ip ip-addr
48 | :port :uint16-be))
49 |
50 |
51 | (defcodec net-addrt (ordered-map :time :uint32-le
52 | :services :uint64-le
53 | :ip ip-addr
54 | :port :uint16-be))
55 |
56 |
57 | (defcodec magic (enum :uint32-le
58 | {:main 0xd9b4bef9
59 | :namecoin 0xfeb4bef9
60 | :testnet 0xdab5bffa
61 | :testnet3 0x0709110b}))
62 |
63 |
64 | (defcodec command (compile-frame
65 | (string :ascii :length 12)
66 | (fn [s]
67 | (->> 0 char
68 | repeat
69 | (concat s)
70 | (take 12)
71 | (apply str)))
72 | (fn [s]
73 | (->> s
74 | (take-while (fn [s] (not= s (char 0))))
75 | (apply str)))))
76 |
77 | (defn sha-256 [bytes]
78 | (.digest (java.security.MessageDigest/getInstance "SHA-256") bytes))
79 |
80 | (defn gen-checksum [n bytes]
81 | (take n (sha-256 (sha-256 bytes))))
82 |
83 | (defcodec checksum
84 | (compile-frame [:ubyte :ubyte :ubyte :ubyte]))
85 |
86 | (defcodec relay (enum :byte {true 1
87 | false 0}))
88 |
89 | (defcodec version-payload (compile-frame (ordered-map
90 | :version :int32-le
91 | :services :uint64-le
92 | :timestamp :int64-le
93 | :addr-recv net-addr
94 | :addr-from net-addr
95 | :nonce :uint64-le
96 | :user-agent varstr
97 | :start-height :int32-le
98 | :relay relay)))
99 |
100 | (defcodec verack-payload nil-frame)
101 |
102 | (defcodec addr-payload (compile-frame
103 | (ordered-map :addrs
104 | (repeated net-addrt :prefix varint))))
105 |
106 | (defcodec getaddr-payload nil-frame)
107 |
108 | (defcodec ping-payload (ordered-map :payload :uint64-le))
109 |
110 | (defcodec pong-payload (ordered-map :payload :uint64-le))
111 |
112 | (def reject-keyword->value {:malformed 0x01
113 | :invalid 0x10
114 | :obsolete 0x11
115 | :duplicate 0x12
116 | :nonstandard 0x40
117 | :dust 0x41
118 | :insufficient-fee 0x42
119 | :checkpoint 0x43})
120 |
121 | (def reject-keyword->str {:malformed "REJECT_MALFORMED"
122 | :invalid "REJECT_INVALID"
123 | :obsolete "REJECT_OBSOLETE"
124 | :duplicate "REJECT_DUPLICATE"
125 | :nonstandard "REJECT_NONSTANDARD"
126 | :dust "REJECT_DUST"
127 | :insufficient-fee "REJECT_INSUFFICIENTFEE"
128 | :checkpoint "REJECT_CHECKPOINT"})
129 |
130 |
131 | (defcodec reject-payload (compile-frame (ordered-map :message varstr
132 | :code :byte
133 | :reason varstr)))
134 |
135 |
136 | (defn reject-map [k message]
137 | (when-let [code (k reject-keyword->value)]
138 | {:message message
139 | :code (k reject-keyword->value)
140 | :reason (k reject-keyword->str)}))
141 |
142 | (defcodec inv-vector (compile-frame (ordered-map :type (enum :uint32-le
143 | {:error 0
144 | :msg-tx 1
145 | :msg-block 2})
146 | :hash (repeat 32 :byte))
147 | (fn [{:keys [type bytes]}]
148 | {:type type
149 | :hash (gen-checksum 32 bytes)})
150 | identity))
151 |
152 |
153 | (defcodec block-header (compile-frame (ordered-map :version :uint32-le
154 | :prev-block (repeat 32 :byte)
155 | :merkle-root (repeat 32 :byte)
156 | :timestamp :uint32-le
157 | :bits :uint32-le
158 | :nonce :uint32-le
159 | :txn-count varint)))
160 |
161 |
162 | (defcodec inv-payload (compile-frame
163 | (ordered-map
164 | :inv-vectors (repeated inv-vector
165 | :prefix varint))))
166 |
167 | (defcodec getdata-payload inv-payload)
168 |
169 | (defcodec notfound-payload inv-payload)
170 |
171 |
172 | (defcodec getblocks-payload (ordered-map
173 | :version :uint32-le
174 | :hashes (repeated inv-vector
175 | ;; The spec isn't clear here.
176 | ;; It might be necessary to
177 | ;; decrement the varint prefix.
178 | :prefix varint)))
179 |
180 | (defcodec getheaders-payload getblocks-payload)
181 |
182 |
183 | (defcodec outpoint (ordered-map
184 | :prev-hash (repeat 32 :byte)
185 | :index :uint32-le))
186 |
187 | (defcodec txin (ordered-map
188 | :previous outpoint
189 | :signature-script (repeated :byte :prefix varint)
190 | :sequence :uint32-le))
191 |
192 | (defcodec txout (ordered-map
193 | :value :int64-le
194 | ;; I'm pretending uchars don't exist for now
195 | :pk-script (repeated :byte :prefix varint)))
196 |
197 |
198 | (defcodec tx-payload (ordered-map
199 | :version :uint32-le
200 | :txin-vec (repeated :byte :prefix txin)
201 | :txin-vec (repeated :byte :prefix txout)
202 | :lock-time :uint32-le))
203 |
204 |
205 | (defcodec block-payload (ordered-map
206 | :version :uint32-le
207 | :prev-block (repeat 32 :byte)
208 | :merkle-root (repeat 32 :byte)
209 | :timestamp :int32-le
210 | :bits :int32-le
211 | :nonce :int32-le
212 | :txns (repeated tx-payload :prefix varint)))
213 |
214 |
215 | (defcodec alert-payload (ordered-map
216 | :version :int32-le
217 | :relay-until :int64-le
218 | :expiration :int64-le
219 | :id :int32-le
220 | :cancel :int32-le
221 | :set-cancel (repeated :int32-le :prefix varint)
222 | :min-ver :int32-le
223 | :max-ver :int32-le
224 | :set-subver (repeated varstr :prefix varint)
225 | :priority :int32-le
226 | :comment varstr
227 | :status-bar varstr
228 | :reserved varstr))
229 |
230 |
231 | (def command->payload {"version" version-payload
232 | "verack" verack-payload
233 | "addr" addr-payload
234 | "alert" alert-payload
235 | "block" block-payload
236 | "getaddr" getaddr-payload
237 | "inv" inv-payload
238 | "getdata" getdata-payload
239 | "notfound" notfound-payload
240 | "getblocks" getblocks-payload
241 | "getheaders" getheaders-payload
242 | "ping" ping-payload
243 | "pong" pong-payload
244 | "reject" reject-payload
245 | "tx" tx-payload})
246 |
247 | (let [empty-byte-array (-> [] to-byte-buffer .array)]
248 | (defcodec network-protocol
249 | (e/header (ordered-map :magic magic
250 | :command command
251 | :length :uint32-le
252 | :checksum checksum)
253 | (fn [{:keys [command]}]
254 | (-> command name command->payload))
255 | (fn [{:keys [command magic] :as b}]
256 | ;; TODO: Revisit with a header function that passes the
257 | ;; raw bytes to body->header. "encoding" should disappear.
258 | ;;
259 | ;; Also, this is really annoying. The version isn't in the
260 | ;; header. How do we handle multiple versions of the
261 | ;; bitcoin networking protocol?
262 |
263 | ;; -- johnwalker
264 |
265 | (let [first-encoding (-> command
266 | name
267 | command->payload
268 | (encode b))
269 |
270 | ;; Keeps the empty payload case from exploding.
271 | second-encoding (if first-encoding
272 | (-> first-encoding contiguous .array)
273 | empty-byte-array)]
274 | {:magic magic
275 | :command (name command)
276 | :length (count second-encoding)
277 | :checksum (gen-checksum 4 second-encoding)})))))
278 |
279 |
280 | (defn write-message
281 | "Write a bitcoin network message"
282 | [m]
283 | ;; TODO - asserts
284 | (contiguous (encode network-protocol m)))
285 |
286 |
287 | (defn read-message
288 | "Read a bitcoin network message"
289 | [x]
290 | (decode network-protocol x))
291 |
--------------------------------------------------------------------------------
/src/io/johnwalker/bitcoin_protocol/gloss/extension.clj:
--------------------------------------------------------------------------------
1 | (ns io.johnwalker.bitcoin-protocol.gloss.extension
2 | (:require [io.johnwalker.bitcoin-protocol.gloss.extension.codecs :as codecs]
3 | [gloss.core :refer [compile-frame]]))
4 |
5 | (defn header
6 | "I copied ztellman here. codecs/header is slightly different."
7 | [frame header->body body->header]
8 | (codecs/header
9 | (compile-frame frame)
10 | header->body
11 | body->header))
12 |
--------------------------------------------------------------------------------
/src/io/johnwalker/bitcoin_protocol/gloss/extension/codecs.clj:
--------------------------------------------------------------------------------
1 | (ns io.johnwalker.bitcoin-protocol.gloss.extension.codecs
2 | (:use [gloss.data bytes string primitives]
3 | [gloss.core protocols structure formats])
4 | (:require [gloss.core :refer [ordered-map]]))
5 |
6 | (defn header
7 | "It's the same as the gloss.core.codecs/header, but in read-codec
8 | the header and body are merged."
9 | [codec header->body body->header]
10 | (let [read-codec (compose-callback
11 | codec
12 | (fn [v b]
13 | (let [body (header->body v)
14 | [success m x] (read-bytes body b)]
15 | (if (and success (map? v))
16 | [success (merge m v) x]
17 | [success m x]))))]
18 | (reify
19 | Reader
20 | (read-bytes [_ buf-seq]
21 | (read-bytes read-codec buf-seq))
22 | Writer
23 | (sizeof [_]
24 | nil)
25 | (write-bytes [_ buf val]
26 | (let [header (body->header val)
27 | body (header->body header)]
28 | (if (and (sizeof codec) (sizeof body))
29 | (with-buffer [buf (+ (sizeof codec) (sizeof body))]
30 | (write-bytes codec buf header)
31 | (write-bytes body buf val))
32 | (concat
33 | (write-bytes codec buf header)
34 | (write-bytes body buf val))))))))
35 |
--------------------------------------------------------------------------------
/test/io/johnwalker/bitcoin_protocol_test.clj:
--------------------------------------------------------------------------------
1 | (ns io.johnwalker.bitcoin-protocol_test
2 | (:require [clojure.test :refer :all]
3 | [clojure.test.check :as tc]
4 | [clojure.test.check.clojure-test :refer [defspec]]
5 | [clojure.test.check.generators :as gen]
6 | [clojure.test.check.properties :as prop]
7 | [io.johnwalker.bitcoin-protocol :as pm]
8 | [gloss.io :refer :all]))
9 |
10 | (defn- str-bytes [h]
11 | (clojure.string/trimr
12 | (apply str
13 | (for [x (range (.remaining h))]
14 | (format "%02X " (.get h x))))))
15 |
16 | (defn to-unsigned [x]
17 | (bit-and x 0xFF))
18 |
19 | (defn expected-bytes [length]
20 | (+ (dec length)
21 | (* 2 length)))
22 |
23 | (deftest raw-varint-encoding
24 | (is (= (str-bytes (first (encode pm/varint 0x05))) "05"))
25 | (is (= (str-bytes (first (encode pm/varint 0x32))) "32"))
26 | (is (= (str-bytes (first (encode pm/varint 0xFC))) "FC"))
27 | (is (= (str-bytes (first (encode pm/varint 0xFD))) "FD FD 00"))
28 | (is (= (str-bytes (first (encode pm/varint 0xFF))) "FD FF 00"))
29 | (is (= (str-bytes (first (encode pm/varint 0xFFFE))) "FD FE FF"))
30 | (is (= (str-bytes (first (encode pm/varint 0xFFFF))) "FD FF FF"))
31 | (is (= (str-bytes (first (encode pm/varint 0x10000))) "FE 00 00 01 00"))
32 | (is (= (str-bytes (first (encode pm/varint 0x100000))) "FE 00 00 10 00"))
33 | (is (= (str-bytes (first (encode pm/varint 0xFFFFFFFF))) "FE FF FF FF FF"))
34 | (is (= (str-bytes (first (encode pm/varint 0x100000000))) "FF 00 00 00 00 01 00 00 00"))
35 | (is (= (str-bytes (first (encode pm/varint 0xFFFFFFFFFF))) "FF FF FF FF FF FF 00 00 00")))
36 |
37 |
38 | (defspec isomorphic-varint
39 | 150
40 | (prop/for-all [i gen/pos-int]
41 | (= i (decode pm/varint (encode pm/varint i)))))
42 |
43 |
44 | (deftest raw-varstr-encoding
45 | (let [[satoshi-varint satoshi-str] (encode pm/varstr "/Satoshi:0.7.2/")]
46 | ;; Length of "/Satoshi:0.7.2/" is 15
47 | (is (= (str-bytes satoshi-varint) "0F"))
48 | ;; Char array of "/Satoshi:0.7.2/" is correct
49 | (is (= (str-bytes satoshi-str) "2F 53 61 74 6F 73 68 69 3A 30 2E 37 2E 32 2F"))))
50 |
51 |
52 | (defspec isomorphic-varstr
53 | 150
54 | (prop/for-all [s gen/string-ascii]
55 | (= s (decode pm/varstr (encode pm/varstr s)))))
56 |
57 |
58 | (defspec isomorphic-ipaddr
59 | 150
60 | (prop/for-all [ip (gen/vector gen/byte 4)]
61 | (let [ubv (mapv to-unsigned ip)
62 | ip-str (apply str (interpose "." ubv))]
63 | (= ip-str (decode pm/ip-addr (encode pm/ip-addr ip-str))))))
64 |
65 |
66 | (deftest raw-netaddrt-encoding
67 | (= (apply str-bytes (encode pm/net-addrt
68 | {:time 1
69 | :services 1
70 | :ip "192.168.1.1"
71 | :port 0xFF}))
72 | "01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF C0 A8 01 01 00 FF"))
73 |
74 | (defspec isomorphic-netaddrt
75 | 150
76 | (prop/for-all [ip (gen/vector gen/byte 4)
77 | i gen/pos-int
78 | j gen/pos-int
79 | k gen/byte]
80 | (let [ubv (mapv to-unsigned ip)
81 | ip-str (apply str (interpose "." ubv))
82 | k (to-unsigned k)]
83 | (= {:time i
84 | :services j
85 | :ip ip-str
86 | :port k}
87 | (decode pm/net-addrt (encode pm/net-addrt {:time i
88 | :services j
89 | :ip ip-str
90 | :port k}))))))
91 |
92 | (deftest raw-checksum
93 | ;; First four bytes of hello double SHA-256 is '95 95 C9 DF'
94 | (is (= (-> pm/checksum
95 | (encode (->> "hello"
96 | .getBytes
97 | (pm/gen-checksum 4)))
98 | first
99 | str-bytes)
100 | "95 95 C9 DF")))
101 |
102 |
103 | (defspec isomorphic-checksum
104 | 150
105 | (prop/for-all [i (gen/vector gen/byte 4)]
106 | (let [ubv (mapv to-unsigned i)]
107 | (= ubv (decode pm/checksum (encode pm/checksum ubv))))))
108 |
109 |
110 | (deftest version-payload
111 | ;; Example #1 on Wiki (with relay codec appended to end)
112 | ;; 0000 f9 be b4 d9 76 65 72 73 69 6f 6e 00 00 00 00 00 ....version.....
113 | ;; 0010 64 00 00 00 35 8d 49 32 62 ea 00 00 01 00 00 00 d...5.I2b.......
114 | ;; 0020 00 00 00 00 11 b2 d0 50 00 00 00 00 01 00 00 00 .......P........
115 | ;; 0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ................
116 | ;; 0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
117 | ;; 0050 00 00 00 00 00 00 00 00 ff ff 00 00 00 00 00 00 ................
118 | ;; 0060 3b 2e b3 5d 8c e6 17 65 0f 2f 53 61 74 6f 73 68 ;..]...e./Satosh
119 | ;; 0070 69 3a 30 2e 37 2e 32 2f c0 3e 03 00 i:0.7.2/.>..
120 |
121 | ;; Message Header:
122 | ;; F9 BE B4 D9 - Main network magic bytes
123 | ;; 76 65 72 73 69 6F 6E 00 00 00 00 00 - "version" command
124 | ;; 64 00 00 00 - Payload is 100 bytes long
125 | ;; 3B 64 8D 5A - payload checksum
126 |
127 | ;; Version message:
128 | ;; 62 EA 00 00 - 60002 (protocol version 60002)
129 | ;; 01 00 00 00 00 00 00 00 - 1 (NODE_NETWORK services)
130 | ;; 11 B2 D0 50 00 00 00 00 - Tue Dec 18 10:12:33 PST 2012
131 | ;; 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00 00 00 - Recipient address info - see Network Address
132 | ;; 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00 00 00 - Sender address info - see Network Address
133 | ;; 3B 2E B3 5D 8C E6 17 65 - Node ID
134 | ;; 0F 2F 53 61 74 6F 73 68 69 3A 30 2E 37 2E 32 2F - "/Satoshi:0.7.2/" sub-version string (string is 15 bytes long)
135 | ;; C0 3E 03 00 - Last block sending node has is block #212672
136 |
137 | (is (= (->> {:version 60002 :services 1 :timestamp 1355854353
138 | :addr-recv {:services 1
139 | :ip "0.0.0.0"
140 | :port 0}
141 | :addr-from {:services 1
142 | :ip "0.0.0.0"
143 | :port 0}
144 | :nonce 7284544412836900411
145 | :user-agent "/Satoshi:0.7.2/"
146 | :start-height 212672
147 | :relay true}
148 | (encode pm/version-payload)
149 | (map str-bytes)
150 | (interpose " ")
151 | (apply str))
152 | (apply str (interpose " " '("62 EA 00 00"
153 | "01 00 00 00 00 00 00 00"
154 | "11 B2 D0 50 00 00 00 00"
155 | "01 00 00 00 00 00 00 00"
156 | "00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00 00 00"
157 | "01 00 00 00 00 00 00 00"
158 | "00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00 00 00"
159 | "3B 2E B3 5D 8C E6 17 65"
160 | "0F"
161 | "2F 53 61 74 6F 73 68 69 3A 30 2E 37 2E 32 2F"
162 | "C0 3E 03 00"
163 | "01"))))))
164 |
165 |
166 | (deftest version-message
167 | ;; NOTE: There just happens to be a collision between these two
168 | ;; examples. This is a version without the relay byte.
169 |
170 | ;; F9 BE B4 D9 - Main network magic bytes
171 | ;; 76 65 72 73 69 6F 6E 00 00 00 00 00 - "version" command
172 | ;; 64 00 00 00 - Payload is 100 bytes long
173 | ;; 3B 64 8D 5A - payload checksum
174 |
175 | ;; Version message:
176 | ;; 62 EA 00 00 - 60002 (protocol version 60002)
177 | ;; 01 00 00 00 00 00 00 00 - 1 (NODE_NETWORK services)
178 | ;; 11 B2 D0 50 00 00 00 00 - Tue Dec 18 10:12:33 PST 2012
179 | ;; 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00 00 00 - Recipient address info - see Network Address
180 | ;; 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00 00 00 - Sender address info - see Network Address
181 | ;; 3B 2E B3 5D 8C E6 17 65 - Node ID
182 | ;; 0F 2F 53 61 74 6F 73 68 69 3A 30 2E 37 2E 32 2F - "/Satoshi:0.7.2/" sub-version string (string is 15 bytes long)
183 | ;; C0 3E 03 00 - Last block sending node has is block #212672
184 |
185 |
186 | (= (apply str (interpose " " (map str-bytes (encode pm/network-protocol
187 | {:command "version"
188 | :magic :main
189 | :version 60001
190 | :services 1
191 | :timestamp 0x50D0B211
192 | :addr-recv {:services 1 :ip "0.0.0.0" :port 0}
193 | :addr-from {:services 1 :ip "0.0.0.0" :port 0}
194 | :nonce 0x6517E68C5DB32E3B
195 | :user-agent "/Satoshi:0.7.2/"
196 | :start-height 212672
197 | :relay true}))))
198 | (apply str (interpose " " '("F9 BE B4 D9"
199 | "76 65 72 73 69 6F 6E 00 00 00 00 00"
200 | "65"
201 | "85 27 39 E3"
202 | "61 EA 00 00"
203 | "01 00 00 00 00 00 00 00"
204 | "11 B2 D0 50 00 00 00 00"
205 | "01 00 00 00 00 00 00 00"
206 | "00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00"
207 | "00 00"
208 | "01 00 00 00 00 00 00 00"
209 | "00 00 00 00 00 00 00 00 00 00 FF FF 00 00 00 00"
210 | "00 00"
211 | "3B 2E B3 5D 8C E6 17 65"
212 | "0F"
213 | "2F 53 61 74 6F 73 68 69 3A 30 2E 37 2E 32 2F"
214 | "C0 3E 03 00"
215 | "01")))))
216 |
217 |
218 | (deftest verack-message
219 | ;; Message header:
220 | ;; F9 BE B4 D9 - Main network magic bytes
221 | ;; 76 65 72 61 63 6B 00 00 00 00 00 00 - "verack" command
222 | ;; 00 00 00 00 - Payload is 0 bytes long
223 | ;; 5D F6 E0 E2 - Checksum
224 | (is (= (map str-bytes (encode pm/network-protocol
225 | {:magic :main
226 | :command "verack"}))
227 | '("F9 BE B4 D9"
228 | "76 65 72 61 63 6B 00 00 00 00 00 00"
229 | "00 00 00 00"
230 | "5D F6 E0 E2"))))
231 |
232 |
233 | (deftest addr-message
234 | (is (= (apply str (interpose " " (map str-bytes (encode pm/network-protocol {:magic :main
235 | :command "addr"
236 | :addrs [{:time 0x4D1015E2 :services 1 :ip "10.0.0.1" :port 8333}]}))))
237 | (apply str (interpose " " '("F9 BE B4 D9"
238 | "61 64 64 72 00 00 00 00 00 00 00 00"
239 | "1F 00 00 00"
240 | "ED 52 39 9B"
241 | "01"
242 | "E2 15 10 4D"
243 | "01 00 00 00 00 00 00 00"
244 | "00 00 00 00 00 00 00 00 00 00 FF FF 0A 00 00 01"
245 | "20 8D"))))))
246 |
--------------------------------------------------------------------------------