├── .gitignore ├── LICENSE ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── uifuture │ │ │ └── springbootblockchain │ │ │ ├── SpringBootBlockchainApplication.java │ │ │ ├── bd │ │ │ └── RocksDBUtils.java │ │ │ ├── block │ │ │ ├── Block.java │ │ │ └── Blockchain.java │ │ │ ├── cli │ │ │ └── CLI.java │ │ │ ├── controller │ │ │ └── IndexController.java │ │ │ ├── pow │ │ │ ├── PowResult.java │ │ │ └── ProofOfWork.java │ │ │ ├── transaction │ │ │ ├── MerkleTree.java │ │ │ ├── SpendableOutputResult.java │ │ │ ├── TXInput.java │ │ │ ├── TXOutput.java │ │ │ ├── Transaction.java │ │ │ └── UTXOSet.java │ │ │ ├── util │ │ │ ├── Base58Check.java │ │ │ ├── BtcAddressUtils.java │ │ │ ├── ByteUtils.java │ │ │ ├── SerializeUtils.java │ │ │ └── redis │ │ │ │ ├── RedisCommand.java │ │ │ │ └── RedisHandle.java │ │ │ └── wallet │ │ │ ├── Wallet.java │ │ │ └── WalletUtils.java │ └── resources │ │ └── application.properties └── test │ └── java │ └── com │ └── uifuture │ └── blockchainspringboot │ ├── BlockchainTest.java │ ├── ProofTest.java │ └── SpringBootBlockchainApplicationTests.java └── target └── classes └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | ### IntelliJ IDEA ### 23 | blockchain.db 24 | *.dat 25 | .idea 26 | *.iws 27 | *.iml 28 | *.ipr 29 | 30 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 31 | hs_err_pid* 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.uifuture 7 | spring-boot-blockchain 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-boot-blockchain 12 | 区块链项目 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.5.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | UTF-8 26 | 1.8 27 | 1.8 28 | true 29 | 4.1.29.Final 30 | 24.0-jre 31 | 5.0.4 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | 51 | org.apache.commons 52 | commons-lang3 53 | 54 | 55 | commons-codec 56 | commons-codec 57 | 58 | 59 | commons-cli 60 | commons-cli 61 | 1.4 62 | 63 | 64 | com.google.guava 65 | guava 66 | ${guava.version} 67 | 68 | 69 | org.projectlombok 70 | lombok 71 | 72 | 73 | 74 | 75 | com.esotericsoftware 76 | kryo 77 | 4.0.1 78 | 79 | 80 | 81 | 82 | org.rocksdb 83 | rocksdbjni 84 | 5.9.2 85 | 86 | 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-starter-data-redis 91 | 92 | 93 | 94 | javax 95 | javaee-api 96 | 7.0 97 | provided 98 | 99 | 100 | org.json 101 | json 102 | 20160810 103 | 104 | 105 | 106 | 107 | org.bouncycastle 108 | bcprov-jdk15on 109 | 1.59 110 | 111 | 112 | 113 | net.sf.jopt-simple 114 | jopt-simple 115 | ${jopt-simple.version} 116 | 117 | 118 | 119 | 120 | 121 | 122 | org.springframework.boot 123 | spring-boot-maven-plugin 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/SpringBootBlockchainApplication.java: -------------------------------------------------------------------------------- 1 | package com.uifuture.springbootblockchain; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SpringBootBlockchainApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SpringBootBlockchainApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/bd/RocksDBUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.bd; 6 | 7 | import com.google.common.collect.Maps; 8 | import com.uifuture.springbootblockchain.block.Block; 9 | import com.uifuture.springbootblockchain.transaction.TXOutput; 10 | import com.uifuture.springbootblockchain.util.SerializeUtils; 11 | import lombok.Getter; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.rocksdb.RocksDB; 15 | import org.rocksdb.RocksDBException; 16 | 17 | import java.util.Map; 18 | 19 | /** 20 | * 存储 21 | * 暂时选择Redis,但是后期需要更改数据库,所以一定要做到数据库存储之间的兼容 22 | * 暂定10分钟生成一个区块 23 | * @author chenhx 24 | * @version RocksDB.java, v 0.1 2018-10-13 下午 4:46 25 | */ 26 | @Slf4j 27 | public class RocksDBUtils { 28 | /** 29 | * 区块链数据文件 30 | */ 31 | private static final String DB_FILE = "blockchain.db"; 32 | /** 33 | * 区块桶Key 34 | */ 35 | private static final String BLOCKS_BUCKET_KEY = "blocks"; 36 | /** 37 | * 链状态桶Key 38 | */ 39 | private static final String CHAINSTATE_BUCKET_KEY = "chainstate"; 40 | 41 | /** 42 | * 最新一个区块 43 | */ 44 | private static final String LAST_BLOCK_KEY = "l"; 45 | 46 | private volatile static RocksDBUtils instance; 47 | private RocksDB db; 48 | /** 49 | * chainstate buckets 50 | */ 51 | @Getter 52 | private Map chainstateBucket; 53 | 54 | /** 55 | * block buckets 56 | */ 57 | private Map blocksBucket; 58 | 59 | private RocksDBUtils() { 60 | openDB(); 61 | initBlockBucket(); 62 | initChainStateBucket(); 63 | } 64 | 65 | public static RocksDBUtils getInstance() { 66 | if (instance == null) { 67 | synchronized (RocksDBUtils.class) { 68 | if (instance == null) { 69 | instance = new RocksDBUtils(); 70 | } 71 | } 72 | } 73 | return instance; 74 | } 75 | 76 | /** 77 | * 打开数据库 78 | */ 79 | private void openDB() { 80 | try { 81 | db = RocksDB.open(DB_FILE); 82 | } catch (RocksDBException e) { 83 | log.error("Fail to open db ! ", e); 84 | throw new RuntimeException("Fail to open db ! ", e); 85 | } 86 | } 87 | 88 | /** 89 | * 初始化 blocks 数据桶 90 | */ 91 | private void initBlockBucket() { 92 | try { 93 | byte[] blockBucketKey = SerializeUtils.serialize(BLOCKS_BUCKET_KEY); 94 | byte[] blockBucketBytes = db.get(blockBucketKey); 95 | if (blockBucketBytes != null) { 96 | blocksBucket = (Map) SerializeUtils.deserialize(blockBucketBytes); 97 | } else { 98 | blocksBucket = Maps.newHashMap(); 99 | db.put(blockBucketKey, SerializeUtils.serialize(blocksBucket)); 100 | } 101 | } catch (RocksDBException e) { 102 | log.error("Fail to init block bucket ! ", e); 103 | throw new RuntimeException("Fail to init block bucket ! ", e); 104 | } 105 | } 106 | 107 | /** 108 | * 初始化 blocks 数据桶 109 | */ 110 | private void initChainStateBucket() { 111 | try { 112 | byte[] chainstateBucketKey = SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY); 113 | byte[] chainstateBucketBytes = db.get(chainstateBucketKey); 114 | if (chainstateBucketBytes != null) { 115 | chainstateBucket = (Map) SerializeUtils.deserialize(chainstateBucketBytes); 116 | } else { 117 | chainstateBucket = Maps.newHashMap(); 118 | db.put(chainstateBucketKey, SerializeUtils.serialize(chainstateBucket)); 119 | } 120 | } catch (RocksDBException e) { 121 | log.error("Fail to init chainstate bucket ! ", e); 122 | throw new RuntimeException("Fail to init chainstate bucket ! ", e); 123 | } 124 | } 125 | 126 | /** 127 | * 保存最新一个区块的Hash值 128 | * 129 | * @param tipBlockHash 130 | */ 131 | public void putLastBlockHash(String tipBlockHash) { 132 | try { 133 | blocksBucket.put(LAST_BLOCK_KEY, SerializeUtils.serialize(tipBlockHash)); 134 | db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); 135 | } catch (RocksDBException e) { 136 | log.error("Fail to put last block hash ! tipBlockHash=" + tipBlockHash, e); 137 | throw new RuntimeException("Fail to put last block hash ! tipBlockHash=" + tipBlockHash, e); 138 | } 139 | } 140 | 141 | /** 142 | * 查询最新一个区块的Hash值 143 | * 144 | * @return 145 | */ 146 | public String getLastBlockHash() { 147 | byte[] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY); 148 | if (lastBlockHashBytes != null) { 149 | return (String) SerializeUtils.deserialize(lastBlockHashBytes); 150 | } 151 | return ""; 152 | } 153 | 154 | /** 155 | * 保存区块 156 | * 157 | * @param block 158 | */ 159 | public void putBlock(Block block) { 160 | try { 161 | blocksBucket.put(block.getHash(), SerializeUtils.serialize(block)); 162 | db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); 163 | } catch (RocksDBException e) { 164 | log.error("Fail to put block ! block=" + block.toString(), e); 165 | throw new RuntimeException("Fail to put block ! block=" + block.toString(), e); 166 | } 167 | } 168 | 169 | /** 170 | * 查询区块 171 | * 172 | * @param blockHash 173 | * @return 174 | */ 175 | public Block getBlock(String blockHash) { 176 | byte[] blockBytes = blocksBucket.get(blockHash); 177 | if (blockBytes != null) { 178 | return (Block) SerializeUtils.deserialize(blockBytes); 179 | } 180 | throw new RuntimeException("Fail to get block , don`t exist ! blockHash=" + blockHash); 181 | } 182 | 183 | /** 184 | * 获取最新一个区块 185 | * 186 | * @return 187 | */ 188 | public Block getLastBlock() { 189 | String lastBlockHash = getLastBlockHash(); 190 | if (StringUtils.isBlank(lastBlockHash)) { 191 | throw new RuntimeException("ERROR: Fail to get last block hash ! "); 192 | } 193 | Block lastBlock = getBlock(lastBlockHash); 194 | if (lastBlock == null) { 195 | throw new RuntimeException("ERROR: Fail to get last block ! "); 196 | } 197 | return lastBlock; 198 | } 199 | 200 | /** 201 | * 清空chainstate bucket 202 | */ 203 | public void cleanChainStateBucket() { 204 | try { 205 | chainstateBucket.clear(); 206 | } catch (Exception e) { 207 | log.error("Fail to clear chainstate bucket ! ", e); 208 | throw new RuntimeException("Fail to clear chainstate bucket ! ", e); 209 | } 210 | } 211 | 212 | /** 213 | * 一次性放入所有UTXO数据 214 | * 215 | * @param utxoDatas 216 | */ 217 | public void initAllUTXOs(Map utxoDatas) { 218 | try { 219 | chainstateBucket.putAll(utxoDatas); 220 | db.put(SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY), SerializeUtils.serialize(chainstateBucket)); 221 | } catch (RocksDBException e) { 222 | log.error("Fail to init all UTXOs data into chainstate bucket ! ", e); 223 | throw new RuntimeException("Fail to init all UTXOs data into chainstate bucket ! ", e); 224 | } 225 | } 226 | 227 | /** 228 | * 保存UTXO数据 229 | * 230 | * @param key 交易ID 231 | * @param utxos UTXOs 232 | */ 233 | public void putUTXOs(String key, TXOutput[] utxos) { 234 | try { 235 | chainstateBucket.put(key, SerializeUtils.serialize(utxos)); 236 | db.put(SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY), SerializeUtils.serialize(chainstateBucket)); 237 | } catch (Exception e) { 238 | log.error("Fail to put UTXOs into chainstate bucket ! key=" + key, e); 239 | throw new RuntimeException("Fail to put UTXOs into chainstate bucket ! key=" + key, e); 240 | } 241 | } 242 | 243 | /** 244 | * 查询UTXO数据 245 | * 246 | * @param key 交易ID 247 | */ 248 | public TXOutput[] getUTXOs(String key) { 249 | byte[] utxosByte = chainstateBucket.get(key); 250 | if (utxosByte != null) { 251 | return (TXOutput[]) SerializeUtils.deserialize(utxosByte); 252 | } 253 | return null; 254 | } 255 | 256 | 257 | /** 258 | * 删除 UTXO 数据 259 | * 260 | * @param key 交易ID 261 | */ 262 | public void deleteUTXOs(String key) { 263 | try { 264 | chainstateBucket.remove(key); 265 | db.put(SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY), SerializeUtils.serialize(chainstateBucket)); 266 | } catch (Exception e) { 267 | log.error("Fail to delete UTXOs by key ! key=" + key, e); 268 | throw new RuntimeException("Fail to delete UTXOs by key ! key=" + key, e); 269 | } 270 | } 271 | 272 | /** 273 | * 关闭数据库 274 | */ 275 | public void closeDB() { 276 | try { 277 | db.close(); 278 | } catch (Exception e) { 279 | log.error("Fail to close db ! ", e); 280 | throw new RuntimeException("Fail to close db ! ", e); 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/block/Block.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.block; 6 | 7 | import com.uifuture.springbootblockchain.pow.PowResult; 8 | import com.uifuture.springbootblockchain.pow.ProofOfWork; 9 | import com.uifuture.springbootblockchain.transaction.MerkleTree; 10 | import com.uifuture.springbootblockchain.transaction.Transaction; 11 | import com.uifuture.springbootblockchain.util.ByteUtils; 12 | import lombok.Data; 13 | import lombok.ToString; 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | import java.math.BigInteger; 17 | 18 | /** 19 | * 区块 20 | * 暂定区块体积为4M 21 | * @author chenhx 22 | * @version Block.java, v 0.1 2018-10-11 下午 9:16 23 | */ 24 | @Slf4j 25 | @Data 26 | @ToString 27 | public class Block { 28 | private String version = "1.0.0"; 29 | /** 30 | * 区块hash值 31 | */ 32 | private String hash; 33 | /** 34 | * 前一个区块的hash值 35 | */ 36 | private String prevBlockHash; 37 | /** 38 | * 交易信息 39 | */ 40 | private Transaction[] transactions; 41 | /** 42 | * 区块创建时间(单位:毫秒) 43 | */ 44 | private long timeStamp; 45 | /** 46 | * 工作量证明计数器 挖到时的运算值 47 | */ 48 | private BigInteger nonce; 49 | /** 50 | * 区块高度 51 | */ 52 | private int height; 53 | 54 | /** 55 | * 默克尔树rootHash 56 | */ 57 | private String merkleRoot; 58 | 59 | /** 60 | * 难度目标值 61 | */ 62 | private BigInteger target; 63 | /** 64 | *

创建创世区块

