From 911a54347d3ed72834e468c5b4ac32eb6b8502bd Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 15 Mar 2019 20:15:28 +0200 Subject: [PATCH] Initial commit --- .gitignore | 7 + build.gradle | 43 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++++ gradlew.bat | 84 ++++++ settings.gradle | 2 + src/main/java/com/craigraw/drongo/Drongo.java | 69 +++++ src/main/java/com/craigraw/drongo/Main.java | 77 ++++++ .../com/craigraw/drongo/OutputDescriptor.java | 259 +++++++++++++++++ .../com/craigraw/drongo/TransactionTask.java | 79 ++++++ src/main/java/com/craigraw/drongo/Utils.java | 223 +++++++++++++++ .../java/com/craigraw/drongo/WatchWallet.java | 59 ++++ .../com/craigraw/drongo/address/Address.java | 28 ++ .../craigraw/drongo/address/P2PKAddress.java | 30 ++ .../craigraw/drongo/address/P2PKHAddress.java | 29 ++ .../craigraw/drongo/address/P2SHAddress.java | 32 +++ .../drongo/address/P2WPKHAddress.java | 32 +++ .../craigraw/drongo/crypto/ChildNumber.java | 61 ++++ .../drongo/crypto/DeterministicHierarchy.java | 51 ++++ .../drongo/crypto/DeterministicKey.java | 113 ++++++++ .../com/craigraw/drongo/crypto/ECKey.java | 101 +++++++ .../drongo/crypto/HDKeyDerivation.java | 52 ++++ .../craigraw/drongo/crypto/LazyECPoint.java | 52 ++++ .../com/craigraw/drongo/protocol/Base58.java | 216 +++++++++++++++ .../com/craigraw/drongo/protocol/Bech32.java | 209 ++++++++++++++ .../drongo/protocol/ProtocolException.java | 22 ++ .../craigraw/drongo/protocol/Ripemd160.java | 157 +++++++++++ .../com/craigraw/drongo/protocol/Script.java | 169 ++++++++++++ .../craigraw/drongo/protocol/ScriptChunk.java | 71 +++++ .../drongo/protocol/ScriptOpCodes.java | 164 +++++++++++ .../drongo/protocol/ScriptPattern.java | 184 ++++++++++++ .../craigraw/drongo/protocol/Sha256Hash.java | 261 ++++++++++++++++++ .../craigraw/drongo/protocol/Transaction.java | 188 +++++++++++++ .../drongo/protocol/TransactionInput.java | 85 ++++++ .../drongo/protocol/TransactionOutPoint.java | 42 +++ .../drongo/protocol/TransactionOutput.java | 65 +++++ .../drongo/protocol/TransactionPart.java | 119 ++++++++ .../drongo/protocol/TransactionWitness.java | 38 +++ .../protocol/UnsafeByteArrayOutputStream.java | 138 +++++++++ .../com/craigraw/drongo/protocol/VarInt.java | 117 ++++++++ .../drongo/rpc/BitcoinJSONRPCClient.java | 128 +++++++++ .../craigraw/drongo/rpc/BitcoinRPCError.java | 23 ++ .../drongo/rpc/BitcoinRPCException.java | 115 ++++++++ src/main/resources/log4j.properties | 10 + .../craigraw/drongo/OutputDescriptorTest.java | 43 +++ .../com/craigraw/drongo/WatchWalletTest.java | 58 ++++ 47 files changed, 4283 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/craigraw/drongo/Drongo.java create mode 100644 src/main/java/com/craigraw/drongo/Main.java create mode 100644 src/main/java/com/craigraw/drongo/OutputDescriptor.java create mode 100644 src/main/java/com/craigraw/drongo/TransactionTask.java create mode 100644 src/main/java/com/craigraw/drongo/Utils.java create mode 100644 src/main/java/com/craigraw/drongo/WatchWallet.java create mode 100644 src/main/java/com/craigraw/drongo/address/Address.java create mode 100644 src/main/java/com/craigraw/drongo/address/P2PKAddress.java create mode 100644 src/main/java/com/craigraw/drongo/address/P2PKHAddress.java create mode 100644 src/main/java/com/craigraw/drongo/address/P2SHAddress.java create mode 100644 src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java create mode 100644 src/main/java/com/craigraw/drongo/crypto/ChildNumber.java create mode 100644 src/main/java/com/craigraw/drongo/crypto/DeterministicHierarchy.java create mode 100644 src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java create mode 100644 src/main/java/com/craigraw/drongo/crypto/ECKey.java create mode 100644 src/main/java/com/craigraw/drongo/crypto/HDKeyDerivation.java create mode 100644 src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/Base58.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/Bech32.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/ProtocolException.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/Ripemd160.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/Script.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/Transaction.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/TransactionInput.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/TransactionPart.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/UnsafeByteArrayOutputStream.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/VarInt.java create mode 100644 src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java create mode 100644 src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java create mode 100644 src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java create mode 100644 src/main/resources/log4j.properties create mode 100644 src/test/java/com/craigraw/drongo/OutputDescriptorTest.java create mode 100644 src/test/java/com/craigraw/drongo/WatchWalletTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f93c9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea +.gradle +*iml +build +/*.properties +out +*.log \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..fc041b6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' version '4.0.2' +} + +group 'com.craigraw' +version '0.1' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile 'org.zeromq:jeromq:0.5.0' + compile 'com.googlecode.json-simple:json-simple:1.1.1' + compile 'org.bouncycastle:bcprov-jdk15on:1.60' + implementation 'org.slf4j:slf4j-api:1.7.25' + runtime 'org.slf4j:slf4j-log4j12:1.7.25' + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +task(runDrongo, dependsOn: 'classes', type: JavaExec) { + main = 'com.craigraw.drongo.Main' + classpath = sourceSets.main.runtimeClasspath + args 'drongo.properties' +} + +jar { + manifest { + attributes "Main-Class": "com.craigraw.drongo.Main" + } + + baseName = 'drongo' + version = '0.1' +} + +shadowJar { + version = '0.1' + classifier = 'all' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01b8bf6b1f99cad9213fc495b33ad5bbab8efd20 GIT binary patch literal 54329 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

<5G2M1WZP5 zzqSuO91lJod(SBDDw<*sX(+F6Uq~YAeYV#2A;XQu_p=N5X+#cmu19Qk>QAnV=k!?wbk5I;tDWgFc}0NkvC*G=V+Yh1cyeJVq~9czZiDXe+S=VfL2g`LWo8om z$Y~FQc6MFjV-t1Y`^D9XMwY*U_re2R?&(O~68T&D4S{X`6JYU-pz=}ew-)V0AOUT1 zVOkHAB-8uBcRjLvz<9HS#a@X*Kc@|W)nyiSgi|u5$Md|P()%2(?olGg@ypoJwp6>m z*dnfjjWC>?_1p;%1brqZyDRR;8EntVA92EJ3ByOxj6a+bhPl z;a?m4rQAV1@QU^#M1HX)0+}A<7TCO`ZR_RzF}X9-M>cRLyN4C+lCk2)kT^3gN^`IT zNP~fAm(wyIoR+l^lQDA(e1Yv}&$I!n?&*p6?lZcQ+vGLLd~fM)qt}wsbf3r=tmVYe zl)ntf#E!P7wlakP9MXS7m0nsAmqxZ*)#j;M&0De`oNmFgi$ov#!`6^4)iQyxg5Iuj zjLAhzQ)r`^hf7`*1`Rh`X;LVBtDSz@0T?kkT1o!ijeyTGt5vc^Cd*tmNgiNo^EaWvaC8$e+nb_{W01j3%=1Y&92YacjCi>eNbwk%-gPQ@H-+4xskQ}f_c=jg^S-# zYFBDf)2?@5cy@^@FHK5$YdAK9cI;!?Jgd}25lOW%xbCJ>By3=HiK@1EM+I46A)Lsd zeT|ZH;KlCml=@;5+hfYf>QNOr^XNH%J-lvev)$Omy8MZ`!{`j>(J5cG&ZXXgv)TaF zg;cz99i$4CX_@3MIb?GL0s*8J=3`#P(jXF(_(6DXZjc@(@h&=M&JG)9&Te1?(^XMW zjjC_70|b=9hB6pKQi`S^Ls7JyJw^@P>Ko^&q8F&?>6i;#CbxUiLz1ZH4lNyd@QACd zu>{!sqjB!2Dg}pbAXD>d!3jW}=5aN0b;rw*W>*PAxm7D)aw(c*RX2@bTGEI|RRp}vw7;NR2wa;rXN{L{Q#=Fa z$x@ms6pqb>!8AuV(prv>|aU8oWV={C&$c zMa=p=CDNOC2tISZcd8~18GN5oTbKY+Vrq;3_obJlfSKRMk;Hdp1`y`&LNSOqeauR_ z^j*Ojl3Ohzb5-a49A8s|UnM*NM8tg}BJXdci5%h&;$afbmRpN0&~9rCnBA`#lG!p zc{(9Y?A0Y9yo?wSYn>iigf~KP$0*@bGZ>*YM4&D;@{<%Gg5^uUJGRrV4 z(aZOGB&{_0f*O=Oi0k{@8vN^BU>s3jJRS&CJOl3o|BE{FAA&a#2YYiX3pZz@|Go-F z|Fly;7eX2OTs>R}<`4RwpHFs9nwh)B28*o5qK1Ge=_^w0m`uJOv!=&!tzt#Save(C zgKU=Bsgql|`ui(e1KVxR`?>Dx>(rD1$iWp&m`v)3A!j5(6vBm*z|aKm*T*)mo(W;R zNGo2`KM!^SS7+*9YxTm6YMm_oSrLceqN*nDOAtagULuZl5Q<7mOnB@Hq&P|#9y{5B z!2x+2s<%Cv2Aa0+u{bjZXS);#IFPk(Ph-K7K?3i|4ro> zRbqJoiOEYo(Im^((r}U4b8nvo_>4<`)ut`24?ILnglT;Pd&U}$lV3U$F9#PD(O=yV zgNNA=GW|(E=&m_1;uaNmipQe?pon4{T=zK!N!2_CJL0E*R^XXIKf*wi!>@l}3_P9Z zF~JyMbW!+n-+>!u=A1ESxzkJy$DRuG+$oioG7(@Et|xVbJ#BCt;J43Nvj@MKvTxzy zMmjNuc#LXBxFAwIGZJk~^!q$*`FME}yKE8d1f5Mp}KHNq(@=Z8YxV}0@;YS~|SpGg$_jG7>_8WWYcVx#4SxpzlV9N4aO>K{c z$P?a_fyDzGX$Of3@ykvedGd<@-R;M^Shlj*SswJLD+j@hi_&_>6WZ}#AYLR0iWMK|A zH_NBeu(tMyG=6VO-=Pb>-Q#$F*or}KmEGg*-n?vWQREURdB#+6AvOj*I%!R-4E_2$ zU5n9m>RWs|Wr;h2DaO&mFBdDb-Z{APGQx$(L`if?C|njd*fC=rTS%{o69U|meRvu?N;Z|Y zbT|ojL>j;q*?xXmnHH#3R4O-59NV1j=uapkK7}6@Wo*^Nd#(;$iuGsb;H315xh3pl zHaJ>h-_$hdNl{+|Zb%DZH%ES;*P*v0#}g|vrKm9;j-9e1M4qX@zkl&5OiwnCz=tb6 zz<6HXD+rGIVpGtkb{Q^LIgExOm zz?I|oO9)!BOLW#krLmWvX5(k!h{i>ots*EhpvAE;06K|u_c~y{#b|UxQ*O@Ks=bca z^_F0a@61j3I(Ziv{xLb8AXQj3;R{f_l6a#H5ukg5rxwF9A$?Qp-Mo54`N-SKc}fWp z0T)-L@V$$&my;l#Ha{O@!fK4-FSA)L&3<${Hcwa7ue`=f&YsXY(NgeDU#sRlT3+9J z6;(^(sjSK@3?oMo$%L-nqy*E;3pb0nZLx6 z;h5)T$y8GXK1DS-F@bGun8|J(v-9o=42&nLJy#}M5D0T^5VWBNn$RpC zZzG6Bt66VY4_?W=PX$DMpKAI!d`INr) zkMB{XPQ<52rvWVQqgI0OL_NWxoe`xxw&X8yVftdODPj5|t}S6*VMqN$-h9)1MBe0N zYq?g0+e8fJCoAksr0af1)FYtz?Me!Cxn`gUx&|T;)695GG6HF7!Kg1zzRf_{VWv^bo81v4$?F6u2g|wxHc6eJQAg&V z#%0DnWm2Rmu71rPJ8#xFUNFC*V{+N_qqFH@gYRLZ6C?GAcVRi>^n3zQxORPG)$-B~ z%_oB?-%Zf7d*Fe;cf%tQwcGv2S?rD$Z&>QC2X^vwYjnr5pa5u#38cHCt4G3|efuci z@3z=#A13`+ztmp;%zjXwPY_aq-;isu*hecWWX_=Z8paSqq7;XYnUjK*T>c4~PR4W7 z#C*%_H&tfGx`Y$w7`dXvVhmovDnT>btmy~SLf>>~84jkoQ%cv=MMb+a{JV&t0+1`I z32g_Y@yDhKe|K^PevP~MiiVl{Ou7^Mt9{lOnXEQ`xY^6L8D$705GON{!1?1&YJEl#fTf5Z)da=yiEQ zGgtC-soFGOEBEB~ZF_{7b(76En>d}mI~XIwNw{e>=Fv)sgcw@qOsykWr?+qAOZSVrQfg}TNI ztKNG)1SRrAt6#Q?(me%)>&A_^DM`pL>J{2xu>xa$3d@90xR61TQDl@fu%_85DuUUA za9tn64?At;{`BAW6oykwntxHeDpXsV#{tmt5RqdN7LtcF4vR~_kZNT|wqyR#z^Xcd zFdymVRZvyLfTpBT>w9<)Ozv@;Yk@dOSVWbbtm^y@@C>?flP^EgQPAwsy75bveo=}T zFxl(f)s)j(0#N_>Or(xEuV(n$M+`#;Pc$1@OjXEJZumkaekVqgP_i}p`oTx;terTx zZpT+0dpUya2hqlf`SpXN{}>PfhajNk_J0`H|2<5E;U5Vh4F8er z;RxLSFgpGhkU>W?IwdW~NZTyOBrQ84H7_?gviIf71l`EETodG9a1!8e{jW?DpwjL? zGEM&eCzwoZt^P*8KHZ$B<%{I}>46IT%jJ3AnnB5P%D2E2Z_ z1M!vr#8r}1|KTqWA4%67ZdbMW2YJ81b(KF&SQ2L1Qn(y-=J${p?xLMx3W7*MK;LFQ z6Z`aU;;mTL4XrrE;HY*Rkh6N%?qviUGNAKiCB~!P}Z->IpO6E(gGd7I#eDuT7j|?nZ zK}I(EJ>$Kb&@338M~O+em9(L!+=0zBR;JAQesx|3?Ok90)D1aS9P?yTh6Poh8Cr4X zk3zc=f2rE7jj+aP7nUsr@~?^EGP>Q>h#NHS?F{Cn`g-gD<8F&dqOh-0sa%pfL`b+1 zUsF*4a~)KGb4te&K0}bE>z3yb8% zibb5Q%Sfiv7feb1r0tfmiMv z@^4XYwg@KZI=;`wC)`1jUA9Kv{HKe2t$WmRcR4y8)VAFjRi zaz&O7Y2tDmc5+SX(bj6yGHYk$dBkWc96u3u&F)2yEE~*i0F%t9Kg^L6MJSb&?wrXi zGSc;_rln$!^ybwYBeacEFRsVGq-&4uC{F)*Y;<0y7~USXswMo>j4?~5%Zm!m@i@-> zXzi82sa-vpU{6MFRktJy+E0j#w`f`>Lbog{zP|9~hg(r{RCa!uGe>Yl536cn$;ouH za#@8XMvS-kddc1`!1LVq;h57~zV`7IYR}pp3u!JtE6Q67 zq3H9ZUcWPm2V4IukS}MCHSdF0qg2@~ufNx9+VMjQP&exiG_u9TZAeAEj*jw($G)zL zq9%#v{wVyOAC4A~AF=dPX|M}MZV)s(qI9@aIK?Pe+~ch|>QYb+78lDF*Nxz2-vpRbtQ*F4$0fDbvNM#CCatgQ@z1+EZWrt z2dZfywXkiW=no5jus-92>gXn5rFQ-COvKyegmL=4+NPzw6o@a?wGE-1Bt;pCHe;34K%Z z-FnOb%!nH;)gX+!a3nCk?5(f1HaWZBMmmC@lc({dUah+E;NOros{?ui1zPC-Q0);w zEbJmdE$oU$AVGQPdm{?xxI_0CKNG$LbY*i?YRQ$(&;NiA#h@DCxC(U@AJ$Yt}}^xt-EC_ z4!;QlLkjvSOhdx!bR~W|Ezmuf6A#@T`2tsjkr>TvW*lFCMY>Na_v8+{Y|=MCu1P8y z89vPiH5+CKcG-5lzk0oY>~aJC_0+4rS@c@ZVKLAp`G-sJB$$)^4*A!B zmcf}lIw|VxV9NSoJ8Ag3CwN&d7`|@>&B|l9G8tXT^BDHOUPrtC70NgwN4${$k~d_4 zJ@eo6%YQnOgq$th?0{h`KnqYa$Nz@vlHw<%!C5du6<*j1nwquk=uY}B8r7f|lY+v7 zm|JU$US08ugor8E$h3wH$c&i~;guC|3-tqJy#T;v(g( zBZtPMSyv%jzf->435yM(-UfyHq_D=6;ouL4!ZoD+xI5uCM5ay2m)RPmm$I}h>()hS zO!0gzMxc`BPkUZ)WXaXam%1;)gedA7SM8~8yIy@6TPg!hR0=T>4$Zxd)j&P-pXeSF z9W`lg6@~YDhd19B9ETv(%er^Xp8Yj@AuFVR_8t*KS;6VHkEDKI#!@l!l3v6`W1`1~ zP{C@keuV4Q`Rjc08lx?zmT$e$!3esc9&$XZf4nRL(Z*@keUbk!GZi(2Bmyq*saOD? z3Q$V<*P-X1p2}aQmuMw9nSMbOzuASsxten7DKd6A@ftZ=NhJ(0IM|Jr<91uAul4JR zADqY^AOVT3a(NIxg|U;fyc#ZnSzw2cr}#a5lZ38>nP{05D)7~ad7JPhw!LqOwATXtRhK!w0X4HgS1i<%AxbFmGJx9?sEURV+S{k~g zGYF$IWSlQonq6}e;B(X(sIH|;52+(LYW}v_gBcp|x%rEAVB`5LXg_d5{Q5tMDu0_2 z|LOm$@K2?lrLNF=mr%YP|U-t)~9bqd+wHb4KuPmNK<}PK6e@aosGZK57=Zt+kcszVOSbe;`E^dN! ze7`ha3WUUU7(nS0{?@!}{0+-VO4A{7+nL~UOPW9_P(6^GL0h${SLtqG!} zKl~Ng5#@Sy?65wk9z*3SA`Dpd4b4T^@C8Fhd8O)k_4%0RZL5?#b~jmgU+0|DB%0Z) zql-cPC>A9HPjdOTpPC` zQwvF}uB5kG$Xr4XnaH#ruSjM*xG?_hT7y3G+8Ox`flzU^QIgb_>2&-f+XB6MDr-na zSi#S+c!ToK84<&m6sCiGTd^8pNdXo+$3^l3FL_E`0 z>8it5YIDxtTp2Tm(?}FX^w{fbfgh7>^8mtvN>9fWgFN_*a1P`Gz*dyOZF{OV7BC#j zQV=FQM5m>47xXgapI$WbPM5V`V<7J9tD)oz@d~MDoM`R^Y6-Na(lO~uvZlpu?;zw6 zVO1faor3dg#JEb5Q*gz4<W8tgC3nE2BG2jeIQs1)<{In&7hJ39x=;ih;CJDy)>0S1at*7n?Wr0ahYCpFjZ|@u91Zl7( zv;CSBRC65-6f+*JPf4p1UZ)k=XivKTX6_bWT~7V#rq0Xjas6hMO!HJN8GdpBKg_$B zwDHJF6;z?h<;GXFZan8W{XFNPpOj!(&I1`&kWO86p?Xz`a$`7qV7Xqev|7nn_lQuX ziGpU1MMYt&5dE2A62iX3;*0WzNB9*nSTzI%62A+N?f?;S>N@8M=|ef3gtQTIA*=yq zQAAjOqa!CkHOQo4?TsqrrsJLclXcP?dlAVv?v`}YUjo1Htt;6djP@NPFH+&p1I+f_ z)Y279{7OWomY8baT(4TAOlz1OyD{4P?(DGv3XyJTA2IXe=kqD)^h(@*E3{I~w;ws8 z)ZWv7E)pbEM zd3MOXRH3mQhks9 zv6{s;k0y5vrcjXaVfw8^>YyPo=oIqd5IGI{)+TZq5Z5O&hXAw%ZlL}^6FugH;-%vP zAaKFtt3i^ag226=f0YjzdPn6|4(C2sC5wHFX{7QF!tG1E-JFA`>eZ`}$ymcRJK?0c zN363o{&ir)QySOFY0vcu6)kX#;l??|7o{HBDVJN+17rt|w3;(C_1b>d;g9Gp=8YVl zYTtA52@!7AUEkTm@P&h#eg+F*lR zQ7iotZTcMR1frJ0*V@Hw__~CL>_~2H2cCtuzYIUD24=Cv!1j6s{QS!v=PzwQ(a0HS zBKx04KA}-Ue+%9d`?PG*hIij@54RDSQpA7|>qYVIrK_G6%6;#ZkR}NjUgmGju)2F`>|WJoljo)DJgZr4eo1k1i1+o z1D{>^RlpIY8OUaOEf5EBu%a&~c5aWnqM zxBpJq98f=%M^{4mm~5`CWl%)nFR64U{(chmST&2jp+-r z3675V<;Qi-kJud%oWnCLdaU-)xTnMM%rx%Jw6v@=J|Ir=4n-1Z23r-EVf91CGMGNz zb~wyv4V{H-hkr3j3WbGnComiqmS0vn?n?5v2`Vi>{Ip3OZUEPN7N8XeUtF)Ry6>y> zvn0BTLCiqGroFu|m2zG-;Xb6;W`UyLw)@v}H&(M}XCEVXZQoWF=Ykr5lX3XWwyNyF z#jHv)A*L~2BZ4lX?AlN3X#axMwOC)PoVy^6lCGse9bkGjb=qz%kDa6}MOmSwK`cVO zt(e*MW-x}XtU?GY5}9{MKhRhYOlLhJE5=ca+-RmO04^ z66z{40J=s=ey9OCdc(RCzy zd7Zr1%!y3}MG(D=wM_ebhXnJ@MLi7cImDkhm0y{d-Vm81j`0mbi4lF=eirlr)oW~a zCd?26&j^m4AeXEsIUXiTal)+SPM4)HX%%YWF1?(FV47BaA`h9m67S9x>hWMVHx~Hg z1meUYoLL(p@b3?x|9DgWeI|AJ`Ia84*P{Mb%H$ZRROouR4wZhOPX15=KiBMHl!^JnCt$Az`KiH^_d>cev&f zaG2>cWf$=A@&GP~DubsgYb|L~o)cn5h%2`i^!2)bzOTw2UR!>q5^r&2Vy}JaWFUQE04v>2;Z@ZPwXr?y&G(B^@&y zsd6kC=hHdKV>!NDLIj+3rgZJ|dF`%N$DNd;B)9BbiT9Ju^Wt%%u}SvfM^=|q-nxDG zuWCQG9e#~Q5cyf8@y76#kkR^}{c<_KnZ0QsZcAT|YLRo~&tU|N@BjxOuy`#>`X~Q< z?R?-Gsk$$!oo(BveQLlUrcL#eirhgBLh`qHEMg`+sR1`A=1QX7)ZLMRT+GBy?&mM8 zQG^z-!Oa&J-k7I(3_2#Q6Bg=NX<|@X&+YMIOzfEO2$6Mnh}YV!m!e^__{W@-CTprr zbdh3f=BeCD$gHwCrmwgM3LAv3!Mh$wM)~KWzp^w)Cu6roO7uUG5z*}i0_0j47}pK; ztN530`ScGatLOL06~zO)Qmuv`h!gq5l#wx(EliKe&rz-5qH(hb1*fB#B+q`9=jLp@ zOa2)>JTl7ovxMbrif`Xe9;+fqB1K#l=Dv!iT;xF zdkCvS>C5q|O;}ns3AgoE({Ua-zNT-9_5|P0iANmC6O76Sq_(AN?UeEQJ>#b54fi3k zFmh+P%b1x3^)0M;QxXLP!BZ^h|AhOde*{9A=f3|Xq*JAs^Y{eViF|=EBfS6L%k4ip zk+7M$gEKI3?bQg?H3zaE@;cyv9kv;cqK$VxQbFEsy^iM{XXW0@2|DOu$!-k zSFl}Y=jt-VaT>Cx*KQnHTyXt}f9XswFB9ibYh+k2J!ofO+nD?1iw@mwtrqI4_i?nE zhLkPp41ED62me}J<`3RN80#vjW;wt`pP?%oQ!oqy7`miL>d-35a=qotK$p{IzeSk# ze_$CFYp_zIkrPFVaW^s#U4xT1lI^A0IBe~Y<4uS%zSV=wcuLr%gQT=&5$&K*bwqx| zWzCMiz>7t^Et@9CRUm9E+@hy~sBpm9fri$sE1zgLU((1?Yg{N1Sars=DiW&~Zw=3I zi7y)&oTC?UWD2w97xQ&5vx zRXEBGeJ(I?Y}eR0_O{$~)bMJRTsNUPIfR!xU9PE7A>AMNr_wbrFK>&vVw=Y;RH zO$mlpmMsQ}-FQ2cSj7s7GpC+~^Q~dC?y>M}%!-3kq(F3hGWo9B-Gn02AwUgJ>Z-pKOaj zysJBQx{1>Va=*e@sLb2z&RmQ7ira;aBijM-xQ&cpR>X3wP^foXM~u1>sv9xOjzZpX z0K;EGouSYD~oQ&lAafj3~EaXfFShC+>VsRlEMa9cg9i zFxhCKO}K0ax6g4@DEA?dg{mo>s+~RPI^ybb^u--^nTF>**0l5R9pocwB?_K)BG_)S zyLb&k%XZhBVr7U$wlhMqwL)_r&&n%*N$}~qijbkfM|dIWP{MyLx}X&}ES?}7i;9bW zmTVK@zR)7kE2+L42Q`n4m0VVg5l5(W`SC9HsfrLZ=v%lpef=Gj)W59VTLe+Z$8T8i z4V%5+T0t8LnM&H>Rsm5C%qpWBFqgTwL{=_4mE{S3EnBXknM&u8n}A^IIM4$s3m(Rd z>zq=CP-!9p9es2C*)_hoL@tDYABn+o#*l;6@7;knWIyDrt5EuakO99S$}n((Fj4y} zD!VvuRzghcE{!s;jC*<_H$y6!6QpePo2A3ZbX*ZzRnQq*b%KK^NF^z96CHaWmzU@f z#j;y?X=UP&+YS3kZx7;{ zDA{9(wfz7GF`1A6iB6fnXu0?&d|^p|6)%3$aG0Uor~8o? z*e}u#qz7Ri?8Uxp4m_u{a@%bztvz-BzewR6bh*1Xp+G=tQGpcy|4V_&*aOqu|32CM zz3r*E8o8SNea2hYJpLQ-_}R&M9^%@AMx&`1H8aDx4j%-gE+baf2+9zI*+Pmt+v{39 zDZ3Ix_vPYSc;Y;yn68kW4CG>PE5RoaV0n@#eVmk?p$u&Fy&KDTy!f^Hy6&^-H*)#u zdrSCTJPJw?(hLf56%2;_3n|ujUSJOU8VPOTlDULwt0jS@j^t1WS z!n7dZIoT+|O9hFUUMbID4Ec$!cc($DuQWkocVRcYSikFeM&RZ=?BW)mG4?fh#)KVG zcJ!<=-8{&MdE)+}?C8s{k@l49I|Zwswy^ZN3;E!FKyglY~Aq?4m74P-0)sMTGXqd5(S<-(DjjM z&7dL-Mr8jhUCAG$5^mI<|%`;JI5FVUnNj!VO2?Jiqa|c2;4^n!R z`5KK0hyB*F4w%cJ@Un6GC{mY&r%g`OX|1w2$B7wxu97%<@~9>NlXYd9RMF2UM>(z0 zouu4*+u+1*k;+nFPk%ly!nuMBgH4sL5Z`@Rok&?Ef=JrTmvBAS1h?C0)ty5+yEFRz zY$G=coQtNmT@1O5uk#_MQM1&bPPnspy5#>=_7%WcEL*n$;t3FUcXxMpcXxMpA@1(( z32}FUxI1xoH;5;M_i@j?f6mF_p3Cd1DTb=dTK#qJneN`*d+pvYD*L?M(1O%DEmB>$ zs6n;@Lcm9c7=l6J&J(yBnm#+MxMvd-VKqae7;H7p-th(nwc}?ov%$8ckwY%n{RAF3 zTl^SF7qIWdSa7%WJ@B^V-wD|Z)9IQkl$xF>ebi>0AwBv5oh5$D*C*Pyj?j_*pT*IMgu3 z$p#f0_da0~Wq(H~yP##oQ}x66iYFc0O@JFgyB>ul@qz{&<14#Jy@myMM^N%oy0r|b zDPBoU!Y$vUxi%_kPeb4Hrc>;Zd^sftawKla0o|3mk@B)339@&p6inAo(Su3qlK2a) zf?EU`oSg^?f`?y=@Vaq4Dps8HLHW zIe~fHkXwT>@)r+5W7#pW$gzbbaJ$9e;W-u#VF?D=gsFfFlBJ5wR>SB;+f)sFJsYJ| z29l2Ykg+#1|INd=uj3&d)m@usb;VbGnoI1RHvva@?i&>sP&;Lt!ZY=e!=d-yZ;QV% zP@(f)+{|<*XDq%mvYKwIazn8HS`~mW%9+B|`&x*n?Y$@l{uy@ z^XxQnuny+p0JG0h)#^7}C|Btyp7=P#A2ed1vP0KGw9+~-^y4~S$bRm3gCT{+7Z<(A zJ&tg=7X|uKPKd6%z@IcZ@FgQe=rS&&1|O!s#>B_z!M_^B`O(SqE>|x- zh{~)$RW_~jXj)}mO>_PZvGdD|vtN44=Tp!oCP0>)gYeJ;n*&^BZG{$>y%Yb|L zeBUI#470!F`GM-U$?+~k+g9lj5C-P_i1%c3Zbo!@EjMJDoxQ7%jHHKeMVw&_(aoL? z%*h*aIt9-De$J>ZRLa7aWcLn<=%D+u0}RV9ys#TBGLAE%Vh`LWjWUi`Q3kpW;bd)YD~f(#$jfNdx}lOAq=#J*aV zz;K>I?)4feI+HrrrhDVkjePq;L7r87;&vm|7qaN z_>XhM8GU6I5tSr3O2W4W%m6wDH#=l32!%LRho(~*d3GfA6v-ND^0trp-qZs(B(ewD z3y3@ZV!2`DZ6b6c(Ftqg-s715;=lZqGF>H+z+c&7NeDz!We+7WNk>X*b7OZmlcTnf z{C1CB67e@xbWprDhN+t!B%4od#|>yQA$5mBM>XdhP?1U^%aD&^=PYWQEY*8Mr%h~R zOVzrd9}6RSl}Lt42r166_*s|U<1}`{l(H}m8H=D+oG>*=+=W^%IMB&CHZ-?)78G2b z)9kj_ldMecB_65eV&R+(yQ$2`ol&&7$&ns_{%A6cC2C*C6dY7qyWrHSYyOBl$0=$> z-YgkNlH{1MR-FXx7rD=4;l%6Ub3OMx9)A|Y7KLnvb`5OB?hLb#o@Wu(k|;_b!fbq( zX|rh*D3ICnZF{5ipmz8`5UV3Otwcso0I#;Q(@w+Pyj&Qa(}Uq2O(AcLU(T`+x_&~?CFLly*`fdP6NU5A|ygPXM>}(+) zkTRUw*cD<% zzFnMeB(A4A9{|Zx2*#!sRCFTk2|AMy5+@z8ws0L-{mt(9;H#}EGePUWxLabB_fFcp zLiT)TDLUXPbV2$Cde<9gv4=;u5aQ$kc9|GE2?AQZsS~D%AR`}qP?-kS_bd>C2r(I; zOc&r~HB7tUOQgZOpH&7C&q%N612f?t(MAe(B z@A!iZi)0qo^Nyb`#9DkzKjoI4rR1ghi1wJU5Tejt!ISGE93m@qDNYd|gg9(s|8-&G zcMnsX0=@2qQQ__ujux#EJ=veg&?3U<`tIWk~F=vm+WTviUvueFk&J@TcoGO{~C%6NiiNJ*0FJBQ!3Ab zm59ILI24e8!=;-k%yEf~YqN_UJ8k z0GVIS0n^8Yc)UK1eQne}<0XqzHkkTl*8VrWr zo}y?WN5@TL*1p>@MrUtxq0Vki($sn_!&;gR2e$?F4^pe@J_BQS&K3{4n+f7tZX4wQn z*Z#0eBs&H8_t`w^?ZYx=BGgyUI;H$i*t%(~8BRZ4gH+nJT0R-3lzdn4JY=xfs!YpF zQdi3kV|NTMB}uxx^KP!`=S(}{s*kfb?6w^OZpU?Wa~7f@Q^pV}+L@9kfDE`c@h5T* zY@@@?HJI)j;Y#l8z|k8y#lNTh2r?s=X_!+jny>OsA7NM~(rh3Tj7?e&pD!Jm28*UL zmRgopf0sV~MzaHDTW!bPMNcymg=!OS2bD@6Z+)R#227ET3s+2m-(W$xXBE#L$Whsi zjz6P+4cGBQkJY*vc1voifsTD}?H$&NoN^<=zK~75d|WSU4Jaw`!GoPr$b>4AjbMy+ z%4;Kt7#wwi)gyzL$R97(N?-cKygLClUk{bBPjSMLdm|MG-;oz70mGNDus zdGOi}L59=uz=VR2nIux^(D85f)1|tK&c!z1KS6tgYd^jgg6lT^5h42tZCn#Q-9k>H zVby-zby2o_GjI!zKn8ZuQ`asmp6R@=FR9kJ_Vja#I#=wtQWTes>INZynAoj$5 zN^9Ws&hvDhu*lY=De$Zby12$N&1#U2W1OHzuh;fSZH4igQodAG1K*;%>P9emF7PPD z>XZ&_hiFcX9rBXQ8-#bgSQ!5coh=(>^8gL%iOnnR>{_O#bF>l+6yZQ4R42{Sd#c7G zHy!)|g^tmtT4$YEk9PUIM8h)r?0_f=aam-`koGL&0Zp*c3H2SvrSr60s|0VtFPF^) z-$}3C94MKB)r#398;v@)bMN#qH}-%XAyJ_V&k@k+GHJ^+YA<*xmxN8qT6xd+3@i$( z0`?f(la@NGP*H0PT#Od3C6>0hxarvSr3G;0P=rG^v=nB5sfJ}9&klYZ>G1BM2({El zg0i|%d~|f2e(yWsh%r)XsV~Fm`F*Gsm;yTQV)dW!c8^WHRfk~@iC$w^h=ICTD!DD;~TIlIoVUh*r@aS|%Ae3Io zU~>^l$P8{6Ro~g26!@NToOZ(^5f8p`*6ovpcQdIDf%)?{NPPwHB>l*f_prp9XDCM8 zG`(I8xl|w{x(c`}T_;LJ!%h6L=N=zglX2Ea+2%Q8^GA>jow-M>0w{XIE-yz|?~M+; zeZO2F3QK@>(rqR|i7J^!1YGH^9MK~IQPD}R<6^~VZWErnek^xHV>ZdiPc4wesiYVL z2~8l7^g)X$kd}HC74!Y=Uq^xre22Osz!|W@zsoB9dT;2Dx8iSuK!Tj+Pgy0-TGd)7 zNy)m@P3Le@AyO*@Z2~+K9t2;=7>-*e(ZG`dBPAnZLhl^zBIy9G+c)=lq0UUNV4+N% zu*Nc4_cDh$ou3}Re}`U&(e^N?I_T~#42li13_LDYm`bNLC~>z0ZG^o6=IDdbIf+XFTfe>SeLw4UzaK#4CM4HNOs- zz>VBRkL@*A7+XY8%De)|BYE<%pe~JzZN-EU4-s_P9eINA^Qvy3z?DOTlkS!kfBG_7 zg{L6N2(=3y=iY)kang=0jClzAWZqf+fDMy-MH&Px&6X36P^!0gj%Z0JLvg~oB$9Z| zgl=6_$4LSD#(2t{Eg=2|v_{w7op+)>ehcvio@*>XM!kz+xfJees9(ObmZ~rVGH>K zWaiBlWGEV{JU=KQ>{!0+EDe-+Z#pO zv{^R<7A^gloN;Tx$g`N*Z5OG!5gN^Xj=2<4D;k1QuN5N{4O`Pfjo3Ht_RRYSzsnhTK?YUf)z4WjNY z>R04WTIh4N(RbY*hPsjKGhKu;&WI)D53RhTUOT}#QBDfUh%lJSy88oqBFX)1pt>;M z>{NTkPPk8#}DUO;#AV8I7ZQsC?Wzxn|3ubiQYI|Fn_g4r)%eNZ~ zSvTYKS*9Bcw{!=C$=1` zGQ~1D97;N!8rzKPX5WoqDHosZIKjc!MS+Q9ItJK?6Wd%STS2H!*A#a4t5 zJ-Rz_`n>>Up%|81tJR2KND<6Uoe82l={J~r*D5c_bThxVxJ<}?b0Sy}L1u|Yk=e&t z0b5c2X(#x^^fI)l<2=3b=|1OH_)-2beVEH9IzpS*Es0!4Or+xE$%zdgY+VTK2}#fpxSPtD^1a6Z)S%5eqVDzs`rL1U;Zep@^Y zWf#dJzp_iWP{z=UEepfZ4ltYMb^%H7_m4Pu81CP@Ra)ds+|Oi~a>Xi(RBCy2dTu-R z$dw(E?$QJUA3tTIf;uZq!^?_edu~bltHs!5WPM-U=R74UsBwN&nus2c?`XAzNUYY|fasp?z$nFwXQYnT`iSR<=N`1~h3#L#lF-Fc1D#UZhC2IXZ{#IDYl_r8 z?+BRvo_fPGAXi+bPVzp=nKTvN_v*xCrb^n=3cQ~No{JzfPo@YWh=7K(M_$Jk*+9u* zEY4Ww3A|JQ`+$z(hec&3&3wxV{q>D{fj!Euy2>tla^LP_2T8`St2em~qQp zm{Tk<>V3ecaP1ghn}kzS7VtKksV*27X+;Y6#I$urr=25xuC=AIP7#Jp+)L67G6>EZ zA~n}qEWm6A8GOK!3q9Yw*Z07R(qr{YBOo5&4#pD_O(O^y0a{UlC6w@ZalAN0Rq_E0 zVA!pI-6^`?nb7`y(3W5OsoVJ^MT!7r57Jm{FS{(GWAWwAh$dBpffjcOZUpPv$tTc} zv~jnA{+|18GmMDq7VK6Sb=-2nzz^7TDiixA{mf%8eQC|x>*=)((3}twJCoh~V4m3) zM5fwDbrTpnYR`lIO7Il7Eq@)St{h>Nllv+5Hk2FAE8fdD*YT|zJix?!cZ-=Uqqieb z-~swMc+yvTu(h?fT4K_UuVDqTup3%((3Q!0*Tfwyl`3e27*p{$ zaJMMF-Pb=3imlQ*%M6q5dh3tT+^%wG_r)q5?yHvrYAmc-zUo*HtP&qP#@bfcX~jwn!$k~XyC#Ox9i7dO7b4}b^f zrVEPkeD%)l0-c_gazzFf=__#Q6Pwv_V=B^h=)CYCUszS6g!}T!r&pL)E*+2C z5KCcctx6Otpf@x~7wZz*>qB_JwO!uI@9wL0_F>QAtg3fvwj*#_AKvsaD?!gcj+zp) zl2mC)yiuumO+?R2`iiVpf_E|9&}83;^&95y96F6T#E1}DY!|^IW|pf-3G0l zE&_r{24TQAa`1xj3JMev)B_J-K2MTo{nyRKWjV#+O}2ah2DZ>qnYF_O{a6Gy{aLJi#hWo3YT3U7yVxoNrUyw31163sHsCUQG|rriZFeoTcP` zFV<&;-;5x0n`rqMjx2^_7y)dHPV@tJC*jHQo!~1h`#z)Gu7m@0@z*e?o|S#5#Ht~%GC|r zd?EY_E0XKUQ2o7*e3D9{Lt7s#x~`hjzwQ{TYw;Fq8la&)%4Vj_N@ivmaSNw9X3M$MAG97a&m1SODLZ-#$~7&@ zrB~0E+38b6sfezlmhDej*KRVbzptE0Xg%$xpjqoeL;-LwmKIR#%+EZ7U|&;9rS6lo8u9iOD;-3HF{Gm=EL@W zG8L9&8=FxGHICO+MX@lC?DpY4GAE9!S+7hKsTmr8%hFI9QGI4sCj&?Of-yA98KvLsP z|k5cP?Z zay4&3t8e5RgA_@c7z{RX6d`;{B~l03#AD@RJD1{;4x93d7mD15wnFLi^LI%`Z~6@ zq9}|AG1Lq-1~Fb{1b?}bFLaSnWm!7L)P8#%g{{}}u@Q`4N{s3LiD4kSqTnM8UNN4XQi57LZRzkkL9+rJ{_?juO;cZL=MIT2H1q-=Tt1G666hVaPojp^(AM>6 zDQQf0_>1u=rvT+6(5 zAQR5%mlLdhkl4MpIyY0GN9VrGYkq?1sF8F(VeB0u3{p`h6IgEBC}Jr!^-)@5@<8s( zXyiL`ENayjlbGx}3q2T;y&|@~&$+T=hN0iS4BAARQ_JBclEeBW7}$3lx|!Ee&vs&o z=A4b##+t=rylLD-dc(X)^d?KbmU^9uZ)zXbIPC%pD{s(>p9*fu8&(?$LE67%%b-e) z!IU|lpUpK`<&YPqJnj5wb8(;a)JoC~+Kb`Fq-HL<>X@DYPqu4t9tLfS9C>Kn*Ho zl3Zz2y8;bCi@KYchQ;1JTPXL`ZMCb4R7fLlP_qKJ`aTs3H2Q6`g3GdtURX%yk`~xS z#|RDc0Y|%b+$^QYCSEG~ZF;*rT;@T=Ko6uwRJ&RasW^4$W<^nS^v|}UmIHe`P{(x| zI&y@A&b6=G2#r*st8^|19`Yw20=}MF9@@6zIuB%!vd7J%E|@zK(MRvFif-szGX^db zIvb}^{t9g(lZhLP&h6;2p>69mWE3ss6di_-KeYjPVskOMEu?5m_A>;o`6 z5ot9G8pI8Jwi@yJExKVZVw-3FD7TW3Ya{_*rS5+LicF^BX(Mq)H&l_B5o9^ zpcL6s^X}J-_9RAs(wk7s1J$cjO~jo*4l3!1V)$J+_j7t8g4A=ab`L(-{#G?z>z@KneXt&ZOv>m);*lTA}gRhYxtJt;0QZ<#l+OWu6(%(tdZ`LkXb}TQjhal;1vd{D+b@g7G z25i;qgu#ieYC?Fa?iwzeLiJa|vAU1AggN5q{?O?J9YU|xHi}PZb<6>I7->aWA4Y7-|a+7)RQagGQn@cj+ED7h6!b>XIIVI=iT(

    xR8>x!-hF($8?9?2$_G0!Ov-PHdEZo(@$?ZcCM)7YB>$ZH zMWhPJRjqPm%P_V5#UMfZ_L}+C(&-@fiUm`Gvj-V2YSM@AwZ4+@>lf-7*yxYxYzJG9 z8Z>T-V-h|PI-K8#1LBs++!+=;G&ed}>Qgs%CA|)bQd$SYzJ8U?H+Pb2&Bf=hSo*HL zELt9Z&2dz8&QQ^NY<~PP+wu57Eu>N@zkBFwO!w+BO}S0Xa(XN?BY)~WGZ<~bbZC&C zlJR|EK1_BLx*FK@OvkyG#ANGZbW~h5*xsx24d9toyTm-JUKo$r%(W42t>}}xax;qL zaw}VpEIzc=)VsC}Yx9kb@Fhh4bEWXlb4-DIH+tzLMlaT-I#A!e zKkZtQ^c@m*;P`&@?i@8tZ&Nel~z27L^F*m1}Rg^-xTzqy}3Mmq4jjJ zJC;ZK#U6QdBoE~b+-^xIyHSxNAYFGGB2WifSL_@3*CnzN18{kDvLM;dN50Jan0*YL zysmN}*Wyag#N?qeBO*E})kZMhzVKMFI zDJmEG_Wsed#Z_9T6Bi+-#s5oCG_$W<;8y%ubb!E>m!Z=HcX$Bn<&6a4a2Chp>^pAB zp^7;RF-lQa$1Ct5l88Ak4)(sYu$IRd5RwLPKa|y3wT%gBAk>pg*z=8s4UmZK(jK)g9^;e+#jYwF69JTFlz)U-(XXg zVD)U0B}ikjXJzsrW~I@l1yli*n|ww}_xpCY3<26Dc~n-dpoOqM{Yl-J@$IpVw7>YtzDZx zm}rqKSP(PM@M<^E+@ndf@wwxe$H(}rbzF`SGkwj1!{}Q6TTpZBhPDXdbCOaApGUN{ zp2q!e{c-`;@|>B9}2F<0G^h<$k%JitT<6nO`x0+K5ENk(~hYea8D*w-By=7s}!4= zEoMdOGi9B3%80sqaGRk?gj6fRr0Fa>BuM;1>R*i3bMU5rwG3r+@a~dnKMBZ_F6p*D zSRYfrDus5nFWJ%X>N6PgH~k zoB<3qHH^YyRy53{hNY>5xN6Eca!2jh-~3)NhoknTATWJ!&07-OYK-DUfkw!51UCML zP%@F<)A4~r{TkOKV9%x#edO(7H_Ke!J~A!tmmodA8dcLhhp0O@++ z35`8{H{So#b*sdgj8}LRCS%J zMNaioFbuoChaX&t7Y?OKWH~o|eKoy3#xH1@U=XTh@!Q~vn|%by)=@}Z~4PJ z#rEgEqtziT(C6b(ZY(f6TML12y;4W&hc|Wk^qF-Z1s^|{r;$!-$%|%?L5*qkt|0_#E8Vm^z>=DH zA)i=K;T0iy&HZUpgwtjWd=X{jWOQ{Vfx1iEWh^jM_jtfULMGKh;?UFn9d2W&&uVkI znCG!maf1t{Up0-*%Tdhm0F4C37_#;%@ma4c@(iAP_aZ){`hdlr=SCOwrW zCS`?8iWZGp-Jd2JaP~we_KLo04??+L+utj7_Ns~95mHW&?m6N)fbK6{TH82eKPdw* zyvp48VDX+auZ&A=LBr9ZzGzH+JHsC3p)|Bj{LquB=03Jv#0I!^36fe2=|kle_y}%Y zZMUr8YRuvpM(Yn?ik*}SUI%Qksmt(!<}vZl9k#%ZmL*phd>@;KK(izsGu1Pw3@gi% z8p#5HtQ8`>v<~M9-&pH{t`g;c>K?mcz8tk)kZB8|dc;byKSO&A!E(z=xHg{sp{>G+ zouA_g>SkebBfF}|RJUj274Y^1>;6s-eX)HzLvOD>Y1B#-Z854a=er5qqP4DvqU1IL z@VWKv&GuY%VqR$Y*Q&i3TF>jL@Uz_aKXQO$@3>X%wo>f-m<~=ye(bo_NNgIUKCT^* z3um;yNvFYd2dz%BImY}j_l*DvAuvj3Ev^cyap}Y4*`r*cE2i-e{jAGR`}Mk3WH}a5 zZ?mR>|=Izi2&RGE4_MJ(~Dz6D>7h=alt^eb2+Vd5Zh# zp`ZKBEzPQQHhds7y$?({(za}(Eve7P)~cR7yl$!N-j!maYX4zTjm{bu4*V@u)GYCA zM4{J97aDL`0J*tw;)~ZEF#Tb49m(s})Pxg}Nd_LQK2|8U9)fM!kz0rtUWz7dL{eUi zA(b07DqfmE9{hbrwrw#y?>ka@(p<#%J;XUWD6y;uZzKIrj231k^Xv>aV8O>(sDfCg@6$-_BI1rTWK3XbZ0xiZX`!QGFhWH$?;sOH?B<_4`KXd2TyX zViEvhZ!60PDc_QlVMh@e4$G?8P#0=6f2ve4d0S>Azth>50p#~Cx_~lOT&)vK%v9Mz z9J4WWMsU+Uul}8}SS9#=J9-0CXJo`-pjDLU{>Ut8dKIHMr}mW4{g_CwL^6n^%lNrb zN!T9a5yXWgpW9HnvbeE=II_8QZSPJxkw0IYBm}N!rT;bC8HRp?=|!5H)2+jsgyiqRIXnfwga8gMYN&vNAS~9r)D$peKR(j{E{TdRFU#B z<;Vl20JSOBn1$@~*W?Zk!!15f4HO>})HqKDn9MIH(`G?tN}H#xiehlE(3um>iCb$N zLD+Q@#TMJT8(G@h4UmfJ2+Ox`jD@Re{595tBwu5LH=ttNH@_8_$z5^-t4Cyf*bi)u ztx%NyZm=*{*DMOO^o6gJmm@E+WRd8yRwGaR^akm04&0lK=jL?hhqr%e6Mwx?Ws&JD zaQ5_EPnl}{ZoPhs$$2Ev?e{KIke~}D2u(QPJLV%&5@#~7@6T1jfD9g!cQaM9JgX&|LGoQE{Lh@=M65w z9alK+Q1=Ih4>Sg+ZLzH&q|WF$&FbK5JpOv|ddHyKj)r~3TH&<^x)VSPx8`PQ35i7NJ=jp(aN%iIR}7#z`P(|}jD1o% zZF9~T^QZ0Fdqv{mM8A#sSiZ(v9LGKCOtm-kiVCd#@<6s%wu#1Q1#=~%w> zrl?pthDR))hp&>qly?jMHL=53fPJ`lM?glcJuEH}CM{V{6U>hf73S~4!KXMEw^&Y7 z4{w&iLu_}AAbxDH1M=J~?GrWLND238JO$zVat1B%^L*33e$7|XA zls1r#cuaQ>#;0;+D!~HTl_8AL&$j%g1Kx7v24#aF{Q+p+h31$*S9%rXT9jjF=TNc( z23%Sr1IG1osJ(uAL_m04g~L~_ZYydDSj5l zGP6t#d5z@uBUZa|u?}9>N3u}1gNGOygP5L5Cxf4go3x?Kq#b7GTk=gZnnUuN++0zn z27%%V!d$FubU`2K2%!}ctgD)j;4nflhF2PE(VywWALKM&Bd+m+2=?>R0Il#dv;m)5 zts4r(Yp$l4crwsdomvk;s7a)g6-~uvQR3Y?Ik8WR*yTg??;)sRiuEjn-If_YydA%m z@wRljzltj_#crXi3e*T*B9(2_xD4t6{=Vn7Z$-=5jeAG2;u_ib`CIw}_3i1&CW+@f zX(6!tCnX8~j$!`DJUo6vF#C%afu3<0ZHR4vJx?6K84-%V@7nxrT>s+`+#jQRguME{ zj)XKcQl8)yXdv*CAm>mHg(A1flmgS@n)c*_`dRa{s|H#)r>#)JdP9yAb=+o$h(!x{ zUIRALkEsd}L_Jb6SRXRZJl0t0KmG9d@k$4loYX)@MpgpXm+$>OO;+wsU}%~sMSk>$ z%sxsAB3pH@vyV;WpKi8m@;5s|!64z>M=WfWc?)ZXuaj55`WGwvA5oI;7ejXIX$@~c z8nt*O`PL3n@K?G;R)z1-6%dGZ!D*@TGHA~$z^KL_W-Su$|ysw+^L+E~k@$rgI{Q!?8-0E!8 zxM1)H2Ia=)v|0=5#_nsENYw|{A9NH0eDY*iW-h?79B5slt`(DXoRbW$9~>amy7XH( zR-_o?F9f>fNlmVQ^tlEa>bob+eGEz(iwrysCSL_qHaOvz>oZ6-<@`Yk78*~=-Hf$7iBwJ~-ifEs1-!r|d|(zgR~z=> zIInVoYz>zLUx*dIZu&Jxh2EDv?C$#LQdB!Yf)-q_53BkF4K;_jvD{(WFzkHqQ9ZE( z<%u`;VW(gpeXol(ZIc;%&59NBvTpl}`LN(IXOb3Y`bn`aN{<|3e{9BH#Zzp66|u)| z>Do<1WAqZyBC5Fv!I~<^5quNgk63qfCf|)FV#V)}!AAc&xWZuMf$Ct)-zP^xj()iw z>-*+o^?QRy{iMFTcM%H>ovhdiFL(aKco{7`0B1p=0B1qje(@IAS(_Q^JN%B4Y(}iO zbQcdoz&Hr703cSVJNNiAFdDq$7QSpac`gCU4L^G#tz{7O8;Bob%0yI;ubxP@5K3t0 z1-2+o57JrJE}aUk&!{VbuB+8~kkDN%cB>PFNrO%>oWK|0VIe(*M3l{){UzjE(yNx? za6e&zYF1dO&M}XviL;G-(iao>Hb1hTi2@U;Cg<8vlze2rbP=$k^wo!bQ6!6;@-~~) z??Zr9ow zA=l~)->N9Co}($XV}|D~o6=y>dJmYt?dtS?7h%KVm*EViR=vieKx2H$jfN_7sarUf zmSPznK6b+CmpQ@@2_jz$Z;uI8h*b0{FAUxTVwhGVYU5Jv&=!=^lYd%!U+i^irr>bM zzS-;46hU%`k9W?*#aA!loZ^7kQ-1d8BjD@C`u9G4nf&WdYnK}MH0^Y2s{gf9993(*A|G`f;iqo97N*~28;L6JPpJBBH4?^SgR5% zu%Yg3cJXp&_F-)NWGW0&J!R=tA3n=wK`qsRV6vO2y`u-y#hGk}Ulzti1=T!l`GPJS z=G4qAj~5F6ni1Vl57OFmut_+3a`qw0K}a<${V#*R`Rh!Ar%Rgw)+{Uc~8t-%Ihbq z-j+|>cbi;~yfyxkl4}LS^4QNXjSeB$4N@c%^hvmKtx z0pRve5B^)M{%_1@ZfZ$qfJ)8)TIgpItLK6NcyoUNz-Mjk@Ka&lMpD<*3J{3+tSkSr zZYI74MtK0d8Nh}Aj0?C^0))Z*0$Ko|4`5-fYw#Ztx|e`M)@=6g0nNk%s4v4`0NDV3 zk$(aNj2kYlyp9eg0Cite{bxChmkiMtuw(CkDy9OY{&D}pkOpXIL^z{~#&0%1E{ zK>kKWfRLbwwWXniwY9mU&99s0sLU*`5Fi`R0H`V1bHxF7)Oh~@{qLkxKW*>VxO>Mc z_9Xz6CBOv$`cuIK{DNOpS@b_v_iMb2Qk2^-fHr0VWM=p)9vIcH@vQ6}bS*6Yn+<0` zHS-Vv-qdTr#{}n3wF3e|XZ$C;U)Qd{m8L}r&_O_ewZqTP@pJJM`6Zf!wef%L?Uz~3 zpTS_ne+l+mInQ6()XNOo&n#$?|C{C4&G0hQ=rg7e;4A)%PJcP|_)Ff=moW%6^ug z8A_gu6#(#0?fWxw=jFpM^OZb5obmUE|C2J}zt06c~G6javMT=uh?kFRJn{;a>`(Kf~)={S*9)sq#zMmpb6ju-(@G1p8+%!%NJUqO#AJ zLyrH1`9}=EfBQ1Nly7}TZE*Sx)c-E#`m*{jB`KeY#NB?E=#S?4w?O4ff|v4t&jdW4 zzd`U1Vt_B1UW$Z0Gx_`c2GegzhP~u`sr&TIN$CF@od2W(^^)qPP{uQrcGz!F{ex`A zOQx5i1kX&Gk-x$8hdJ>6Qlj7`)yr7$XDZp4-=+e5Uu^!Y>-Li5WoYd)iE;dIll<|% z{z+`)CCkeg&Sw^b#NTH5b42G$f|v1g&jg|=|DOc^tHoYMG(A({rT+%i|7@$5p)Jq& zu9?4q|IdLgFWc>9B)~ISBVax9V!-~>SoO!R`1K^~<^J \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..aa346c0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'drongo' + diff --git a/src/main/java/com/craigraw/drongo/Drongo.java b/src/main/java/com/craigraw/drongo/Drongo.java new file mode 100644 index 0000000..e48f434 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/Drongo.java @@ -0,0 +1,69 @@ +package com.craigraw.drongo; + +import com.craigraw.drongo.rpc.BitcoinJSONRPCClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.zeromq.SocketType; +import org.zeromq.ZContext; +import org.zeromq.ZMQ; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class Drongo { + private static final Logger log = LoggerFactory.getLogger(Drongo.class); + + private String nodeZmqAddress; + private BitcoinJSONRPCClient bitcoinJSONRPCClient; + private List watchWallets; + private String[] notifyRecipients; + + public Drongo(String nodeZmqAddress, Map nodeRpc, List watchWallets, String[] notifyRecipients) { + this.nodeZmqAddress = nodeZmqAddress; + this.bitcoinJSONRPCClient = new BitcoinJSONRPCClient(nodeRpc.get("host"), nodeRpc.get("port"), nodeRpc.get("user"), nodeRpc.get("password")); + this.watchWallets = watchWallets; + this.notifyRecipients = notifyRecipients; + } + + public void start() { + ExecutorService executorService = null; + + try { + executorService = Executors.newFixedThreadPool(2); + + try (ZContext context = new ZContext()) { + ZMQ.Socket subscriber = context.createSocket(SocketType.SUB); + subscriber.setRcvHWM(0); + subscriber.connect(nodeZmqAddress); + + String subscription = "rawtx"; + subscriber.subscribe(subscription.getBytes(ZMQ.CHARSET)); + + while (true) { + String topic = subscriber.recvStr(); + if (topic == null) + break; + byte[] data = subscriber.recv(); + assert (topic.equals(subscription)); + + if(subscriber.hasReceiveMore()) { + byte[] endData = subscriber.recv(); + } + + TransactionTask transactionTask = new TransactionTask(this, data); + executorService.submit(transactionTask); + } + } + } finally { + if(executorService != null) { + executorService.shutdown(); + } + } + } + + public BitcoinJSONRPCClient getBitcoinJSONRPCClient() { + return bitcoinJSONRPCClient; + } +} diff --git a/src/main/java/com/craigraw/drongo/Main.java b/src/main/java/com/craigraw/drongo/Main.java new file mode 100644 index 0000000..aab68e6 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/Main.java @@ -0,0 +1,77 @@ +package com.craigraw.drongo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.*; + +public class Main { + private static final Logger log = LoggerFactory.getLogger(Main.class); + + public static void main(String [] args) { + String propertiesFile = "./drongo.properties"; + if(args.length > 0) { + propertiesFile = args[0]; + } + + Properties properties = new Properties(); + properties.setProperty("nodeAddress", "localhost"); + + try { + File file = new File(propertiesFile); + properties.load(new FileInputStream(propertiesFile)); + log.info("Loaded properties from " + file.getCanonicalPath()); + } catch (IOException e) { + log.error("Could not load properties from provided path " + propertiesFile); + } + + String nodeZmqAddress = properties.getProperty("node.zmqpubrawtx"); + if(nodeZmqAddress == null) { + log.error("Property node.zmqpubrawtx not set, provide the zmqpubrawtx setting of the local node"); + System.exit(1); + } + + Map rpcConnection = new LinkedHashMap() { + { + put("host", properties.getProperty("node.rpcconnect", "127.0.0.1")); + put("port", properties.getProperty("node.rpcport", "8332")); + put("user", properties.getProperty("node.rpcuser")); + put("password", properties.getProperty("node.rpcpassword")); + } + }; + + List watchWallets = new ArrayList<>(); + int walletNumber = 1; + WatchWallet wallet = getWalletFromProperties(properties, walletNumber); + if(wallet == null) { + log.error("Property wallet.name.1 and/or wallet.pubkey.1 not set, provide wallet name and Base58 encoded key starting with xpub or ypub"); + System.exit(1); + } + while(wallet != null) { + watchWallets.add(wallet); + wallet = getWalletFromProperties(properties, ++walletNumber); + } + + String notifyRecipients = properties.getProperty("notify.recipients"); + if(notifyRecipients == null) { + log.error("Property notify.recipients not set, provide comma separated email addresses to receive wallet change notifications"); + System.exit(1); + } + + Drongo drongo = new Drongo(nodeZmqAddress, rpcConnection, watchWallets, notifyRecipients.split(",")); + drongo.start(); + } + + private static WatchWallet getWalletFromProperties(Properties properties, int walletNumber) { + String walletName = properties.getProperty("wallet.name." + walletNumber); + String walletPubKey = properties.getProperty("wallet.pubkey." + walletNumber); + if(walletName != null && walletPubKey != null) { + return new WatchWallet(walletName, walletPubKey); + } + + return null; + } +} diff --git a/src/main/java/com/craigraw/drongo/OutputDescriptor.java b/src/main/java/com/craigraw/drongo/OutputDescriptor.java new file mode 100644 index 0000000..11c756d --- /dev/null +++ b/src/main/java/com/craigraw/drongo/OutputDescriptor.java @@ -0,0 +1,259 @@ +package com.craigraw.drongo; + +import com.craigraw.drongo.address.Address; +import com.craigraw.drongo.address.P2PKHAddress; +import com.craigraw.drongo.address.P2SHAddress; +import com.craigraw.drongo.address.P2WPKHAddress; +import com.craigraw.drongo.crypto.ChildNumber; +import com.craigraw.drongo.crypto.DeterministicKey; +import com.craigraw.drongo.crypto.ECKey; +import com.craigraw.drongo.crypto.LazyECPoint; +import com.craigraw.drongo.protocol.Base58; +import com.craigraw.drongo.protocol.Script; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class OutputDescriptor { + private static final Logger log = LoggerFactory.getLogger(OutputDescriptor.class); + + private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub". + private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub". + private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub" + + private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("(.+)\\((\\[[^\\]]+\\])?(xpub[^/\\)]+)(/[/\\d*']+)?\\)\\)?"); + + private String script; + private int parentFingerprint; + private String keyDerivationPath; + private DeterministicKey pubKey; + private String childDerivationPath; + private ChildNumber pubKeyChildNumber; + + public OutputDescriptor(String script, int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) { + this.script = script; + this.parentFingerprint = parentFingerprint; + this.keyDerivationPath = keyDerivationPath; + this.pubKey = pubKey; + this.childDerivationPath = childDerivationPath; + this.pubKeyChildNumber = pubKeyChildNumber; + } + + public String getScript() { + return script; + } + + public int getParentFingerprint() { + return parentFingerprint; + } + + public List getKeyDerivation() { + return parsePath(keyDerivationPath); + } + + public DeterministicKey getPubKey() { + return pubKey; + } + + public List getChildDerivation() { + return getChildDerivation(0); + } + + public List getChildDerivation(int wildCardReplacement) { + return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + + public boolean describesMultipleAddresses() { + return childDerivationPath.endsWith("/*"); + } + + public List getReceivingDerivation(int wildCardReplacement) { + if(describesMultipleAddresses()) { + if(childDerivationPath.endsWith("0/*")) { + return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + + if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + } + + throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString()); + } + + public List getChangeDerivation(int wildCardReplacement) { + if(describesMultipleAddresses()) { + if(childDerivationPath.endsWith("0/*")) { + return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement); + } + + if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + } + + throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString()); + } + + private List getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) { + List path = new ArrayList<>(); + path.add(firstChild); + path.addAll(parsePath(derivationPath, wildCardReplacement)); + + return path; + } + + public Address getAddress(DeterministicKey childKey) { + Address address = null; + if(script.equals("pkh")) { + address = new P2PKHAddress(childKey.getPubKeyHash()); + } else if(script.equals("sh(wpkh")) { + Address p2wpkhAddress = new P2WPKHAddress(childKey.getPubKeyHash()); + Script receivingP2wpkhScript = p2wpkhAddress.getOutputScript(); + address = P2SHAddress.fromProgram(receivingP2wpkhScript.getProgram()); + } else if(script.equals("wpkh")) { + address = new P2WPKHAddress(childKey.getPubKeyHash()); + } else { + throw new IllegalStateException("Cannot determine address for script " + script); + } + + return address; + } + + // See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md + public static OutputDescriptor getOutputDescriptor(String descriptor) { + String script; + String keyDerivationPath =""; + String extPubKey = null; + String childDerivationPath = "/0/*"; + + Matcher matcher = DESCRIPTOR_PATTERN.matcher(descriptor); + if(matcher.matches()) { + script = matcher.group(1); + if(matcher.group(2) != null) { + keyDerivationPath = matcher.group(2); + } + + extPubKey = matcher.group(3); + if(matcher.group(4) != null) { + childDerivationPath = matcher.group(4); + } + } else if (descriptor.startsWith("xpub")) { + extPubKey = descriptor; + script = "pkh"; + } else if(descriptor.startsWith("ypub")) { + extPubKey = descriptor; + script = "sh(wpkh"; + } else if(descriptor.startsWith("zpub")) { + extPubKey = descriptor; + script = "wpkh"; + } else { + throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor); + } + + byte[] serializedKey = Base58.decodeChecked(extPubKey); + ByteBuffer buffer = ByteBuffer.wrap(serializedKey); + int header = buffer.getInt(); + if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub)) { + throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4)); + } + + int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative + final int parentFingerprint = buffer.getInt(); + final int i = buffer.getInt(); + ChildNumber childNumber; + List path; + + if(depth == 0) { + //Poorly formatted extended public key, add first child path element + childNumber = new ChildNumber(0, false); + } else if ((i & ChildNumber.HARDENED_BIT) != 0) { + childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened + } else { + childNumber = new ChildNumber(i, false); + } + path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber))); + + //Remove account level for depth 4 keys + if(depth == 4 && (descriptor.startsWith("xpub") || descriptor.startsWith("ypub") || descriptor.startsWith("zpub"))) { + log.warn("Output descriptor describes a public key derived at depth 4; change addresses not available"); + childDerivationPath = "/*"; + } + + byte[] chainCode = new byte[32]; + buffer.get(chainCode); + byte[] data = new byte[33]; + buffer.get(data); + if(buffer.hasRemaining()) { + throw new IllegalArgumentException("Found unexpected data in key"); + } + + DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint); + return new OutputDescriptor(script, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber); + } + + public static List parsePath(String path) { + return parsePath(path, 0); + } + + public static List parsePath(String path, int wildcardReplacement) { + String[] parsedNodes = path.replace("M", "").split("/"); + List nodes = new ArrayList<>(); + + for (String n : parsedNodes) { + n = n.replaceAll(" ", ""); + if (n.length() == 0) continue; + boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'"); + if (isHard) n = n.substring(0, n.length() - 1); + if (n.equals("*")) n = Integer.toString(wildcardReplacement); + int nodeNumber = Integer.parseInt(n); + nodes.add(new ChildNumber(nodeNumber, isHard)); + } + + return nodes; + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(script); + builder.append("("); + builder.append(getExtendedPublicKey()); + builder.append(childDerivationPath); + builder.append(")"); + + if(script.contains("(")){ + builder.append(")"); + } + + return builder.toString(); + } + + public String getExtendedPublicKey() { + return Base58.encodeChecked(getExtendedPublicKeyBytes()); + } + + public byte[] getExtendedPublicKeyBytes() { + ByteBuffer buffer = ByteBuffer.allocate(78); + buffer.putInt(bip32HeaderP2PKHXPub); + + List childPath = parsePath(childDerivationPath); + int depth = 5 - childPath.size(); + buffer.put((byte)depth); + + buffer.putInt(parentFingerprint); + + buffer.putInt(pubKeyChildNumber.i()); + + buffer.put(pubKey.getChainCode()); + buffer.put(pubKey.getPubKey()); + + return buffer.array(); + } +} diff --git a/src/main/java/com/craigraw/drongo/TransactionTask.java b/src/main/java/com/craigraw/drongo/TransactionTask.java new file mode 100644 index 0000000..e05dd6c --- /dev/null +++ b/src/main/java/com/craigraw/drongo/TransactionTask.java @@ -0,0 +1,79 @@ +package com.craigraw.drongo; + +import com.craigraw.drongo.address.Address; +import com.craigraw.drongo.protocol.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class TransactionTask implements Runnable { + private static final Logger log = LoggerFactory.getLogger(Drongo.class); + + private Drongo drongo; + private byte[] transactionData; + + public TransactionTask(Drongo drongo, byte[] transactionData) { + this.drongo = drongo; + this.transactionData = transactionData; + } + + @Override + public void run() { + Transaction transaction = new Transaction(transactionData); + Map referencedTransactions = new HashMap<>(); + + Sha256Hash txid = transaction.getTxId(); + StringBuilder builder = new StringBuilder("Txid: " + txid.toString() + " "); + StringJoiner inputJoiner = new StringJoiner(", ", "[", "]"); + + int vin = 0; + for(TransactionInput input : transaction.getInputs()) { + if(input.isCoinBase()) { + inputJoiner.add("Coinbase:" + vin); + } else { + String referencedTxID = input.getOutpoint().getHash().toString(); + long referencedVout = input.getOutpoint().getIndex(); + + Transaction referencedTransaction = referencedTransactions.get(referencedTxID); + if(referencedTransaction == null) { + String referencedTransactionHex = drongo.getBitcoinJSONRPCClient().getRawTransaction(referencedTxID); + referencedTransaction = new Transaction(Utils.hexToBytes(referencedTransactionHex)); + referencedTransactions.put(referencedTxID, referencedTransaction); + } + + TransactionOutput referencedOutput = referencedTransaction.getOutputs().get((int)referencedVout); + if(referencedOutput.getScript().containsToAddress()) { + Address[] inputAddresses = referencedOutput.getScript().getToAddresses(); + input.getOutpoint().setAddresses(inputAddresses); + inputJoiner.add((inputAddresses.length == 1 ? inputAddresses[0] : Arrays.asList(inputAddresses)) + ":" + vin); + } else { + log.warn("Could not determine nature of referenced input tx: " + referencedTxID + ":" + referencedVout); + } + } + + vin++; + } + + builder.append(inputJoiner.toString() + " => "); + StringJoiner outputJoiner = new StringJoiner(", ", "[", "]"); + + int vout = 0; + for(TransactionOutput output : transaction.getOutputs()) { + try { + if(output.getScript().containsToAddress()) { + Address[] outputAddresses = output.getScript().getToAddresses(); + output.setAddresses(outputAddresses); + outputJoiner.add((outputAddresses.length == 1 ? outputAddresses[0] : Arrays.asList(outputAddresses)) + ":" + vout + " (" + output.getValue() + ")"); + } + } catch(ProtocolException e) { + log.debug("Invalid script for output " + vout + " detected (" + e.getMessage() + "). Skipping..."); + } + + vout++; + } + + builder.append(outputJoiner.toString()); + log.info(builder.toString()); + } +} diff --git a/src/main/java/com/craigraw/drongo/Utils.java b/src/main/java/com/craigraw/drongo/Utils.java new file mode 100644 index 0000000..367ceb4 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/Utils.java @@ -0,0 +1,223 @@ +package com.craigraw.drongo; + +import com.craigraw.drongo.crypto.ChildNumber; +import com.craigraw.drongo.protocol.ProtocolException; +import com.craigraw.drongo.protocol.Ripemd160; +import com.craigraw.drongo.protocol.Sha256Hash; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; + +import javax.xml.bind.DatatypeConverter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; + +public class Utils { + public static final int MAX_INITIAL_ARRAY_LENGTH = 20; + private final static char[] hexArray = "0123456789abcdef".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] hexToBytes(final String data) { + return decodeHex(data.toCharArray()); + } + + public static byte[] decodeHex(final char[] data) { + + final int len = data.length; + + if ((len & 0x01) != 0) { + throw new ProtocolException("Odd number of characters."); + } + + final byte[] out = new byte[len >> 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(data[j], j) << 4; + j++; + f = f | toDigit(data[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + protected static int toDigit(final char ch, final int index) { + final int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new ProtocolException("Illegal hexadecimal character " + ch + " at index " + index); + } + return digit; + } + + /** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */ + public static long readUint32(byte[] bytes, int offset) { + return (bytes[offset] & 0xffl) | + ((bytes[offset + 1] & 0xffl) << 8) | + ((bytes[offset + 2] & 0xffl) << 16) | + ((bytes[offset + 3] & 0xffl) << 24); + } + + /** Parse 8 bytes from the byte array (starting at the offset) as signed 64-bit integer in little endian format. */ + public static long readInt64(byte[] bytes, int offset) { + return (bytes[offset] & 0xffl) | + ((bytes[offset + 1] & 0xffl) << 8) | + ((bytes[offset + 2] & 0xffl) << 16) | + ((bytes[offset + 3] & 0xffl) << 24) | + ((bytes[offset + 4] & 0xffl) << 32) | + ((bytes[offset + 5] & 0xffl) << 40) | + ((bytes[offset + 6] & 0xffl) << 48) | + ((bytes[offset + 7] & 0xffl) << 56); + } + + /** Parse 2 bytes from the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */ + public static int readUint16(byte[] bytes, int offset) { + return (bytes[offset] & 0xff) | + ((bytes[offset + 1] & 0xff) << 8); + } + + /** Parse 2 bytes from the stream as unsigned 16-bit integer in little endian format. */ + public static int readUint16FromStream(InputStream is) { + try { + return (is.read() & 0xff) | + ((is.read() & 0xff) << 8); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + /** Parse 4 bytes from the stream as unsigned 32-bit integer in little endian format. */ + public static long readUint32FromStream(InputStream is) { + try { + return (is.read() & 0xffl) | + ((is.read() & 0xffl) << 8) | + ((is.read() & 0xffl) << 16) | + ((is.read() & 0xffl) << 24); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + /** Write 2 bytes to the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */ + public static void uint16ToByteArrayLE(int val, byte[] out, int offset) { + out[offset] = (byte) (0xFF & val); + out[offset + 1] = (byte) (0xFF & (val >> 8)); + } + + /** Write 4 bytes to the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */ + public static void uint32ToByteArrayLE(long val, byte[] out, int offset) { + out[offset] = (byte) (0xFF & val); + out[offset + 1] = (byte) (0xFF & (val >> 8)); + out[offset + 2] = (byte) (0xFF & (val >> 16)); + out[offset + 3] = (byte) (0xFF & (val >> 24)); + } + + /** Write 8 bytes to the byte array (starting at the offset) as signed 64-bit integer in little endian format. */ + public static void int64ToByteArrayLE(long val, byte[] out, int offset) { + out[offset] = (byte) (0xFF & val); + out[offset + 1] = (byte) (0xFF & (val >> 8)); + out[offset + 2] = (byte) (0xFF & (val >> 16)); + out[offset + 3] = (byte) (0xFF & (val >> 24)); + out[offset + 4] = (byte) (0xFF & (val >> 32)); + out[offset + 5] = (byte) (0xFF & (val >> 40)); + out[offset + 6] = (byte) (0xFF & (val >> 48)); + out[offset + 7] = (byte) (0xFF & (val >> 56)); + } + + /** Write 2 bytes to the output stream as unsigned 16-bit integer in little endian format. */ + public static void uint16ToByteStreamLE(int val, OutputStream stream) throws IOException { + stream.write((int) (0xFF & val)); + stream.write((int) (0xFF & (val >> 8))); + } + + /** Write 4 bytes to the output stream as unsigned 32-bit integer in little endian format. */ + public static void uint32ToByteStreamLE(long val, OutputStream stream) throws IOException { + stream.write((int) (0xFF & val)); + stream.write((int) (0xFF & (val >> 8))); + stream.write((int) (0xFF & (val >> 16))); + stream.write((int) (0xFF & (val >> 24))); + } + + /** Write 8 bytes to the output stream as signed 64-bit integer in little endian format. */ + public static void int64ToByteStreamLE(long val, OutputStream stream) throws IOException { + stream.write((int) (0xFF & val)); + stream.write((int) (0xFF & (val >> 8))); + stream.write((int) (0xFF & (val >> 16))); + stream.write((int) (0xFF & (val >> 24))); + stream.write((int) (0xFF & (val >> 32))); + stream.write((int) (0xFF & (val >> 40))); + stream.write((int) (0xFF & (val >> 48))); + stream.write((int) (0xFF & (val >> 56))); + } + + /** + * Returns a copy of the given byte array in reverse order. + */ + public static byte[] reverseBytes(byte[] bytes) { + // We could use the XOR trick here but it's easier to understand if we don't. If we find this is really a + // performance issue the matter can be revisited. + byte[] buf = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) + buf[i] = bytes[bytes.length - 1 - i]; + return buf; + } + + /** + * Calculates RIPEMD160(SHA256(input)). This is used in Address calculations. + */ + public static byte[] sha256hash160(byte[] input) { + byte[] sha256 = Sha256Hash.hash(input); + return Ripemd160.getHash(sha256); + } + + /** Convert to a string path, starting with "M/" */ + public static String formatHDPath(List path) { + StringJoiner joiner = new StringJoiner("/"); + joiner.add("M"); + for(ChildNumber number : path) { + joiner.add(number.toString()); + } + + return joiner.toString(); + } + + public static List appendChild(List path, ChildNumber childNumber) { + List childPath = new ArrayList<>(path); + childPath.add(childNumber); + return Collections.unmodifiableList(childPath); + } + + static HMac createHmacSha512Digest(byte[] key) { + SHA512Digest digest = new SHA512Digest(); + HMac hMac = new HMac(digest); + hMac.init(new KeyParameter(key)); + return hMac; + } + + public static byte[] hmacSha512(HMac hmacSha512, byte[] input) { + hmacSha512.reset(); + hmacSha512.update(input, 0, input.length); + byte[] out = new byte[64]; + hmacSha512.doFinal(out, 0); + return out; + } + + public static byte[] hmacSha512(byte[] key, byte[] data) { + return hmacSha512(createHmacSha512Digest(key), data); + } +} diff --git a/src/main/java/com/craigraw/drongo/WatchWallet.java b/src/main/java/com/craigraw/drongo/WatchWallet.java new file mode 100644 index 0000000..c9ad3fe --- /dev/null +++ b/src/main/java/com/craigraw/drongo/WatchWallet.java @@ -0,0 +1,59 @@ +package com.craigraw.drongo; + +import com.craigraw.drongo.address.Address; +import com.craigraw.drongo.crypto.*; + +import java.util.HashMap; +import java.util.List; + +public class WatchWallet { + private static final int LOOK_AHEAD_LIMIT = 500; + + private String name; + private String extPubKey; + + private OutputDescriptor outputDescriptor; + private DeterministicHierarchy hierarchy; + + private HashMap addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2); + + public WatchWallet(String name, String descriptor) { + this.name = name; + this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor); + this.hierarchy = new DeterministicHierarchy(outputDescriptor.getPubKey()); + + } + + public void initialiseAddresses() { + if(outputDescriptor.describesMultipleAddresses()) { + for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { + List receivingDerivation = outputDescriptor.getReceivingDerivation(index); + Address address = getAddress(receivingDerivation); + addresses.put(address.toString(), Utils.formatHDPath(receivingDerivation)); + } + + for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { + List changeDerivation = outputDescriptor.getChangeDerivation(index); + Address address = getAddress(changeDerivation); + addresses.put(address.toString(), Utils.formatHDPath(changeDerivation)); + } + } else { + List derivation = outputDescriptor.getChildDerivation(); + Address address = getAddress(derivation); + addresses.put(address.toString(), Utils.formatHDPath(derivation)); + } + } + + public Address getReceivingAddress(int index) { + return getAddress(outputDescriptor.getReceivingDerivation(index)); + } + + public Address getChangeAddress(int index) { + return getAddress(outputDescriptor.getChangeDerivation(index)); + } + + private Address getAddress(List path) { + DeterministicKey childKey = hierarchy.get(path); + return outputDescriptor.getAddress(childKey); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/Address.java b/src/main/java/com/craigraw/drongo/address/Address.java new file mode 100644 index 0000000..d82441d --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/Address.java @@ -0,0 +1,28 @@ +package com.craigraw.drongo.address; + +import com.craigraw.drongo.protocol.Base58; +import com.craigraw.drongo.protocol.Script; + +public abstract class Address { + protected final byte[] pubKeyHash; + + public Address(byte[] pubKeyHash) { + this.pubKeyHash = pubKeyHash; + } + + public byte[] getPubKeyHash() { + return pubKeyHash; + } + + public String getAddress() { + return Base58.encodeChecked(getVersion(), pubKeyHash); + } + + public String toString() { + return getAddress(); + } + + public abstract int getVersion(); + + public abstract Script getOutputScript(); +} diff --git a/src/main/java/com/craigraw/drongo/address/P2PKAddress.java b/src/main/java/com/craigraw/drongo/address/P2PKAddress.java new file mode 100644 index 0000000..b741fc0 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2PKAddress.java @@ -0,0 +1,30 @@ +package com.craigraw.drongo.address; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.protocol.Script; +import com.craigraw.drongo.protocol.ScriptChunk; +import com.craigraw.drongo.protocol.ScriptOpCodes; + +import java.util.ArrayList; +import java.util.List; + +public class P2PKAddress extends Address { + private byte[] pubKey; + + public P2PKAddress(byte[] pubKey) { + super(Utils.sha256hash160(pubKey)); + this.pubKey = pubKey; + } + + public int getVersion() { + return 0; + } + + public Script getOutputScript() { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(pubKey.length, pubKey)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null)); + + return new Script(chunks); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/P2PKHAddress.java b/src/main/java/com/craigraw/drongo/address/P2PKHAddress.java new file mode 100644 index 0000000..d93f120 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2PKHAddress.java @@ -0,0 +1,29 @@ +package com.craigraw.drongo.address; + +import com.craigraw.drongo.protocol.Script; +import com.craigraw.drongo.protocol.ScriptChunk; +import com.craigraw.drongo.protocol.ScriptOpCodes; + +import java.util.ArrayList; +import java.util.List; + +public class P2PKHAddress extends Address { + public P2PKHAddress(byte[] pubKeyHash) { + super(pubKeyHash); + } + + public int getVersion() { + return 0; + } + + public Script getOutputScript() { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_DUP, null)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null)); + chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null)); + + return new Script(chunks); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/P2SHAddress.java b/src/main/java/com/craigraw/drongo/address/P2SHAddress.java new file mode 100644 index 0000000..5df38da --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2SHAddress.java @@ -0,0 +1,32 @@ +package com.craigraw.drongo.address; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.protocol.Script; +import com.craigraw.drongo.protocol.ScriptChunk; +import com.craigraw.drongo.protocol.ScriptOpCodes; + +import java.util.ArrayList; +import java.util.List; + +public class P2SHAddress extends Address { + public P2SHAddress(byte[] pubKeyHash) { + super(pubKeyHash); + } + + public int getVersion() { + return 5; + } + + public Script getOutputScript() { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null)); + chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUAL, null)); + + return new Script(chunks); + } + + public static P2SHAddress fromProgram(byte[] program) { + return new P2SHAddress(Utils.sha256hash160(program)); + } +} diff --git a/src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java b/src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java new file mode 100644 index 0000000..e1b66b9 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2WPKHAddress.java @@ -0,0 +1,32 @@ +package com.craigraw.drongo.address; + +import com.craigraw.drongo.protocol.Bech32; +import com.craigraw.drongo.protocol.Script; +import com.craigraw.drongo.protocol.ScriptChunk; + +import java.util.ArrayList; +import java.util.List; + +public class P2WPKHAddress extends Address { + public static final String HRP = "bc"; + + public P2WPKHAddress(byte[] pubKeyHash) { + super(pubKeyHash); + } + + public int getVersion() { + return 0; + } + + public String getAddress() { + return Bech32.encode(HRP, getVersion(), pubKeyHash); + } + + public Script getOutputScript() { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(Script.encodeToOpN(getVersion()), null)); + chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash)); + + return new Script(chunks); + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/ChildNumber.java b/src/main/java/com/craigraw/drongo/crypto/ChildNumber.java new file mode 100644 index 0000000..da13318 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/ChildNumber.java @@ -0,0 +1,61 @@ +package com.craigraw.drongo.crypto; + +import java.util.Locale; + +public class ChildNumber { + /** + * The bit that's set in the child number to indicate whether this key is "hardened". Given a hardened key, it is + * not possible to derive a child public key if you know only the hardened public key. With a non-hardened key this + * is possible, so you can derive trees of public keys given only a public parent, but the downside is that it's + * possible to leak private keys if you disclose a parent public key and a child private key (elliptic curve maths + * allows you to work upwards). + */ + public static final int HARDENED_BIT = 0x80000000; + + public static final ChildNumber ZERO = new ChildNumber(0); + public static final ChildNumber ZERO_HARDENED = new ChildNumber(0, true); + public static final ChildNumber ONE = new ChildNumber(1); + public static final ChildNumber ONE_HARDENED = new ChildNumber(1, true); + + /** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/ + private final int i; + + public ChildNumber(int childNumber, boolean isHardened) { + if (hasHardenedBit(childNumber)) + throw new IllegalArgumentException("Most significant bit is reserved and shouldn't be set: " + childNumber); + i = isHardened ? (childNumber | HARDENED_BIT) : childNumber; + } + + public ChildNumber(int i) { + this.i = i; + } + + private static boolean hasHardenedBit(int a) { + return (a & HARDENED_BIT) != 0; + } + + public boolean isHardened() { + return hasHardenedBit(i); + } + + public int num() { + return i & (~HARDENED_BIT); + } + + /** Returns the uint32 encoded form of the path element, including the most significant bit. */ + public int i() { return i; } + + public String toString() { + return String.format(Locale.US, "%d%s", num(), isHardened() ? "H" : ""); + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return i == ((ChildNumber)o).i; + } + + public int hashCode() { + return i; + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/DeterministicHierarchy.java b/src/main/java/com/craigraw/drongo/crypto/DeterministicHierarchy.java new file mode 100644 index 0000000..1f53bee --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/DeterministicHierarchy.java @@ -0,0 +1,51 @@ +package com.craigraw.drongo.crypto; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.protocol.Base58; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class DeterministicHierarchy { + private final Map, DeterministicKey> keys = new HashMap<>(); + private final List rootPath; + // Keep track of how many child keys each node has. This is kind of weak. + private final Map, ChildNumber> lastChildNumbers = new HashMap<>(); + + public DeterministicHierarchy(DeterministicKey rootKey) { + putKey(rootKey); + rootPath = rootKey.getPath(); + } + + public final void putKey(DeterministicKey key) { + List path = key.getPath(); + // Update our tracking of what the next child in each branch of the tree should be. Just assume that keys are + // inserted in order here. + final DeterministicKey parent = key.getParent(); + if (parent != null) + lastChildNumbers.put(parent.getPath(), key.getChildNumber()); + keys.put(path, key); + } + + /** + * Returns a key for the given path, optionally creating it. + * + * @param path the path to the key + * @return next newly created key using the child derivation function + * @throws IllegalArgumentException if create is false and the path was not found. + */ + public DeterministicKey get(List path) { + if(!keys.containsKey(path)) { + if(path.size() == 0) { + throw new IllegalArgumentException("Can't derive the master key: nothing to derive from."); + } + + DeterministicKey parent = get(path.subList(0, path.size() - 1)); + putKey(HDKeyDerivation.deriveChildKey(parent, path.get(path.size() - 1))); + } + + return keys.get(path); + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java b/src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java new file mode 100644 index 0000000..88e8645 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/DeterministicKey.java @@ -0,0 +1,113 @@ +package com.craigraw.drongo.crypto; + + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.protocol.Base58; +import com.craigraw.drongo.protocol.Sha256Hash; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; + +public class DeterministicKey extends ECKey { + private final DeterministicKey parent; + private final List childNumberPath; + private final int depth; + private int parentFingerprint; // 0 if this key is root node of key hierarchy + + /** 32 bytes */ + private final byte[] chainCode; + + /** + * Constructs a key from its components, including its public key data and possibly-redundant + * information about its parent key. Invoked when deserializing, but otherwise not something that + * you normally should use. + */ + public DeterministicKey(List childNumberPath, + byte[] chainCode, + LazyECPoint publicAsPoint, + int depth, + int parentFingerprint) { + super(compressPoint(publicAsPoint)); + if(chainCode.length != 32) { + throw new IllegalArgumentException("Chaincode not 32 bytes in length"); + } + this.parent = null; + this.childNumberPath = childNumberPath; + this.chainCode = Arrays.copyOf(chainCode, chainCode.length); + this.depth = depth; + this.parentFingerprint = parentFingerprint; + } + + public DeterministicKey(List childNumberPath, + byte[] chainCode, + LazyECPoint publicAsPoint, + DeterministicKey parent) { + super(compressPoint(publicAsPoint)); + if(chainCode.length != 32) { + throw new IllegalArgumentException("Chaincode not 32 bytes in length"); + } + this.parent = parent; + this.childNumberPath = childNumberPath; + this.chainCode = Arrays.copyOf(chainCode, chainCode.length); + this.depth = parent == null ? 0 : parent.depth + 1; + this.parentFingerprint = (parent != null) ? parent.getFingerprint() : 0; + } + + /** + * Return this key's depth in the hierarchy, where the root node is at depth zero. + * This may be different than the number of segments in the path if this key was + * deserialized without access to its parent. + */ + public int getDepth() { + return depth; + } + + /** Returns the first 32 bits of the result of {@link #getIdentifier()}. */ + public int getFingerprint() { + // TODO: why is this different than armory's fingerprint? BIP 32: "The first 32 bits of the identifier are called the fingerprint." + return ByteBuffer.wrap(Arrays.copyOfRange(getIdentifier(), 0, 4)).getInt(); + } + + /** + * Returns RIPE-MD160(SHA256(pub key bytes)). + */ + public byte[] getIdentifier() { + return Utils.sha256hash160(getPubKey()); + } + + /** + * Returns the path through some DeterministicHierarchy which reaches this keys position in the tree. + * A path can be written as 0/1/0 which means the first child of the root, the second child of that node, then + * the first child of that node. + */ + public List getPath() { + return childNumberPath; + } + + public DeterministicKey getParent() { + return parent; + } + + /** Returns the last element of the path returned by {@link DeterministicKey#getPath()} */ + public ChildNumber getChildNumber() { + return childNumberPath.size() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1); + } + + public byte[] getChainCode() { + return chainCode; + } + + public static String toBase58(byte[] ser) { + return Base58.encode(addChecksum(ser)); + } + + static byte[] addChecksum(byte[] input) { + int inputLength = input.length; + byte[] checksummed = new byte[inputLength + 4]; + System.arraycopy(input, 0, checksummed, 0, inputLength); + byte[] checksum = Sha256Hash.hashTwice(input); + System.arraycopy(checksum, 0, checksummed, inputLength, 4); + return checksummed; + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/ECKey.java b/src/main/java/com/craigraw/drongo/crypto/ECKey.java new file mode 100644 index 0000000..3fd2e9a --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/ECKey.java @@ -0,0 +1,101 @@ +package com.craigraw.drongo.crypto; + +import com.craigraw.drongo.Utils; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.math.ec.FixedPointUtil; + +import java.math.BigInteger; +import java.security.SecureRandom; + +public class ECKey { + // The parameters of the secp256k1 curve that Bitcoin uses. + private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); + + /** The parameters of the secp256k1 curve that Bitcoin uses. */ + public static final ECDomainParameters CURVE; + + /** + * Equal to CURVE.getN().shiftRight(1), used for canonicalising the S value of a signature. If you aren't + * sure what this is about, you can ignore it. + */ + public static final BigInteger HALF_CURVE_ORDER; + + private static final SecureRandom secureRandom; + + static { + // Tell Bouncy Castle to precompute data that's needed during secp256k1 calculations. + FixedPointUtil.precompute(CURVE_PARAMS.getG()); + CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), + CURVE_PARAMS.getH()); + HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1); + secureRandom = new SecureRandom(); + } + + protected final LazyECPoint pub; + + private byte[] pubKeyHash; + + protected ECKey(LazyECPoint pub) { + this.pub = pub; + } + + /** + * Utility for compressing an elliptic curve point. Returns the same point if it's already compressed. + * See the ECKey class docs for a discussion of point compression. + */ + public static ECPoint compressPoint(ECPoint point) { + return getPointWithCompression(point, true); + } + + public static LazyECPoint compressPoint(LazyECPoint point) { + return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get())); + } + + private static ECPoint getPointWithCompression(ECPoint point, boolean compressed) { + if (point.isCompressed() == compressed) + return point; + point = point.normalize(); + BigInteger x = point.getAffineXCoord().toBigInteger(); + BigInteger y = point.getAffineYCoord().toBigInteger(); + return CURVE.getCurve().createPoint(x, y, compressed); + } + + /** + * Gets the raw public key value. This appears in transaction scriptSigs. Note that this is not the same + * as the pubKeyHash/address. + */ + public byte[] getPubKey() { + return pub.getEncoded(); + } + + /** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */ + public ECPoint getPubKeyPoint() { + return pub.get(); + } + + /** Gets the hash160 form of the public key (as seen in addresses). */ + public byte[] getPubKeyHash() { + if (pubKeyHash == null) + pubKeyHash = Utils.sha256hash160(this.pub.getEncoded()); + return pubKeyHash; + } + + /** + * Returns public key point from the given private key. To convert a byte array into a BigInteger, + * use {@code new BigInteger(1, bytes);} + */ + public static ECPoint publicPointFromPrivate(BigInteger privKey) { + /* + * TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group order, + * but that could change in future versions. + */ + if (privKey.bitLength() > CURVE.getN().bitLength()) { + privKey = privKey.mod(CURVE.getN()); + } + return new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey); + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/HDKeyDerivation.java b/src/main/java/com/craigraw/drongo/crypto/HDKeyDerivation.java new file mode 100644 index 0000000..849493e --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/HDKeyDerivation.java @@ -0,0 +1,52 @@ +package com.craigraw.drongo.crypto; + +import com.craigraw.drongo.Utils; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class HDKeyDerivation { + public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) { + RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber); + return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), parent); + } + + public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) { + if(childNumber.isHardened()) { + throw new IllegalArgumentException("Can't use private derivation with public keys only."); + } + + byte[] parentPublicKey = parent.getPubKeyPoint().getEncoded(true); + if(parentPublicKey.length != 33) { + throw new IllegalArgumentException("Parent pubkey must be 33 bytes, but is " + parentPublicKey.length); + } + + ByteBuffer data = ByteBuffer.allocate(37); + data.put(parentPublicKey); + data.putInt(childNumber.i()); + byte[] i = Utils.hmacSha512(parent.getChainCode(), data.array()); + if(i.length != 64) { + throw new IllegalStateException("HmacSHA512 output must be 64 bytes, is" + i.length); + } + + byte[] il = Arrays.copyOfRange(i, 0, 32); + byte[] chainCode = Arrays.copyOfRange(i, 32, 64); + BigInteger ilInt = new BigInteger(1, il); + + final BigInteger N = ECKey.CURVE.getN(); + ECPoint Ki = ECKey.publicPointFromPrivate(ilInt).add(parent.getPubKeyPoint()); + + return new RawKeyBytes(Ki.getEncoded(true), chainCode); + } + + public static class RawKeyBytes { + public final byte[] keyBytes, chainCode; + + public RawKeyBytes(byte[] keyBytes, byte[] chainCode) { + this.keyBytes = keyBytes; + this.chainCode = chainCode; + } + } +} diff --git a/src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java b/src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java new file mode 100644 index 0000000..34d278b --- /dev/null +++ b/src/main/java/com/craigraw/drongo/crypto/LazyECPoint.java @@ -0,0 +1,52 @@ +package com.craigraw.drongo.crypto; + +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; + +import java.util.Arrays; + +public class LazyECPoint { + // If curve is set, bits is also set. If curve is unset, point is set and bits is unset. Point can be set along + // with curve and bits when the cached form has been accessed and thus must have been converted. + + private final ECCurve curve; + private final byte[] bits; + + // This field is effectively final - once set it won't change again. However it can be set after + // construction. + private ECPoint point; + + public LazyECPoint(ECCurve curve, byte[] bits) { + this.curve = curve; + this.bits = bits; + } + + public LazyECPoint(ECPoint point) { + this.point = point; + this.curve = null; + this.bits = null; + } + + public ECPoint get() { + if (point == null) + point = curve.decodePoint(bits); + return point; + } + + // Delegated methods. + + public ECPoint getDetachedPoint() { + return get().getDetachedPoint(); + } + + public boolean isCompressed() { + return get().isCompressed(); + } + + public byte[] getEncoded() { + if (bits != null) + return Arrays.copyOf(bits, bits.length); + else + return get().getEncoded(); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Base58.java b/src/main/java/com/craigraw/drongo/protocol/Base58.java new file mode 100644 index 0000000..f42dd63 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Base58.java @@ -0,0 +1,216 @@ +/* + * Copyright 2011 Google Inc. + * Copyright 2018 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.craigraw.drongo.protocol; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + * Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings. + *

    + * Note that this is not the same base58 as used by Flickr, which you may find referenced around the Internet. + *

    + * Satoshi explains: why base-58 instead of standard base-64 encoding? + *

      + *
    • Don't want 0OIl characters that look the same in some fonts and + * could be used to create visually identical looking account numbers.
    • + *
    • A string with non-alphanumeric characters is not as easily accepted as an account number.
    • + *
    • E-mail usually won't line-break if there's no punctuation to break at.
    • + *
    • Doubleclicking selects the whole number as one word if it's all alphanumeric.
    • + *
    + *

    + * However, note that the encoding/decoding runs in O(n²) time, so it is not useful for large data. + *

    + * The basic idea of the encoding is to treat the data bytes as a large number represented using + * base-256 digits, convert the number to be represented using base-58 digits, preserve the exact + * number of leading zeros (which are otherwise lost during the mathematical operations on the + * numbers), and finally represent the resulting base-58 digits as alphanumeric ASCII characters. + */ +public class Base58 { + public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); + private static final char ENCODED_ZERO = ALPHABET[0]; + private static final int[] INDEXES = new int[128]; + static { + Arrays.fill(INDEXES, -1); + for (int i = 0; i < ALPHABET.length; i++) { + INDEXES[ALPHABET[i]] = i; + } + } + + /** + * Encodes the given bytes as a base58 string (no checksum is appended). + * + * @param input the bytes to encode + * @return the base58-encoded string + */ + public static String encode(byte[] input) { + if (input.length == 0) { + return ""; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input.length && input[zeros] == 0) { + ++zeros; + } + // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters) + input = Arrays.copyOf(input, input.length); // since we modify it in-place + char[] encoded = new char[input.length * 2]; // upper bound + int outputStart = encoded.length; + for (int inputStart = zeros; inputStart < input.length; ) { + encoded[--outputStart] = ALPHABET[divmod(input, inputStart, 256, 58)]; + if (input[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. + while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) { + ++outputStart; + } + while (--zeros >= 0) { + encoded[--outputStart] = ENCODED_ZERO; + } + // Return encoded string (including encoded leading zeros). + return new String(encoded, outputStart, encoded.length - outputStart); + } + + /** + * Encodes the bytes as a base58 string. A checksum is appended. + * + * @param payload the bytes to encode, e.g. pubkey hash + * @return the base58-encoded string + */ + public static String encodeChecked(byte[] payload) { + // A stringified buffer is: + // data bytes + 4 bytes check code (a truncated hash) + byte[] addressBytes = new byte[payload.length + 4]; + System.arraycopy(payload, 0, addressBytes, 0, payload.length); + byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length); + System.arraycopy(checksum, 0, addressBytes, payload.length, 4); + return Base58.encode(addressBytes); + } + + /** + * Encodes the given version and bytes as a base58 string. A checksum is appended. + * + * @param version the version to encode + * @param payload the bytes to encode, e.g. pubkey hash + * @return the base58-encoded string + */ + public static String encodeChecked(int version, byte[] payload) { + if (version < 0 || version > 255) + throw new IllegalArgumentException("Version not in range."); + + // A stringified buffer is: + // 1 byte version + data bytes + 4 bytes check code (a truncated hash) + byte[] addressBytes = new byte[1 + payload.length + 4]; + addressBytes[0] = (byte) version; + System.arraycopy(payload, 0, addressBytes, 1, payload.length); + byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length + 1); + System.arraycopy(checksum, 0, addressBytes, payload.length + 1, 4); + return Base58.encode(addressBytes); + } + + /** + * Decodes the given base58 string into the original data bytes. + * + * @param input the base58-encoded string to decode + * @return the decoded data bytes + */ + public static byte[] decode(String input) { + if (input.length() == 0) { + return new byte[0]; + } + // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). + byte[] input58 = new byte[input.length()]; + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + int digit = c < 128 ? INDEXES[c] : -1; + if (digit < 0) { + throw new ProtocolException("Invalid character " + c + " at position " + i); + } + input58[i] = (byte) digit; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input58.length && input58[zeros] == 0) { + ++zeros; + } + // Convert base-58 digits to base-256 digits. + byte[] decoded = new byte[input.length()]; + int outputStart = decoded.length; + for (int inputStart = zeros; inputStart < input58.length; ) { + decoded[--outputStart] = divmod(input58, inputStart, 58, 256); + if (input58[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Ignore extra leading zeroes that were added during the calculation. + while (outputStart < decoded.length && decoded[outputStart] == 0) { + ++outputStart; + } + // Return decoded data (including original number of leading zeros). + return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); + } + + public static BigInteger decodeToBigInteger(String input) { + return new BigInteger(1, decode(input)); + } + + /** + * Decodes the given base58 string into the original data bytes, using the checksum in the + * last 4 bytes of the decoded data to verify that the rest are correct. The checksum is + * removed from the returned data. + * + * @param input the base58-encoded string to decode (which should include the checksum) + */ + public static byte[] decodeChecked(String input) { + byte[] decoded = decode(input); + if (decoded.length < 4) + throw new ProtocolException("Input too short: " + decoded.length); + byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4); + byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length); + byte[] actualChecksum = Arrays.copyOfRange(Sha256Hash.hashTwice(data), 0, 4); + if (!Arrays.equals(checksum, actualChecksum)) + throw new ProtocolException("Invalid checksum"); + return data; + } + + /** + * Divides a number, represented as an array of bytes each containing a single digit + * in the specified base, by the given divisor. The given number is modified in-place + * to contain the quotient, and the return value is the remainder. + * + * @param number the number to divide + * @param firstDigit the index within the array of the first non-zero digit + * (this is used for optimization by skipping the leading zeros) + * @param base the base in which the number's digits are represented (up to 256) + * @param divisor the number to divide by (up to 256) + * @return the remainder of the division operation + */ + private static byte divmod(byte[] number, int firstDigit, int base, int divisor) { + // this is just long division which accounts for the base of the input digits + int remainder = 0; + for (int i = firstDigit; i < number.length; i++) { + int digit = (int) number[i] & 0xFF; + int temp = remainder * base + digit; + number[i] = (byte) (temp / divisor); + remainder = temp % divisor; + } + return (byte) remainder; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Bech32.java b/src/main/java/com/craigraw/drongo/protocol/Bech32.java new file mode 100644 index 0000000..bb67301 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Bech32.java @@ -0,0 +1,209 @@ +package com.craigraw.drongo.protocol; + +/* + * Copyright 2018 Coinomi Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.Locale; + +public class Bech32 { + /** The Bech32 character set for encoding. */ + private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + /** The Bech32 character set for decoding. */ + private static final byte[] CHARSET_REV = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + }; + + public static class Bech32Data { + public final String hrp; + public final byte[] data; + + private Bech32Data(final String hrp, final byte[] data) { + this.hrp = hrp; + this.data = data; + } + } + + /** Find the polynomial with value coefficients mod the generator as 30-bit. */ + private static int polymod(final byte[] values) { + int c = 1; + for (byte v_i: values) { + int c0 = (c >>> 25) & 0xff; + c = ((c & 0x1ffffff) << 5) ^ (v_i & 0xff); + if ((c0 & 1) != 0) c ^= 0x3b6a57b2; + if ((c0 & 2) != 0) c ^= 0x26508e6d; + if ((c0 & 4) != 0) c ^= 0x1ea119fa; + if ((c0 & 8) != 0) c ^= 0x3d4233dd; + if ((c0 & 16) != 0) c ^= 0x2a1462b3; + } + return c; + } + + /** Expand a HRP for use in checksum computation. */ + private static byte[] expandHrp(final String hrp) { + int hrpLength = hrp.length(); + byte ret[] = new byte[hrpLength * 2 + 1]; + for (int i = 0; i < hrpLength; ++i) { + int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII + ret[i] = (byte) ((c >>> 5) & 0x07); + ret[i + hrpLength + 1] = (byte) (c & 0x1f); + } + ret[hrpLength] = 0; + return ret; + } + + /** Verify a checksum. */ + private static boolean verifyChecksum(final String hrp, final byte[] values) { + byte[] hrpExpanded = expandHrp(hrp); + byte[] combined = new byte[hrpExpanded.length + values.length]; + System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length); + System.arraycopy(values, 0, combined, hrpExpanded.length, values.length); + return polymod(combined) == 1; + } + + /** Create a checksum. */ + private static byte[] createChecksum(final String hrp, final byte[] values) { + byte[] hrpExpanded = expandHrp(hrp); + byte[] enc = new byte[hrpExpanded.length + values.length + 6]; + System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length); + System.arraycopy(values, 0, enc, hrpExpanded.length, values.length); + int mod = polymod(enc) ^ 1; + byte[] ret = new byte[6]; + for (int i = 0; i < 6; ++i) { + ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31); + } + return ret; + } + + /** Encode a Bech32 string. */ + public static String encode(final Bech32Data bech32) { + return encode(bech32.hrp, bech32.data); + } + + /** Encode a Bech32 string. */ + public static String encode(String hrp, int version, final byte[] values) { + return encode(hrp, encode(0, values)); + } + + /** Encode a Bech32 string. */ + public static String encode(String hrp, final byte[] values) { + if(hrp.length() < 1) { + throw new ProtocolException("Human-readable part is too short"); + } + + if(hrp.length() > 83) { + throw new ProtocolException("Human-readable part is too long"); + } + + hrp = hrp.toLowerCase(Locale.ROOT); + byte[] checksum = createChecksum(hrp, values); + byte[] combined = new byte[values.length + checksum.length]; + System.arraycopy(values, 0, combined, 0, values.length); + System.arraycopy(checksum, 0, combined, values.length, checksum.length); + StringBuilder sb = new StringBuilder(hrp.length() + 1 + combined.length); + sb.append(hrp); + sb.append('1'); + for (byte b : combined) { + sb.append(CHARSET.charAt(b)); + } + return sb.toString(); + } + + /** Decode a Bech32 string. */ + public static Bech32Data decode(final String str) { + boolean lower = false, upper = false; + if (str.length() < 8) + throw new ProtocolException("Input too short: " + str.length()); + if (str.length() > 90) + throw new ProtocolException("Input too long: " + str.length()); + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + if (c < 33 || c > 126) throw new ProtocolException("Invalid character " + c + " at position " + i); + if (c >= 'a' && c <= 'z') { + if (upper) + throw new ProtocolException("Invalid character " + c + " at position " + i); + lower = true; + } + if (c >= 'A' && c <= 'Z') { + if (lower) + throw new ProtocolException("Invalid character " + c + " at position " + i); + upper = true; + } + } + final int pos = str.lastIndexOf('1'); + if (pos < 1) throw new ProtocolException("Missing human-readable part"); + final int dataPartLength = str.length() - 1 - pos; + if (dataPartLength < 6) throw new ProtocolException("Data part too short: " + dataPartLength); + byte[] values = new byte[dataPartLength]; + for (int i = 0; i < dataPartLength; ++i) { + char c = str.charAt(i + pos + 1); + if (CHARSET_REV[c] == -1) throw new ProtocolException("Invalid character " + c + " at position " + i); + values[i] = CHARSET_REV[c]; + } + String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT); + if (!verifyChecksum(hrp, values)) throw new ProtocolException("Invalid checksum"); + return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6)); + } + + private static byte[] encode(int witnessVersion, byte[] witnessProgram) { + byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true); + byte[] bytes = new byte[1 + convertedProgram.length]; + bytes[0] = (byte) (Script.encodeToOpN(witnessVersion) & 0xff); + System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length); + return bytes; + } + + /** + * Helper for re-arranging bits into groups. + */ + private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits, + final int toBits, final boolean pad) { + int acc = 0; + int bits = 0; + ByteArrayOutputStream out = new ByteArrayOutputStream(64); + final int maxv = (1 << toBits) - 1; + final int max_acc = (1 << (fromBits + toBits - 1)) - 1; + for (int i = 0; i < inLen; i++) { + int value = in[i + inStart] & 0xff; + if ((value >>> fromBits) != 0) { + throw new ProtocolException( + String.format("Input value '%X' exceeds '%d' bit size", value, fromBits)); + } + acc = ((acc << fromBits) | value) & max_acc; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + out.write((acc >>> bits) & maxv); + } + } + if (pad) { + if (bits > 0) + out.write((acc << (toBits - bits)) & maxv); + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) { + throw new ProtocolException("Could not convert bits, invalid padding"); + } + return out.toByteArray(); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/ProtocolException.java b/src/main/java/com/craigraw/drongo/protocol/ProtocolException.java new file mode 100644 index 0000000..5280452 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ProtocolException.java @@ -0,0 +1,22 @@ +package com.craigraw.drongo.protocol; + +public class ProtocolException extends RuntimeException { + public ProtocolException() { + } + + public ProtocolException(String message) { + super(message); + } + + public ProtocolException(String message, Throwable cause) { + super(message, cause); + } + + public ProtocolException(Throwable cause) { + super(cause); + } + + public ProtocolException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Ripemd160.java b/src/main/java/com/craigraw/drongo/protocol/Ripemd160.java new file mode 100644 index 0000000..65ea60b --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Ripemd160.java @@ -0,0 +1,157 @@ +package com.craigraw.drongo.protocol; + +/* + * Bitcoin cryptography library + * Copyright (c) Project Nayuki + * + * https://www.nayuki.io/page/bitcoin-cryptography-library + * https://github.com/nayuki/Bitcoin-Cryptography-Library + */ + +import static java.lang.Integer.rotateLeft; +import java.util.Arrays; +import java.util.Objects; + + +/** + * Computes the RIPEMD-160 hash of an array of bytes. Not instantiable. + */ +public final class Ripemd160 { + + private static final int BLOCK_LEN = 64; // In bytes + + + + /*---- Static functions ----*/ + + /** + * Computes and returns a 20-byte (160-bit) hash of the specified binary message. + * Each call will return a new byte array object instance. + * @param msg the message to compute the hash of + * @return a 20-byte array representing the message's RIPEMD-160 hash + * @throws NullPointerException if the message is {@code null} + */ + public static byte[] getHash(byte[] msg) { + // Compress whole message blocks + Objects.requireNonNull(msg); + int[] state = {0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0}; + int off = msg.length / BLOCK_LEN * BLOCK_LEN; + compress(state, msg, off); + + // Final blocks, padding, and length + byte[] block = new byte[BLOCK_LEN]; + System.arraycopy(msg, off, block, 0, msg.length - off); + off = msg.length % block.length; + block[off] = (byte)0x80; + off++; + if (off + 8 > block.length) { + compress(state, block, block.length); + Arrays.fill(block, (byte)0); + } + long len = (long)msg.length << 3; + for (int i = 0; i < 8; i++) + block[block.length - 8 + i] = (byte)(len >>> (i * 8)); + compress(state, block, block.length); + + // Int32 array to bytes in little endian + byte[] result = new byte[state.length * 4]; + for (int i = 0; i < result.length; i++) + result[i] = (byte)(state[i / 4] >>> (i % 4 * 8)); + return result; + } + + + + /*---- Private functions ----*/ + + private static void compress(int[] state, byte[] blocks, int len) { + if (len % BLOCK_LEN != 0) + throw new IllegalArgumentException(); + for (int i = 0; i < len; i += BLOCK_LEN) { + + // Message schedule + int[] schedule = new int[16]; + for (int j = 0; j < BLOCK_LEN; j++) + schedule[j / 4] |= (blocks[i + j] & 0xFF) << (j % 4 * 8); + + // The 80 rounds + int al = state[0], ar = state[0]; + int bl = state[1], br = state[1]; + int cl = state[2], cr = state[2]; + int dl = state[3], dr = state[3]; + int el = state[4], er = state[4]; + for (int j = 0; j < 80; j++) { + int temp; + temp = rotateLeft(al + f(j, bl, cl, dl) + schedule[RL[j]] + KL[j / 16], SL[j]) + el; + al = el; + el = dl; + dl = rotateLeft(cl, 10); + cl = bl; + bl = temp; + temp = rotateLeft(ar + f(79 - j, br, cr, dr) + schedule[RR[j]] + KR[j / 16], SR[j]) + er; + ar = er; + er = dr; + dr = rotateLeft(cr, 10); + cr = br; + br = temp; + } + int temp = state[1] + cl + dr; + state[1] = state[2] + dl + er; + state[2] = state[3] + el + ar; + state[3] = state[4] + al + br; + state[4] = state[0] + bl + cr; + state[0] = temp; + } + } + + + private static int f(int i, int x, int y, int z) { + assert 0 <= i && i < 80; + if (i < 16) return x ^ y ^ z; + if (i < 32) return (x & y) | (~x & z); + if (i < 48) return (x | ~y) ^ z; + if (i < 64) return (x & z) | (y & ~z); + return x ^ (y | ~z); + } + + + /*---- Class constants ----*/ + + private static final int[] KL = {0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E}; // Round constants for left line + private static final int[] KR = {0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000}; // Round constants for right line + + private static final int[] RL = { // Message schedule for left line + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13}; + + private static final int[] RR = { // Message schedule for right line + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11}; + + private static final int[] SL = { // Left-rotation for left line + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6}; + + private static final int[] SR = { // Left-rotation for right line + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11}; + + + + /*---- Miscellaneous ----*/ + + private Ripemd160() {} // Not instantiable + +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Script.java b/src/main/java/com/craigraw/drongo/protocol/Script.java new file mode 100644 index 0000000..6282bf9 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Script.java @@ -0,0 +1,169 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.address.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.craigraw.drongo.protocol.ScriptOpCodes.*; + +public class Script { + public static final long MAX_SCRIPT_ELEMENT_SIZE = 520; + + // The program is a set of chunks where each element is either [opcode] or [data, data, data ...] + protected List chunks; + + protected byte[] program; + + public Script(byte[] programBytes) { + program = programBytes; + parse(programBytes); + } + + public Script(List chunks) { + this.chunks = Collections.unmodifiableList(new ArrayList<>(chunks)); + } + + private static final ScriptChunk[] STANDARD_TRANSACTION_SCRIPT_CHUNKS = { + new ScriptChunk(ScriptOpCodes.OP_DUP, null, 0), + new ScriptChunk(ScriptOpCodes.OP_HASH160, null, 1), + new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null, 23), + new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null, 24), + }; + + private void parse(byte[] program) { + chunks = new ArrayList<>(5); // Common size. + ByteArrayInputStream bis = new ByteArrayInputStream(program); + int initialSize = bis.available(); + while (bis.available() > 0) { + int startLocationInProgram = initialSize - bis.available(); + int opcode = bis.read(); + + long dataToRead = -1; + if (opcode >= 0 && opcode < OP_PUSHDATA1) { + // Read some bytes of data, where how many is the opcode value itself. + dataToRead = opcode; + } else if (opcode == OP_PUSHDATA1) { + if (bis.available() < 1) throw new ProtocolException("Unexpected end of script"); + dataToRead = bis.read(); + } else if (opcode == OP_PUSHDATA2) { + // Read a short, then read that many bytes of data. + if (bis.available() < 2) throw new ProtocolException("Unexpected end of script"); + dataToRead = Utils.readUint16FromStream(bis); + } else if (opcode == OP_PUSHDATA4) { + // Read a uint32, then read that many bytes of data. + // Though this is allowed, because its value cannot be > 520, it should never actually be used + if (bis.available() < 4) throw new ProtocolException("Unexpected end of script"); + dataToRead = Utils.readUint32FromStream(bis); + } + + ScriptChunk chunk; + if (dataToRead == -1) { + chunk = new ScriptChunk(opcode, null, startLocationInProgram); + } else { + if (dataToRead > bis.available()) + throw new ProtocolException("Push of data element that is larger than remaining data"); + byte[] data = new byte[(int)dataToRead]; + if(dataToRead != 0 && bis.read(data, 0, (int)dataToRead) != dataToRead) { + throw new ProtocolException(); + } + + chunk = new ScriptChunk(opcode, data, startLocationInProgram); + } + // Save some memory by eliminating redundant copies of the same chunk objects. + for (ScriptChunk c : STANDARD_TRANSACTION_SCRIPT_CHUNKS) { + if (c.equals(chunk)) chunk = c; + } + chunks.add(chunk); + } + } + + /** Returns the serialized program as a newly created byte array. */ + public byte[] getProgram() { + try { + // Don't round-trip as Bitcoin Core doesn't and it would introduce a mismatch. + if (program != null) + return Arrays.copyOf(program, program.length); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + for (ScriptChunk chunk : chunks) { + chunk.write(bos); + } + program = bos.toByteArray(); + return program; + } catch (IOException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + + /** + * Returns true if this script has the required form to contain a destination address + */ + public boolean containsToAddress() { + return ScriptPattern.isP2PK(this) || ScriptPattern.isP2PKH(this) || ScriptPattern.isP2SH(this) || ScriptPattern.isP2WH(this) || ScriptPattern.isSentToMultisig(this); + } + + /** + *

    If the program somehow pays to a hash, returns the hash.

    + * + *

    Otherwise this method throws a ScriptException.

    + */ + public byte[] getPubKeyHash() throws ProtocolException { + if (ScriptPattern.isP2PKH(this)) + return ScriptPattern.extractHashFromP2PKH(this); + else if (ScriptPattern.isP2SH(this)) + return ScriptPattern.extractHashFromP2SH(this); + else if (ScriptPattern.isP2WH(this)) + return ScriptPattern.extractHashFromP2WH(this); + else + throw new ProtocolException("Script not in the standard scriptPubKey form"); + } + + /** + * Gets the destination address from this script, if it's in the required form. + */ + public Address[] getToAddresses() { + if (ScriptPattern.isP2PK(this)) + return new Address[] { new P2PKAddress( ScriptPattern.extractPKFromP2PK(this)) }; + else if (ScriptPattern.isP2PKH(this)) + return new Address[] { new P2PKHAddress( ScriptPattern.extractHashFromP2PKH(this)) }; + else if (ScriptPattern.isP2SH(this)) + return new Address[] { new P2SHAddress(ScriptPattern.extractHashFromP2SH(this)) }; + else if (ScriptPattern.isP2WH(this)) + return new Address[] { new P2WPKHAddress(ScriptPattern.extractHashFromP2WH(this)) }; + else if (ScriptPattern.isSentToMultisig(this)) + return ScriptPattern.extractMultisigAddresses(this); + else + throw new ProtocolException("Cannot cast this script to an address"); + } + + public static int decodeFromOpN(int opcode) { + if((opcode != OP_0 && opcode != OP_1NEGATE) && (opcode < OP_1 || opcode > OP_16)) { + throw new ProtocolException("decodeFromOpN called on non OP_N opcode: " + opcode); + } + + if (opcode == OP_0) + return 0; + else if (opcode == OP_1NEGATE) + return -1; + else + return opcode + 1 - OP_1; + } + + public static int encodeToOpN(int value) { + if(value < -1 || value > 16) { + throw new ProtocolException("encodeToOpN called for " + value + " which we cannot encode in an opcode."); + } + if (value == 0) + return OP_0; + else if (value == -1) + return OP_1NEGATE; + else + return value - 1 + OP_1; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java b/src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java new file mode 100644 index 0000000..89d67aa --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ScriptChunk.java @@ -0,0 +1,71 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; + +import java.io.IOException; +import java.io.OutputStream; + +import static com.craigraw.drongo.protocol.ScriptOpCodes.*; + +public class ScriptChunk { + /** Operation to be executed. Opcodes are defined in {@link ScriptOpCodes}. */ + public final int opcode; + + /** + * For push operations, this is the vector to be pushed on the stack. For {@link ScriptOpCodes#OP_0}, the vector is + * empty. Null for non-push operations. + */ + public final byte[] data; + + private int startLocationInProgram; + + public ScriptChunk(int opcode, byte[] data) { + this(opcode, data, -1); + } + + public ScriptChunk(int opcode, byte[] data, int startLocationInProgram) { + this.opcode = opcode; + this.data = data; + this.startLocationInProgram = startLocationInProgram; + } + + public boolean equalsOpCode(int opcode) { + return opcode == this.opcode; + } + + /** + * If this chunk is a single byte of non-pushdata content (could be OP_RESERVED or some invalid Opcode) + */ + public boolean isOpCode() { + return opcode > OP_PUSHDATA4; + } + + public void write(OutputStream stream) throws IOException { + if (isOpCode()) { + if(data != null) throw new IllegalStateException("Data must be null for opcode chunk"); + stream.write(opcode); + } else if (data != null) { + if (opcode < OP_PUSHDATA1) { + if(data.length != opcode) throw new IllegalStateException("Data length must equal opcode value"); + stream.write(opcode); + } else if (opcode == OP_PUSHDATA1) { + if(data.length > 0xFF) throw new IllegalStateException("Data length must be less than or equal to 256"); + stream.write(OP_PUSHDATA1); + stream.write(data.length); + } else if (opcode == OP_PUSHDATA2) { + if(data.length > 0xFFFF) throw new IllegalStateException("Data length must be less than or equal to 65536"); + stream.write(OP_PUSHDATA2); + Utils.uint16ToByteStreamLE(data.length, stream); + } else if (opcode == OP_PUSHDATA4) { + if(data.length > Script.MAX_SCRIPT_ELEMENT_SIZE) throw new IllegalStateException("Data length must be less than or equal to " + Script.MAX_SCRIPT_ELEMENT_SIZE); + stream.write(OP_PUSHDATA4); + Utils.uint32ToByteStreamLE(data.length, stream); + } else { + throw new RuntimeException("Unimplemented"); + } + stream.write(data); + } else { + stream.write(opcode); // smallNum + } + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java b/src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java new file mode 100644 index 0000000..c1a4691 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ScriptOpCodes.java @@ -0,0 +1,164 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.craigraw.drongo.protocol; + +import java.util.Map; + +/** + * Various constants that define the assembly-like scripting language that forms part of the Bitcoin protocol. + * See {@link Script} for details. Also provides a method to convert them to a string. + */ +public class ScriptOpCodes { + // push value + public static final int OP_0 = 0x00; // push empty vector + public static final int OP_FALSE = OP_0; + public static final int OP_PUSHDATA1 = 0x4c; + public static final int OP_PUSHDATA2 = 0x4d; + public static final int OP_PUSHDATA4 = 0x4e; + public static final int OP_1NEGATE = 0x4f; + public static final int OP_RESERVED = 0x50; + public static final int OP_1 = 0x51; + public static final int OP_TRUE = OP_1; + public static final int OP_2 = 0x52; + public static final int OP_3 = 0x53; + public static final int OP_4 = 0x54; + public static final int OP_5 = 0x55; + public static final int OP_6 = 0x56; + public static final int OP_7 = 0x57; + public static final int OP_8 = 0x58; + public static final int OP_9 = 0x59; + public static final int OP_10 = 0x5a; + public static final int OP_11 = 0x5b; + public static final int OP_12 = 0x5c; + public static final int OP_13 = 0x5d; + public static final int OP_14 = 0x5e; + public static final int OP_15 = 0x5f; + public static final int OP_16 = 0x60; + + // control + public static final int OP_NOP = 0x61; + public static final int OP_VER = 0x62; + public static final int OP_IF = 0x63; + public static final int OP_NOTIF = 0x64; + public static final int OP_VERIF = 0x65; + public static final int OP_VERNOTIF = 0x66; + public static final int OP_ELSE = 0x67; + public static final int OP_ENDIF = 0x68; + public static final int OP_VERIFY = 0x69; + public static final int OP_RETURN = 0x6a; + + // stack ops + public static final int OP_TOALTSTACK = 0x6b; + public static final int OP_FROMALTSTACK = 0x6c; + public static final int OP_2DROP = 0x6d; + public static final int OP_2DUP = 0x6e; + public static final int OP_3DUP = 0x6f; + public static final int OP_2OVER = 0x70; + public static final int OP_2ROT = 0x71; + public static final int OP_2SWAP = 0x72; + public static final int OP_IFDUP = 0x73; + public static final int OP_DEPTH = 0x74; + public static final int OP_DROP = 0x75; + public static final int OP_DUP = 0x76; + public static final int OP_NIP = 0x77; + public static final int OP_OVER = 0x78; + public static final int OP_PICK = 0x79; + public static final int OP_ROLL = 0x7a; + public static final int OP_ROT = 0x7b; + public static final int OP_SWAP = 0x7c; + public static final int OP_TUCK = 0x7d; + + // splice ops + public static final int OP_CAT = 0x7e; + public static final int OP_SUBSTR = 0x7f; + public static final int OP_LEFT = 0x80; + public static final int OP_RIGHT = 0x81; + public static final int OP_SIZE = 0x82; + + // bit logic + public static final int OP_INVERT = 0x83; + public static final int OP_AND = 0x84; + public static final int OP_OR = 0x85; + public static final int OP_XOR = 0x86; + public static final int OP_EQUAL = 0x87; + public static final int OP_EQUALVERIFY = 0x88; + public static final int OP_RESERVED1 = 0x89; + public static final int OP_RESERVED2 = 0x8a; + + // numeric + public static final int OP_1ADD = 0x8b; + public static final int OP_1SUB = 0x8c; + public static final int OP_2MUL = 0x8d; + public static final int OP_2DIV = 0x8e; + public static final int OP_NEGATE = 0x8f; + public static final int OP_ABS = 0x90; + public static final int OP_NOT = 0x91; + public static final int OP_0NOTEQUAL = 0x92; + public static final int OP_ADD = 0x93; + public static final int OP_SUB = 0x94; + public static final int OP_MUL = 0x95; + public static final int OP_DIV = 0x96; + public static final int OP_MOD = 0x97; + public static final int OP_LSHIFT = 0x98; + public static final int OP_RSHIFT = 0x99; + public static final int OP_BOOLAND = 0x9a; + public static final int OP_BOOLOR = 0x9b; + public static final int OP_NUMEQUAL = 0x9c; + public static final int OP_NUMEQUALVERIFY = 0x9d; + public static final int OP_NUMNOTEQUAL = 0x9e; + public static final int OP_LESSTHAN = 0x9f; + public static final int OP_GREATERTHAN = 0xa0; + public static final int OP_LESSTHANOREQUAL = 0xa1; + public static final int OP_GREATERTHANOREQUAL = 0xa2; + public static final int OP_MIN = 0xa3; + public static final int OP_MAX = 0xa4; + public static final int OP_WITHIN = 0xa5; + + // crypto + public static final int OP_RIPEMD160 = 0xa6; + public static final int OP_SHA1 = 0xa7; + public static final int OP_SHA256 = 0xa8; + public static final int OP_HASH160 = 0xa9; + public static final int OP_HASH256 = 0xaa; + public static final int OP_CODESEPARATOR = 0xab; + public static final int OP_CHECKSIG = 0xac; + public static final int OP_CHECKSIGVERIFY = 0xad; + public static final int OP_CHECKMULTISIG = 0xae; + public static final int OP_CHECKMULTISIGVERIFY = 0xaf; + + // block state + /** Check lock time of the block. Introduced in BIP 65, replacing OP_NOP2 */ + public static final int OP_CHECKLOCKTIMEVERIFY = 0xb1; + public static final int OP_CHECKSEQUENCEVERIFY = 0xb2; + + // expansion + public static final int OP_NOP1 = 0xb0; + /** Deprecated by BIP 65 */ + @Deprecated + public static final int OP_NOP2 = OP_CHECKLOCKTIMEVERIFY; + /** Deprecated by BIP 112 */ + @Deprecated + public static final int OP_NOP3 = OP_CHECKSEQUENCEVERIFY; + public static final int OP_NOP4 = 0xb3; + public static final int OP_NOP5 = 0xb4; + public static final int OP_NOP6 = 0xb5; + public static final int OP_NOP7 = 0xb6; + public static final int OP_NOP8 = 0xb7; + public static final int OP_NOP9 = 0xb8; + public static final int OP_NOP10 = 0xb9; + public static final int OP_INVALIDOPCODE = 0xff; +} \ No newline at end of file diff --git a/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java b/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java new file mode 100644 index 0000000..e9ad2d3 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java @@ -0,0 +1,184 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.address.Address; +import com.craigraw.drongo.address.P2PKAddress; + +import java.util.ArrayList; +import java.util.List; + +import static com.craigraw.drongo.protocol.ScriptOpCodes.*; +import static com.craigraw.drongo.protocol.Script.decodeFromOpN; + +public class ScriptPattern { + /** + * Returns true if this script is of the form {@code DUP HASH160 EQUALVERIFY CHECKSIG}, ie, payment to an + * public key like {@code 2102f3b08938a7f8d2609d567aebc4989eeded6e2e880c058fdf092c5da82c3bc5eeac}. + */ + public static boolean isP2PK(Script script) { + List chunks = script.chunks; + if (chunks.size() != 2) + return false; + if (!chunks.get(0).equalsOpCode(0x21)) + return false; + byte[] chunk2data = chunks.get(0).data; + if (chunk2data == null) + return false; + if (chunk2data.length != 33) + return false; + if (!chunks.get(1).equalsOpCode(OP_CHECKSIG)) + return false; + return true; + } + + /** + * Extract the pubkey from a P2PK scriptPubKey. It's important that the script is in the correct form, so you + * will want to guard calls to this method with {@link #isP2PK(Script)}. + */ + public static byte[] extractPKFromP2PK(Script script) { + return script.chunks.get(0).data; + } + + /** + * Returns true if this script is of the form {@code DUP HASH160 EQUALVERIFY CHECKSIG}, ie, payment to an + * address like {@code 1VayNert3x1KzbpzMGt2qdqrAThiRovi8}. This form was originally intended for the case where you wish + * to send somebody money with a written code because their node is offline, but over time has become the standard + * way to make payments due to the short and recognizable base58 form addresses come in. + */ + public static boolean isP2PKH(Script script) { + List chunks = script.chunks; + if (chunks.size() != 5) + return false; + if (!chunks.get(0).equalsOpCode(OP_DUP)) + return false; + if (!chunks.get(1).equalsOpCode(OP_HASH160)) + return false; + byte[] chunk2data = chunks.get(2).data; + if (chunk2data == null) + return false; + if (chunk2data.length != 20) + return false; + if (!chunks.get(3).equalsOpCode(OP_EQUALVERIFY)) + return false; + if (!chunks.get(4).equalsOpCode(OP_CHECKSIG)) + return false; + return true; + } + + /** + * Extract the pubkey hash from a P2PKH scriptPubKey. It's important that the script is in the correct form, so you + * will want to guard calls to this method with {@link #isP2PKH(Script)}. + */ + public static byte[] extractHashFromP2PKH(Script script) { + return script.chunks.get(2).data; + } + + /** + *

    + * Whether or not this is a scriptPubKey representing a P2SH output. In such outputs, the logic that + * controls reclamation is not actually in the output at all. Instead there's just a hash, and it's up to the + * spending input to provide a program matching that hash. + *

    + *

    + * P2SH is described by BIP16. + *

    + */ + public static boolean isP2SH(Script script) { + List chunks = script.chunks; + // We check for the effective serialized form because BIP16 defines a P2SH output using an exact byte + // template, not the logical program structure. Thus you can have two programs that look identical when + // printed out but one is a P2SH script and the other isn't! :( + // We explicitly test that the op code used to load the 20 bytes is 0x14 and not something logically + // equivalent like {@code OP_HASH160 OP_PUSHDATA1 0x14 <20 bytes of script hash> OP_EQUAL} + if (chunks.size() != 3) + return false; + if (!chunks.get(0).equalsOpCode(OP_HASH160)) + return false; + ScriptChunk chunk1 = chunks.get(1); + if (chunk1.opcode != 0x14) + return false; + byte[] chunk1data = chunk1.data; + if (chunk1data == null) + return false; + if (chunk1data.length != 20) + return false; + if (!chunks.get(2).equalsOpCode(OP_EQUAL)) + return false; + return true; + } + + /** + * Returns whether this script matches the format used for multisig outputs: + * {@code [n] [keys...] [m] CHECKMULTISIG} + */ + public static boolean isSentToMultisig(Script script) { + List chunks = script.chunks; + if (chunks.size() < 4) return false; + ScriptChunk chunk = chunks.get(chunks.size() - 1); + // Must end in OP_CHECKMULTISIG[VERIFY]. + if (!chunk.isOpCode()) return false; + if (!(chunk.equalsOpCode(OP_CHECKMULTISIG) || chunk.equalsOpCode(OP_CHECKMULTISIGVERIFY))) return false; + try { + // Second to last chunk must be an OP_N opcode and there should be that many data chunks (keys). + ScriptChunk m = chunks.get(chunks.size() - 2); + if (!m.isOpCode()) return false; + int numKeys = decodeFromOpN(m.opcode); + if (numKeys < 1 || chunks.size() != 3 + numKeys) return false; + for (int i = 1; i < chunks.size() - 2; i++) { + if (chunks.get(i).isOpCode()) return false; + } + // First chunk must be an OP_N opcode too. + if (decodeFromOpN(chunks.get(0).opcode) < 1) return false; + } catch (IllegalStateException e) { + return false; // Not an OP_N opcode. + } + return true; + } + + public static Address[] extractMultisigAddresses(Script script) { + List
    addresses = new ArrayList<>(); + + List chunks = script.chunks; + for (int i = 1; i < chunks.size() - 2; i++) { + byte[] pubKey = chunks.get(i).data; + addresses.add(new P2PKAddress(pubKey)); + } + + return addresses.toArray(new Address[addresses.size()]); + } + + /** + * Extract the script hash from a P2SH scriptPubKey. It's important that the script is in the correct form, so you + * will want to guard calls to this method with {@link #isP2SH(Script)}. + */ + public static byte[] extractHashFromP2SH(Script script) { + return script.chunks.get(1).data; + } + + /** + * Returns true if this script is of the form {@code OP_0 }. This can either be a P2WPKH or P2WSH scriptPubKey. These + * two script types were introduced with segwit. + */ + public static boolean isP2WH(Script script) { + List chunks = script.chunks; + if (chunks.size() != 2) + return false; + if (!chunks.get(0).equalsOpCode(OP_0)) + return false; + byte[] chunk1data = chunks.get(1).data; + if (chunk1data == null) + return false; + if (chunk1data.length != 20 && chunk1data.length != 32) + return false; + return true; + } + + /** + * Extract the pubkey hash from a P2WPKH or the script hash from a P2WSH scriptPubKey. It's important that the + * script is in the correct form, so you will want to guard calls to this method with + * {@link #isP2WH(Script)}. + */ + public static byte[] extractHashFromP2WH(Script script) { + return script.chunks.get(1).data; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java b/src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java new file mode 100644 index 0000000..e17fb96 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Sha256Hash.java @@ -0,0 +1,261 @@ +/* + * Copyright 2011 Google Inc. + * Copyright 2014 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +public class Sha256Hash implements Comparable { + public static final int LENGTH = 32; // bytes + public static final Sha256Hash ZERO_HASH = wrap(new byte[LENGTH]); + + private final byte[] bytes; + + private Sha256Hash(byte[] rawHashBytes) { + if(rawHashBytes.length != LENGTH) { + throw new ProtocolException(); + } + + this.bytes = rawHashBytes; + } + + /** + * Creates a new instance that wraps the given hash value. + * + * @param rawHashBytes the raw hash bytes to wrap + * @return a new instance + * @throws IllegalArgumentException if the given array length is not exactly 32 + */ + public static Sha256Hash wrap(byte[] rawHashBytes) { + return new Sha256Hash(rawHashBytes); + } + + /** + * Creates a new instance that wraps the given hash value (represented as a hex string). + * + * @param hexString a hash value represented as a hex string + * @return a new instance + * @throws IllegalArgumentException if the given string is not a valid + * hex string, or if it does not represent exactly 32 bytes + */ + public static Sha256Hash wrap(String hexString) { + return wrap(Utils.hexToBytes(hexString)); + } + + /** + * Creates a new instance that wraps the given hash value, but with byte order reversed. + * + * @param rawHashBytes the raw hash bytes to wrap + * @return a new instance + * @throws IllegalArgumentException if the given array length is not exactly 32 + */ + public static Sha256Hash wrapReversed(byte[] rawHashBytes) { + return wrap(Utils.reverseBytes(rawHashBytes)); + } + + /** + * Creates a new instance containing the calculated (one-time) hash of the given bytes. + * + * @param contents the bytes on which the hash value is calculated + * @return a new instance containing the calculated (one-time) hash + */ + public static Sha256Hash of(byte[] contents) { + return wrap(hash(contents)); + } + + /** + * Creates a new instance containing the hash of the calculated hash of the given bytes. + * + * @param contents the bytes on which the hash value is calculated + * @return a new instance containing the calculated (two-time) hash + */ + public static Sha256Hash twiceOf(byte[] contents) { + return wrap(hashTwice(contents)); + } + + /** + * Creates a new instance containing the hash of the calculated hash of the given bytes. + * + * @param content1 first bytes on which the hash value is calculated + * @param content2 second bytes on which the hash value is calculated + * @return a new instance containing the calculated (two-time) hash + */ + public static Sha256Hash twiceOf(byte[] content1, byte[] content2) { + return wrap(hashTwice(content1, content2)); + } + + /** + * Returns a new SHA-256 MessageDigest instance. + * + * This is a convenience method which wraps the checked + * exception that can never occur with a RuntimeException. + * + * @return a new SHA-256 MessageDigest instance + */ + public static MessageDigest newDigest() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); // Can't happen. + } + } + + /** + * Calculates the SHA-256 hash of the given bytes. + * + * @param input the bytes to hash + * @return the hash (in big-endian order) + */ + public static byte[] hash(byte[] input) { + return hash(input, 0, input.length); + } + + /** + * Calculates the SHA-256 hash of the given byte range. + * + * @param input the array containing the bytes to hash + * @param offset the offset within the array of the bytes to hash + * @param length the number of bytes to hash + * @return the hash (in big-endian order) + */ + public static byte[] hash(byte[] input, int offset, int length) { + MessageDigest digest = newDigest(); + digest.update(input, offset, length); + return digest.digest(); + } + + /** + * Calculates the SHA-256 hash of the given bytes, + * and then hashes the resulting hash again. + * + * @param input the bytes to hash + * @return the double-hash (in big-endian order) + */ + public static byte[] hashTwice(byte[] input) { + return hashTwice(input, 0, input.length); + } + + /** + * Calculates the hash of hash on the given chunks of bytes. This is equivalent to concatenating the two + * chunks and then passing the result to {@link #hashTwice(byte[])}. + */ + public static byte[] hashTwice(byte[] input1, byte[] input2) { + MessageDigest digest = newDigest(); + digest.update(input1); + digest.update(input2); + return digest.digest(digest.digest()); + } + + /** + * Calculates the SHA-256 hash of the given byte range, + * and then hashes the resulting hash again. + * + * @param input the array containing the bytes to hash + * @param offset the offset within the array of the bytes to hash + * @param length the number of bytes to hash + * @return the double-hash (in big-endian order) + */ + public static byte[] hashTwice(byte[] input, int offset, int length) { + MessageDigest digest = newDigest(); + digest.update(input, offset, length); + return digest.digest(digest.digest()); + } + + /** + * Calculates the hash of hash on the given byte ranges. This is equivalent to + * concatenating the two ranges and then passing the result to {@link #hashTwice(byte[])}. + */ + public static byte[] hashTwice(byte[] input1, int offset1, int length1, + byte[] input2, int offset2, int length2) { + MessageDigest digest = newDigest(); + digest.update(input1, offset1, length1); + digest.update(input2, offset2, length2); + return digest.digest(digest.digest()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return Arrays.equals(bytes, ((Sha256Hash)o).bytes); + } + + /** + * Returns the last four bytes of the wrapped hash. This should be unique enough to be a suitable hash code even for + * blocks, where the goal is to try and get the first bytes to be zeros (i.e. the value as a big integer lower + * than the target value). + */ + @Override + public int hashCode() { + // Use the last 4 bytes, not the first 4 which are often zeros in Bitcoin. + return fromBytes(bytes[LENGTH - 4], bytes[LENGTH - 3], bytes[LENGTH - 2], bytes[LENGTH - 1]); + } + + /** + * Returns the {@code int} value whose byte representation is the given 4 bytes, in big-endian + * order; equivalent to {@code Ints.fromByteArray(new byte[] {b1, b2, b3, b4})}. + * + * @since 7.0 + */ + public static int fromBytes(byte b1, byte b2, byte b3, byte b4) { + return b1 << 24 | (b2 & 0xFF) << 16 | (b3 & 0xFF) << 8 | (b4 & 0xFF); + } + + @Override + public String toString() { + return Utils.bytesToHex(bytes); + } + + /** + * Returns the bytes interpreted as a positive integer. + */ + public BigInteger toBigInteger() { + return new BigInteger(1, bytes); + } + + /** + * Returns the internal byte array, without defensively copying. Therefore do NOT modify the returned array. + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Returns a reversed copy of the internal byte array. + */ + public byte[] getReversedBytes() { + return Utils.reverseBytes(bytes); + } + + @Override + public int compareTo(final Sha256Hash other) { + for (int i = LENGTH - 1; i >= 0; i--) { + final int thisByte = this.bytes[i] & 0xff; + final int otherByte = other.bytes[i] & 0xff; + if (thisByte > otherByte) + return 1; + if (thisByte < otherByte) + return -1; + } + return 0; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Transaction.java b/src/main/java/com/craigraw/drongo/protocol/Transaction.java new file mode 100644 index 0000000..1076640 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/Transaction.java @@ -0,0 +1,188 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.address.Address; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.craigraw.drongo.Utils.uint32ToByteStreamLE; + +public class Transaction extends TransactionPart { + private long version; + private long lockTime; + + private Sha256Hash cachedTxId; + private Sha256Hash cachedWTxId; + + private ArrayList inputs; + private ArrayList outputs; + + public Transaction(byte[] rawtx) { + super(rawtx, 0); + } + + public Sha256Hash getTxId() { + if (cachedTxId == null) { + if (!hasWitnesses() && cachedWTxId != null) { + cachedTxId = cachedWTxId; + } else { + ByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(length < 32 ? 32 : length + 32); + try { + bitcoinSerializeToStream(stream, false); + } catch (IOException e) { + throw new RuntimeException(e); // cannot happen + } + cachedTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(stream.toByteArray())); + } + } + return cachedTxId; + } + + public Sha256Hash getWTxId() { + if (cachedWTxId == null) { + if (!hasWitnesses() && cachedTxId != null) { + cachedWTxId = cachedTxId; + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + bitcoinSerializeToStream(baos, hasWitnesses()); + } catch (IOException e) { + throw new RuntimeException(e); // cannot happen + } + cachedWTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(baos.toByteArray())); + } + } + return cachedWTxId; + } + + public boolean hasWitnesses() { + for (TransactionInput in : inputs) + if (in.hasWitness()) + return true; + return false; + } + + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + boolean useSegwit = hasWitnesses(); + bitcoinSerializeToStream(stream, useSegwit); + } + + /** + * Serialize according to BIP144 or the + * classic format, depending on if segwit is + * desired. + */ + protected void bitcoinSerializeToStream(OutputStream stream, boolean useSegwit) throws IOException { + // version + uint32ToByteStreamLE(version, stream); + // marker, flag + if (useSegwit) { + stream.write(0); + stream.write(1); + } + // txin_count, txins + stream.write(new VarInt(inputs.size()).encode()); + for (TransactionInput in : inputs) + in.bitcoinSerialize(stream); + // txout_count, txouts + stream.write(new VarInt(outputs.size()).encode()); + for (TransactionOutput out : outputs) + out.bitcoinSerialize(stream); + // script_witnisses + if (useSegwit) { + for (TransactionInput in : inputs) { + in.getWitness().bitcoinSerializeToStream(stream); + } + } + // lock_time + uint32ToByteStreamLE(lockTime, stream); + } + + /** + * Deserialize according to BIP144 or + * the classic format, depending on if the + * transaction is segwit or not. + */ + public void parse() { + // version + version = readUint32(); + // peek at marker + byte marker = rawtx[cursor]; + boolean useSegwit = marker == 0; + // marker, flag + if (useSegwit) { + readBytes(2); + } + // txin_count, txins + parseInputs(); + // txout_count, txouts + parseOutputs(); + // script_witnesses + if (useSegwit) + parseWitnesses(); + // lock_time + lockTime = readUint32(); + + length = cursor - offset; + } + + private void parseInputs() { + long numInputs = readVarInt(); + inputs = new ArrayList<>(Math.min((int) numInputs, Utils.MAX_INITIAL_ARRAY_LENGTH)); + for (long i = 0; i < numInputs; i++) { + TransactionInput input = new TransactionInput(this, rawtx, cursor); + inputs.add(input); + long scriptLen = readVarInt(TransactionOutPoint.MESSAGE_LENGTH); + cursor += scriptLen + 4; + } + } + + private void parseOutputs() { + long numOutputs = readVarInt(); + outputs = new ArrayList<>(Math.min((int) numOutputs, Utils.MAX_INITIAL_ARRAY_LENGTH)); + for (long i = 0; i < numOutputs; i++) { + TransactionOutput output = new TransactionOutput(this, rawtx, cursor); + outputs.add(output); + long scriptLen = readVarInt(8); + cursor += scriptLen; + } + } + + private void parseWitnesses() { + int numWitnesses = inputs.size(); + for (int i = 0; i < numWitnesses; i++) { + long pushCount = readVarInt(); + TransactionWitness witness = new TransactionWitness((int) pushCount); + inputs.get(i).setWitness(witness); + for (int y = 0; y < pushCount; y++) { + long pushSize = readVarInt(); + byte[] push = readBytes((int) pushSize); + witness.setPush(y, push); + } + } + } + + /** Returns an unmodifiable view of all inputs. */ + public List getInputs() { + return Collections.unmodifiableList(inputs); + } + + /** Returns an unmodifiable view of all outputs. */ + public List getOutputs() { + return Collections.unmodifiableList(outputs); + } + + public static final void main(String[] args) { + String hex = "0100000001e0ea4cd2f1307820d5f33e61aa6b636d8ff94fa7e3b1913f058fb1c8a765fde0340000006a47304402201aa0955638da2902ba972100816d21bde55d0415b98064b7fa511ffefa41397702203f9c93e27557b5b04187784e79f2c1eb74a3202a73085ddfb4509069b90cbbed0121023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ffffffff3510270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac10270000000000002321023cb3e593fb85c5659688528e9a4f1c4c7f19206edc7e517d20f794ba686fd6d6ac2a91a401000000001976a9141f924ac57c8e44cfbf860fbe0a3ea072b5fb8d0f88ac00000000"; + byte[] transactionBytes = Utils.hexToBytes(hex); + Transaction transaction = new Transaction(transactionBytes); + + Address[] addresses = transaction.getOutputs().get(3).getScript().getToAddresses(); + System.out.println(addresses[0]); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java b/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java new file mode 100644 index 0000000..fcb41f5 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java @@ -0,0 +1,85 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; + +import java.io.IOException; +import java.io.OutputStream; + +public class TransactionInput extends TransactionPart { + // Allows for altering transactions after they were broadcast. Values below NO_SEQUENCE-1 mean it can be altered. + private long sequence; + + // Data needed to connect to the output of the transaction we're gathering coins from. + private TransactionOutPoint outpoint; + + private byte[] scriptBytes; + + private Script script; + + private TransactionWitness witness; + + public TransactionInput(Transaction transaction, byte[] rawtx, int offset) { + super(rawtx, offset); + setParent(transaction); + } + + protected void parse() throws ProtocolException { + outpoint = new TransactionOutPoint(rawtx, cursor, this); + cursor += outpoint.getMessageSize(); + int scriptLen = (int) readVarInt(); + length = cursor - offset + scriptLen + 4; + scriptBytes = readBytes(scriptLen); + sequence = readUint32(); + } + + public byte[] getScriptBytes() { + return scriptBytes; + } + + public Script getScript() { + if(script == null) { + script = new Script(scriptBytes); + } + + return script; + } + + public TransactionWitness getWitness() { + return witness != null ? witness : TransactionWitness.EMPTY; + } + + public void setWitness(TransactionWitness witness) { + this.witness = witness; + } + + public boolean hasWitness() { + return witness != null && witness.getPushCount() != 0; + } + + public TransactionOutPoint getOutpoint() { + return outpoint; + } + + public long getSequenceNumber() { + return sequence; + } + + public void setSequenceNumber(long sequence) { + this.sequence = sequence; + } + + /** + * Coinbase transactions have special inputs with hashes of zero. If this is such an input, returns true. + */ + public boolean isCoinBase() { + return outpoint.getHash().equals(Sha256Hash.ZERO_HASH) && + (outpoint.getIndex() & 0xFFFFFFFFL) == 0xFFFFFFFFL; // -1 but all is serialized to the wire as unsigned int. + } + + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + outpoint.bitcoinSerialize(stream); + stream.write(new VarInt(scriptBytes.length).encode()); + stream.write(scriptBytes); + Utils.uint32ToByteStreamLE(sequence, stream); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java b/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java new file mode 100644 index 0000000..f8bf861 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java @@ -0,0 +1,42 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.address.Address; + +public class TransactionOutPoint extends TransactionPart { + + static final int MESSAGE_LENGTH = 36; + + /** Hash of the transaction to which we refer. */ + private Sha256Hash hash; + /** Which output of that transaction we are talking about. */ + private long index; + + private Address[] addresses; + + public TransactionOutPoint(byte[] rawtx, int offset, TransactionPart parent) { + super(rawtx, offset); + setParent(parent); + } + + protected void parse() throws ProtocolException { + length = MESSAGE_LENGTH; + hash = readHash(); + index = readUint32(); + } + + public Sha256Hash getHash() { + return hash; + } + + public long getIndex() { + return index; + } + + public Address[] getAddresses() { + return addresses; + } + + public void setAddresses(Address[] addresses) { + this.addresses = addresses; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java b/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java new file mode 100644 index 0000000..0cc993c --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java @@ -0,0 +1,65 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.address.Address; + +import java.io.IOException; +import java.io.OutputStream; + +public class TransactionOutput extends TransactionPart { + // The output's value is kept as a native type in order to save class instances. + private long value; + + // A transaction output has a script used for authenticating that the redeemer is allowed to spend + // this output. + private byte[] scriptBytes; + + private Script script; + + private int scriptLen; + + private Address[] addresses; + + public TransactionOutput(Transaction transaction, byte[] rawtx, int offset) { + super(rawtx, offset); + setParent(transaction); + } + + protected void parse() throws ProtocolException { + value = readInt64(); + scriptLen = (int) readVarInt(); + length = cursor - offset + scriptLen; + scriptBytes = readBytes(scriptLen); + } + + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + Utils.int64ToByteStreamLE(value, stream); + // TODO: Move script serialization into the Script class, where it belongs. + stream.write(new VarInt(scriptBytes.length).encode()); + stream.write(scriptBytes); + } + + public byte[] getScriptBytes() { + return scriptBytes; + } + + public Script getScript() { + if(script == null) { + script = new Script(scriptBytes); + } + + return script; + } + + public long getValue() { + return value; + } + + public Address[] getAddresses() { + return addresses; + } + + public void setAddresses(Address[] addresses) { + this.addresses = addresses; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java b/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java new file mode 100644 index 0000000..151d10e --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java @@ -0,0 +1,119 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; + +public abstract class TransactionPart { + private static final Logger log = LoggerFactory.getLogger(TransactionPart.class); + + public static final int MAX_SIZE = 0x02000000; // 32MB + public static final int UNKNOWN_LENGTH = Integer.MIN_VALUE; + + protected byte[] rawtx; + + // The offset is how many bytes into the provided byte array this message payload starts at. + protected int offset; + // The cursor keeps track of where we are in the byte array as we parse it. + // Note that it's relative to the start of the array NOT the start of the message payload. + protected int cursor; + + protected TransactionPart parent; + + protected int length = UNKNOWN_LENGTH; + + public TransactionPart(byte[] rawtx, int offset) { + this.rawtx = rawtx; + this.cursor = this.offset = offset; + + parse(); + } + + protected abstract void parse() throws ProtocolException; + + public final void setParent(TransactionPart parent) { + this.parent = parent; + } + + /** + * This returns a correct value by parsing the message. + */ + public final int getMessageSize() { + if (length == UNKNOWN_LENGTH) { + throw new ProtocolException(); + } + + return length; + } + + protected long readUint32() throws ProtocolException { + try { + long u = Utils.readUint32(rawtx, cursor); + cursor += 4; + return u; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ProtocolException(e); + } + } + + protected long readInt64() throws ProtocolException { + try { + long u = Utils.readInt64(rawtx, cursor); + cursor += 8; + return u; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ProtocolException(e); + } + } + + protected byte[] readBytes(int length) throws ProtocolException { + if ((length > MAX_SIZE) || (cursor + length > rawtx.length)) { + throw new ProtocolException("Claimed value length too large: " + length); + } + try { + byte[] b = new byte[length]; + System.arraycopy(rawtx, cursor, b, 0, length); + cursor += length; + return b; + } catch (IndexOutOfBoundsException e) { + throw new ProtocolException(e); + } + } + + protected long readVarInt() throws ProtocolException { + return readVarInt(0); + } + + protected long readVarInt(int offset) throws ProtocolException { + try { + VarInt varint = new VarInt(rawtx, cursor + offset); + cursor += offset + varint.getOriginalSizeInBytes(); + return varint.value; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ProtocolException(e); + } + } + + protected Sha256Hash readHash() throws ProtocolException { + // We have to flip it around, as it's been read off the wire in little endian. + // Not the most efficient way to do this but the clearest. + return Sha256Hash.wrapReversed(readBytes(32)); + } + + public final void bitcoinSerialize(OutputStream stream) throws IOException { + // 1st check for cached bytes. + if (rawtx != null && length != UNKNOWN_LENGTH) { + stream.write(rawtx, offset, length); + return; + } + + bitcoinSerializeToStream(stream); + } + + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + log.error("Error: {} class has not implemented bitcoinSerializeToStream method. Generating message with no payload", getClass()); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java b/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java new file mode 100644 index 0000000..947336a --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java @@ -0,0 +1,38 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class TransactionWitness { + public static final TransactionWitness EMPTY = new TransactionWitness(0); + + private final List pushes; + + public TransactionWitness(int pushCount) { + pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH)); + } + + public void setPush(int i, byte[] value) { + while (i >= pushes.size()) { + pushes.add(new byte[]{}); + } + pushes.set(i, value); + } + + public int getPushCount() { + return pushes.size(); + } + + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + stream.write(new VarInt(pushes.size()).encode()); + for (int i = 0; i < pushes.size(); i++) { + byte[] push = pushes.get(i); + stream.write(new VarInt(push.length).encode()); + stream.write(push); + } + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/UnsafeByteArrayOutputStream.java b/src/main/java/com/craigraw/drongo/protocol/UnsafeByteArrayOutputStream.java new file mode 100644 index 0000000..9ed2f79 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/UnsafeByteArrayOutputStream.java @@ -0,0 +1,138 @@ +package com.craigraw.drongo.protocol; + +/* + * Copyright 2011 Steve Coughlan. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + *

    An unsynchronized implementation of ByteArrayOutputStream that will return the backing byte array if its length == size(). + * This avoids unneeded array copy where the BOS is simply being used to extract a byte array of known length from a + * 'serialized to stream' method.

    + * + *

    Unless the final length can be accurately predicted the only performance this will yield is due to unsynchronized + * methods.

    + * + * @author git + */ +public class UnsafeByteArrayOutputStream extends ByteArrayOutputStream { + + public UnsafeByteArrayOutputStream() { + super(32); + } + + public UnsafeByteArrayOutputStream(int size) { + super(size); + } + + /** + * Writes the specified byte to this byte array output stream. + * + * @param b the byte to be written. + */ + @Override + public void write(int b) { + int newcount = count + 1; + if (newcount > buf.length) { + buf = copyOf(buf, Math.max(buf.length << 1, newcount)); + } + buf[count] = (byte) b; + count = newcount; + } + + /** + * Writes {@code len} bytes from the specified byte array + * starting at offset {@code off} to this byte array output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + */ + @Override + public void write(byte[] b, int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + int newcount = count + len; + if (newcount > buf.length) { + buf = copyOf(buf, Math.max(buf.length << 1, newcount)); + } + System.arraycopy(b, off, buf, count, len); + count = newcount; + } + + /** + * Writes the complete contents of this byte array output stream to + * the specified output stream argument, as if by calling the output + * stream's write method using {@code out.write(buf, 0, count)}. + * + * @param out the output stream to which to write the data. + * @throws IOException if an I/O error occurs. + */ + @Override + public void writeTo(OutputStream out) throws IOException { + out.write(buf, 0, count); + } + + /** + * Resets the {@code count} field of this byte array output + * stream to zero, so that all currently accumulated output in the + * output stream is discarded. The output stream can be used again, + * reusing the already allocated buffer space. + * + * @see java.io.ByteArrayInputStream#count + */ + @Override + public void reset() { + count = 0; + } + + /** + * Creates a newly allocated byte array. Its size is the current + * size of this output stream and the valid contents of the buffer + * have been copied into it. + * + * @return the current contents of this output stream, as a byte array. + * @see java.io.ByteArrayOutputStream#size() + */ + @Override + public byte toByteArray()[] { + return count == buf.length ? buf : copyOf(buf, count); + } + + /** + * Returns the current size of the buffer. + * + * @return the value of the {@code count} field, which is the number + * of valid bytes in this output stream. + * @see java.io.ByteArrayOutputStream#count + */ + @Override + public int size() { + return count; + } + + private static byte[] copyOf(byte[] in, int length) { + byte[] out = new byte[length]; + System.arraycopy(in, 0, out, 0, Math.min(length, in.length)); + return out; + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/VarInt.java b/src/main/java/com/craigraw/drongo/protocol/VarInt.java new file mode 100644 index 0000000..f753e0c --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/VarInt.java @@ -0,0 +1,117 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; + +/** + * A variable-length encoded unsigned integer using Satoshi's encoding (a.k.a. "CompactSize"). + */ +public class VarInt { + public final long value; + private final int originallyEncodedSize; + + /** + * Constructs a new VarInt with the given unsigned long value. + * + * @param value the unsigned long value (beware widening conversion of negatives!) + */ + public VarInt(long value) { + this.value = value; + originallyEncodedSize = getSizeInBytes(); + } + + /** + * Constructs a new VarInt with the value parsed from the specified offset of the given buffer. + * + * @param buf the buffer containing the value + * @param offset the offset of the value + */ + public VarInt(byte[] buf, int offset) { + int first = 0xFF & buf[offset]; + if (first < 253) { + value = first; + originallyEncodedSize = 1; // 1 data byte (8 bits) + } else if (first == 253) { + value = Utils.readUint16(buf, offset + 1); + originallyEncodedSize = 3; // 1 marker + 2 data bytes (16 bits) + } else if (first == 254) { + value = Utils.readUint32(buf, offset + 1); + originallyEncodedSize = 5; // 1 marker + 4 data bytes (32 bits) + } else { + value = Utils.readInt64(buf, offset + 1); + originallyEncodedSize = 9; // 1 marker + 8 data bytes (64 bits) + } + } + + /** + * Returns the original number of bytes used to encode the value if it was + * deserialized from a byte array, or the minimum encoded size if it was not. + */ + public int getOriginalSizeInBytes() { + return originallyEncodedSize; + } + + /** + * Returns the minimum encoded size of the value. + */ + public final int getSizeInBytes() { + return sizeOf(value); + } + + /** + * Returns the minimum encoded size of the given unsigned long value. + * + * @param value the unsigned long value (beware widening conversion of negatives!) + */ + public static int sizeOf(long value) { + // if negative, it's actually a very large unsigned long value + if (value < 0) return 9; // 1 marker + 8 data bytes + if (value < 253) return 1; // 1 data byte + if (value <= 0xFFFFL) return 3; // 1 marker + 2 data bytes + if (value <= 0xFFFFFFFFL) return 5; // 1 marker + 4 data bytes + return 9; // 1 marker + 8 data bytes + } + + /** + * Encodes the value into its minimal representation. + * + * @return the minimal encoded bytes of the value + */ + public byte[] encode() { + byte[] bytes; + switch (sizeOf(value)) { + case 1: + return new byte[]{(byte) value}; + case 3: + bytes = new byte[3]; + bytes[0] = (byte) 253; + Utils.uint16ToByteArrayLE((int) value, bytes, 1); + return bytes; + case 5: + bytes = new byte[5]; + bytes[0] = (byte) 254; + Utils.uint32ToByteArrayLE(value, bytes, 1); + return bytes; + default: + bytes = new byte[9]; + bytes[0] = (byte) 255; + Utils.int64ToByteArrayLE(value, bytes, 1); + return bytes; + } + } +} diff --git a/src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java b/src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java new file mode 100644 index 0000000..deea868 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/rpc/BitcoinJSONRPCClient.java @@ -0,0 +1,128 @@ +package com.craigraw.drongo.rpc; + +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.*; +import java.nio.charset.Charset; +import java.util.*; + +public class BitcoinJSONRPCClient { + private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class); + public static final Charset QUERY_CHARSET = Charset.forName("ISO8859-1"); + public static final String RESPONSE_ID = "drongo"; + + public final URL rpcURL; + private final URL noAuthURL; + private final String authStr; + + public BitcoinJSONRPCClient(String host, String port, String user, String password) { + this.rpcURL = getConnectUrl(host, port, user, password); + + try { + this.noAuthURL = new URI(rpcURL.getProtocol(), null, rpcURL.getHost(), rpcURL.getPort(), rpcURL.getPath(), rpcURL.getQuery(), null).toURL(); + } catch (MalformedURLException | URISyntaxException ex) { + throw new IllegalArgumentException(rpcURL.toString(), ex); + } + + this.authStr = rpcURL.getUserInfo() == null ? null : new String(Base64.getEncoder().encode(rpcURL.getUserInfo().getBytes(QUERY_CHARSET)), QUERY_CHARSET); + } + + private URL getConnectUrl(String host, String port, String user, String password) { + try { + return new URL("http://" + user + ':' + password + "@" + host + ":" + (port == null ? "8332" : port) + "/"); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid RPC connection details", e); + } + } + + public Object query(String method, Object... o) throws BitcoinRPCException { + HttpURLConnection conn; + try { + conn = (HttpURLConnection) noAuthURL.openConnection(); + + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setRequestProperty("Authorization", "Basic " + authStr); + byte[] r = prepareRequest(method, o); + log.debug("Bitcoin JSON-RPC request: " + new String(r, QUERY_CHARSET)); + conn.getOutputStream().write(r); + conn.getOutputStream().close(); + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + InputStream errorStream = conn.getErrorStream(); + throw new BitcoinRPCException(method, + Arrays.deepToString(o), + responseCode, + conn.getResponseMessage(), + errorStream == null ? null : new String(loadStream(errorStream, true))); + } + return loadResponse(conn.getInputStream(), RESPONSE_ID, true); + } catch (IOException ex) { + throw new BitcoinRPCException(method, Arrays.deepToString(o), ex); + } + } + + protected byte[] prepareRequest(final String method, final Object... params) { + return JSONObject.toJSONString(new LinkedHashMap() { + { + put("method", method); + put("params", Arrays.asList(params)); + put("id", RESPONSE_ID); + put("jsonrpc", "1.0"); + } + }).getBytes(QUERY_CHARSET); + } + + private static byte[] loadStream(InputStream in, boolean close) throws IOException { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (;;) { + int nr = in.read(buffer); + + if (nr == -1) + break; + if (nr == 0) + throw new IOException("Read timed out"); + + o.write(buffer, 0, nr); + } + return o.toByteArray(); + } + + @SuppressWarnings("rawtypes") + public Object loadResponse(InputStream in, Object expectedID, boolean close) throws IOException, BitcoinRPCException { + try { + String r = new String(loadStream(in, close), QUERY_CHARSET); + log.debug("Bitcoin JSON-RPC response: " + r); + try { + JSONParser jsonParser = new JSONParser(); + Map response = (Map) jsonParser.parse(r); + + if (!expectedID.equals(response.get("id"))) + throw new BitcoinRPCException("Wrong response ID (expected: " + String.valueOf(expectedID) + ", response: " + response.get("id") + ")"); + + if (response.get("error") != null) + throw new BitcoinRPCException(new BitcoinRPCError((Map)response.get("error"))); + + return response.get("result"); + } catch (ClassCastException | ParseException ex) { + throw new BitcoinRPCException("Invalid server response format (data: \"" + r + "\")"); + } + } finally { + if (close) + in.close(); + } + } + + public String getRawTransaction(String txId) throws BitcoinRPCException { + return (String) query("getrawtransaction", txId); + } +} diff --git a/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java new file mode 100644 index 0000000..a9ba4b1 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCError.java @@ -0,0 +1,23 @@ +package com.craigraw.drongo.rpc; + +import java.util.Map; + +public class BitcoinRPCError { + private int code; + private String message; + + @SuppressWarnings({ "rawtypes" }) + public BitcoinRPCError(Map errorMap) { + Number n = (Number) errorMap.get("code"); + this.code = n != null ? n.intValue() : 0; + this.message = (String) errorMap.get("message"); + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java new file mode 100644 index 0000000..31f319e --- /dev/null +++ b/src/main/java/com/craigraw/drongo/rpc/BitcoinRPCException.java @@ -0,0 +1,115 @@ +package com.craigraw.drongo.rpc; + +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +public class BitcoinRPCException extends RuntimeException { + private static final Logger log = LoggerFactory.getLogger(BitcoinJSONRPCClient.class); + + private String rpcMethod; + private String rpcParams; + private int responseCode; + private String responseMessage; + private String response; + private BitcoinRPCError rpcError; + + /** + * Creates a new instance of BitcoinRPCException with response + * detail. + * + * @param method the rpc method called + * @param params the parameters sent + * @param responseCode the HTTP code received + * @param responseMessage the HTTP response message + * @param response the error stream received + */ + @SuppressWarnings("rawtypes") + public BitcoinRPCException(String method, + String params, + int responseCode, + String responseMessage, + String response) { + super("RPC Query Failed (method: " + method + ", params: " + params + ", response code: " + responseCode + " responseMessage " + responseMessage + ", response: " + response); + this.rpcMethod = method; + this.rpcParams = params; + this.responseCode = responseCode; + this.responseMessage = responseMessage; + this.response = response; + if ( responseCode == 500 ) { + // Bitcoind application error when handle the request + // extract code/message for callers to handle + try { + JSONParser jsonParser = new JSONParser(); + Map error = (Map) ((Map)jsonParser.parse(response)).get("error"); + if ( error != null ) { + rpcError = new BitcoinRPCError(error); + } + } catch(ParseException e) { + log.error("Could not parse bitcoind error", e); + } + } + } + + public BitcoinRPCException(String method, String params, Throwable cause) { + super("RPC Query Failed (method: " + method + ", params: " + params + ")", cause); + this.rpcMethod = method; + this.rpcParams = params; + } + + /** + * Constructs an instance of BitcoinRPCException with the + * specified detail message. + * + * @param msg the detail message. + */ + public BitcoinRPCException(String msg) { + super(msg); + } + + public BitcoinRPCException(BitcoinRPCError error) { + super(error.getMessage()); + this.rpcError = error; + } + + public BitcoinRPCException(String message, Throwable cause) { + super(message, cause); + } + + public int getResponseCode() { + return responseCode; + } + + public String getRpcMethod() { + return rpcMethod; + } + + public String getRpcParams() { + return rpcParams; + } + + /** + * @return the HTTP response message + */ + public String getResponseMessage() { + return responseMessage; + } + + /** + * @return response message from bitcored + */ + public String getResponse() { + return this.response; + } + + /** + * @return response message from bitcored + */ + public BitcoinRPCError getRPCError() { + return this.rpcError; + } +} diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..0f03697 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootLogger=INFO, stdout, file + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d %c - %m%n + +log4j.appender.file=org.apache.log4j.FileAppender +log4j.appender.file.File=sentinel.log +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=[%-5p] %d %c - %m%n \ No newline at end of file diff --git a/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java b/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java new file mode 100644 index 0000000..d27da9a --- /dev/null +++ b/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java @@ -0,0 +1,43 @@ +package com.craigraw.drongo; + +import org.junit.Assert; +import org.junit.Test; + +public class OutputDescriptorTest { + + @Test + public void electrumP2PKH() { + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z"); + Assert.assertEquals("pkh(xpub6BemYiVEULcbpkxh3wp6KUzfzGPFL7JNcxbfQcXxGnJ6sPugTkR69neX8RT9iXdMHFV1FCge72a21WpoHjgoeBTcZju3JKyFf9DztGT2FhE/0/*)", descriptor.toString()); + } + + @Test + public void iancolemanP2PKH() { + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z"); + Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString()); + } + + @Test + public void electrumP2WPKH() { + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR"); + Assert.assertEquals("wpkh(xpub6CqLiu9VMua6V5yFXtXrfZgJqWsG2a8dQdBuk34KFdCCYXvCtx41CmWugPJVZNzBXyHCWy8uHgVUMpePCxh2S3VXueYG8dWLDh49dQ9MJGu/0/*)", descriptor.toString()); + } + + @Test + public void iancolemanP2SHP2WPKH() { + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os"); + Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString()); + } + + @Test + public void bip84P2WPKH() { + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"); + Assert.assertEquals("wpkh(xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V/0/*)", descriptor.toString()); + } + + @Test + public void redditP2SHP2WPKH() { + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr"); + Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP/0/*))", descriptor.toString()); + } +} diff --git a/src/test/java/com/craigraw/drongo/WatchWalletTest.java b/src/test/java/com/craigraw/drongo/WatchWalletTest.java new file mode 100644 index 0000000..b7ae1fc --- /dev/null +++ b/src/test/java/com/craigraw/drongo/WatchWalletTest.java @@ -0,0 +1,58 @@ +package com.craigraw.drongo; + +import org.junit.Assert; +import org.junit.Test; + +public class WatchWalletTest { + + @Test + public void electrumP2PKH() { + WatchWallet wallet = new WatchWallet("", "xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z"); + + Assert.assertEquals("1QEjP9f7KRtJobfwmRuykpLjaR5QchGo8q", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("17kCok3XAUHyL6kjzBF44e1YuzMmRXPuu5", wallet.getReceivingAddress(1).toString()); + Assert.assertEquals("1Dh3Lofy2cFdEQ2rk4Eq6fbPeQQ63pDdRN", wallet.getChangeAddress(0).toString()); + } + + @Test + public void iancolemanP2PKH() { + WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z"); + + Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString()); + } + + @Test + public void electrumP2WPKH() { + WatchWallet wallet = new WatchWallet("", "zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR"); + + Assert.assertEquals("bc1q4s5v0u9qmmcp25mnr3mfzhyftjzw8mccqawmwf", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("bc1qffy90ge6wljh53t07q4al2pgsmuqgy48wrk8wq", wallet.getReceivingAddress(1).toString()); + Assert.assertEquals("bc1q87fg9yjxratt4hemjn0m4re97n2p39ssq5xhv4", wallet.getChangeAddress(0).toString()); + } + + @Test + public void iancolemanP2SHP2WPKH() { + WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os"); + + Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString()); + } + + @Test + public void bip84P2WPKH() { + WatchWallet wallet = new WatchWallet("", "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"); + + Assert.assertEquals("bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g", wallet.getReceivingAddress(1).toString()); + Assert.assertEquals("bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", wallet.getChangeAddress(0).toString()); + } + + @Test + public void redditP2SHP2WPKH() { + WatchWallet wallet = new WatchWallet("", "ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr"); + + Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString()); + } +}