├── .gitignore ├── README.md ├── build.gradle ├── docs_cn.md ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── com └── hecochain └── metatx ├── MetaTx.java ├── MetaTxDemo.java └── signer ├── Constant.java ├── MetaData.java ├── MetaRawData.java ├── MetaTxService.java └── Util.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | .idea 13 | gradle 14 | 15 | # Mobile Tools for Java (J2ME) 16 | .mtj.tmp/ 17 | 18 | # Package Files # 19 | *.jar 20 | *.war 21 | *.nar 22 | *.ear 23 | *.zip 24 | *.tar.gz 25 | *.rar 26 | 27 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 28 | hs_err_pid* 29 | 30 | ### Gradle template 31 | .gradle 32 | **/build/ 33 | !src/**/build/ 34 | 35 | # Ignore Gradle GUI config 36 | gradle-app.setting 37 | 38 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 39 | !gradle-wrapper.jar 40 | 41 | # Cache of project 42 | .gradletasknamecache 43 | 44 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 45 | # gradle/wrapper/gradle-wrapper.properties 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [中文介绍](./docs_cn.md) 4 | 5 | There are several steps to integrate meta tx: 6 | 7 | 1) decode original tx 8 | 9 | 2) compose a meta tx format 10 | 11 | 3) send to network 12 | 13 | please follow the demo to intergate -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.hecochain' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | maven { 10 | url 'https://dl.bintray.com/ethereum/maven/' 11 | } 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | compile("org.web3j:core:4.3.0") 17 | compile("org.web3j:crypto:4.3.0") 18 | compile("org.ethereum:ethereumj-core:1.10.0-RELEASE") 19 | 20 | compileOnly 'org.projectlombok:lombok:1.18.10' 21 | annotationProcessor 'org.projectlombok:lombok:1.18.10' 22 | 23 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' 24 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' 25 | } 26 | 27 | test { 28 | useJUnitPlatform() 29 | } -------------------------------------------------------------------------------- /docs_cn.md: -------------------------------------------------------------------------------- 1 | 2 | # 元交易解析 3 | 4 | ## 元交易原理 5 | 6 | 1. 代付手续费地址 A,签名已经封装好的签名信息SignedTx中的一些信息,并且生成签名 data。封装该 data 到 SignedTx 的 data 中。整体节点解析以后,会根据 data 中的 feePercent 进行手续费的扣除折算。 7 | 2. 元交易标识:SignedTx 的 data hex String 的开头为 0x234d6574615472616e73616374696f6e23 8 | 9 | ## 元交易的构造流程 10 | 11 | * 1. 获取初始签名交易 SignedTx。 (hex String) 12 | * 2. 解析 SignedTx,获取其对应的 nonce, gasprice,gaslimit,from地址,to 地址,value,data 13 | 14 | 15 | 16 | 17 | ``` 18 | Transaction transaction = new Transaction(Hex.decode(this.removeHexPrefix(rawTx))); 19 | ``` 20 | 21 | * 3. RLP 按照 nonce,gasprice,gaslimit,to addr,value,data,from 地址,feePercent, blockNumLimit, chainId(手续费折扣比例,万分之一计算)进行编码获取待签名的数据 22 | 23 | ``` 24 | 25 | MetaRawData metaRawData = new MetaRawData( 26 | transaction.getNonce(), 27 | transaction.getGasPrice(), 28 | transaction.getGasLimit(), 29 | transaction.getReceiveAddress(), 30 | transaction.getValue(), 31 | transaction.getData(), 32 | transaction.getSender(), 33 | BigInteger.valueOf(feePercent), 34 | BigInteger.valueOf(blockNumber), 35 | BigInteger.valueOf(chainId) 36 | ); 37 | 38 | byte[] encodeData = metaRawData.getEncodeData(); 39 | 40 | // 编码方法为 41 | 42 | private synchronized void rlpEncode() { 43 | if (!this.isEncode) { 44 | List result = new ArrayList(); 45 | result.add(RlpString.create(nonce)); 46 | result.add(RlpString.create(gasPrice)); 47 | result.add(RlpString.create(gasLimit)); 48 | result.add(RlpString.create(receiveAddress)); 49 | 50 | result.add(RlpString.create(value)); 51 | result.add(RlpString.create(data)); 52 | result.add(RlpString.create(sendAddress)); 53 | result.add(RlpString.create(feePercent)); 54 | result.add(RlpString.create(blockNumber)); 55 | result.add(RlpString.create(chainId)); 56 | 57 | RlpList rlpList = new RlpList(result); 58 | this.rlpEncodeData = RlpEncoder.encode(rlpList); 59 | 60 | this.rawHash = HashUtil.sha3(this.rlpEncodeData); 61 | this.isEncode = true; 62 | } 63 | } 64 | 65 | ``` 66 | 67 | * 4. 代付手续费地址 A,签名第3步获取到的encodeData。并且依据chainId 组装成 EIP155的 Signature 68 | 69 | ``` 70 | private Sign.SignatureData getSignatureData(byte[] encodeData, String privateKey, int chainId) { 71 | ECKeyPair aPair = ECKeyPair.create(Numeric.toBigInt(privateKey)); 72 | Sign.SignatureData signatureData = Sign.signMessage(encodeData, aPair); 73 | Sign.SignatureData eip155SignatureData = TransactionEncoder.createEip155SignatureData(signatureData, metaChainId); 74 | 75 | return eip155SignatureData; 76 | } 77 | 78 | ``` 79 | 80 | * 5. 拼装最新的data字段, 将第4部的signaturedata 传入下面方法 81 | 82 | ``` 83 | private String getMetaData(Transaction transaction, int blockNumber, int feePercent, Sign.SignatureData signatureData) { 84 | MetaData metaData = new MetaData( 85 | transaction.getData(), 86 | BigInteger.valueOf(blockNumber), 87 | BigInteger.valueOf(feePercent), 88 | signatureData.getV(), 89 | signatureData.getR(), 90 | signatureData.getS() 91 | ); 92 | 93 | byte[] encodeData = metaData.getRlpEncoded(); 94 | 95 | return Constant.META_DATA_PREFIX + Hex.toHexString(encodeData); 96 | } 97 | 98 | // encode 规范为 99 | public byte[] getRlpEncoded() { 100 | if (this.rlpEncoded != null) { 101 | return this.rlpEncoded; 102 | } else { 103 | List finalResult = new ArrayList(); 104 | 105 | finalResult.add(RlpString.create(blockNumLimit)); 106 | finalResult.add(RlpString.create(feePercent)); 107 | finalResult.add(RlpString.create(Bytes.trimLeadingZeroes(this.V))); 108 | finalResult.add(RlpString.create(Bytes.trimLeadingZeroes(this.R))); 109 | finalResult.add(RlpString.create(Bytes.trimLeadingZeroes(this.S))); 110 | finalResult.add(RlpString.create(data)); 111 | 112 | RlpList finalRlpList = new RlpList(finalResult); 113 | byte[] finalEncodeData = RlpEncoder.encode(finalRlpList); 114 | this.rlpEncoded = finalEncodeData; 115 | return finalEncodeData; 116 | } 117 | } 118 | 119 | ``` 120 | 121 | * 6. 更新 SignedTx 中的 data 字段。 122 | 123 | ``` 124 | // 其中 metaData 是第5步获取到的 hex string 125 | 126 | Transaction tx = new Transaction(transaction.getNonce(), transaction.getGasPrice(), 127 | transaction.getGasLimit(), transaction.getReceiveAddress(), transaction.getValue(), 128 | ByteUtil.hexStringToBytes(metaData), ByteUtil.bigIntegerToBytes(transaction.getSignature().r),ByteUtil.bigIntegerToBytes(transaction.getSignature().s), 129 | transaction.getSignature().v, chainId); 130 | 131 | return "0x" + Hex.toHexString(tx.getEncoded()); 132 | 133 | ``` 134 | 135 | * 至此获取足够的信息进行广播 136 | 137 | ``` 138 | EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(metaSignedTx).send(); 139 | ``` 140 | 141 | ## 元交易decode 142 | 143 | ``` 144 | public void metaDataDecodeTest() throws Exception { 145 | // 元交易 hex string 146 | String rawhex = "0xf8c602850dbcac8e0082c35094000000000000000000000000000000000000000001b85e234d6574615472616e73616374696f6e23f84b822f930a83050a00a0e2e037042f160bca30ff54921eed7fd8886f5f5cd8e3f7c1a414f2d7f093a2f2a0615d0bd271e181897078229a6bc463269cb463dfb33cf31ba08c30fb9065c5c480830509ffa034ae31e249554cc4727b52581ff2055c0964f429b3ec66c90d98acd8f0e3eb9ba038337050ef236268a3ffd1ba0cd3a344193b93cdff92443bb9345c099e2fe30b"; 147 | 148 | // 拼装的 data 149 | String hex = "0x234d6574615472616e73616374696f6e23f84b822f930a83050a00a0e2e037042f160bca30ff54921eed7fd8886f5f5cd8e3f7c1a414f2d7f093a2f2a0615d0bd271e181897078229a6bc463269cb463dfb33cf31ba08c30fb9065c5c480"; 150 | String rawData = hex.substring(Constant.META_DATA_PREFIX.length()); 151 | System.out.println(rawData); 152 | 153 | // 解析拼装的 data,获取 blocknumlimit, feepercent, 原来交易的 data 154 | MetaData metaData = new MetaData(ByteUtil.hexStringToBytes(rawData)); 155 | 156 | System.out.println(metaData.getBlockNumLimit()); 157 | System.out.println(metaData.getFeePercent()); 158 | System.out.println(metaData.getRealV(BigInteger.valueOf(chainId))); 159 | System.out.println("data " + Hex.toHexString(metaData.getData())); 160 | Sign.SignatureData signatureData = metaData.getSignatureData(); 161 | 162 | System.out.println("r:" + Numeric.toHexString(signatureData.getR())); 163 | System.out.println("s:" + Numeric.toHexString(signatureData.getS())); 164 | System.out.println("v:" + Numeric.toHexString(signatureData.getV())); 165 | 166 | Transaction transaction = new Transaction(Hex.decode(Util.removeHexPrefix(rawhex))); 167 | 168 | Transaction transaction1 = new Transaction(transaction.getNonce(), transaction.getGasPrice(), 169 | transaction.getGasLimit(), transaction.getReceiveAddress(), transaction.getValue(), 170 | metaData.getData(), ByteUtil.bigIntegerToBytes(transaction.getSignature().r),ByteUtil.bigIntegerToBytes(transaction.getSignature().s), 171 | transaction.getSignature().v, chainId); 172 | 173 | 174 | 175 | MetaRawData metaRawData = new MetaRawData( 176 | transaction1.getNonce(), 177 | transaction1.getGasPrice(), 178 | transaction1.getGasLimit(), 179 | transaction1.getReceiveAddress(), 180 | transaction1.getValue(), 181 | transaction1.getData(), 182 | transaction1.getSender(), 183 | BigInteger.valueOf(feePercent), 184 | BigInteger.valueOf(metaData.getBlockNumLimit().intValue()), 185 | BigInteger.valueOf(chainId) 186 | ); 187 | byte[] encodeData = metaRawData.getEncodeData(); 188 | 189 | System.out.println("encode hash :" + Hex.toHexString(Hash.sha3(encodeData))); 190 | 191 | // 回复代支付手续费的地址 192 | ECDSASignature signature = new ECDSASignature(new BigInteger(1, signatureData.getR()), (new BigInteger(1, signatureData.getS()))); 193 | 194 | String addressRecovered = ""; 195 | BigInteger publicKey = Sign.recoverFromSignature(metaData.getRealV(BigInteger.valueOf(chainId)).intValue(), signature, Hash.sha3(encodeData)); 196 | byte[] pub = ByteUtil.bigIntegerToBytes(publicKey); 197 | if (publicKey != null) { 198 | addressRecovered = "0x" + Keys.getAddress(publicKey); 199 | System.out.println("recover address:" + addressRecovered); 200 | 201 | Assert.assertEquals("0xaE5FFda3163c68cDa9a9DBDd1c599A8337911d2d", addressRecovered.toLowerCase()); 202 | } 203 | 204 | } 205 | 206 | ``` 207 | 208 | ## 元交易链上处理 209 | 210 | ``` 211 | 如果交易 data 字段符合元交易的格式,并且能够正常解析,那么就进入 mempool。 否者,拒绝改交易,不能进入mempool。 212 | 213 | unc metaTransactionCheck(tx *types.Transaction, b Backend,) error { 214 | if types.IsMetaTransaction(tx.Data()) { 215 | metaData, err := types.DecodeMetaData(tx.Data(), b.CurrentBlock().Number()) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) 221 | from, err := signer.Sender(tx) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | addr, err := metaData.ParseMetaData(tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), metaData.Payload, from, b.ChainConfig().ChainID) 227 | if err != nil { 228 | return err 229 | } 230 | log.Debug("metaTransfer found, feeaddr:", addr.Hex() + " feePercent : " + strconv.FormatUint(metaData.FeePercent, 10)) 231 | } 232 | return nil 233 | } 234 | 235 | ``` -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'metatx-demo' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/hecochain/metatx/MetaTx.java: -------------------------------------------------------------------------------- 1 | package com.hecochain.metatx; 2 | 3 | import lombok.Data; 4 | 5 | import java.math.BigInteger; 6 | import java.util.Date; 7 | 8 | @Data 9 | public class MetaTx { 10 | 11 | private Long id; 12 | 13 | private String txHash; 14 | 15 | private long feePercent; 16 | 17 | private String rawTx; 18 | 19 | private String metaRawTx; 20 | 21 | private BigInteger blockHeight; 22 | 23 | private String rawTxHash; 24 | 25 | private BigInteger from_addr_balance; 26 | 27 | private Date created; 28 | 29 | private Date updated; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/hecochain/metatx/MetaTxDemo.java: -------------------------------------------------------------------------------- 1 | package com.hecochain.metatx; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.hecochain.metatx.signer.MetaData; 5 | import com.hecochain.metatx.signer.MetaTxService; 6 | import com.hecochain.metatx.signer.Util; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.bouncycastle.util.encoders.Hex; 9 | import org.ethereum.util.ByteUtil; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.web3j.protocol.Web3j; 12 | import org.web3j.protocol.core.DefaultBlockParameterName; 13 | import org.ethereum.core.*; 14 | import org.web3j.protocol.http.HttpService; 15 | 16 | import java.math.BigInteger; 17 | 18 | @Slf4j 19 | public class MetaTxDemo { 20 | 21 | @Autowired 22 | private MetaTxService metaTxService; 23 | 24 | @Autowired 25 | private ObjectMapper objectMapper; 26 | 27 | public void encode(String tx) throws Exception { 28 | //your rpc 29 | Web3j web3j = Web3j.build(new HttpService("https://http-mainnet-node.huobichain.com")); 30 | 31 | int blockNumber = web3j.ethBlockNumber().send().getBlockNumber().intValue(); 32 | 33 | Transaction transaction = new Transaction(Hex.decode(Util.removeHexPrefix(tx))); 34 | String from = Hex.toHexString(transaction.getSender()); 35 | BigInteger balance = web3j.ethGetBalance(Util.addHexPrefix(from), DefaultBlockParameterName.LATEST).send().getBalance(); 36 | 37 | Integer feePercent = 1000;//1000/10000 38 | String privateKey = "acdd38021ad3d7400aba48dec54a2a21d0f5196bb4835a0458ffe930f99afed3";//your private key, the account should have balance 39 | int chainId = 128;//mainnet 128; testnet 256 40 | int blockExp = blockNumber + 100; // set a expire block 41 | 42 | Transaction metaTransaction = metaTxService.getMetaSignedRawTx(tx, feePercent, privateKey, chainId, blockExp); 43 | 44 | String metaRawTx = "0x" + Hex.toHexString(metaTransaction.getEncoded()); 45 | log.debug("raw tx : {}, meta tx : {}", tx, metaRawTx); 46 | 47 | //send this to the network or response to user 48 | 49 | // EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(metaRawTx).send(); 50 | } 51 | 52 | public void decode() throws Exception { 53 | Transaction tx = new Transaction(Hex.decode("f8d0820113843b9aca0082c3509488b7790f47e8875c0f8cf63e507c550586be55d688016345785d8a0000b860234d6574615472616e73616374696f6e23f84d831c96648207d0820124a0d98bcbc63448eebfe290d287cd63dcda129e0ae10c87aa10777909eeb336ac39a022510088aef0f7221f9a95575efe9476baf4e66560920891c8dca091aca0cb6e80820123a04d7f8d6a891b2b88297e97f6a09cb19e6713c49fc3f3bd7bf14a981833c03729a01250b7dfa70c9d74c9d010e6aeb03de1e8616b3074d297b2a08a15238e72114e")); 54 | String rawData = Hex.toHexString(tx.getData()); 55 | System.out.println(rawData); 56 | MetaData metaData = new MetaData(ByteUtil.hexStringToBytes(rawData.substring("234d6574615472616e73616374696f6e23".length()))); 57 | metaData.rlpPares(); 58 | System.out.println("hh"); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/hecochain/metatx/signer/Constant.java: -------------------------------------------------------------------------------- 1 | package com.hecochain.metatx.signer; 2 | 3 | public class Constant { 4 | public static final String META_DATA_PREFIX = "0x234d6574615472616e73616374696f6e23"; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/hecochain/metatx/signer/MetaData.java: -------------------------------------------------------------------------------- 1 | package com.hecochain.metatx.signer; 2 | 3 | import org.ethereum.util.ByteUtil; 4 | import org.web3j.crypto.Sign; 5 | import org.web3j.rlp.*; 6 | import org.web3j.utils.Bytes; 7 | import org.web3j.utils.Numeric; 8 | 9 | import java.math.BigInteger; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class MetaData { 14 | 15 | private BigInteger blockNumLimit; 16 | 17 | private BigInteger feePercent; 18 | 19 | private byte[] R; 20 | 21 | private byte[] S; 22 | 23 | private byte[] V; 24 | 25 | private byte[] data; 26 | 27 | private byte[] rlpEncoded; 28 | 29 | private Sign.SignatureData signatureData; 30 | 31 | protected boolean parsed; 32 | 33 | public MetaData(byte[] rawData) { 34 | this.rlpEncoded = rawData; 35 | this.parsed = false; 36 | } 37 | 38 | public MetaData(byte[] data, BigInteger blockNumLimit, BigInteger feePercent, byte[] V, byte[] R, byte[] S) { 39 | this.data = data == null ? ByteUtil.EMPTY_BYTE_ARRAY : data; 40 | this.blockNumLimit = blockNumLimit; 41 | this.feePercent = feePercent; 42 | this.V = V; 43 | this.S = S; 44 | this.R = R; 45 | this.parsed = true; 46 | this.rlpEncoded = null; 47 | } 48 | 49 | public byte[] getRlpEncoded() { 50 | if (this.rlpEncoded != null) { 51 | return this.rlpEncoded; 52 | } else { 53 | List finalResult = new ArrayList(); 54 | 55 | finalResult.add(RlpString.create(blockNumLimit)); 56 | finalResult.add(RlpString.create(feePercent)); 57 | finalResult.add(RlpString.create(Bytes.trimLeadingZeroes(this.V))); 58 | finalResult.add(RlpString.create(Bytes.trimLeadingZeroes(this.R))); 59 | finalResult.add(RlpString.create(Bytes.trimLeadingZeroes(this.S))); 60 | finalResult.add(RlpString.create(data)); 61 | 62 | RlpList finalRlpList = new RlpList(finalResult); 63 | byte[] finalEncodeData = RlpEncoder.encode(finalRlpList); 64 | this.rlpEncoded = finalEncodeData; 65 | return finalEncodeData; 66 | } 67 | } 68 | 69 | public synchronized void rlpPares() throws Exception { 70 | if (!this.parsed) { 71 | RlpList list = RlpDecoder.decode(this.rlpEncoded); 72 | RlpList values = (RlpList) list.getValues().get(0); 73 | 74 | if (values.getValues().size() < 6) { 75 | throw new Exception("rlp length does not right"); 76 | } 77 | 78 | this.blockNumLimit = ((RlpString) values.getValues().get(0)).asPositiveBigInteger(); 79 | this.feePercent = ((RlpString) values.getValues().get(1)).asPositiveBigInteger(); 80 | 81 | this.V = Numeric.toBytesPadded( 82 | Numeric.toBigInt(((RlpString) values.getValues().get(2)).getBytes()), 32); 83 | this.R = Numeric.toBytesPadded( 84 | Numeric.toBigInt(((RlpString) values.getValues().get(3)).getBytes()), 32); 85 | this.S = Numeric.toBytesPadded( 86 | Numeric.toBigInt(((RlpString) values.getValues().get(4)).getBytes()), 32); 87 | 88 | this.data = ((RlpString) values.getValues().get(5)).getBytes(); 89 | 90 | this.signatureData = new Sign.SignatureData(V, R, S); 91 | this.parsed = true; 92 | } 93 | } 94 | 95 | public BigInteger getFeePercent() { 96 | try { 97 | this.rlpPares(); 98 | } catch (Exception e) { 99 | return null; 100 | } 101 | return this.feePercent; 102 | } 103 | 104 | public byte[] getData() { 105 | try { 106 | this.rlpPares(); 107 | } catch (Exception e) { 108 | return null; 109 | } 110 | return this.data; 111 | } 112 | 113 | public BigInteger getBlockNumLimit() { 114 | try { 115 | this.rlpPares(); 116 | } catch (Exception e) { 117 | return null; 118 | } 119 | 120 | return this.blockNumLimit; 121 | } 122 | 123 | public Sign.SignatureData getSignatureData() { 124 | try { 125 | this.rlpPares(); 126 | } catch (Exception e) { 127 | return null; 128 | } 129 | 130 | return this.signatureData; 131 | } 132 | 133 | public BigInteger getRealV(BigInteger chainId) { 134 | try { 135 | this.rlpPares(); 136 | } catch (Exception e) { 137 | return null; 138 | } 139 | 140 | BigInteger chainIDMul = new BigInteger("2").multiply(chainId); 141 | return new BigInteger(1, this.signatureData.getV()).subtract(BigInteger.valueOf(35)).subtract(chainIDMul); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/hecochain/metatx/signer/MetaRawData.java: -------------------------------------------------------------------------------- 1 | package com.hecochain.metatx.signer; 2 | 3 | import org.ethereum.crypto.HashUtil; 4 | import org.ethereum.util.ByteUtil; 5 | import org.web3j.rlp.RlpEncoder; 6 | import org.web3j.rlp.RlpList; 7 | import org.web3j.rlp.RlpString; 8 | import org.web3j.rlp.RlpType; 9 | 10 | import java.math.BigInteger; 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class MetaRawData { 16 | private byte[] nonce; 17 | private byte[] gasPrice; 18 | private byte[] gasLimit; 19 | private byte[] receiveAddress; 20 | private byte[] value; 21 | private byte[] data; 22 | protected byte[] sendAddress; 23 | private BigInteger feePercent; 24 | private BigInteger chainId; 25 | private BigInteger blockNumber; 26 | private byte[] rlpEncodeData; 27 | private byte[] rawHash; 28 | private boolean isEncode; 29 | 30 | public MetaRawData(byte[] nonce, byte[] gasPrice, byte[] gasLimit, byte[] receiveAddress, byte[] value, byte[] data, byte[] senderAddress, BigInteger feePercent, BigInteger blockNumber, BigInteger chainId) { 31 | this.chainId = chainId; 32 | this.nonce = Arrays.equals(nonce, ByteUtil.ZERO_BYTE_ARRAY) ? ByteUtil.EMPTY_BYTE_ARRAY : nonce; 33 | this.gasPrice = Arrays.equals(gasPrice, ByteUtil.ZERO_BYTE_ARRAY) ? ByteUtil.EMPTY_BYTE_ARRAY : gasPrice; 34 | this.gasLimit = Arrays.equals(gasLimit, ByteUtil.ZERO_BYTE_ARRAY) ? ByteUtil.EMPTY_BYTE_ARRAY : gasLimit; 35 | this.receiveAddress = receiveAddress == null ? ByteUtil.EMPTY_BYTE_ARRAY : receiveAddress; 36 | this.value = Arrays.equals(value, ByteUtil.ZERO_BYTE_ARRAY) ? ByteUtil.EMPTY_BYTE_ARRAY : value; 37 | this.data = data == null ? ByteUtil.EMPTY_BYTE_ARRAY : data; 38 | this.chainId = chainId; 39 | this.feePercent = feePercent; 40 | this.sendAddress = senderAddress; 41 | this.blockNumber = blockNumber; 42 | this.isEncode = false; 43 | this.rlpEncodeData = null; 44 | this.rawHash = null; 45 | } 46 | 47 | private synchronized void rlpEncode() { 48 | if (!this.isEncode) { 49 | List result = new ArrayList(); 50 | result.add(RlpString.create(nonce)); 51 | result.add(RlpString.create(gasPrice)); 52 | result.add(RlpString.create(gasLimit)); 53 | result.add(RlpString.create(receiveAddress)); 54 | 55 | result.add(RlpString.create(value)); 56 | result.add(RlpString.create(data)); 57 | result.add(RlpString.create(sendAddress)); 58 | result.add(RlpString.create(feePercent)); 59 | result.add(RlpString.create(blockNumber)); 60 | result.add(RlpString.create(chainId)); 61 | 62 | RlpList rlpList = new RlpList(result); 63 | this.rlpEncodeData = RlpEncoder.encode(rlpList); 64 | 65 | this.rawHash = HashUtil.sha3(this.rlpEncodeData); 66 | this.isEncode = true; 67 | } 68 | } 69 | 70 | public byte[] getEncodeData() { 71 | if (this.rlpEncodeData != null) { 72 | return this.rlpEncodeData; 73 | } 74 | this.rlpEncode(); 75 | return this.rlpEncodeData; 76 | } 77 | 78 | public byte[] getHash() { 79 | if (this.rawHash != null) { 80 | return this.rawHash; 81 | } 82 | this.rlpEncode(); 83 | return this.rawHash; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/hecochain/metatx/signer/MetaTxService.java: -------------------------------------------------------------------------------- 1 | package com.hecochain.metatx.signer; 2 | 3 | import org.bouncycastle.util.encoders.Hex; 4 | import org.ethereum.core.Transaction; 5 | import org.ethereum.util.ByteUtil; 6 | import org.web3j.crypto.ECKeyPair; 7 | import org.web3j.crypto.Sign; 8 | import org.web3j.crypto.TransactionEncoder; 9 | import org.web3j.utils.Numeric; 10 | 11 | import java.math.BigInteger; 12 | 13 | public class MetaTxService { 14 | 15 | public Transaction getMetaSignedRawTx(String rawTx, int feePercent, String privateKey, int chainId, int blockNumber) { 16 | Transaction transaction = new Transaction(Hex.decode(Util.removeHexPrefix(rawTx))); 17 | Sign.SignatureData signatureData = this.getMetaSignatureData(transaction, feePercent, privateKey, chainId, blockNumber); 18 | String metaData = this.getMetaData(transaction, blockNumber, feePercent, signatureData); 19 | 20 | Transaction transaction1 = new Transaction( 21 | transaction.getNonce(), 22 | transaction.getGasPrice(), 23 | transaction.getGasLimit(), 24 | transaction.getReceiveAddress(), 25 | transaction.getValue(), 26 | ByteUtil.hexStringToBytes(metaData), 27 | ByteUtil.bigIntegerToBytes(transaction.getSignature().r), 28 | ByteUtil.bigIntegerToBytes(transaction.getSignature().s), 29 | transaction.getSignature().v, 30 | transaction.getChainId()); 31 | 32 | // return "0x" + Hex.toHexString(transaction1.getEncoded()); 33 | return transaction1; 34 | 35 | } 36 | 37 | private Sign.SignatureData getMetaSignatureData(Transaction transaction, int feePercent, String privateKey, int chainId, int blockNumber) { 38 | //(byte[] nonce, byte[] gasPrice, byte[] gasLimit, byte[] receiveAddress, byte[] value, byte[] data, byte[] senderAddress, BigInteger feePercent, BigInteger blockNumber, BigInteger chainId) { 39 | MetaRawData metaRawData = new MetaRawData( 40 | transaction.getNonce(), 41 | transaction.getGasPrice(), 42 | transaction.getGasLimit(), 43 | transaction.getReceiveAddress(), 44 | transaction.getValue(), 45 | transaction.getData(), 46 | transaction.getSender(), 47 | BigInteger.valueOf(feePercent), 48 | BigInteger.valueOf(blockNumber), 49 | BigInteger.valueOf(chainId) 50 | ); 51 | byte[] encodeData = metaRawData.getEncodeData(); 52 | Sign.SignatureData eip155SignatureData = this.getSignatureData(encodeData, privateKey, chainId); 53 | 54 | return eip155SignatureData; 55 | 56 | } 57 | 58 | private Sign.SignatureData getSignatureData(byte[] encodeData, String privateKey, int chainId) { 59 | ECKeyPair aPair = ECKeyPair.create(Numeric.toBigInt(privateKey)); 60 | Sign.SignatureData signatureData = Sign.signMessage(encodeData, aPair); 61 | Sign.SignatureData eip155SignatureData = TransactionEncoder.createEip155SignatureData(signatureData, chainId); 62 | 63 | return eip155SignatureData; 64 | } 65 | 66 | private String getMetaData(Transaction transaction, int blockNumber, int feePercent, Sign.SignatureData signatureData) { 67 | MetaData metaData = new MetaData( 68 | transaction.getData(), 69 | BigInteger.valueOf(blockNumber), 70 | BigInteger.valueOf(feePercent), 71 | signatureData.getV(), 72 | signatureData.getR(), 73 | signatureData.getS() 74 | ); 75 | 76 | byte[] encodeData = metaData.getRlpEncoded(); 77 | 78 | return Constant.META_DATA_PREFIX + Hex.toHexString(encodeData); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/hecochain/metatx/signer/Util.java: -------------------------------------------------------------------------------- 1 | package com.hecochain.metatx.signer; 2 | 3 | import org.apache.commons.codec.binary.Base64; 4 | import org.springframework.util.StringUtils; 5 | 6 | import javax.crypto.Cipher; 7 | import javax.crypto.spec.SecretKeySpec; 8 | import java.security.GeneralSecurityException; 9 | import java.util.Arrays; 10 | 11 | public class Util { 12 | 13 | public static String removeHexPrefix(String input) { 14 | if (StringUtils.isEmpty(input)) { 15 | return ""; 16 | } 17 | 18 | String sub = "0x"; 19 | int index = input.indexOf(sub); 20 | if (index < 0) { 21 | return input; 22 | } else { 23 | return input.substring(index + 2); 24 | } 25 | } 26 | 27 | public static String addHexPrefix(String input) { 28 | if (StringUtils.isEmpty(input)){ 29 | return "0x0"; 30 | } 31 | String sub = "0x"; 32 | int index = input.indexOf(sub); 33 | if (index < 0) { 34 | return sub + input; 35 | } else { 36 | return input; 37 | } 38 | } 39 | 40 | public byte[] decryptAES(String cipherText, String password) throws GeneralSecurityException { 41 | byte[] cipherBytes = Base64.decodeBase64(cipherText); 42 | 43 | byte[] pwdBytes = pad(password.getBytes()); 44 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 45 | 46 | SecretKeySpec sKeySpec = new SecretKeySpec(pwdBytes, "AES"); 47 | cipher.init(Cipher.DECRYPT_MODE, sKeySpec); 48 | 49 | return cipher.doFinal(cipherBytes); 50 | } 51 | 52 | public String encryptAES(byte[] plainBytes, String password) throws GeneralSecurityException { 53 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 54 | byte[] keyBytes = new byte[32]; 55 | Arrays.fill(keyBytes, (byte) 0); 56 | 57 | System.arraycopy(password.getBytes(), 0, keyBytes, 0, password.length()); 58 | 59 | SecretKeySpec sKeySpec = new SecretKeySpec(keyBytes, "AES"); 60 | cipher.init(Cipher.ENCRYPT_MODE, sKeySpec); 61 | 62 | byte[] cipherBytes = cipher.doFinal(plainBytes); 63 | return Base64.encodeBase64String(cipherBytes); 64 | } 65 | 66 | private byte[] pad(byte[] data) { 67 | int blockSize = 32; 68 | byte[] padded = new byte[blockSize - data.length % blockSize + data.length]; 69 | System.arraycopy(data, 0, padded, 0, data.length); 70 | for (int i = data.length; i < padded.length; i++) { 71 | padded[i] = (byte) 0; 72 | } 73 | 74 | return padded; 75 | } 76 | } 77 | --------------------------------------------------------------------------------