65 | * 66 | * @param coinbase 67 | * @return 68 | */ 69 | public static Block newGenesisBlock(Transaction coinbase) { 70 | return Block.newBlock(ByteUtils.ZERO_HASH, new Transaction[]{coinbase}, 0); 71 | } 72 | 73 | /** 74 | *

创建新区块

75 | * 76 | * @param previousHash 77 | * @param transactions 78 | * @return 79 | */ 80 | public static Block newBlock(String previousHash, Transaction[] transactions, int height) { 81 | Block block = new Block(); 82 | block.setPrevBlockHash(previousHash); 83 | block.setTimeStamp(System.currentTimeMillis()); 84 | block.setHeight(height); 85 | block.setTransactions(transactions); 86 | block.setMerkleRoot(ByteUtils.bytesToHexString(block.hashTransaction())); 87 | log.info("block.hashTransaction={}", block.hashTransaction()); 88 | log.info("block={}", block); 89 | log.info("block.getMerkleRoot={}", ByteUtils.hexStringToByte(block.getMerkleRoot())); 90 | 91 | ProofOfWork pow = ProofOfWork.newProofOfWork(block); 92 | //计算 93 | PowResult powResult = pow.run(); 94 | block.setHash(powResult.getHash()); 95 | block.setNonce(powResult.getNonce()); 96 | log.info("block={}", block); 97 | return block; 98 | } 99 | 100 | /** 101 | * 对区块中的交易信息进行Hash计算 102 | * 获取根节点Hash 103 | * @return 104 | */ 105 | public byte[] hashTransaction() { 106 | byte[][] txIdArrays = new byte[this.getTransactions().length][]; 107 | for (int i = 0; i < this.getTransactions().length; i++) { 108 | txIdArrays[i] = this.getTransactions()[i].hash(); 109 | } 110 | return new MerkleTree(txIdArrays).getRoot().getHash(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/block/Blockchain.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.block; 6 | 7 | import com.google.common.collect.Lists; 8 | import com.google.common.collect.Maps; 9 | import com.uifuture.springbootblockchain.bd.RocksDBUtils; 10 | import com.uifuture.springbootblockchain.transaction.TXInput; 11 | import com.uifuture.springbootblockchain.transaction.TXOutput; 12 | import com.uifuture.springbootblockchain.transaction.Transaction; 13 | import com.uifuture.springbootblockchain.util.ByteUtils; 14 | import lombok.AllArgsConstructor; 15 | import lombok.Data; 16 | import lombok.NoArgsConstructor; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.apache.commons.codec.binary.Hex; 19 | import org.apache.commons.lang3.ArrayUtils; 20 | import org.apache.commons.lang3.StringUtils; 21 | import org.apache.commons.lang3.time.DateFormatUtils; 22 | import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; 23 | 24 | import java.util.Arrays; 25 | import java.util.Date; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | /** 30 | * 区块链 31 | * AllArgsConstructor 所有参数的构造函数 32 | * @author chenhx 33 | * @version Blockchain.java, v 0.1 2018-10-11 下午 9:20 34 | */ 35 | 36 | @Data 37 | @AllArgsConstructor 38 | @NoArgsConstructor 39 | @Slf4j 40 | public class Blockchain { 41 | 42 | private String lastBlockHash; 43 | 44 | 45 | /** 46 | * 从 DB 中恢复区块链数据 47 | * 48 | * @return 49 | */ 50 | public static Blockchain initBlockchainFromDB() { 51 | String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); 52 | if (lastBlockHash == null) { 53 | throw new RuntimeException("ERROR: Fail to init blockchain from db. "); 54 | } 55 | return new Blockchain(lastBlockHash); 56 | } 57 | 58 | 59 | /** 60 | *

创建区块链

61 | * 62 | * @param address 钱包地址 63 | * @return 64 | */ 65 | public static Blockchain createBlockchain(String address) { 66 | String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); 67 | if (StringUtils.isBlank(lastBlockHash)) { 68 | // 创建 coinBase 交易,创世奖励 69 | // String genesisCoinbaseData = "The Times " + DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss SSS") + " Chancellor on brink of second bailout for banks"; 70 | String genesisCoinbaseData = "创世区块时间: " + DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss SSS") + " 去中心化区块链的开始"; 71 | Transaction coinbaseTX = Transaction.newCoinbaseTX(address, genesisCoinbaseData); 72 | Block genesisBlock = Block.newGenesisBlock(coinbaseTX); 73 | lastBlockHash = genesisBlock.getHash(); 74 | RocksDBUtils.getInstance().putBlock(genesisBlock); 75 | RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash); 76 | } 77 | return new Blockchain(lastBlockHash); 78 | } 79 | 80 | /** 81 | * 打包交易,进行挖矿 82 | * 83 | * @param transactions 84 | */ 85 | public Block mineBlock(Transaction[] transactions) { 86 | // 挖矿前,先验证交易记录 87 | for (Transaction tx : transactions) { 88 | if (!this.verifyTransactions(tx)) { 89 | log.error("ERROR: Fail to mine block ! Invalid transaction ! tx=" + tx.toString()); 90 | throw new RuntimeException("ERROR: Fail to mine block ! Invalid transaction ! "); 91 | } 92 | } 93 | Block lastBlock = RocksDBUtils.getInstance().getLastBlock(); 94 | //创建新区块 95 | Block block = Block.newBlock(lastBlockHash, transactions, lastBlock.getHeight() + 1); 96 | this.addBlock(block); 97 | return block; 98 | } 99 | 100 | /** 101 | *

添加区块

102 | * 103 | * @param block 104 | */ 105 | private void addBlock(Block block) { 106 | RocksDBUtils.getInstance().putLastBlockHash(block.getHash()); 107 | RocksDBUtils.getInstance().putBlock(block); 108 | this.lastBlockHash = block.getHash(); 109 | } 110 | 111 | /** 112 | *

添加区块

113 | * 114 | * @param block 115 | */ 116 | public void saveBlock(Block block) { 117 | Block existBlock = RocksDBUtils.getInstance().getBlock(block.getHash()); 118 | if (existBlock != null) { 119 | return; 120 | } 121 | // 保存区块数据 122 | RocksDBUtils.getInstance().putBlock(block); 123 | Block lastBlock = RocksDBUtils.getInstance().getLastBlock(); 124 | 125 | if (block.getHeight() > lastBlock.getHeight()) { 126 | RocksDBUtils.getInstance().putLastBlockHash(block.getHash()); 127 | this.lastBlockHash = block.getHash(); 128 | } 129 | } 130 | 131 | /** 132 | * 获取区块链迭代器 133 | * 134 | * @return 135 | */ 136 | public BlockchainIterator getBlockchainIterator() { 137 | return new BlockchainIterator(lastBlockHash); 138 | } 139 | 140 | /** 141 | * 查找所有的 unspent transaction outputs 142 | * 143 | * @return 144 | */ 145 | public Map findAllUTXOs() { 146 | Map allSpentTXOs = this.getAllSpentTXOs(); 147 | Map allUTXOs = Maps.newHashMap(); 148 | // 再次遍历所有区块中的交易输出 149 | for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { 150 | Block block = blockchainIterator.next(); 151 | for (Transaction transaction : block.getTransactions()) { 152 | 153 | String txId = Hex.encodeHexString(transaction.getTxId()); 154 | 155 | int[] spentOutIndexArray = allSpentTXOs.get(txId); 156 | TXOutput[] txOutputs = transaction.getOutputs(); 157 | for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) { 158 | if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) { 159 | continue; 160 | } 161 | TXOutput[] UTXOArray = allUTXOs.get(txId); 162 | if (UTXOArray == null) { 163 | UTXOArray = new TXOutput[]{txOutputs[outIndex]}; 164 | } else { 165 | UTXOArray = ArrayUtils.add(UTXOArray, txOutputs[outIndex]); 166 | } 167 | allUTXOs.put(txId, UTXOArray); 168 | } 169 | } 170 | } 171 | return allUTXOs; 172 | } 173 | 174 | /** 175 | * 从交易输入中查询区块链中所有已被花费了的交易输出 176 | * 177 | * @return 交易ID以及对应的交易输出下标地址 178 | */ 179 | private Map getAllSpentTXOs() { 180 | // 定义TxId ——> spentOutIndex[],存储交易ID与已被花费的交易输出数组索引值 181 | Map spentTXOs = Maps.newHashMap(); 182 | for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { 183 | Block block = blockchainIterator.next(); 184 | 185 | for (Transaction transaction : block.getTransactions()) { 186 | // 如果是 coinbase 交易,直接跳过,因为它不存在引用前一个区块的交易输出 187 | if (transaction.isCoinbase()) { 188 | continue; 189 | } 190 | for (TXInput txInput : transaction.getInputs()) { 191 | String inTxId = Hex.encodeHexString(txInput.getTxId()); 192 | int[] spentOutIndexArray = spentTXOs.get(inTxId); 193 | if (spentOutIndexArray == null) { 194 | spentOutIndexArray = new int[]{txInput.getTxOutputIndex()}; 195 | } else { 196 | spentOutIndexArray = ArrayUtils.add(spentOutIndexArray, txInput.getTxOutputIndex()); 197 | } 198 | spentTXOs.put(inTxId, spentOutIndexArray); 199 | } 200 | } 201 | } 202 | return spentTXOs; 203 | } 204 | 205 | /** 206 | * 依据交易ID查询交易信息 207 | * 208 | * @param txId 交易ID 209 | * @return 210 | */ 211 | private Transaction findTransaction(byte[] txId) { 212 | for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) { 213 | Block block = iterator.next(); 214 | for (Transaction tx : block.getTransactions()) { 215 | if (Arrays.equals(tx.getTxId(), txId)) { 216 | return tx; 217 | } 218 | } 219 | } 220 | throw new RuntimeException("ERROR: Can not found tx by txId ! "); 221 | } 222 | 223 | /** 224 | * 进行交易签名 225 | * 226 | * @param tx 交易数据 227 | * @param privateKey 私钥 228 | */ 229 | public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception { 230 | // 先来找到这笔新的交易中,交易输入所引用的前面的多笔交易的数据 231 | Map prevTxMap = Maps.newHashMap(); 232 | for (TXInput txInput : tx.getInputs()) { 233 | Transaction prevTx = this.findTransaction(txInput.getTxId()); 234 | prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx); 235 | } 236 | tx.sign(privateKey, prevTxMap); 237 | } 238 | 239 | /** 240 | * 交易签名验证 241 | * 242 | * @param tx 243 | */ 244 | public boolean verifyTransactions(Transaction tx) { 245 | if (tx.isCoinbase()) { 246 | return true; 247 | } 248 | Map prevTx = Maps.newHashMap(); 249 | for (TXInput txInput : tx.getInputs()) { 250 | Transaction transaction = this.findTransaction(txInput.getTxId()); 251 | prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction); 252 | } 253 | try { 254 | return tx.verify(prevTx); 255 | } catch (Exception e) { 256 | log.error("Fail to verify transaction ! transaction invalid ! ", e); 257 | throw new RuntimeException("Fail to verify transaction ! transaction invalid ! ", e); 258 | } 259 | } 260 | 261 | /** 262 | * 获取本地节点区块的最大高度 263 | * 264 | * @return 265 | */ 266 | public long getBestHeight() { 267 | Block lastBlock = RocksDBUtils.getInstance().getLastBlock(); 268 | return lastBlock.getHeight(); 269 | } 270 | 271 | /** 272 | * 获取区块链中所有区块的hash值 273 | * 274 | * @return 275 | */ 276 | public List getAllBlockHash() { 277 | List blockHashes = Lists.newArrayList(); 278 | for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { 279 | Block block = blockchainIterator.next(); 280 | blockHashes.add(block.getHash()); 281 | } 282 | return blockHashes; 283 | } 284 | 285 | /** 286 | * 根据hash查询区块 287 | * 288 | * @param hash 289 | * @return 290 | */ 291 | public Block getBlockByHash(String hash) { 292 | return RocksDBUtils.getInstance().getBlock(hash); 293 | } 294 | 295 | /** 296 | * 区块链迭代器 297 | */ 298 | public class BlockchainIterator { 299 | 300 | private String currentBlockHash; 301 | 302 | private BlockchainIterator(String currentBlockHash) { 303 | this.currentBlockHash = currentBlockHash; 304 | } 305 | 306 | /** 307 | * 是否有下一个区块 308 | * 309 | * @return 310 | */ 311 | public boolean hashNext() { 312 | if (ByteUtils.ZERO_HASH.equals(currentBlockHash)) { 313 | return false; 314 | } 315 | Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); 316 | if (lastBlock == null) { 317 | return false; 318 | } 319 | // 创世区块直接放行 320 | if (ByteUtils.ZERO_HASH.equals(lastBlock.getPrevBlockHash())) { 321 | return true; 322 | } 323 | return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null; 324 | } 325 | 326 | /** 327 | * 返回区块 328 | * 329 | * @return 330 | */ 331 | public Block next() { 332 | Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); 333 | if (currentBlock != null) { 334 | this.currentBlockHash = currentBlock.getPrevBlockHash(); 335 | return currentBlock; 336 | } 337 | return null; 338 | } 339 | } 340 | 341 | } 342 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/cli/CLI.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.cli; 6 | 7 | import com.uifuture.springbootblockchain.bd.RocksDBUtils; 8 | import com.uifuture.springbootblockchain.block.Block; 9 | import com.uifuture.springbootblockchain.block.Blockchain; 10 | import com.uifuture.springbootblockchain.pow.ProofOfWork; 11 | import com.uifuture.springbootblockchain.transaction.TXOutput; 12 | import com.uifuture.springbootblockchain.transaction.Transaction; 13 | import com.uifuture.springbootblockchain.transaction.UTXOSet; 14 | import com.uifuture.springbootblockchain.util.Base58Check; 15 | import com.uifuture.springbootblockchain.wallet.Wallet; 16 | import com.uifuture.springbootblockchain.wallet.WalletUtils; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.apache.commons.cli.CommandLine; 19 | import org.apache.commons.cli.CommandLineParser; 20 | import org.apache.commons.cli.DefaultParser; 21 | import org.apache.commons.cli.Option; 22 | import org.apache.commons.cli.Options; 23 | import org.apache.commons.lang3.StringUtils; 24 | import org.apache.commons.lang3.math.NumberUtils; 25 | 26 | import java.util.Arrays; 27 | import java.util.Set; 28 | 29 | /** 30 | * 命令行解析器 31 | * 32 | * @author chenhx 33 | * @version CLI.java, v 0.1 2018-10-15 下午 6:47 34 | */ 35 | 36 | @Slf4j 37 | public class CLI { 38 | 39 | private String[] args; 40 | private Options options = new Options(); 41 | 42 | public CLI(String[] args) { 43 | this.args = args; 44 | 45 | Option helpCmd = Option.builder("h").desc("show help").build(); 46 | options.addOption(helpCmd); 47 | 48 | Option address = Option.builder("address").hasArg(true).desc("Source wallet address").build(); 49 | Option sendFrom = Option.builder("from").hasArg(true).desc("Source wallet address").build(); 50 | Option sendTo = Option.builder("to").hasArg(true).desc("Destination wallet address").build(); 51 | Option sendAmount = Option.builder("amount").hasArg(true).desc("Amount to send").build(); 52 | 53 | options.addOption(address); 54 | options.addOption(sendFrom); 55 | options.addOption(sendTo); 56 | options.addOption(sendAmount); 57 | } 58 | 59 | public static void main(String[] args) { 60 | CLI cli = new CLI(args); 61 | cli.parse(); 62 | } 63 | 64 | /** 65 | * 命令行解析入口 66 | */ 67 | public void parse() { 68 | this.validateArgs(args); 69 | try { 70 | CommandLineParser parser = new DefaultParser(); 71 | CommandLine cmd = parser.parse(options, args); 72 | switch (args[0]) { 73 | case "createblockchain": 74 | //创建区块 75 | String createblockchainAddress = cmd.getOptionValue("address"); 76 | if (StringUtils.isBlank(createblockchainAddress)) { 77 | help(); 78 | } 79 | this.createBlockchain(createblockchainAddress); 80 | break; 81 | case "mining": 82 | //创建区块 83 | String createblockAddress = cmd.getOptionValue("address"); 84 | if (StringUtils.isBlank(createblockAddress)) { 85 | help(); 86 | } 87 | this.mining(createblockAddress); 88 | break; 89 | case "getbalance": 90 | String getBalanceAddress = cmd.getOptionValue("address"); 91 | if (StringUtils.isBlank(getBalanceAddress)) { 92 | help(); 93 | } 94 | this.getBalance(getBalanceAddress); 95 | break; 96 | case "send": 97 | String sendFrom = cmd.getOptionValue("from"); 98 | String sendTo = cmd.getOptionValue("to"); 99 | String sendAmount = cmd.getOptionValue("amount"); 100 | if (StringUtils.isBlank(sendFrom) || 101 | StringUtils.isBlank(sendTo) || 102 | !NumberUtils.isDigits(sendAmount)) { 103 | help(); 104 | } 105 | this.send(sendFrom, sendTo, Integer.valueOf(sendAmount)); 106 | break; 107 | case "createwallet": 108 | this.createWallet(); 109 | break; 110 | case "printaddresses": 111 | this.printAddresses(); 112 | break; 113 | case "printchain": 114 | this.printChain(); 115 | break; 116 | case "h": 117 | this.help(); 118 | break; 119 | default: 120 | this.help(); 121 | } 122 | } catch (Exception e) { 123 | log.error("Fail to parse cli command ! ", e); 124 | } finally { 125 | RocksDBUtils.getInstance().closeDB(); 126 | } 127 | } 128 | 129 | /** 130 | * 挖矿 131 | */ 132 | private void mining(String address) { 133 | // 检查钱包地址是否合法 134 | try { 135 | Base58Check.base58ToBytes(address); 136 | } catch (Exception e) { 137 | log.error("ERROR: sender address invalid ! address=" + address, e); 138 | throw new RuntimeException("ERROR: sender address invalid ! address=" + address, e); 139 | } 140 | Blockchain blockchain = Blockchain.createBlockchain(address); 141 | //挖矿奖励 142 | Transaction rewardTX = Transaction.newCoinbaseTX(address, ""); 143 | //TODO 交易需要发送到其他的节点上去 还需要获取其他的交易 144 | Block block = blockchain.mineBlock(new Transaction[]{rewardTX}); 145 | new UTXOSet(blockchain).update(block); 146 | log.info("Done ! "); 147 | } 148 | 149 | /** 150 | * 创建区块链 151 | * 152 | * @param address 153 | */ 154 | private void createBlockchain(String address) { 155 | Blockchain blockchain = Blockchain.createBlockchain(address); 156 | UTXOSet utxoSet = new UTXOSet(blockchain); 157 | utxoSet.reIndex(); 158 | log.info("Done ! "); 159 | } 160 | 161 | /** 162 | * 验证入参 163 | * 164 | * @param args 165 | */ 166 | private void validateArgs(String[] args) { 167 | if (args == null || args.length < 1) { 168 | help(); 169 | } 170 | } 171 | 172 | /** 173 | * 创建钱包 174 | * 175 | * @throws Exception 176 | */ 177 | private void createWallet() { 178 | Wallet wallet = WalletUtils.getInstance().createWallet(); 179 | log.info("wallet address : " + wallet.getAddress()); 180 | } 181 | 182 | /** 183 | * 打印钱包地址 184 | */ 185 | private void printAddresses() { 186 | Set addresses = WalletUtils.getInstance().getAddresses(); 187 | if (addresses == null || addresses.isEmpty()) { 188 | log.info("There isn't address"); 189 | return; 190 | } 191 | for (String address : addresses) { 192 | log.info("Wallet address: " + address); 193 | } 194 | } 195 | 196 | /** 197 | * 查询钱包余额 198 | * 199 | * @param address 钱包地址 200 | */ 201 | private void getBalance(String address) { 202 | // 检查钱包地址是否合法 203 | try { 204 | Base58Check.base58ToBytes(address); 205 | } catch (Exception e) { 206 | log.error("ERROR: invalid wallet address", e); 207 | throw new RuntimeException("ERROR: invalid wallet address", e); 208 | } 209 | 210 | // 得到公钥Hash值 211 | byte[] versionedPayload = Base58Check.base58ToBytes(address); 212 | byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length); 213 | 214 | Blockchain blockchain = Blockchain.createBlockchain(address); 215 | UTXOSet utxoSet = new UTXOSet(blockchain); 216 | 217 | TXOutput[] txOutputs = utxoSet.findUTXOs(pubKeyHash); 218 | int balance = 0; 219 | if (txOutputs != null && txOutputs.length > 0) { 220 | for (TXOutput txOutput : txOutputs) { 221 | balance += txOutput.getValue(); 222 | } 223 | } 224 | log.info("Balance of '{}': {}\n", new Object[]{address, balance}); 225 | } 226 | 227 | /** 228 | * 转账 229 | * 230 | * @param from 231 | * @param to 232 | * @param amount 233 | * @throws Exception 234 | */ 235 | private void send(String from, String to, int amount) throws Exception { 236 | // 检查钱包地址是否合法 237 | try { 238 | Base58Check.base58ToBytes(from); 239 | } catch (Exception e) { 240 | log.error("ERROR: sender address invalid ! address=" + from, e); 241 | throw new RuntimeException("ERROR: sender address invalid ! address=" + from, e); 242 | } 243 | // 检查钱包地址是否合法 244 | try { 245 | Base58Check.base58ToBytes(to); 246 | } catch (Exception e) { 247 | log.error("ERROR: receiver address invalid ! address=" + to, e); 248 | throw new RuntimeException("ERROR: receiver address invalid ! address=" + to, e); 249 | } 250 | if (amount < 1) { 251 | log.error("ERROR: amount invalid ! amount=" + amount); 252 | throw new RuntimeException("ERROR: amount invalid ! amount=" + amount); 253 | } 254 | Blockchain blockchain = Blockchain.createBlockchain(from); 255 | // 新交易 256 | Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain); 257 | // 奖励 258 | Transaction rewardTx = Transaction.newCoinbaseTX(from, ""); 259 | Block newBlock = blockchain.mineBlock(new Transaction[]{transaction, rewardTx}); 260 | new UTXOSet(blockchain).update(newBlock); 261 | log.info("Success!"); 262 | } 263 | 264 | /** 265 | * 打印帮助信息 266 | */ 267 | private void help() { 268 | System.out.println("Usage:"); 269 | System.out.println(" createwallet - 生成一个新的密钥对并将其保存到钱包文件中"); 270 | System.out.println(" printaddresses - 打印所有钱包地址"); 271 | System.out.println(" getbalance -address ADDRESS - 获取地址的余额"); 272 | System.out.println(" createblockchain -address ADDRESS - 创建一个区块链,并向指定地址发送创世奖励"); 273 | System.out.println(" printchain - 打印区块链的所有块"); 274 | System.out.println(" send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO"); 275 | System.exit(0); 276 | } 277 | 278 | /** 279 | * 打印出区块链中的所有区块 280 | */ 281 | private void printChain() { 282 | Blockchain blockchain = Blockchain.initBlockchainFromDB(); 283 | for (Blockchain.BlockchainIterator iterator = blockchain.getBlockchainIterator(); iterator.hashNext(); ) { 284 | Block block = iterator.next(); 285 | if (block != null) { 286 | boolean validate = ProofOfWork.newProofOfWork(block).validate(); 287 | log.info(block.toString() + ", validate = " + validate); 288 | } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.controller; 6 | 7 | import com.uifuture.springbootblockchain.block.Block; 8 | import com.uifuture.springbootblockchain.block.Blockchain; 9 | import org.json.JSONObject; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.BufferedReader; 16 | import java.io.IOException; 17 | import java.io.PrintWriter; 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | /** 24 | * @author chenhx 25 | * @version IndexController.java, v 0.1 2018-10-13 下午 4:21 26 | */ 27 | @Controller 28 | @RequestMapping("ui") 29 | public class IndexController { 30 | 31 | /** 32 | * 该Servlet用于输出整个区块链的数据 33 | * @param req 34 | * @param resp 35 | * @throws IOException 36 | */ 37 | @RequestMapping("chain") 38 | public void chain(HttpServletRequest req, HttpServletResponse resp) throws IOException { 39 | Blockchain blockChain = Blockchain.initBlockchainFromDB(); 40 | Map response = new HashMap(); 41 | List blocks = new ArrayList<>(); 42 | while (blockChain.getBlockchainIterator().hashNext()){ 43 | blocks.add(blockChain.getBlockchainIterator().next()); 44 | } 45 | response.put("blocks", blocks); 46 | response.put("length", blockChain.getAllBlockHash().size()); 47 | 48 | JSONObject jsonResponse = new JSONObject(response); 49 | resp.setContentType("application/json"); 50 | PrintWriter printWriter = resp.getWriter(); 51 | printWriter.println(jsonResponse); 52 | printWriter.close(); 53 | } 54 | 55 | /** 56 | * 该Servlet用于接收并处理新的交易信息 57 | * @param req 58 | * @param resp 59 | * @throws IOException 60 | */ 61 | @RequestMapping("transactions/new") 62 | public void transactionsNew(HttpServletRequest req, HttpServletResponse resp) throws IOException { 63 | 64 | req.setCharacterEncoding("utf-8"); 65 | // 读取客户端传递过来的数据并转换成JSON格式 66 | BufferedReader reader = req.getReader(); 67 | String input = null; 68 | StringBuffer requestBody = new StringBuffer(); 69 | while ((input = reader.readLine()) != null) { 70 | requestBody.append(input); 71 | } 72 | JSONObject jsonValues = new JSONObject(requestBody.toString()); 73 | 74 | // 检查所需要的字段是否位于POST的data中 75 | String[] required = { "sender", "recipient", "amount" }; 76 | for (String string : required) { 77 | if (!jsonValues.has(string)) { 78 | // 如果没有需要的字段就返回错误信息 79 | resp.sendError(400, "Missing values"); 80 | } 81 | } 82 | 83 | // 新建交易信息 84 | // Blockchain blockChain = Blockchain.getInstance(); 85 | // int index = blockChain.newTransactions(jsonValues.getString("sender"), jsonValues.getString("recipient"), 86 | // jsonValues.getLong("amount")); 87 | int index = 1; 88 | 89 | // 返回json格式的数据给客户端 90 | resp.setContentType("application/json"); 91 | PrintWriter printWriter = resp.getWriter(); 92 | printWriter.println(new JSONObject().append("message", "Transaction will be added to Block " + index)); 93 | printWriter.close(); 94 | } 95 | 96 | /** 97 | * 该Servlet用于运行工作算法的证明来获得下一个证明,也就是所谓的挖矿 98 | * @param req 99 | * @param resp 100 | * @throws IOException 101 | */ 102 | @RequestMapping("mine") 103 | public void mine(HttpServletRequest req, HttpServletResponse resp) throws IOException { 104 | // Blockchain blockChain = Blockchain.getInstance(); 105 | // Map lastBlock = blockChain.lastBlock(); 106 | // long lastProof = Long.parseLong(lastBlock.get("proof") + ""); 107 | // long proof = blockChain.proofOfWork(lastProof); 108 | // 109 | // // 给工作量证明的节点提供奖励,发送者为 "0" 表明是新挖出的币 110 | // String uuid = (String) req.getServletContext().getAttribute("uuid"); 111 | // blockChain.newTransactions("0", uuid, 1); 112 | // 113 | // // 构建新的区块 114 | // Map newBlock = blockChain.newBlock(proof, null); 115 | // Map response = new HashMap<>(); 116 | // response.put("message", "New Block Forged"); 117 | // response.put("index", newBlock.get("index")); 118 | // response.put("transactions", newBlock.get("transactions")); 119 | // response.put("proof", newBlock.get("proof")); 120 | // response.put("previous_hash", newBlock.get("previous_hash")); 121 | // 122 | // // 返回新区块的数据给客户端 123 | // resp.setContentType("application/json"); 124 | // PrintWriter printWriter = resp.getWriter(); 125 | // printWriter.println(new JSONObject(response)); 126 | // printWriter.close(); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/pow/PowResult.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.pow; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.math.BigInteger; 12 | 13 | /** 14 | * 工作量计算结果 15 | * @author chenhx 16 | * @version PowResult.java, v 0.1 2018-10-13 下午 4:50 17 | */ 18 | @Data 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | public class PowResult { 22 | 23 | /** 24 | * 计数器 25 | */ 26 | private BigInteger nonce; 27 | /** 28 | * hash值 29 | */ 30 | private String hash; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/pow/ProofOfWork.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.pow; 6 | 7 | import com.uifuture.springbootblockchain.block.Block; 8 | import com.uifuture.springbootblockchain.util.ByteUtils; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.codec.digest.DigestUtils; 14 | import org.apache.commons.lang3.StringUtils; 15 | 16 | import java.math.BigInteger; 17 | 18 | /** 19 | * 工作量证明 20 | * @author chenhx 21 | * @version ProofOfWork.java, v 0.1 2018-10-13 下午 4:50 22 | */ 23 | 24 | @Data 25 | @AllArgsConstructor 26 | @NoArgsConstructor 27 | @Slf4j 28 | public class ProofOfWork { 29 | private static final int INIT_MAX = 15; 30 | /** 31 | * 目标最大值 32 | */ 33 | private static final BigInteger TARGET_MAX = BigInteger.valueOf(1).shiftLeft((256 - INIT_MAX)); 34 | 35 | /** 36 | * 难度目标 初始为1 37 | * 固定一个时间点,多久调整一次 38 | * 暂定为2018个区块调整一次 39 | */ 40 | public static BigInteger difficulty = BigInteger.valueOf(1); 41 | /** 42 | * 区块 43 | */ 44 | private Block block; 45 | /** 46 | * 当前目标值 47 | */ 48 | private BigInteger target; 49 | 50 | /** 51 | * 创建新的工作量证明,设定难度目标值 52 | *

53 | * 对1进行移位运算,将1向左移动 (256 - difficulty) 位,得到我们的难度目标值 54 | * 55 | * @param block 56 | * @return 57 | */ 58 | public static ProofOfWork newProofOfWork(Block block) { 59 | BigInteger targetValue = TARGET_MAX.divide(difficulty); 60 | return new ProofOfWork(block, targetValue); 61 | } 62 | 63 | /** 64 | * 运行工作量证明,开始挖矿,找到小于难度目标值的Hash 65 | * @return 66 | */ 67 | public PowResult run() { 68 | BigInteger nonce = new BigInteger("0"); 69 | String shaHex = ""; 70 | this.block.setTarget(target); 71 | long startTime = System.currentTimeMillis(); 72 | while (nonce.compareTo(TARGET_MAX) < 0) { 73 | this.block.setNonce(nonce); 74 | byte[] data = this.prepareData(nonce); 75 | shaHex = DigestUtils.sha256Hex(data); 76 | if (new BigInteger(shaHex, 16).compareTo(this.target) < 0) { 77 | log.info("花费时间: {}秒", (float) (System.currentTimeMillis() - startTime) / 1000); 78 | log.info("正确的Hash值: {}", shaHex); 79 | break; 80 | } else { 81 | log.info("当前运算量: {}", nonce); 82 | nonce = nonce.add(new BigInteger("1")); 83 | } 84 | } 85 | return new PowResult(nonce, shaHex); 86 | } 87 | 88 | /** 89 | * 验证区块是否有效 90 | * 91 | * @return 92 | */ 93 | public boolean validate() { 94 | byte[] data = this.prepareData(this.getBlock().getNonce()); 95 | return new BigInteger(DigestUtils.sha256Hex(data), 16).compareTo(this.target) < 0; 96 | } 97 | 98 | /** 99 | * 准备数据 100 | *

101 | * 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换 102 | * @param nonce 103 | * @return 104 | */ 105 | private byte[] prepareData(BigInteger nonce) { 106 | byte[] prevBlockHashBytes = {}; 107 | if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) { 108 | prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray(); 109 | } 110 | return ByteUtils.merge( 111 | prevBlockHashBytes, 112 | ByteUtils.hexStringToByte(this.getBlock().getTarget().toString(16)), 113 | ByteUtils.hexStringToByte(this.getBlock().getMerkleRoot()), 114 | ByteUtils.toBytes(this.getBlock().getTimeStamp()), 115 | ByteUtils.toBytes(this.getBlock().getHeight()), 116 | ByteUtils.hexStringToByte(nonce.toString(16)) 117 | ); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/transaction/MerkleTree.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.transaction; 6 | 7 | import com.google.common.collect.Lists; 8 | import com.uifuture.springbootblockchain.util.ByteUtils; 9 | import lombok.Data; 10 | import org.apache.commons.codec.digest.DigestUtils; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * 默克尔树 16 | * 17 | * @author chenhx 18 | * @version MerkleTree.java, v 0.1 2018-10-16 下午 6:06 19 | */ 20 | 21 | @Data 22 | public class MerkleTree { 23 | 24 | /** 25 | * 根节点 26 | */ 27 | private Node root; 28 | /** 29 | * 叶子节点Hash 30 | */ 31 | private byte[][] leafHashes; 32 | 33 | public MerkleTree(byte[][] leafHashes) { 34 | constructTree(leafHashes); 35 | } 36 | 37 | /** 38 | * 构建叶子节点 39 | * 40 | * @param hash 41 | * @return 42 | */ 43 | private static Node constructLeafNode(byte[] hash) { 44 | Node leaf = new Node(); 45 | leaf.hash = hash; 46 | return leaf; 47 | } 48 | 49 | /** 50 | * 从底部叶子节点开始往上构建整个Merkle Tree 51 | * @param leafHashes 52 | */ 53 | private void constructTree(byte[][] leafHashes) { 54 | if (leafHashes == null || leafHashes.length < 1) { 55 | throw new RuntimeException("ERROR:Fail to construct merkle tree ! leafHashes data invalid ! "); 56 | } 57 | this.leafHashes = leafHashes; 58 | List parents = bottomLevel(leafHashes); 59 | while (parents.size() > 1) { 60 | parents = internalLevel(parents); 61 | } 62 | root = parents.get(0); 63 | } 64 | 65 | /** 66 | * 构建一个层级节点 67 | * 68 | * @param children 69 | * @return 70 | */ 71 | private List internalLevel(List children) { 72 | List parents = Lists.newArrayListWithCapacity(children.size() / 2); 73 | for (int i = 0; i < children.size() - 1; i += 2) { 74 | Node child1 = children.get(i); 75 | Node child2 = children.get(i + 1); 76 | 77 | Node parent = constructInternalNode(child1, child2); 78 | parents.add(parent); 79 | } 80 | // 内部节点奇数个,只对left节点进行计算 81 | if (children.size() % 2 != 0) { 82 | Node child = children.get(children.size() - 1); 83 | Node parent = constructInternalNode(child, null); 84 | parents.add(parent); 85 | } 86 | return parents; 87 | } 88 | 89 | /** 90 | * 底部节点构建 91 | * 92 | * @param hashes 93 | * @return 94 | */ 95 | private List bottomLevel(byte[][] hashes) { 96 | List parents = Lists.newArrayListWithCapacity(hashes.length / 2); 97 | 98 | for (int i = 0; i < hashes.length - 1; i += 2) { 99 | Node leaf1 = constructLeafNode(hashes[i]); 100 | Node leaf2 = constructLeafNode(hashes[i + 1]); 101 | 102 | Node parent = constructInternalNode(leaf1, leaf2); 103 | parents.add(parent); 104 | } 105 | 106 | if (hashes.length % 2 != 0) { 107 | Node leaf = constructLeafNode(hashes[hashes.length - 1]); 108 | // 奇数个节点的情况,复制最后一个节点 109 | Node parent = constructInternalNode(leaf, leaf); 110 | parents.add(parent); 111 | } 112 | 113 | return parents; 114 | } 115 | 116 | /** 117 | * 构建内部节点 118 | * 119 | * @param leftChild 120 | * @param rightChild 121 | * @return 122 | */ 123 | private Node constructInternalNode(Node leftChild, Node rightChild) { 124 | Node parent = new Node(); 125 | if (rightChild == null) { 126 | parent.hash = leftChild.hash; 127 | } else { 128 | parent.hash = internalHash(leftChild.hash, rightChild.hash); 129 | } 130 | parent.left = leftChild; 131 | parent.right = rightChild; 132 | return parent; 133 | } 134 | 135 | /** 136 | * 计算内部节点Hash 137 | * 138 | * @param leftChildHash 139 | * @param rightChildHash 140 | * @return 141 | */ 142 | private byte[] internalHash(byte[] leftChildHash, byte[] rightChildHash) { 143 | byte[] mergedBytes = ByteUtils.merge(leftChildHash, rightChildHash); 144 | return DigestUtils.sha256(mergedBytes); 145 | } 146 | 147 | /** 148 | * Merkle Tree节点 149 | */ 150 | @Data 151 | public static class Node { 152 | private byte[] hash; 153 | private Node left; 154 | private Node right; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/transaction/SpendableOutputResult.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.transaction; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.util.Map; 12 | 13 | /** 14 | * 查询结果 15 | * @author chenhx 16 | * @version SpendableOutputResult.java, v 0.1 2018-10-15 下午 6:51 17 | */ 18 | @Data 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | public class SpendableOutputResult { 22 | 23 | /** 24 | * 交易时的支付金额 25 | */ 26 | private int accumulated; 27 | /** 28 | * 未花费的交易 29 | */ 30 | private Map unspentOuts; 31 | 32 | } -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/transaction/TXInput.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.transaction; 6 | 7 | import com.uifuture.springbootblockchain.util.BtcAddressUtils; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.util.Arrays; 13 | 14 | /** 15 | * 交易输入 16 | * 17 | * @author chenhx 18 | * @version TXInput.java, v 0.1 2018-10-15 下午 6:23 19 | */ 20 | @Data 21 | public class TXInput { 22 | 23 | /** 24 | * 交易Id的hash值 25 | * 包含了它所指向的UTXO的交易的Hash值。 26 | */ 27 | private byte[] txId; 28 | private String txIdStr; 29 | /** 30 | * 交易输出索引 31 | * 定义了它所指向的UTXO在上一笔交易中交易输出数组的位置。 32 | */ 33 | private int txOutputIndex; 34 | /** 35 | * 签名 36 | */ 37 | private byte[] signature; 38 | private String signatureStr; 39 | /** 40 | * 公钥 41 | */ 42 | private byte[] pubKey; 43 | private String pubKeyStr; 44 | 45 | public TXInput() { 46 | } 47 | 48 | public TXInput(byte[] txId, int txOutputIndex, byte[] signature, byte[] pubKey) { 49 | this.txId = txId; 50 | this.txOutputIndex = txOutputIndex; 51 | this.signature = signature; 52 | this.pubKey = pubKey; 53 | } 54 | 55 | /** 56 | * 检查公钥hash是否用于交易输入 57 | * 58 | * @param pubKeyHash 59 | * @return 60 | */ 61 | public boolean usesKey(byte[] pubKeyHash) { 62 | byte[] lockingHash = BtcAddressUtils.ripeMD160Hash(this.getPubKey()); 63 | return Arrays.equals(lockingHash, pubKeyHash); 64 | } 65 | 66 | public String getTxIdStr() { 67 | if(txId!=null){ 68 | txIdStr = Arrays.toString(txId); 69 | } 70 | return txIdStr; 71 | } 72 | 73 | public String getSignatureStr() { 74 | if(signatureStr!=null){ 75 | signatureStr = Arrays.toString(signature); 76 | } 77 | return signatureStr; 78 | } 79 | 80 | public String getPubKeyStr() { 81 | if(pubKey!=null){ 82 | pubKeyStr = Arrays.toString(pubKey); 83 | } 84 | return pubKeyStr; 85 | } 86 | 87 | public static void main(String[] args) { 88 | byte[] pubKey = {84, 104, 101, 32, 84, 105, 109, 101, 115, 32, 50, 48, 50, 50, 45, 48, 54, 45, 48, 49, 32, 49, 55, 58, 49, 48, 58, 48, 55, 32, 51, 54, 52, 32, 67, 104, 97, 110, 99, 101, 108, 108, 111, 114, 32, 111, 110, 32, 98, 114, 105, 110, 107, 32, 111, 102, 32, 115, 101, 99, 111, 110, 100, 32, 98, 97, 105, 108, 111, 117, 116, 32, 102, 111, 114, 32, 98, 97, 110, 107, 115}; 89 | System.out.println(new String(pubKey)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/transaction/TXOutput.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.transaction; 6 | 7 | import com.uifuture.springbootblockchain.util.Base58Check; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.util.Arrays; 13 | 14 | /** 15 | * 交易输出 16 | * 17 | * @author chenhx 18 | * @version TXOutput.java, v 0.1 2018-10-15 下午 6:23 19 | */ 20 | @Data 21 | @AllArgsConstructor 22 | @NoArgsConstructor 23 | public class TXOutput { 24 | 25 | /** 26 | * 数值 27 | */ 28 | private int value; 29 | /** 30 | * 公钥Hash 31 | */ 32 | private byte[] pubKeyHash; 33 | 34 | /** 35 | * 创建交易输出 36 | * 37 | * @param value 38 | * @param address 39 | * @return 40 | */ 41 | public static TXOutput newTXOutput(int value, String address) { 42 | // 反向转化为 byte 数组 43 | byte[] versionedPayload = Base58Check.base58ToBytes(address); 44 | byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length); 45 | return new TXOutput(value, pubKeyHash); 46 | } 47 | 48 | /** 49 | * 检查交易输出是否能够使用指定的公钥 50 | * 51 | * @param pubKeyHash 52 | * @return 53 | */ 54 | public boolean isLockedWithKey(byte[] pubKeyHash) { 55 | return Arrays.equals(this.getPubKeyHash(), pubKeyHash); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/transaction/Transaction.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.transaction; 6 | 7 | import com.uifuture.springbootblockchain.bd.RocksDBUtils; 8 | import com.uifuture.springbootblockchain.block.Blockchain; 9 | import com.uifuture.springbootblockchain.util.BtcAddressUtils; 10 | import com.uifuture.springbootblockchain.util.SerializeUtils; 11 | import com.uifuture.springbootblockchain.wallet.Wallet; 12 | import com.uifuture.springbootblockchain.wallet.WalletUtils; 13 | import lombok.AllArgsConstructor; 14 | import lombok.Data; 15 | import lombok.NoArgsConstructor; 16 | import lombok.extern.slf4j.Slf4j; 17 | import org.apache.commons.codec.DecoderException; 18 | import org.apache.commons.codec.binary.Hex; 19 | import org.apache.commons.codec.digest.DigestUtils; 20 | import org.apache.commons.lang3.ArrayUtils; 21 | import org.apache.commons.lang3.StringUtils; 22 | import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; 23 | import org.bouncycastle.jce.ECNamedCurveTable; 24 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 25 | import org.bouncycastle.jce.spec.ECParameterSpec; 26 | import org.bouncycastle.jce.spec.ECPublicKeySpec; 27 | import org.bouncycastle.math.ec.ECPoint; 28 | 29 | import java.math.BigInteger; 30 | import java.nio.charset.StandardCharsets; 31 | import java.security.KeyFactory; 32 | import java.security.PublicKey; 33 | import java.security.Security; 34 | import java.security.Signature; 35 | import java.util.Arrays; 36 | import java.util.Iterator; 37 | import java.util.Map; 38 | /** 39 | * 交易 40 | * 41 | * @author chenhx 42 | * @version Transaction.java, v 0.1 2018-10-15 下午 6:22 43 | */ 44 | @Data 45 | @AllArgsConstructor 46 | @NoArgsConstructor 47 | @Slf4j 48 | public class Transaction { 49 | private static final Integer OUT_PUT_VALUE = 210000; 50 | /** 51 | * 100 UB 创世奖励再加100UB 52 | */ 53 | private static final Integer CREATION_VALUE = 1000000 * 100; 54 | /** 55 | * 交易的Hash 56 | */ 57 | private byte[] txId; 58 | /** 59 | * 交易输入 60 | */ 61 | private TXInput[] inputs; 62 | /** 63 | * 交易输出 64 | */ 65 | private TXOutput[] outputs; 66 | /** 67 | * 创建日期 68 | */ 69 | private long createTime; 70 | 71 | /** 72 | * 创建CoinBase交易 73 | * 74 | * @param to 收账的钱包地址 75 | * @param data 解锁脚本数据 76 | * @return 77 | */ 78 | public static Transaction newCoinbaseTX(String to, String data) { 79 | if (StringUtils.isBlank(data)) { 80 | data = String.format("Reward to '%s'", to); 81 | } 82 | // 创建交易输入 83 | TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes(StandardCharsets.UTF_8)); 84 | //获取当前区块大小 85 | Integer size = RocksDBUtils.getInstance().getChainstateBucket().size(); 86 | Integer multiple = size / OUT_PUT_VALUE + 1; 87 | Integer value = CREATION_VALUE / multiple; 88 | if (size == 0) { 89 | //创世区块 90 | value = value * 2; 91 | } 92 | 93 | // 创建交易输出 94 | TXOutput txOutput = TXOutput.newTXOutput(value, to); 95 | // 创建交易 96 | Transaction tx = new Transaction(null, new TXInput[]{txInput}, 97 | new TXOutput[]{txOutput}, System.currentTimeMillis()); 98 | // 设置交易ID 99 | tx.setTxId(tx.hash()); 100 | return tx; 101 | } 102 | 103 | /** 104 | * 创建 交易 挖矿奖励 105 | * 106 | * @param to 收账的钱包地址 107 | * @return 108 | */ 109 | public static Transaction newRewardTX(String to, Blockchain blockchain) throws DecoderException { 110 | //获取当前区块大小 111 | Integer size = RocksDBUtils.getInstance().getChainstateBucket().size(); 112 | Integer multiple = size / OUT_PUT_VALUE + 1; 113 | Integer value = CREATION_VALUE / multiple; 114 | if (size == 0) { 115 | //创世区块 116 | value = value * 2; 117 | } 118 | 119 | String data = String.format("Reward to '%s'", to); 120 | // 创建交易输入 121 | TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes(StandardCharsets.UTF_8)); 122 | 123 | // 创建交易输出 124 | TXOutput txOutput = TXOutput.newTXOutput(value, to); 125 | // 创建交易 126 | Transaction tx = new Transaction(null, new TXInput[]{txInput}, 127 | new TXOutput[]{txOutput}, System.currentTimeMillis()); 128 | // 设置交易ID 129 | tx.setTxId(tx.hash()); 130 | return tx; 131 | } 132 | 133 | /** 134 | * 从 from 向 to 支付一定的 amount 的金额 135 | * 136 | * @param from 支付钱包地址 137 | * @param to 收款钱包地址 138 | * @param amount 交易金额 139 | * @param blockchain 区块链 140 | * @return 141 | */ 142 | public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception { 143 | // 获取钱包 144 | Wallet senderWallet = WalletUtils.getInstance().getWallet(from); 145 | byte[] pubKey = senderWallet.getPublicKey(); 146 | byte[] pubKeyHash = BtcAddressUtils.ripeMD160Hash(pubKey); 147 | 148 | SpendableOutputResult result = new UTXOSet(blockchain).findSpendableOutputs(pubKeyHash, amount); 149 | int accumulated = result.getAccumulated(); 150 | Map unspentOuts = result.getUnspentOuts(); 151 | 152 | if (accumulated < amount) { 153 | log.error("ERROR: Not enough funds ! accumulated=" + accumulated + ", amount=" + amount); 154 | throw new RuntimeException("ERROR: Not enough funds ! "); 155 | } 156 | Iterator> iterator = unspentOuts.entrySet().iterator(); 157 | 158 | TXInput[] txInputs = {}; 159 | while (iterator.hasNext()) { 160 | Map.Entry entry = iterator.next(); 161 | String txIdStr = entry.getKey(); 162 | int[] outIds = entry.getValue(); 163 | byte[] txId = Hex.decodeHex(txIdStr); 164 | for (int outIndex : outIds) { 165 | txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, null, pubKey)); 166 | } 167 | } 168 | 169 | TXOutput[] txOutput = {}; 170 | txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput(amount, to)); 171 | if (accumulated > amount) { 172 | txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput((accumulated - amount), from)); 173 | } 174 | 175 | Transaction newTx = new Transaction(null, txInputs, txOutput, System.currentTimeMillis()); 176 | newTx.setTxId(newTx.hash()); 177 | 178 | // 进行交易签名 179 | blockchain.signTransaction(newTx, senderWallet.getPrivateKey()); 180 | 181 | return newTx; 182 | } 183 | 184 | /** 185 | * 计算交易信息的Hash值 186 | * 187 | * @return 188 | */ 189 | public byte[] hash() { 190 | // 使用序列化的方式对Transaction对象进行深度复制 191 | byte[] serializeBytes = SerializeUtils.serialize(this); 192 | Transaction copyTx = (Transaction) SerializeUtils.deserialize(serializeBytes); 193 | copyTx.setTxId(new byte[]{}); 194 | return DigestUtils.sha256(SerializeUtils.serialize(copyTx)); 195 | } 196 | 197 | /** 198 | * 是否为 Coinbase 交易 199 | * 200 | * @return 201 | */ 202 | public boolean isCoinbase() { 203 | return this.getInputs().length == 1 204 | && this.getInputs()[0].getTxId().length == 0 205 | && this.getInputs()[0].getTxOutputIndex() == -1; 206 | } 207 | 208 | /** 209 | * 创建用于签名的交易数据副本,交易输入的 signature 和 pubKey 需要设置为null 210 | * 211 | * @return 212 | */ 213 | public Transaction trimmedCopy() { 214 | TXInput[] tmpTXInputs = new TXInput[this.getInputs().length]; 215 | for (int i = 0; i < this.getInputs().length; i++) { 216 | TXInput txInput = this.getInputs()[i]; 217 | tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null); 218 | } 219 | 220 | TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length]; 221 | for (int i = 0; i < this.getOutputs().length; i++) { 222 | TXOutput txOutput = this.getOutputs()[i]; 223 | tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash()); 224 | } 225 | 226 | return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs, this.getCreateTime()); 227 | } 228 | 229 | 230 | /** 231 | * 签名 232 | * 233 | * @param privateKey 私钥 234 | * @param prevTxMap 前面多笔交易集合 235 | */ 236 | public void sign(BCECPrivateKey privateKey, Map prevTxMap) throws Exception { 237 | // coinbase 交易信息不需要签名,因为它不存在交易输入信息 238 | if (this.isCoinbase()) { 239 | return; 240 | } 241 | // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据 242 | for (TXInput txInput : this.getInputs()) { 243 | if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { 244 | throw new RuntimeException("ERROR: Previous transaction is not correct"); 245 | } 246 | } 247 | 248 | // 创建用于签名的交易信息的副本 249 | Transaction txCopy = this.trimmedCopy(); 250 | 251 | Security.addProvider(new BouncyCastleProvider()); 252 | Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); 253 | ecdsaSign.initSign(privateKey); 254 | 255 | for (int i = 0; i < txCopy.getInputs().length; i++) { 256 | TXInput txInputCopy = txCopy.getInputs()[i]; 257 | // 获取交易输入TxID对应的交易数据 258 | Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId())); 259 | // 获取交易输入所对应的上一笔交易中的交易输出 260 | TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; 261 | txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); 262 | txInputCopy.setSignature(null); 263 | // 得到要签名的数据,即交易ID 264 | txCopy.setTxId(txCopy.hash()); 265 | txInputCopy.setPubKey(null); 266 | 267 | // 对整个交易信息仅进行签名,即对交易ID进行签名 268 | ecdsaSign.update(txCopy.getTxId()); 269 | byte[] signature = ecdsaSign.sign(); 270 | 271 | // 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名 272 | // 注意是将得到的签名赋值给原交易信息中的交易输入 273 | this.getInputs()[i].setSignature(signature); 274 | } 275 | } 276 | 277 | 278 | /** 279 | * 验证交易信息 280 | * @param prevTxMap 前面多笔交易集合 281 | * @return 282 | */ 283 | public boolean verify(Map prevTxMap) throws Exception { 284 | // coinbase 交易信息不需要签名,也就无需验证 285 | if (this.isCoinbase()) { 286 | return true; 287 | } 288 | 289 | // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据 290 | for (TXInput txInput : this.getInputs()) { 291 | if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { 292 | throw new RuntimeException("ERROR: Previous transaction is not correct"); 293 | } 294 | } 295 | 296 | // 创建用于签名验证的交易信息的副本 297 | Transaction txCopy = this.trimmedCopy(); 298 | 299 | Security.addProvider(new BouncyCastleProvider()); 300 | ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1"); 301 | KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); 302 | Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); 303 | 304 | for (int i = 0; i < this.getInputs().length; i++) { 305 | TXInput txInput = this.getInputs()[i]; 306 | // 获取交易输入TxID对应的交易数据 307 | Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId())); 308 | // 获取交易输入所对应的上一笔交易中的交易输出 309 | TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; 310 | 311 | TXInput txInputCopy = txCopy.getInputs()[i]; 312 | txInputCopy.setSignature(null); 313 | txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); 314 | // 得到要签名的数据,即交易ID 315 | txCopy.setTxId(txCopy.hash()); 316 | txInputCopy.setPubKey(null); 317 | 318 | // 使用椭圆曲线 x,y 点去生成公钥Key 319 | BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33)); 320 | BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65)); 321 | ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y); 322 | 323 | ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters); 324 | PublicKey publicKey = keyFactory.generatePublic(keySpec); 325 | ecdsaVerify.initVerify(publicKey); 326 | ecdsaVerify.update(txCopy.getTxId()); 327 | if (!ecdsaVerify.verify(txInput.getSignature())) { 328 | return false; 329 | } 330 | } 331 | return true; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/transaction/UTXOSet.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.transaction; 6 | 7 | import com.google.common.collect.Maps; 8 | import com.uifuture.springbootblockchain.bd.RocksDBUtils; 9 | import com.uifuture.springbootblockchain.block.Block; 10 | import com.uifuture.springbootblockchain.block.Blockchain; 11 | import com.uifuture.springbootblockchain.util.SerializeUtils; 12 | import lombok.AllArgsConstructor; 13 | import lombok.NoArgsConstructor; 14 | import lombok.Synchronized; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.apache.commons.codec.binary.Hex; 17 | import org.apache.commons.lang3.ArrayUtils; 18 | 19 | import java.util.Map; 20 | 21 | /** 22 | * 未被花费的交易输出池 23 | * 24 | * @author chenhx 25 | * @version UTXOSet.java, v 0.1 2018-10-16 下午 6:07 26 | */ 27 | 28 | @NoArgsConstructor 29 | @AllArgsConstructor 30 | @Slf4j 31 | public class UTXOSet { 32 | 33 | private Blockchain blockchain; 34 | 35 | /** 36 | * 寻找能够花费的交易 37 | * 38 | * @param pubKeyHash 钱包公钥Hash 39 | * @param amount 花费金额 40 | */ 41 | public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) { 42 | Map unspentOuts = Maps.newHashMap(); 43 | int accumulated = 0; 44 | Map chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket(); 45 | for (Map.Entry entry : chainstateBucket.entrySet()) { 46 | String txId = entry.getKey(); 47 | TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(entry.getValue()); 48 | 49 | for (int outId = 0; outId < txOutputs.length; outId++) { 50 | TXOutput txOutput = txOutputs[outId]; 51 | if (txOutput.isLockedWithKey(pubKeyHash) && accumulated < amount) { 52 | accumulated += txOutput.getValue(); 53 | 54 | int[] outIds = unspentOuts.get(txId); 55 | if (outIds == null) { 56 | outIds = new int[]{outId}; 57 | } else { 58 | outIds = ArrayUtils.add(outIds, outId); 59 | } 60 | unspentOuts.put(txId, outIds); 61 | if (accumulated >= amount) { 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | return new SpendableOutputResult(accumulated, unspentOuts); 68 | } 69 | 70 | 71 | /** 72 | * 查找钱包地址对应的所有UTXO 73 | * 74 | * @param pubKeyHash 钱包公钥Hash 75 | * @return 76 | */ 77 | public TXOutput[] findUTXOs(byte[] pubKeyHash) { 78 | TXOutput[] utxos = {}; 79 | Map chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket(); 80 | if (chainstateBucket.isEmpty()) { 81 | return utxos; 82 | } 83 | for (byte[] value : chainstateBucket.values()) { 84 | TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(value); 85 | for (TXOutput txOutput : txOutputs) { 86 | if (txOutput.isLockedWithKey(pubKeyHash)) { 87 | utxos = ArrayUtils.add(utxos, txOutput); 88 | } 89 | } 90 | } 91 | return utxos; 92 | } 93 | 94 | /** 95 | * 重建 UTXO 池索引 96 | */ 97 | @Synchronized 98 | public void reIndex() { 99 | log.info("Start to reIndex UTXO set !"); 100 | RocksDBUtils.getInstance().cleanChainStateBucket(); 101 | Map allUTXOs = blockchain.findAllUTXOs(); 102 | Map allUTXOBytes = Maps.newHashMap(); 103 | for (Map.Entry entry : allUTXOs.entrySet()) { 104 | allUTXOBytes.put(entry.getKey(), SerializeUtils.serialize(entry.getValue())); 105 | } 106 | RocksDBUtils.getInstance().initAllUTXOs(allUTXOBytes); 107 | log.info("ReIndex UTXO set finished ! "); 108 | } 109 | 110 | /** 111 | * 更新UTXO池 112 | *

113 | * 当一个新的区块产生时,需要去做两件事情: 114 | * 1)从UTXO池中移除花费掉了的交易输出; 115 | * 2)保存新的未花费交易输出; 116 | * 117 | * @param tipBlock 最新的区块 118 | */ 119 | @Synchronized 120 | public void update(Block tipBlock) { 121 | if (tipBlock == null) { 122 | log.error("Fail to update UTXO set ! tipBlock is null !"); 123 | throw new RuntimeException("Fail to update UTXO set ! "); 124 | } 125 | for (Transaction transaction : tipBlock.getTransactions()) { 126 | 127 | // 根据交易输入排查出剩余未被使用的交易输出 128 | if (!transaction.isCoinbase()) { 129 | for (TXInput txInput : transaction.getInputs()) { 130 | // 余下未被使用的交易输出 131 | TXOutput[] remainderUTXOs = {}; 132 | String txId = Hex.encodeHexString(txInput.getTxId()); 133 | TXOutput[] txOutputs = RocksDBUtils.getInstance().getUTXOs(txId); 134 | 135 | if (txOutputs == null) { 136 | continue; 137 | } 138 | 139 | for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) { 140 | if (outIndex != txInput.getTxOutputIndex()) { 141 | remainderUTXOs = ArrayUtils.add(remainderUTXOs, txOutputs[outIndex]); 142 | } 143 | } 144 | 145 | // 没有剩余则删除,否则更新 146 | if (remainderUTXOs.length == 0) { 147 | RocksDBUtils.getInstance().deleteUTXOs(txId); 148 | } else { 149 | RocksDBUtils.getInstance().putUTXOs(txId, remainderUTXOs); 150 | } 151 | } 152 | } 153 | 154 | // 新的交易输出保存到DB中 155 | TXOutput[] txOutputs = transaction.getOutputs(); 156 | String txId = Hex.encodeHexString(transaction.getTxId()); 157 | RocksDBUtils.getInstance().putUTXOs(txId, txOutputs); 158 | } 159 | 160 | } 161 | 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/util/Base58Check.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.util; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.IOException; 9 | import java.math.BigInteger; 10 | import java.util.Arrays; 11 | 12 | /** 13 | * Base58 转化工具 14 | * 15 | * @author chenhx 16 | * @version Base58Check.java, v 0.1 2018-10-16 下午 6:08 17 | */ 18 | 19 | public final class Base58Check { 20 | 21 | private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; 22 | private static final BigInteger ALPHABET_SIZE = BigInteger.valueOf(ALPHABET.length()); 23 | 24 | 25 | private Base58Check() { 26 | } // Not instantiable 27 | 28 | /** 29 | * 添加校验码并转化为 Base58 字符串 30 | * 31 | * @param data 32 | * @return 33 | */ 34 | public static String bytesToBase58(byte[] data) { 35 | return rawBytesToBase58(addCheckHash(data)); 36 | } 37 | 38 | /** 39 | * 转化为 Base58 字符串 40 | * 41 | * @param data 42 | * @return 43 | */ 44 | public static String rawBytesToBase58(byte[] data) { 45 | // Convert to base-58 string 46 | StringBuilder sb = new StringBuilder(); 47 | BigInteger num = new BigInteger(1, data); 48 | while (num.signum() != 0) { 49 | BigInteger[] quotrem = num.divideAndRemainder(ALPHABET_SIZE); 50 | sb.append(ALPHABET.charAt(quotrem[1].intValue())); 51 | num = quotrem[0]; 52 | } 53 | 54 | // Add '1' characters for leading 0-value bytes 55 | for (int i = 0; i < data.length && data[i] == 0; i++) { 56 | sb.append(ALPHABET.charAt(0)); 57 | } 58 | return sb.reverse().toString(); 59 | } 60 | 61 | 62 | /*---- Class constants ----*/ 63 | 64 | /** 65 | * 添加校验码并返回待有校验码的原生数据 66 | * 67 | * @param data 68 | * @return 69 | */ 70 | static byte[] addCheckHash(byte[] data) { 71 | try { 72 | byte[] hash = Arrays.copyOf(BtcAddressUtils.doubleHash(data), 4); 73 | ByteArrayOutputStream buf = new ByteArrayOutputStream(); 74 | buf.write(data); 75 | buf.write(hash); 76 | return buf.toByteArray(); 77 | } catch (IOException e) { 78 | throw new AssertionError(e); 79 | } 80 | } 81 | 82 | /** 83 | * 将 Base58Check 字符串转化为 byte 数组,并校验其校验码 84 | * 返回的byte数组带有版本号,但不带有校验码 85 | * 86 | * @param s 87 | * @return 88 | */ 89 | public static byte[] base58ToBytes(String s) { 90 | byte[] concat = base58ToRawBytes(s); 91 | byte[] data = Arrays.copyOf(concat, concat.length - 4); 92 | byte[] hash = Arrays.copyOfRange(concat, concat.length - 4, concat.length); 93 | byte[] rehash = Arrays.copyOf(BtcAddressUtils.doubleHash(data), 4); 94 | if (!Arrays.equals(rehash, hash)) { 95 | throw new IllegalArgumentException("Checksum mismatch"); 96 | } 97 | return data; 98 | } 99 | 100 | 101 | /*---- Miscellaneous ----*/ 102 | 103 | /** 104 | * 将 Base58Check 字符串反转为 byte 数组 105 | * 106 | * @param s 107 | * @return 108 | */ 109 | static byte[] base58ToRawBytes(String s) { 110 | // Parse base-58 string 111 | BigInteger num = BigInteger.ZERO; 112 | for (int i = 0; i < s.length(); i++) { 113 | num = num.multiply(ALPHABET_SIZE); 114 | int digit = ALPHABET.indexOf(s.charAt(i)); 115 | if (digit == -1) { 116 | throw new IllegalArgumentException("Invalid character for Base58Check"); 117 | } 118 | num = num.add(BigInteger.valueOf(digit)); 119 | } 120 | // Strip possible leading zero due to mandatory sign bit 121 | byte[] b = num.toByteArray(); 122 | if (b[0] == 0) { 123 | b = Arrays.copyOfRange(b, 1, b.length); 124 | } 125 | try { 126 | // Convert leading '1' characters to leading 0-value bytes 127 | ByteArrayOutputStream buf = new ByteArrayOutputStream(); 128 | for (int i = 0; i < s.length() && s.charAt(i) == ALPHABET.charAt(0); i++) { 129 | buf.write(0); 130 | } 131 | buf.write(b); 132 | return buf.toByteArray(); 133 | } catch (IOException e) { 134 | throw new AssertionError(e); 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/util/BtcAddressUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.util; 6 | 7 | import org.apache.commons.codec.digest.DigestUtils; 8 | import org.bouncycastle.crypto.digests.RIPEMD160Digest; 9 | 10 | import java.util.Arrays; 11 | 12 | /** 13 | * 地址工具类 14 | * 15 | * @author chenhx 16 | * @version BtcAddressUtils.java, v 0.1 2018-10-16 下午 6:08 17 | */ 18 | 19 | public class BtcAddressUtils { 20 | 21 | /** 22 | * 双重Hash 23 | * 24 | * @param data 25 | * @return 26 | */ 27 | public static byte[] doubleHash(byte[] data) { 28 | return DigestUtils.sha256(DigestUtils.sha256(data)); 29 | } 30 | 31 | /** 32 | * 计算公钥的 RIPEMD160 Hash值 33 | * 34 | * @param pubKey 公钥 35 | * @return ipeMD160Hash(sha256 ( pubkey)) 36 | */ 37 | public static byte[] ripeMD160Hash(byte[] pubKey) { 38 | //1. 先对公钥做 sha256 处理 39 | byte[] shaHashedKey = DigestUtils.sha256(pubKey); 40 | RIPEMD160Digest ripemd160 = new RIPEMD160Digest(); 41 | ripemd160.update(shaHashedKey, 0, shaHashedKey.length); 42 | byte[] output = new byte[ripemd160.getDigestSize()]; 43 | ripemd160.doFinal(output, 0); 44 | return output; 45 | } 46 | 47 | /** 48 | * 生成公钥的校验码 49 | * 50 | * @param payload 51 | * @return 52 | */ 53 | public static byte[] checksum(byte[] payload) { 54 | return Arrays.copyOfRange(doubleHash(payload), 0, 4); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/util/ByteUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.util; 6 | 7 | import org.apache.commons.codec.binary.Hex; 8 | import org.apache.commons.lang3.ArrayUtils; 9 | 10 | import java.nio.ByteBuffer; 11 | import java.util.Arrays; 12 | import java.util.stream.Stream; 13 | 14 | /** 15 | * 字节数组工具类 16 | * @author chenhx 17 | * @version ByteUtils.java, v 0.1 2018-10-11 下午 9:17 18 | */ 19 | public class ByteUtils { 20 | 21 | public static final byte[] EMPTY_ARRAY = new byte[0]; 22 | 23 | public static final byte[] EMPTY_BYTES = new byte[32]; 24 | 25 | public static final String ZERO_HASH = Hex.encodeHexString(EMPTY_BYTES); 26 | 27 | /** 28 | * 将多个字节数组合并成一个字节数组 29 | * 30 | * @param bytes 31 | * @return 32 | */ 33 | public static byte[] merge(byte[]... bytes) { 34 | Stream stream = Stream.of(); 35 | for (byte[] b : bytes) { 36 | stream = Stream.concat(stream, Arrays.stream(ArrayUtils.toObject(b))); 37 | } 38 | return ArrayUtils.toPrimitive(stream.toArray(Byte[]::new)); 39 | } 40 | 41 | /** 42 | * long 类型转 byte[] 43 | * 44 | * @param val 45 | * @return 46 | */ 47 | public static byte[] toBytes(long val) { 48 | return ByteBuffer.allocate(Long.BYTES).putLong(val).array(); 49 | } 50 | 51 | /** 52 | * int 类型转 byte[] 53 | * 54 | * @param val 55 | * @return 56 | */ 57 | public static byte[] toBytes(int val) { 58 | return ByteBuffer.allocate(Integer.BYTES).putInt(val).array(); 59 | } 60 | 61 | /** 62 | * byte[] 转化为 int 63 | * 64 | * @param bytes 65 | * @return 66 | */ 67 | public static int toInt(byte[] bytes) { 68 | return ByteBuffer.wrap(bytes).getInt(); 69 | } 70 | 71 | /** 72 | * 字节数组转16进制字符串 73 | * 74 | * @param src 75 | * @return 76 | */ 77 | public static String bytesToHexString(byte[] src) { 78 | StringBuilder stringBuilder = new StringBuilder(""); 79 | if (src == null || src.length <= 0) { 80 | return null; 81 | } 82 | for (int i = 0; i < src.length; i++) { 83 | int v = src[i] & 0xFF; 84 | String hex = Integer.toHexString(v); 85 | if (hex.length() < 2) { 86 | stringBuilder.append(0); 87 | } 88 | stringBuilder.append(hex); 89 | } 90 | return stringBuilder.toString(); 91 | } 92 | 93 | /** 94 | * 16进制字符串转换成字节数组 95 | * 96 | * @param hex 97 | * @return 98 | */ 99 | public static byte[] hexStringToByte(String hex) { 100 | byte[] b = new byte[hex.length() / 2]; 101 | int j = 0; 102 | for (int i = 0; i < b.length; i++) { 103 | char c0 = hex.charAt(j++); 104 | char c1 = hex.charAt(j++); 105 | b[i] = (byte) ((parse(c0) << 4) | parse(c1)); 106 | } 107 | return b; 108 | } 109 | 110 | private static int parse(char c) { 111 | if (c >= 'a') { 112 | return (c - 'a' + 10) & 0x0f; 113 | } 114 | if (c >= 'A') { 115 | return (c - 'A' + 10) & 0x0f; 116 | } 117 | return (c - '0') & 0x0f; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/util/SerializeUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.util; 6 | 7 | import com.esotericsoftware.kryo.Kryo; 8 | import com.esotericsoftware.kryo.io.Input; 9 | import com.esotericsoftware.kryo.io.Output; 10 | /** 11 | * 序列化工具类 12 | * @author chenhx 13 | * @version SerializeUtils.java, v 0.1 2018-10-11 下午 7:40 14 | */ 15 | public class SerializeUtils { 16 | /** 17 | * 反序列化 18 | * 19 | * @param bytes 对象对应的字节数组 20 | * @return 21 | */ 22 | public static Object deserialize(byte[] bytes) { 23 | Input input = new Input(bytes); 24 | Object obj = new Kryo().readClassAndObject(input); 25 | input.close(); 26 | return obj; 27 | } 28 | 29 | /** 30 | * 序列化 31 | * 32 | * @param object 需要序列化的对象 33 | * @return 34 | */ 35 | public static byte[] serialize(Object object) { 36 | Output output = new Output(4096, -1); 37 | new Kryo().writeClassAndObject(output, object); 38 | byte[] bytes = output.toBytes(); 39 | output.close(); 40 | return bytes; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/util/redis/RedisCommand.java: -------------------------------------------------------------------------------- 1 | package com.uifuture.springbootblockchain.util.redis; 2 | 3 | import org.springframework.data.redis.connection.DataType; 4 | import org.springframework.data.redis.core.ZSetOperations; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.TreeSet; 10 | 11 | /** 12 | * Redis操作的接口 13 | * @author chenhx 14 | * @version RedisHandle.java, v 0.1 2018-10-11 下午 9:18 15 | */ 16 | public interface RedisCommand { 17 | 18 | /** 19 | * 排序通过注册时间的 权重值 20 | * 21 | * @param date 22 | * @return 23 | */ 24 | double getCreateTimeScore(long date); 25 | 26 | /** 27 | * 添加一个list 28 | * 29 | * @param key 30 | * @param objectList 31 | */ 32 | void addList(K key, List objectList); 33 | 34 | /** 35 | * 向list中增加值 36 | * 37 | * @param key 38 | * @param obj 39 | * @return 返回在list中的下标 40 | */ 41 | long addList(K key, V obj); 42 | 43 | /** 44 | * 向list中增加值 45 | * 46 | * @param key 47 | * @param obj 48 | * @return 返回在list中的下标 49 | */ 50 | long addList(K key, V... obj); 51 | 52 | /** 53 | * 输出list 54 | * 55 | * @param key List的key 56 | * @param s 开始下标 57 | * @param e 结束的下标 58 | * @return 59 | */ 60 | List getList(K key, long s, long e); 61 | 62 | /** 63 | * 输出完整的list 64 | * 65 | * @param key 66 | */ 67 | List getList(K key); 68 | 69 | /** 70 | * 获取list集合中元素的个数 71 | * 72 | * @param key 73 | * @return 74 | */ 75 | long getListSize(K key); 76 | 77 | /** 78 | * 移除list中某值 79 | * 移除list中 count个value为object的值,并且返回移除的数量, 80 | * 如果count为0,或者大于list中为value为object数量的总和, 81 | * 那么移除所有value为object的值,并且返回移除数量 82 | * 83 | * @param key 84 | * @param object 85 | * @return 返回移除数量 86 | */ 87 | long removeListValue(K key, V object); 88 | 89 | /** 90 | * 移除list中某值 91 | * 92 | * @param key 93 | * @param object 94 | * @return 返回移除数量 95 | */ 96 | long removeListValue(K key, V... object); 97 | 98 | /** 99 | * 批量删除key对应的value 100 | * 101 | * @param keys 102 | */ 103 | void remove(final K... keys); 104 | 105 | /** 106 | * 删除缓存 107 | * 根据key精确匹配删除 108 | * 不只是针对键值对key-string-value 109 | * 亲测Map可删除 其他的应该也是可以删除的 110 | * 111 | * @param key 112 | */ 113 | void remove(final K key); 114 | 115 | /** 116 | * 通过分数删除ZSet中的值 117 | * 118 | * @param key 119 | * @param s 120 | * @param e 121 | */ 122 | void removeZSetRangeByScore(String key, double s, double e); 123 | 124 | /** 125 | * 设置Set的过期时间 126 | * 127 | * @param key 128 | * @param time 129 | * @return 130 | */ 131 | Boolean setSetExpireTime(String key, Long time); 132 | 133 | /** 134 | * 设置ZSet的过期时间 135 | * 136 | * @param key 137 | * @param time 138 | * @return 139 | */ 140 | Boolean setZSetExpireTime(String key, Long time); 141 | 142 | /** 143 | * 判断缓存中是否有key对应的value 144 | * 145 | * @param key 146 | * @return 147 | */ 148 | Boolean exists(final K key); 149 | 150 | /** 151 | * 读取String缓存 可以是对象 152 | * 153 | * @param key 154 | * @return 155 | */ 156 | V get(final K key); 157 | 158 | /** 159 | * 读取String缓存 可以是对象 160 | * 161 | * @param key 162 | * @return 163 | */ 164 | List get(final K... key); 165 | 166 | 167 | /** 168 | * 写入缓存 可以是对象 169 | * 170 | * @param key 171 | * @param value 172 | */ 173 | void set(final K key, V value); 174 | 175 | /** 176 | * 写入缓存 177 | * 178 | * @param key 179 | * @param value 180 | * @param expireTime 过期时间 -单位s 181 | * @return 182 | */ 183 | void set(final K key, V value, Long expireTime); 184 | 185 | /** 186 | * 设置一个key的过期时间(单位:秒) 187 | * 188 | * @param key 189 | * @param expireTime 190 | * @return 191 | */ 192 | Boolean setExpireTime(K key, Long expireTime); 193 | 194 | /** 195 | * 获取key的类型 196 | * 197 | * @param key 198 | * @return 199 | */ 200 | DataType getType(K key); 201 | 202 | /** 203 | * 设置map过期时间 204 | * 205 | * @param key 206 | * @param time 207 | * @return 208 | */ 209 | Boolean setMapExpireTime(String key, Long time); 210 | 211 | /** 212 | * 删除map中的某个对象 213 | * 214 | * @param key map对应的key 215 | * @param field map中该对象的key 216 | */ 217 | void removeMapField(K key, V... field); 218 | 219 | /** 220 | * 获取map对象 221 | * @param key map对应的key 222 | * @return 223 | */ 224 | Map getMap(K key); 225 | 226 | /** 227 | * 获取map中对象的大小 228 | * @param key map对应的key 229 | * @return 230 | */ 231 | Long getMapSize(K key); 232 | 233 | /** 234 | * 获取map缓存中的某个对象 235 | * @param key map对应的key 236 | * @param field map中该对象的key 237 | * @return 238 | */ 239 | T getMapField(K key, K field); 240 | 241 | /** 242 | * 判断map中对应key的key是否存在 243 | * 244 | * @param key map对应的key 245 | * @return 246 | */ 247 | Boolean hasMapKey(K key, K field); 248 | 249 | /** 250 | * 获取map对应key的value 251 | * 252 | * @param key map对应的key 253 | * @return 254 | */ 255 | List getMapFieldValue(K key); 256 | 257 | /** 258 | * 获取map的key 259 | * 260 | * @param key map对应的key 261 | * @return 262 | */ 263 | Set getMapFieldKey(K key); 264 | 265 | /** 266 | * 添加map 267 | * 268 | * @param key 269 | * @param map 270 | */ 271 | void addMap(K key, Map map); 272 | 273 | /** 274 | * 向key对应的map中添加缓存对象 275 | * 276 | * @param key cache对象key 277 | * @param field map对应的key 278 | * @param value 值 279 | */ 280 | void addMap(K key, K field, Object value); 281 | 282 | /** 283 | * 向key对应的map中添加缓存对象 284 | * 285 | * @param key cache对象key 286 | * @param field map对应的key 287 | * @param time 过期时间-整个MAP的过期时间 288 | * @param value 值 289 | */ 290 | void addMap(K key, K field, V value, long time); 291 | 292 | /** 293 | * 向set中加入对象 294 | * 295 | * @param key 对象key 296 | * @param obj 值 297 | */ 298 | void addSet(K key, V... obj); 299 | 300 | /** 301 | * 处理事务时锁定key 302 | * 303 | * @param key 304 | */ 305 | void watch(String key); 306 | 307 | /** 308 | * 移除set中的某些值 309 | * 310 | * @param key 对象key 311 | * @param obj 值 312 | */ 313 | long removeSetValue(K key, V obj); 314 | 315 | /** 316 | * 移除set中的某些值 317 | * 318 | * @param key 对象key 319 | * @param obj 值 320 | */ 321 | long removeSetValue(K key, V... obj); 322 | 323 | /** 324 | * 获取set的对象数 325 | * 326 | * @param key 对象key 327 | */ 328 | long getSetSize(K key); 329 | 330 | /** 331 | * 判断set中是否存在这个值 332 | * 333 | * @param key 对象key 334 | */ 335 | Boolean hasSetValue(K key, V obj); 336 | 337 | /** 338 | * 获得整个set 339 | * 340 | * @param key 对象key 341 | */ 342 | Set getSet(K key); 343 | 344 | /** 345 | * 获得set 并集 346 | * 347 | * @param key 348 | * @param otherKey 349 | * @return 350 | */ 351 | Set getSetUnion(K key, K otherKey); 352 | 353 | /** 354 | * 获得set 并集 355 | * 356 | * @param key 357 | * @param set 358 | * @return 359 | */ 360 | Set getSetUnion(K key, Set set); 361 | 362 | /** 363 | * 获得set 交集 364 | * 365 | * @param key 366 | * @param otherKey 367 | * @return 368 | */ 369 | Set getSetIntersect(K key, K otherKey); 370 | 371 | /** 372 | * 获得set 交集 373 | * 374 | * @param key 375 | * @param set 376 | * @return 377 | */ 378 | Set getSetIntersect(K key, Set set); 379 | 380 | /** 381 | * 模糊移除 支持*号等匹配移除 382 | * 383 | * @param blears 384 | */ 385 | void removeBlear(K... blears); 386 | 387 | /** 388 | * 修改key名 如果不存在该key或者没有修改成功返回false 389 | * 390 | * @param oldKey 391 | * @param newKey 392 | * @return 393 | */ 394 | Boolean renameIfAbsent(String oldKey, String newKey); 395 | 396 | /** 397 | * 根据正则表达式来移除 Map中的key-value 398 | * 399 | * @param key 400 | * @param blear 401 | */ 402 | void removeMapFieldByRegular(K key, K blear); 403 | 404 | /** 405 | * 移除key 对应的value 406 | * 407 | * @param key 408 | * @param value 409 | * @return 410 | */ 411 | Long removeZSetValue(K key, V... value); 412 | 413 | /** 414 | * 移除key Set 415 | * 416 | * @param key 417 | * @return 418 | */ 419 | void removeSet(K key); 420 | 421 | /** 422 | * 移除key ZSet 423 | * 424 | * @param key 425 | * @return 426 | */ 427 | void removeZSet(K key); 428 | 429 | /** 430 | * 删除,键为K的集合,索引start<=index<=end的元素子集 431 | * 432 | * @param key 433 | * @param start 434 | * @param end 435 | * @return 436 | */ 437 | void removeZSetRange(K key, Long start, Long end); 438 | 439 | /** 440 | * 并集 将key对应的集合和key1对应的集合合并到key2中 441 | * 如果分数相同的值,都会保留 442 | * 原来key2的值会被覆盖 443 | * 444 | * @param key 445 | * @param key1 446 | * @param key2 447 | */ 448 | void setZSetUnionAndStore(String key, String key1, String key2); 449 | 450 | /** 451 | * 获取整个有序集合ZSET,正序 452 | * 453 | * @param key 454 | */ 455 | T getZSetRange(K key); 456 | 457 | /** 458 | * 获取有序集合ZSET 459 | * 键为K的集合,索引start<=index<=end的元素子集,正序 460 | * 461 | * @param key 462 | * @param start 开始位置 463 | * @param end 结束位置 464 | */ 465 | T getZSetRange(K key, long start, long end); 466 | 467 | /** 468 | * 获取整个有序集合ZSET,倒序 469 | * 470 | * @param key 471 | */ 472 | Set getZSetReverseRange(K key); 473 | 474 | /** 475 | * 获取有序集合ZSET 476 | * 键为K的集合,索引start<=index<=end的元素子集,倒序 477 | * 478 | * @param key 479 | * @param start 开始位置 480 | * @param end 结束位置 481 | */ 482 | Set getZSetReverseRange(K key, long start, long end); 483 | 484 | /** 485 | * 通过分数(权值)获取ZSET集合 正序 -从小到大 486 | * 487 | * @param key 488 | * @param start 489 | * @param end 490 | * @return 491 | */ 492 | Set getZSetRangeByScore(String key, double start, double end); 493 | 494 | /** 495 | * 通过分数(权值)获取ZSET集合 倒序 -从大到小 496 | * 497 | * @param key 498 | * @param start 499 | * @param end 500 | * @return 501 | */ 502 | Set getZSetReverseRangeByScore(String key, double start, double end); 503 | 504 | /** 505 | * 键为K的集合,索引start<=index<=end的元素子集 506 | * 返回泛型接口(包括score和value),正序 507 | * 508 | * @param key 509 | * @param start 510 | * @param end 511 | * @return 512 | */ 513 | Set> getZSetRangeWithScores(K key, long start, long end); 514 | 515 | /** 516 | * 键为K的集合,索引start<=index<=end的元素子集 517 | * 返回泛型接口(包括score和value),倒序 518 | * 519 | * @param key 520 | * @param start 521 | * @param end 522 | * @return 523 | */ 524 | Set> getZSetReverseRangeWithScores(K key, long start, long end); 525 | 526 | /** 527 | * 键为K的集合 528 | * 返回泛型接口(包括score和value),正序 529 | * 530 | * @param key 531 | * @return 532 | */ 533 | Set> getZSetRangeWithScores(K key); 534 | 535 | /** 536 | * 键为K的集合 537 | * 返回泛型接口(包括score和value),倒序 538 | * 539 | * @param key 540 | * @return 541 | */ 542 | Set> getZSetReverseRangeWithScores(K key); 543 | 544 | /** 545 | * 键为K的集合,sMin<=score<=sMax的元素个数 546 | * 547 | * @param key 548 | * @param sMin 549 | * @param sMax 550 | * @return 551 | */ 552 | long getZSetCountSize(K key, double sMin, double sMax); 553 | 554 | /** 555 | * 获取Zset 键为K的集合元素个数 556 | * 557 | * @param key 558 | * @return 559 | */ 560 | long getZSetSize(K key); 561 | 562 | /** 563 | * 获取键为K的集合,value为obj的元素分数 564 | * 565 | * @param key 566 | * @param value 567 | * @return 568 | */ 569 | double getZSetScore(K key, V value); 570 | 571 | /** 572 | * 元素分数增加,delta是增量 573 | * 574 | * @param key 575 | * @param value 576 | * @param delta 577 | * @return 578 | */ 579 | double incrementZSetScore(K key, V value, double delta); 580 | 581 | /** 582 | * 添加有序集合ZSET 583 | * 默认按照score升序排列,存储格式K(1)==V(n),V(1)=S(1) 584 | * 585 | * @param key 586 | * @param score 587 | * @param value 588 | * @return 589 | */ 590 | Boolean addZSet(String key, double score, Object value); 591 | 592 | /** 593 | * 添加有序集合ZSET 594 | * 595 | * @param key 596 | * @param value 597 | * @return 598 | */ 599 | Long addZSet(K key, TreeSet value); 600 | 601 | /** 602 | * 添加有序集合ZSET 603 | * 604 | * @param key 605 | * @param score 606 | * @param value 607 | * @return 608 | */ 609 | Boolean addZSet(K key, double[] score, Object[] value); 610 | 611 | /** 612 | * 添加map并设置时间 613 | * 614 | * @param key 615 | * @param fk 616 | * @param value 617 | * @param time 618 | * @return 619 | */ 620 | boolean setMapAndExpireTimeByRedis(K key, K fk, V value, Long time); 621 | } 622 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/util/redis/RedisHandle.java: -------------------------------------------------------------------------------- 1 | package com.uifuture.springbootblockchain.util.redis; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.redis.connection.DataType; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.data.redis.core.ZSetOperations; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.util.CollectionUtils; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import java.util.TreeSet; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.regex.Pattern; 19 | 20 | /** 21 | * 增删改 -不能在这里面抓取异常 -因为可能有事务处理 22 | * @author chenhx 23 | * @version RedisHandle.java, v 0.1 2018-10-11 下午 9:18 24 | */ 25 | @Component 26 | public class RedisHandle implements RedisCommand { 27 | /** 28 | * 默认缓存时间为365天 29 | */ 30 | private static final Long TIME_CACHE = 60 * 60 * 24 * 365L; 31 | private static final Logger LOGGER = LoggerFactory.getLogger(RedisHandle.class); 32 | @Autowired 33 | protected RedisTemplate redisTemplate; 34 | 35 | @Override 36 | public double getCreateTimeScore(long date) { 37 | return date / 100000.0; 38 | } 39 | 40 | 41 | @Override 42 | public void addList(String key, List objectList) { 43 | for (Object obj : objectList) { 44 | addList(key, obj); 45 | } 46 | } 47 | 48 | @Override 49 | public long addList(String key, Object obj) { 50 | return redisTemplate.boundListOps(key).rightPush(obj); 51 | } 52 | 53 | @Override 54 | public long addList(String key, Object... obj) { 55 | return redisTemplate.boundListOps(key).rightPushAll(obj); 56 | } 57 | 58 | @Override 59 | public List getList(String key, long s, long e) { 60 | return redisTemplate.boundListOps(key).range(s, e); 61 | } 62 | 63 | @Override 64 | public List getList(String key) { 65 | return redisTemplate.boundListOps(key).range(0, getListSize(key)); 66 | } 67 | 68 | @Override 69 | public long getListSize(String key) { 70 | return redisTemplate.boundListOps(key).size(); 71 | } 72 | 73 | @Override 74 | public long removeListValue(String key, Object object) { 75 | return redisTemplate.boundListOps(key).remove(0, object); 76 | } 77 | 78 | @Override 79 | public long removeListValue(String key, Object... objects) { 80 | long r = 0; 81 | for (Object object : objects) { 82 | r += removeListValue(key, object); 83 | } 84 | return r; 85 | } 86 | 87 | @Override 88 | public void remove(String... key) { 89 | if (key != null && key.length > 0) { 90 | if (key.length == 1) { 91 | remove(key[0]); 92 | } else { 93 | redisTemplate.delete(CollectionUtils.arrayToList(key)); 94 | } 95 | } 96 | } 97 | 98 | @Override 99 | public void removeBlear(String... blears) { 100 | for (String blear : blears) { 101 | removeBlear(blear); 102 | } 103 | } 104 | 105 | @Override 106 | public Boolean renameIfAbsent(String oldKey, String newKey) { 107 | return redisTemplate.renameIfAbsent(oldKey, newKey); 108 | } 109 | 110 | @Override 111 | public void removeMapFieldByRegular(String key, String blear) { 112 | Map map = getMap(key); 113 | Set stringSet = map.keySet(); 114 | for (String s : stringSet) { 115 | if (Pattern.compile(blear).matcher(s).matches()) { 116 | redisTemplate.boundHashOps(key).delete(s); 117 | } 118 | } 119 | } 120 | 121 | @Override 122 | public Long removeZSetValue(String key, Object... value) { 123 | return redisTemplate.boundZSetOps(key).remove(value); 124 | } 125 | 126 | @Override 127 | public void removeSet(String key) { 128 | //通过设置过期时间移除set 129 | redisTemplate.boundSetOps(key).expire(0, TimeUnit.SECONDS); 130 | } 131 | 132 | @Override 133 | public void removeZSet(String key) { 134 | removeZSetRange(key, 0L, getZSetSize(key)); 135 | } 136 | 137 | @Override 138 | public void removeZSetRange(String key, Long start, Long end) { 139 | redisTemplate.boundZSetOps(key).removeRange(start, end); 140 | } 141 | 142 | @Override 143 | public void setZSetUnionAndStore(String key, String key1, String key2) { 144 | redisTemplate.boundZSetOps(key).unionAndStore(key1, key2); 145 | } 146 | 147 | @Override 148 | public Set getZSetRange(String key) { 149 | return getZSetRange(key, 0, getZSetSize(key)); 150 | } 151 | 152 | @Override 153 | public Set getZSetRange(String key, long s, long e) { 154 | return redisTemplate.boundZSetOps(key).range(s, e); 155 | } 156 | 157 | @Override 158 | public Set getZSetReverseRange(String key) { 159 | return getZSetReverseRange(key, 0, getZSetSize(key)); 160 | } 161 | 162 | @Override 163 | public Set getZSetReverseRange(String key, long start, long end) { 164 | return redisTemplate.boundZSetOps(key).reverseRange(start, end); 165 | } 166 | 167 | @Override 168 | public Set getZSetRangeByScore(String key, double start, double end) { 169 | return redisTemplate.boundZSetOps(key).rangeByScore(start, end); 170 | } 171 | 172 | @Override 173 | public Set getZSetReverseRangeByScore(String key, double start, double end) { 174 | return redisTemplate.boundZSetOps(key).reverseRangeByScore(start, end); 175 | } 176 | 177 | @Override 178 | public Set> getZSetRangeWithScores(String key, long start, long end) { 179 | return redisTemplate.boundZSetOps(key).rangeWithScores(start, end); 180 | } 181 | 182 | @Override 183 | public Set> getZSetReverseRangeWithScores(String key, long start, long end) { 184 | return redisTemplate.boundZSetOps(key).reverseRangeWithScores(start, end); 185 | } 186 | 187 | @Override 188 | public Set> getZSetRangeWithScores(String key) { 189 | return getZSetRangeWithScores(key, 0, getZSetSize(key)); 190 | } 191 | 192 | @Override 193 | public Set> getZSetReverseRangeWithScores(String key) { 194 | return getZSetReverseRangeWithScores(key, 0, getZSetSize(key)); 195 | } 196 | 197 | @Override 198 | public long getZSetCountSize(String key, double sMin, double sMax) { 199 | return redisTemplate.boundZSetOps(key).count(sMin, sMax); 200 | } 201 | 202 | @Override 203 | public long getZSetSize(String key) { 204 | return redisTemplate.boundZSetOps(key).size(); 205 | } 206 | 207 | @Override 208 | public double getZSetScore(String key, Object value) { 209 | return redisTemplate.boundZSetOps(key).score(value); 210 | } 211 | 212 | @Override 213 | public double incrementZSetScore(String key, Object value, double delta) { 214 | return redisTemplate.boundZSetOps(key).incrementScore(value, delta); 215 | } 216 | 217 | @Override 218 | public Boolean addZSet(String key, double score, Object value) { 219 | return redisTemplate.boundZSetOps(key).add(value, score); 220 | } 221 | 222 | @Override 223 | public Long addZSet(String key, TreeSet value) { 224 | return redisTemplate.boundZSetOps(key).add(value); 225 | } 226 | 227 | @Override 228 | public Boolean addZSet(String key, double[] score, Object[] value) { 229 | if (score.length != value.length) { 230 | return false; 231 | } 232 | for (int i = 0; i < score.length; i++) { 233 | if (addZSet(key, score[i], value[i]) == false) { 234 | return false; 235 | } 236 | } 237 | return true; 238 | } 239 | 240 | @Override 241 | public void remove(String key) { 242 | if (exists(key)) { 243 | redisTemplate.delete(key); 244 | } 245 | } 246 | 247 | @Override 248 | public void removeZSetRangeByScore(String key, double s, double e) { 249 | redisTemplate.boundZSetOps(key).removeRangeByScore(s, e); 250 | } 251 | 252 | @Override 253 | public Boolean setSetExpireTime(String key, Long time) { 254 | return redisTemplate.boundSetOps(key).expire(time, TimeUnit.SECONDS); 255 | } 256 | 257 | @Override 258 | public Boolean setZSetExpireTime(String key, Long time) { 259 | return redisTemplate.boundZSetOps(key).expire(time, TimeUnit.SECONDS); 260 | } 261 | 262 | @Override 263 | public Boolean exists(String key) { 264 | return redisTemplate.hasKey(key); 265 | } 266 | 267 | @Override 268 | public Object get(String key) { 269 | return redisTemplate.boundValueOps(key).get(); 270 | } 271 | 272 | @Override 273 | public List get(String... keys) { 274 | List list = new ArrayList(); 275 | for (String key : keys) { 276 | list.add(get(key)); 277 | } 278 | return list; 279 | } 280 | 281 | @Override 282 | public void set(String key, Object value) { 283 | set(key, value, TIME_CACHE); 284 | } 285 | 286 | @Override 287 | public void set(String key, Object value, Long expireTime) { 288 | redisTemplate.boundValueOps(key).set(value, expireTime, TimeUnit.SECONDS); 289 | } 290 | 291 | @Override 292 | public Boolean setExpireTime(String key, Long expireTime) { 293 | return redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); 294 | } 295 | 296 | 297 | @Override 298 | public DataType getType(String key) { 299 | return redisTemplate.type(key); 300 | } 301 | 302 | 303 | @Override 304 | public void removeMapField(String key, Object... field) { 305 | redisTemplate.boundHashOps(key).delete(field); 306 | } 307 | 308 | @Override 309 | public Long getMapSize(String key) { 310 | return redisTemplate.boundHashOps(key).size(); 311 | } 312 | 313 | @Override 314 | public Map getMap(String key) { 315 | return redisTemplate.boundHashOps(key).entries(); 316 | } 317 | 318 | @Override 319 | public T getMapField(String key, String field) { 320 | return (T) redisTemplate.boundHashOps(key).get(field); 321 | } 322 | 323 | @Override 324 | public Boolean hasMapKey(String key, String field) { 325 | return redisTemplate.boundHashOps(key).hasKey(field); 326 | } 327 | 328 | @Override 329 | public List getMapFieldValue(String key) { 330 | return redisTemplate.boundHashOps(key).values(); 331 | } 332 | 333 | @Override 334 | public Set getMapFieldKey(String key) { 335 | return redisTemplate.boundHashOps(key).keys(); 336 | } 337 | 338 | @Override 339 | public void addMap(String key, Map map) { 340 | redisTemplate.boundHashOps(key).putAll(map); 341 | } 342 | 343 | @Override 344 | public void addMap(String key, String field, Object value) { 345 | redisTemplate.boundHashOps(key).put(field, value); 346 | } 347 | 348 | @Override 349 | public void addMap(String key, String field, Object value, long time) { 350 | redisTemplate.boundHashOps(key).put(field, value); 351 | redisTemplate.boundHashOps(key).expire(time, TimeUnit.SECONDS); 352 | } 353 | 354 | @Override 355 | public void watch(String key) { 356 | redisTemplate.watch(key); 357 | } 358 | 359 | @Override 360 | public void addSet(String key, Object... obj) { 361 | redisTemplate.boundSetOps(key).add(obj); 362 | } 363 | 364 | @Override 365 | public long removeSetValue(String key, Object obj) { 366 | return redisTemplate.boundSetOps(key).remove(obj); 367 | } 368 | 369 | @Override 370 | public long removeSetValue(String key, Object... obj) { 371 | if (obj != null && obj.length > 0) { 372 | return redisTemplate.boundSetOps(key).remove(obj); 373 | } 374 | return 0L; 375 | } 376 | 377 | @Override 378 | public long getSetSize(String key) { 379 | return redisTemplate.boundSetOps(key).size(); 380 | } 381 | 382 | @Override 383 | public Boolean hasSetValue(String key, Object obj) { 384 | return redisTemplate.boundSetOps(key).isMember(obj); 385 | } 386 | 387 | @Override 388 | public Set getSet(String key) { 389 | return redisTemplate.boundSetOps(key).members(); 390 | } 391 | 392 | @Override 393 | public Set getSetUnion(String key, String otherKey) { 394 | return redisTemplate.boundSetOps(key).union(otherKey); 395 | } 396 | 397 | @Override 398 | public Set getSetUnion(String key, Set set) { 399 | return redisTemplate.boundSetOps(key).union(set); 400 | } 401 | 402 | @Override 403 | public Set getSetIntersect(String key, String otherKey) { 404 | return redisTemplate.boundSetOps(key).intersect(otherKey); 405 | } 406 | 407 | @Override 408 | public Set getSetIntersect(String key, Set set) { 409 | return redisTemplate.boundSetOps(key).intersect(set); 410 | } 411 | 412 | @Override 413 | public Boolean setMapExpireTime(String key, Long time) { 414 | return redisTemplate.boundHashOps(key).expire(time, TimeUnit.SECONDS); 415 | } 416 | 417 | @Override 418 | public boolean setMapAndExpireTimeByRedis(String key, String fk, Object value, Long time) { 419 | addMap(key, fk, value); 420 | return setMapExpireTime(key, time); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/wallet/Wallet.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.wallet; 6 | 7 | import com.uifuture.springbootblockchain.util.Base58Check; 8 | import com.uifuture.springbootblockchain.util.BtcAddressUtils; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; 13 | import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; 14 | import org.bouncycastle.jce.ECNamedCurveTable; 15 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 16 | import org.bouncycastle.jce.spec.ECParameterSpec; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.IOException; 20 | import java.io.Serializable; 21 | import java.security.KeyPair; 22 | import java.security.KeyPairGenerator; 23 | import java.security.SecureRandom; 24 | import java.security.Security; 25 | 26 | /** 27 | * 钱包 28 | * 29 | * @author chenhx 30 | * @version Wallet.java, v 0.1 2018-10-16 下午 6:09 31 | */ 32 | 33 | @Data 34 | @AllArgsConstructor 35 | @Slf4j 36 | public class Wallet implements Serializable { 37 | private static final long serialVersionUID = 9064268200637906140L; 38 | /** 39 | * 校验码长度 40 | */ 41 | private static final int ADDRESS_CHECKSUM_LEN = 4; 42 | /** 43 | * 私钥 44 | */ 45 | private BCECPrivateKey privateKey; 46 | /** 47 | * 公钥 48 | */ 49 | private byte[] publicKey; 50 | 51 | 52 | public Wallet() { 53 | initWallet(); 54 | } 55 | 56 | /** 57 | * 初始化钱包 58 | */ 59 | private void initWallet() { 60 | try { 61 | KeyPair keyPair = newECKeyPair(); 62 | BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate(); 63 | BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic(); 64 | 65 | byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); 66 | 67 | this.setPrivateKey(privateKey); 68 | this.setPublicKey(publicKeyBytes); 69 | } catch (Exception e) { 70 | log.error("Fail to init wallet ! ", e); 71 | throw new RuntimeException("Fail to init wallet ! ", e); 72 | } 73 | } 74 | 75 | 76 | /** 77 | * 创建新的密钥对 78 | * 79 | * @return 80 | * @throws Exception 81 | */ 82 | private KeyPair newECKeyPair() throws Exception { 83 | // 注册 BC Provider 84 | Security.addProvider(new BouncyCastleProvider()); 85 | // 创建椭圆曲线算法的密钥对生成器,算法为 ECDSA 86 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); 87 | // 椭圆曲线(EC)域参数设定 88 | // bitcoin 为什么会选择 secp256k1,详见:https://bitcointalk.org/index.php?topic=151120.0 89 | ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); 90 | keyPairGenerator.initialize(ecSpec, new SecureRandom()); 91 | return keyPairGenerator.generateKeyPair(); 92 | } 93 | 94 | 95 | /** 96 | * 获取钱包地址 97 | * 98 | * @return 99 | */ 100 | public String getAddress() { 101 | try { 102 | // 1. 获取 ripemdHashedKey 103 | byte[] ripemdHashedKey = BtcAddressUtils.ripeMD160Hash(this.getPublicKey()); 104 | 105 | // 2. 添加版本 0x00 106 | ByteArrayOutputStream addrStream = new ByteArrayOutputStream(); 107 | addrStream.write((byte) 0); 108 | addrStream.write(ripemdHashedKey); 109 | byte[] versionedPayload = addrStream.toByteArray(); 110 | 111 | // 3. 计算校验码 112 | byte[] checksum = BtcAddressUtils.checksum(versionedPayload); 113 | 114 | // 4. 得到 version + paylod + checksum 的组合 115 | addrStream.write(checksum); 116 | byte[] binaryAddress = addrStream.toByteArray(); 117 | 118 | // 5. 执行Base58转换处理 119 | return Base58Check.rawBytesToBase58(binaryAddress); 120 | } catch (IOException e) { 121 | e.printStackTrace(); 122 | } 123 | throw new RuntimeException("Fail to get wallet address ! "); 124 | } 125 | 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/uifuture/springbootblockchain/wallet/WalletUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.springbootblockchain.wallet; 6 | 7 | import com.google.common.collect.Maps; 8 | import com.uifuture.springbootblockchain.util.Base58Check; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Cleanup; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | import javax.crypto.Cipher; 16 | import javax.crypto.CipherInputStream; 17 | import javax.crypto.CipherOutputStream; 18 | import javax.crypto.SealedObject; 19 | import javax.crypto.spec.SecretKeySpec; 20 | import java.io.BufferedInputStream; 21 | import java.io.BufferedOutputStream; 22 | import java.io.File; 23 | import java.io.FileInputStream; 24 | import java.io.FileOutputStream; 25 | import java.io.ObjectInputStream; 26 | import java.io.ObjectOutputStream; 27 | import java.io.Serializable; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.Map; 30 | import java.util.Set; 31 | 32 | /** 33 | * 钱包工具类 34 | * 35 | * @author chenhx 36 | * @version WalletUtils.java, v 0.1 2018-10-16 下午 6:09 37 | */ 38 | 39 | @Slf4j 40 | public class WalletUtils { 41 | /** 42 | * 钱包文件 43 | */ 44 | private final static String WALLET_FILE = "wallet.dat"; 45 | /** 46 | * 加密算法 47 | */ 48 | private static final String ALGORITHM = "AES"; 49 | /** 50 | * 密文 51 | */ 52 | private static final byte[] CIPHER_TEXT = "g8ZcuSjpysW60rqAQRotiD9men7bJVEz".getBytes(StandardCharsets.UTF_8); 53 | /** 54 | * 钱包工具实例 55 | */ 56 | private volatile static WalletUtils instance; 57 | 58 | private WalletUtils() { 59 | initWalletFile(); 60 | } 61 | 62 | /** 63 | * 获取钱包工具单例 64 | * 65 | * @return 66 | */ 67 | public static WalletUtils getInstance() { 68 | if (instance == null) { 69 | synchronized (WalletUtils.class) { 70 | if (instance == null) { 71 | instance = new WalletUtils(); 72 | } 73 | } 74 | } 75 | return instance; 76 | } 77 | 78 | /** 79 | * 初始化钱包文件 80 | */ 81 | private void initWalletFile() { 82 | File file = new File(WALLET_FILE); 83 | if (!file.exists()) { 84 | this.saveToDisk(new Wallets()); 85 | } else { 86 | this.loadFromDisk(); 87 | } 88 | } 89 | 90 | /** 91 | * 获取所有的钱包地址 92 | * 93 | * @return 94 | */ 95 | public Set getAddresses() { 96 | Wallets wallets = this.loadFromDisk(); 97 | return wallets.getAddresses(); 98 | } 99 | 100 | /** 101 | * 获取钱包数据 102 | * 103 | * @param address 钱包地址 104 | * @return 105 | */ 106 | public Wallet getWallet(String address) { 107 | Wallets wallets = this.loadFromDisk(); 108 | return wallets.getWallet(address); 109 | } 110 | 111 | /** 112 | * 创建钱包 113 | * 114 | * @return 115 | */ 116 | public Wallet createWallet() { 117 | Wallet wallet = new Wallet(); 118 | Wallets wallets = this.loadFromDisk(); 119 | wallets.addWallet(wallet); 120 | this.saveToDisk(wallets); 121 | return wallet; 122 | } 123 | 124 | /** 125 | * 保存钱包数据 126 | */ 127 | private void saveToDisk(Wallets wallets) { 128 | try { 129 | if (wallets == null) { 130 | log.error("Fail to save wallet to file ! wallets is null "); 131 | throw new Exception("ERROR: Fail to save wallet to file !"); 132 | } 133 | SecretKeySpec sks = new SecretKeySpec(CIPHER_TEXT, ALGORITHM); 134 | // Create cipher 135 | Cipher cipher = Cipher.getInstance(ALGORITHM); 136 | cipher.init(Cipher.ENCRYPT_MODE, sks); 137 | SealedObject sealedObject = new SealedObject(wallets, cipher); 138 | // Wrap the output stream 139 | @Cleanup CipherOutputStream cos = new CipherOutputStream( 140 | new BufferedOutputStream(new FileOutputStream(WALLET_FILE)), cipher); 141 | @Cleanup ObjectOutputStream outputStream = new ObjectOutputStream(cos); 142 | outputStream.writeObject(sealedObject); 143 | } catch (Exception e) { 144 | log.error("Fail to save wallet to disk !", e); 145 | throw new RuntimeException("Fail to save wallet to disk !"); 146 | } 147 | } 148 | 149 | /** 150 | * 加载钱包数据 151 | */ 152 | private Wallets loadFromDisk() { 153 | try { 154 | SecretKeySpec sks = new SecretKeySpec(CIPHER_TEXT, ALGORITHM); 155 | Cipher cipher = Cipher.getInstance(ALGORITHM); 156 | cipher.init(Cipher.DECRYPT_MODE, sks); 157 | try (CipherInputStream cipherInputStream = new CipherInputStream( 158 | new BufferedInputStream(new FileInputStream(WALLET_FILE)), cipher); 159 | ObjectInputStream inputStream = new ObjectInputStream(cipherInputStream)) { 160 | SealedObject sealedObject = (SealedObject) inputStream.readObject(); 161 | return (Wallets) sealedObject.getObject(cipher); 162 | } 163 | } catch (Exception e) { 164 | log.error("Fail to load wallet from disk ! ", e); 165 | throw new RuntimeException("Fail to load wallet from disk ! "); 166 | } 167 | } 168 | 169 | /** 170 | * 钱包存储对象 171 | */ 172 | @Data 173 | @NoArgsConstructor 174 | @AllArgsConstructor 175 | public static class Wallets implements Serializable { 176 | private static final long serialVersionUID = -1892644908836459804L; 177 | private Map walletMap = Maps.newHashMap(); 178 | 179 | /** 180 | * 添加钱包 181 | * 182 | * @param wallet 183 | */ 184 | private void addWallet(Wallet wallet) { 185 | try { 186 | this.walletMap.put(wallet.getAddress(), wallet); 187 | } catch (Exception e) { 188 | log.error("Fail to add wallet ! ", e); 189 | throw new RuntimeException("Fail to add wallet !"); 190 | } 191 | } 192 | 193 | /** 194 | * 获取所有的钱包地址 195 | * 196 | * @return 197 | */ 198 | Set getAddresses() { 199 | if (walletMap == null) { 200 | log.error("Fail to get address ! walletMap is null ! "); 201 | throw new RuntimeException("Fail to get addresses ! "); 202 | } 203 | return walletMap.keySet(); 204 | } 205 | 206 | /** 207 | * 获取钱包数据 208 | * 209 | * @param address 钱包地址 210 | * @return 211 | */ 212 | Wallet getWallet(String address) { 213 | // 检查钱包地址是否合法 214 | try { 215 | Base58Check.base58ToBytes(address); 216 | } catch (Exception e) { 217 | log.error("Fail to get wallet ! address invalid ! address=" + address, e); 218 | throw new RuntimeException("Fail to get wallet ! "); 219 | } 220 | Wallet wallet = walletMap.get(address); 221 | if (wallet == null) { 222 | log.error("Fail to get wallet ! wallet don`t exist ! address=" + address); 223 | throw new RuntimeException("Fail to get wallet ! "); 224 | } 225 | return wallet; 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #Redis\u914D\u7F6E start 2 | spring.redis.host=127.0.0.1 3 | spring.redis.port=6379 4 | spring.redis.password= 5 | minNumberOfActiveConnections=5 6 | maxReadIdleSeconds=120 7 | keepAlivePeriodSeconds=15 8 | pingTimeoutSeconds=5 9 | pingTTL=7 10 | autoDiscoveryPingFrequency=10 11 | leaderElectionTimeoutSeconds=5 12 | leaderRejectionTimeoutSeconds=10 -------------------------------------------------------------------------------- /src/test/java/com/uifuture/blockchainspringboot/BlockchainTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.blockchainspringboot; 6 | 7 | import com.uifuture.springbootblockchain.cli.CLI; 8 | 9 | /** 10 | * 测试 11 | * 12 | * @author chenhx 13 | * @version BlockchainTest.java, v 0.1 2018-10-11 下午 9:23 14 | */ 15 | public class BlockchainTest { 16 | 17 | public static void main(String[] args) { 18 | // 1AycK28gqDpWJ4e6oo2oqG9Smr43K3QcTW 219999765 179999060 19 | // 15J63sETC6WuoVKZbtKxbWXsiA9W6THRAv 180000235 540000705 20 | try { 21 | //创建钱包 22 | // String[] argss = {"createwallet"}; //18:35:21.926 [main] INFO com.uifuture.springbootblockchain.cli.CLI - wallet address : 16vdpva8tMNJXLjxafmbD7ZEwB3xoZ58AZ 23 | //创建区块链 24 | String[] argss = {"createblockchain", "-address", "16vdpva8tMNJXLjxafmbD7ZEwB3xoZ58AZ"}; 25 | // 打印所有钱包地址 26 | // String[] argss = {"printaddresses"}; 27 | //获取钱包余额 28 | // String[] argss = {"getbalance", "-address", "164mY79vVpcPhB8mjXmCLvjTmceazLfTTF"}; 29 | //交易 30 | // String[] argss = {"send", "-from", "1AycK28gqDpWJ4e6oo2oqG9Smr43K3QcTW" 31 | // , "-to", "15J63sETC6WuoVKZbtKxbWXsiA9W6THRAv", "-amount", "180000235"}; 32 | //打印链 33 | // String[] argss = {"printchain"}; 34 | //进行挖区块 35 | // String[] argss = {"mining", "-address", "164mY79vVpcPhB8mjXmCLvjTmceazLfTTF"}; 36 | //帮助 37 | // String[] argss = {"h"}; 38 | CLI cli = new CLI(argss); 39 | cli.parse(); 40 | } catch (Exception e) { 41 | e.printStackTrace(); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/uifuture/blockchainspringboot/ProofTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * fshows.com 3 | * Copyright (C) 2013-2018 All Rights Reserved. 4 | */ 5 | package com.uifuture.blockchainspringboot; 6 | 7 | import org.apache.commons.codec.digest.DigestUtils; 8 | 9 | /** 10 | * @author chenhx 11 | * @version ProofTest.java, v 0.1 2018-10-13 下午 4:04 12 | */ 13 | public class ProofTest { 14 | /** 15 | * Hashcash的工作量证明算法 16 | * 17 | * @param args 18 | */ 19 | public static void main(String[] args) { 20 | int x = 5; 21 | int y = 0; 22 | long s = System.currentTimeMillis(); 23 | String hash = DigestUtils.sha256Hex((x * y) + ""); 24 | while (!hash.endsWith("0000")) { 25 | y++; 26 | hash = DigestUtils.sha256Hex((x * y) + ""); 27 | } 28 | System.out.println("y=" + y + ",hash=" + hash); 29 | System.out.println("消耗时间:" + (System.currentTimeMillis() - s) + "ms"); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/test/java/com/uifuture/blockchainspringboot/SpringBootBlockchainApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.uifuture.blockchainspringboot; 2 | 3 | import com.uifuture.springbootblockchain.SpringBootBlockchainApplication; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.junit4.SpringRunner; 8 | 9 | @RunWith(SpringRunner.class) 10 | @SpringBootTest(classes = SpringBootBlockchainApplication.class) 11 | public class SpringBootBlockchainApplicationTests { 12 | 13 | @Test 14 | public void contextLoads() { 15 | } 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /target/classes/application.properties: -------------------------------------------------------------------------------- 1 | #Redis\u914D\u7F6E start 2 | spring.redis.host=127.0.0.1 3 | spring.redis.port=6379 4 | spring.redis.password= 5 | minNumberOfActiveConnections=5 6 | maxReadIdleSeconds=120 7 | keepAlivePeriodSeconds=15 8 | pingTimeoutSeconds=5 9 | pingTTL=7 10 | autoDiscoveryPingFrequency=10 11 | leaderElectionTimeoutSeconds=5 12 | leaderRejectionTimeoutSeconds=10 --------------------------------------------------------------------------------