mirror of
https://github.com/Qortal/AT.git
synced 2025-01-30 19:02:14 +00:00
Changed FunctionCodes that perform hashes to use variable-length data.
Before, hashing functions (e.g. MD5_A_INTO_B) would hash immediate data stored in A, putting the result into B. Also, that hash function would only hash the same number of bits as the hash output. For example, MD5_A_INTO_B would only hash the 16 bytes in A1 & A2. Now, hash functions use data-page offset stored in A1 and byte-length stored in A2. Renamed HASH160 to RMD160 and created new HASH160 which performs Bitcoin's double hash of RMD160(SHA256(data)). Refactored & added tests to cover.
This commit is contained in:
parent
9a6b49970e
commit
36029c132f
@ -451,19 +451,15 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* MD5 A into B<br>
|
* MD5 data (offset A1, length A2) into B<br>
|
||||||
* <tt>0x0200</tt>
|
* <tt>0x0200</tt>
|
||||||
|
* MD5 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* MD5 hash stored in B1 and B2. B3 and B4 are zeroed.
|
||||||
*/
|
*/
|
||||||
MD5_A_TO_B(0x0200, 0, false) {
|
MD5_A_TO_B(0x0200, 0, false) {
|
||||||
@Override
|
@Override
|
||||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
ByteBuffer messageByteBuffer = ByteBuffer.allocate(2 * MachineState.VALUE_SIZE);
|
byte[] message = getHashData(state);
|
||||||
messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
messageByteBuffer.putLong(state.a1);
|
|
||||||
messageByteBuffer.putLong(state.a2);
|
|
||||||
|
|
||||||
byte[] message = messageByteBuffer.array();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MessageDigest digester = MessageDigest.getInstance("MD5");
|
MessageDigest digester = MessageDigest.getInstance("MD5");
|
||||||
@ -482,20 +478,16 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Check MD5 of A matches B<br>
|
* Check MD5 of data (offset A1, length A2) matches B<br>
|
||||||
* <tt>0x0201</tt><br>
|
* <tt>0x0201</tt><br>
|
||||||
|
* MD5 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* Other MD5 hash is in B1 and B2. B3 and B4 are ignored.
|
||||||
* Returns 1 if true, 0 if false
|
* Returns 1 if true, 0 if false
|
||||||
*/
|
*/
|
||||||
CHECK_MD5_A_WITH_B(0x0201, 0, true) {
|
CHECK_MD5_A_WITH_B(0x0201, 0, true) {
|
||||||
@Override
|
@Override
|
||||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
ByteBuffer messageByteBuffer = ByteBuffer.allocate(2 * MachineState.VALUE_SIZE);
|
byte[] message = getHashData(state);
|
||||||
messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
messageByteBuffer.putLong(state.a1);
|
|
||||||
messageByteBuffer.putLong(state.a2);
|
|
||||||
|
|
||||||
byte[] message = messageByteBuffer.array();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MessageDigest digester = MessageDigest.getInstance("MD5");
|
MessageDigest digester = MessageDigest.getInstance("MD5");
|
||||||
@ -519,20 +511,15 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* HASH160 A into B<br>
|
* RIPE-MD160 data (offset A1, length A2) into B<br>
|
||||||
* <tt>0x0202</tt>
|
* <tt>0x0202</tt>
|
||||||
|
* RIPE-MD160 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* RIPE-MD160 hash stored in B1, B2 and LSB of B3. B4 is zeroed.
|
||||||
*/
|
*/
|
||||||
HASH160_A_TO_B(0x0202, 0, false) {
|
RMD160_A_TO_B(0x0202, 0, false) {
|
||||||
@Override
|
@Override
|
||||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
ByteBuffer messageByteBuffer = ByteBuffer.allocate(3 * MachineState.VALUE_SIZE);
|
byte[] message = getHashData(state);
|
||||||
messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
messageByteBuffer.putLong(state.a1);
|
|
||||||
messageByteBuffer.putLong(state.a2);
|
|
||||||
messageByteBuffer.putLong(state.a3);
|
|
||||||
|
|
||||||
byte[] message = messageByteBuffer.array();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MessageDigest digester = MessageDigest.getInstance("RIPEMD160");
|
MessageDigest digester = MessageDigest.getInstance("RIPEMD160");
|
||||||
@ -551,21 +538,16 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Check HASH160 of A matches B<br>
|
* Check RIPE-MD160 of data (offset A1, length A2) matches B<br>
|
||||||
* <tt>0x0203</tt><br>
|
* <tt>0x0203</tt><br>
|
||||||
|
* RIPE-MD160 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* Other RIPE-MD160 hash is in B1, B2 and LSB of B3. B4 is ignored.
|
||||||
* Returns 1 if true, 0 if false
|
* Returns 1 if true, 0 if false
|
||||||
*/
|
*/
|
||||||
CHECK_HASH160_A_WITH_B(0x0203, 0, true) {
|
CHECK_RMD160_A_WITH_B(0x0203, 0, true) {
|
||||||
@Override
|
@Override
|
||||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
ByteBuffer messageByteBuffer = ByteBuffer.allocate(3 * MachineState.VALUE_SIZE);
|
byte[] message = getHashData(state);
|
||||||
messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
messageByteBuffer.putLong(state.a1);
|
|
||||||
messageByteBuffer.putLong(state.a2);
|
|
||||||
messageByteBuffer.putLong(state.a3);
|
|
||||||
|
|
||||||
byte[] message = messageByteBuffer.array();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MessageDigest digester = MessageDigest.getInstance("RIPEMD160");
|
MessageDigest digester = MessageDigest.getInstance("RIPEMD160");
|
||||||
@ -591,21 +573,15 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* SHA256 A into B<br>
|
* SHA256 data (offset A1, length A2) into B<br>
|
||||||
* <tt>0x0204</tt>
|
* <tt>0x0204</tt>
|
||||||
|
* SHA256 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* SHA256 hash is stored in B1 through B4.
|
||||||
*/
|
*/
|
||||||
SHA256_A_TO_B(0x0204, 0, false) {
|
SHA256_A_TO_B(0x0204, 0, false) {
|
||||||
@Override
|
@Override
|
||||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
ByteBuffer messageByteBuffer = ByteBuffer.allocate(4 * MachineState.VALUE_SIZE);
|
byte[] message = getHashData(state);
|
||||||
messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
messageByteBuffer.putLong(state.a1);
|
|
||||||
messageByteBuffer.putLong(state.a2);
|
|
||||||
messageByteBuffer.putLong(state.a3);
|
|
||||||
messageByteBuffer.putLong(state.a4);
|
|
||||||
|
|
||||||
byte[] message = messageByteBuffer.array();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MessageDigest digester = MessageDigest.getInstance("SHA-256");
|
MessageDigest digester = MessageDigest.getInstance("SHA-256");
|
||||||
@ -624,22 +600,16 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Check SHA256 of A matches B<br>
|
* Check SHA256 of data (offset A1, length A2) matches B<br>
|
||||||
* <tt>0x0205</tt><br>
|
* <tt>0x0205</tt><br>
|
||||||
|
* SHA256 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* Other SHA256 hash is in B1 through B4.
|
||||||
* Returns 1 if true, 0 if false
|
* Returns 1 if true, 0 if false
|
||||||
*/
|
*/
|
||||||
CHECK_SHA256_A_WITH_B(0x0205, 0, true) {
|
CHECK_SHA256_A_WITH_B(0x0205, 0, true) {
|
||||||
@Override
|
@Override
|
||||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
ByteBuffer messageByteBuffer = ByteBuffer.allocate(4 * MachineState.VALUE_SIZE);
|
byte[] message = getHashData(state);
|
||||||
messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
messageByteBuffer.putLong(state.a1);
|
|
||||||
messageByteBuffer.putLong(state.a2);
|
|
||||||
messageByteBuffer.putLong(state.a3);
|
|
||||||
messageByteBuffer.putLong(state.a4);
|
|
||||||
|
|
||||||
byte[] message = messageByteBuffer.array();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MessageDigest digester = MessageDigest.getInstance("SHA-256");
|
MessageDigest digester = MessageDigest.getInstance("SHA-256");
|
||||||
@ -664,6 +634,75 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* HASH160 data (offset A1, length A2) into B<br>
|
||||||
|
* <tt>0x0206</tt>
|
||||||
|
* Bitcoin's HASH160 hash is equivalent to RMD160(SHA256(data)).<br>
|
||||||
|
* HASH160 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* HASH160 hash stored in B1, B2 and LSB of B3. B4 is zeroed.
|
||||||
|
*/
|
||||||
|
HASH160_A_TO_B(0x0206, 0, false) {
|
||||||
|
@Override
|
||||||
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
|
byte[] message = getHashData(state);
|
||||||
|
|
||||||
|
try {
|
||||||
|
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] sha256Digest = sha256Digester.digest(message);
|
||||||
|
|
||||||
|
MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160");
|
||||||
|
byte[] rmd160Digest = rmd160Digester.digest(sha256Digest);
|
||||||
|
|
||||||
|
ByteBuffer digestByteBuffer = ByteBuffer.wrap(rmd160Digest);
|
||||||
|
digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
state.b1 = digestByteBuffer.getLong();
|
||||||
|
state.b2 = digestByteBuffer.getLong();
|
||||||
|
state.b3 = (long) digestByteBuffer.getInt() & 0xffffffffL;
|
||||||
|
state.b4 = 0L;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new ExecutionException("No SHA-256 or RIPEMD160 message digest service available", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check HASH160 of data (offset A1, length A2) matches B<br>
|
||||||
|
* <tt>0x0207</tt><br>
|
||||||
|
* HASH160 message data starts at address in A1 and byte-length is in A2.<br>
|
||||||
|
* Other HASH160 hash is in B1, B2 and LSB of B3. B4 is ignored.
|
||||||
|
* Returns 1 if true, 0 if false
|
||||||
|
*/
|
||||||
|
CHECK_HASH160_A_WITH_B(0x0207, 0, true) {
|
||||||
|
@Override
|
||||||
|
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||||
|
byte[] message = getHashData(state);
|
||||||
|
|
||||||
|
try {
|
||||||
|
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] sha256Digest = sha256Digester.digest(message);
|
||||||
|
|
||||||
|
MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160");
|
||||||
|
byte[] rmd160Digest = rmd160Digester.digest(sha256Digest);
|
||||||
|
|
||||||
|
ByteBuffer digestByteBuffer = ByteBuffer.allocate(rmd160Digester.getDigestLength());
|
||||||
|
digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
digestByteBuffer.putLong(state.b1);
|
||||||
|
digestByteBuffer.putLong(state.b2);
|
||||||
|
digestByteBuffer.putInt((int) (state.b3 & 0xffffffffL));
|
||||||
|
// NOTE: b4 ignored
|
||||||
|
|
||||||
|
byte[] expectedDigest = digestByteBuffer.array();
|
||||||
|
|
||||||
|
if (Arrays.equals(rmd160Digest, expectedDigest))
|
||||||
|
functionData.returnValue = 1L; // true
|
||||||
|
else
|
||||||
|
functionData.returnValue = 0L; // false
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new ExecutionException("No SHA-256 or RIPEMD160 message digest service available", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* <tt>0x0300</tt><br>
|
* <tt>0x0300</tt><br>
|
||||||
* Returns current block's "timestamp"
|
* Returns current block's "timestamp"
|
||||||
@ -922,7 +961,7 @@ public enum FunctionCode {
|
|||||||
public final int paramCount;
|
public final int paramCount;
|
||||||
public final boolean returnsValue;
|
public final boolean returnsValue;
|
||||||
|
|
||||||
private final static Map<Short, FunctionCode> map = Arrays.stream(FunctionCode.values())
|
private static final Map<Short, FunctionCode> map = Arrays.stream(FunctionCode.values())
|
||||||
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
|
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
|
||||||
|
|
||||||
private FunctionCode(int value, int paramCount, boolean returnsValue) {
|
private FunctionCode(int value, int paramCount, boolean returnsValue) {
|
||||||
@ -976,8 +1015,36 @@ public enum FunctionCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Actually execute function */
|
/** Actually execute function */
|
||||||
abstract protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
|
protected abstract void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
|
||||||
|
|
||||||
// TODO: public abstract String disassemble();
|
// TODO: public abstract String disassemble();
|
||||||
|
|
||||||
|
protected byte[] getHashData(MachineState state) throws ExecutionException {
|
||||||
|
// Validate data offset in A1
|
||||||
|
if (state.a1 < 0L || state.a1 > Integer.MAX_VALUE || state.a1 >= state.numDataPages)
|
||||||
|
throw new ExecutionException("MD5 data offset (A1) out of bounds");
|
||||||
|
|
||||||
|
// Validate data length in A2
|
||||||
|
if (state.a2 < 0L || state.a2 > Integer.MAX_VALUE || state.a1 + byteLengthToDataLength(state.a2) > state.numDataPages)
|
||||||
|
throw new ExecutionException("MD5 data length (A2) invalid");
|
||||||
|
|
||||||
|
final int dataStart = (int) (state.a1 & 0x7fffffffL);
|
||||||
|
final int dataLength = (int) (state.a2 & 0x7fffffffL);
|
||||||
|
|
||||||
|
byte[] message = new byte[dataLength];
|
||||||
|
|
||||||
|
ByteBuffer messageByteBuffer = state.dataByteBuffer.slice();
|
||||||
|
messageByteBuffer.position(dataStart * MachineState.VALUE_SIZE);
|
||||||
|
messageByteBuffer.limit(dataStart * MachineState.VALUE_SIZE + dataLength);
|
||||||
|
|
||||||
|
messageByteBuffer.get(message);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the number of data-page values to contain specific length of bytes. */
|
||||||
|
protected int byteLengthToDataLength(long byteLength) {
|
||||||
|
return (MachineState.VALUE_SIZE - 1 + (int) (byteLength & 0x7fffffffL)) / MachineState.VALUE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ public class DisassemblyTests {
|
|||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0);
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0);
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8fc71e8300000000"));
|
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8fc71e8300000000"));
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(0);
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(0);
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_A_WITH_B.value).putInt(1);
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_RMD160_A_WITH_B.value).putInt(1);
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.ECHO.value).putInt(1);
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.ECHO.value).putInt(1);
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import static common.TestUtils.hexToBytes;
|
import static common.TestUtils.hexToBytes;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import org.ciyam.at.ExecutionException;
|
import org.ciyam.at.ExecutionException;
|
||||||
import org.ciyam.at.FunctionCode;
|
import org.ciyam.at.FunctionCode;
|
||||||
import org.ciyam.at.OpCode;
|
import org.ciyam.at.OpCode;
|
||||||
@ -11,178 +13,49 @@ import common.ExecutableTest;
|
|||||||
|
|
||||||
public class FunctionCodeTests extends ExecutableTest {
|
public class FunctionCodeTests extends ExecutableTest {
|
||||||
|
|
||||||
|
private static final String message = "The quick, brown fox jumped over the lazy dog.";
|
||||||
|
private static final byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
private static final FunctionCode[] bSettingFunctions = new FunctionCode[] { FunctionCode.SET_B1, FunctionCode.SET_B2, FunctionCode.SET_B3, FunctionCode.SET_B4 };
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMD5() throws ExecutionException {
|
public void testMD5() throws ExecutionException {
|
||||||
// MD5 of ffffffffffffffffffffffffffffffff is 8d79cbc9a4ecdde112fc91ba625b13c2
|
testHash("MD5", FunctionCode.MD5_A_TO_B, FunctionCode.CHECK_A_EQUALS_B, "1388a82384756096e627e3671e2624bf");
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
// A3 unused
|
|
||||||
// A4 unused
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.MD5_A_TO_B.value);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8d79cbc9a4ecdde1"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12fc91ba625b13c2"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000000000000"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000000000000"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
|
||||||
|
|
||||||
execute(true);
|
|
||||||
|
|
||||||
assertTrue(state.getIsFinished());
|
|
||||||
assertFalse(state.getHadFatalError());
|
|
||||||
assertEquals("MD5 hashes do not match", 1L, getData(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCHECK_MD5() throws ExecutionException {
|
public void testCHECK_MD5() throws ExecutionException {
|
||||||
// MD5 of ffffffffffffffffffffffffffffffff is 8d79cbc9a4ecdde112fc91ba625b13c2
|
testHash("MD5", null, FunctionCode.CHECK_MD5_A_WITH_B, "1388a82384756096e627e3671e2624bf");
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
// A3 unused
|
|
||||||
// A4 unused
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8d79cbc9a4ecdde1"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12fc91ba625b13c2"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_MD5_A_WITH_B.value).putInt(1);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
|
||||||
|
|
||||||
execute(true);
|
|
||||||
|
|
||||||
assertTrue(state.getIsFinished());
|
|
||||||
assertFalse(state.getHadFatalError());
|
|
||||||
assertEquals("MD5 hashes do not match", 1L, getData(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHASH160() throws ExecutionException {
|
public void testRMD160() throws ExecutionException {
|
||||||
// RIPEMD160 of ffffffffffffffffffffffffffffffffffffffffffffffff is 90e735014ea23aa89190121b229c06d58fc71e83
|
testHash("RIPE-MD160", FunctionCode.RMD160_A_TO_B, FunctionCode.CHECK_A_EQUALS_B, "b5a4b1898af3745dbbb5becb83e72787df9952c9");
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0);
|
|
||||||
// A4 unused
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.HASH160_A_TO_B.value);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("90e735014ea23aa8"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("9190121b229c06d5"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8fc71e8300000000"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000000000000"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
|
||||||
|
|
||||||
execute(true);
|
|
||||||
|
|
||||||
assertTrue(state.getIsFinished());
|
|
||||||
assertFalse(state.getHadFatalError());
|
|
||||||
assertEquals("RIPEMD160 hashes do not match", 1L, getData(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCHECK_HASH160() throws ExecutionException {
|
public void testCHECK_RMD160() throws ExecutionException {
|
||||||
// RIPEMD160 of ffffffffffffffffffffffffffffffffffffffffffffffff is 90e735014ea23aa89190121b229c06d58fc71e83
|
testHash("RIPE-MD160", null, FunctionCode.CHECK_RMD160_A_WITH_B, "b5a4b1898af3745dbbb5becb83e72787df9952c9");
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0);
|
|
||||||
// A4 unused
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("90e735014ea23aa8"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("9190121b229c06d5"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8fc71e8300000000"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_A_WITH_B.value).putInt(1);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
|
||||||
|
|
||||||
execute(true);
|
|
||||||
|
|
||||||
assertEquals("RIPEMD160 hashes do not match", 1L, getData(1));
|
|
||||||
assertTrue(state.getIsFinished());
|
|
||||||
assertFalse(state.getHadFatalError());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSHA256() throws ExecutionException {
|
public void testSHA256() throws ExecutionException {
|
||||||
// SHA256 of ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff is af9613760f72635fbdb44a5a0a63c39f12af30f950a6ee5c971be188e89c4051
|
testHash("SHA256", FunctionCode.SHA256_A_TO_B, FunctionCode.CHECK_A_EQUALS_B, "c01d63749ebe5d6b16f7247015cac2e49a5ac4fb6c7f24bed07b8aa904da97f3");
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SHA256_A_TO_B.value);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("af9613760f72635f"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("bdb44a5a0a63c39f"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12af30f950a6ee5c"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("971be188e89c4051"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
|
||||||
|
|
||||||
execute(true);
|
|
||||||
|
|
||||||
assertTrue(state.getIsFinished());
|
|
||||||
assertFalse(state.getHadFatalError());
|
|
||||||
assertEquals("RIPEMD160 hashes do not match", 1L, getData(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCHECK_SHA256() throws ExecutionException {
|
public void testCHECK_SHA256() throws ExecutionException {
|
||||||
// SHA256 of ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff is af9613760f72635fbdb44a5a0a63c39f12af30f950a6ee5c971be188e89c4051
|
testHash("SHA256", null, FunctionCode.CHECK_SHA256_A_WITH_B, "c01d63749ebe5d6b16f7247015cac2e49a5ac4fb6c7f24bed07b8aa904da97f3");
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff"));
|
}
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("af9613760f72635f"));
|
@Test
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(0);
|
public void testHASH160() throws ExecutionException {
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("bdb44a5a0a63c39f"));
|
testHash("HASH160", FunctionCode.HASH160_A_TO_B, FunctionCode.CHECK_A_EQUALS_B, "54d54a03fd447996ab004dee87fab80bf9477e23");
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0);
|
}
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12af30f950a6ee5c"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(0);
|
|
||||||
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("971be188e89c4051"));
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(0);
|
|
||||||
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_SHA256_A_WITH_B.value).putInt(1);
|
@Test
|
||||||
|
public void testCHECK_HASH160() throws ExecutionException {
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
testHash("HASH160", null, FunctionCode.CHECK_HASH160_A_WITH_B, "54d54a03fd447996ab004dee87fab80bf9477e23");
|
||||||
|
|
||||||
execute(true);
|
|
||||||
|
|
||||||
assertEquals("RIPEMD160 hashes do not match", 1L, getData(1));
|
|
||||||
assertTrue(state.getIsFinished());
|
|
||||||
assertFalse(state.getHadFatalError());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -234,4 +107,57 @@ public class FunctionCodeTests extends ExecutableTest {
|
|||||||
assertTrue(state.getHadFatalError());
|
assertTrue(state.getHadFatalError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void testHash(String hashName, FunctionCode hashFunction, FunctionCode checkFunction, String expected) throws ExecutionException {
|
||||||
|
// Data addr 0 for setting values
|
||||||
|
dataByteBuffer.putLong(0L);
|
||||||
|
// Data addr 1 for results
|
||||||
|
dataByteBuffer.putLong(0L);
|
||||||
|
|
||||||
|
// Data addr 2+ for message
|
||||||
|
dataByteBuffer.put(messageBytes);
|
||||||
|
|
||||||
|
// MD5 data start
|
||||||
|
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2L);
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0);
|
||||||
|
|
||||||
|
// MD5 data length
|
||||||
|
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(messageBytes.length);
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0);
|
||||||
|
|
||||||
|
// A3 unused
|
||||||
|
// A4 unused
|
||||||
|
|
||||||
|
// Optional hash function
|
||||||
|
if (hashFunction != null) {
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(hashFunction.value);
|
||||||
|
// Hash functions usually put result into B, but we need it in A
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expected result goes into B
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.CLEAR_B.value);
|
||||||
|
// Each 16 hex-chars (8 bytes) fits into each B word (B1, B2, B3 and B4)
|
||||||
|
for (int bWord = 0; bWord < 4 && bWord * 16 < expected.length(); ++bWord) {
|
||||||
|
final int beginIndex = bWord * 16;
|
||||||
|
final int endIndex = Math.min(expected.length(), beginIndex + 16);
|
||||||
|
|
||||||
|
String hexChars = expected.substring(beginIndex, endIndex);
|
||||||
|
codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes(hexChars));
|
||||||
|
codeByteBuffer.put(new byte[8 - hexChars.length() / 2]); // pad with zeros
|
||||||
|
|
||||||
|
final FunctionCode bSettingFunction = bSettingFunctions[bWord];
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(bSettingFunction.value).putInt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(checkFunction.value).putInt(1);
|
||||||
|
|
||||||
|
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
||||||
|
|
||||||
|
execute(true);
|
||||||
|
|
||||||
|
assertTrue("MachineState isn't in finished state", state.getIsFinished());
|
||||||
|
assertFalse("MachineState encountered fatal error", state.getHadFatalError());
|
||||||
|
assertEquals(hashName + " hashes do not match", 1L, getData(1));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,16 @@ import org.junit.BeforeClass;
|
|||||||
|
|
||||||
public abstract class ExecutableTest {
|
public abstract class ExecutableTest {
|
||||||
|
|
||||||
|
public static final int CODE_STACK_SIZE = 0x0200;
|
||||||
public static final int DATA_OFFSET = 6 * 2 + 8;
|
public static final int DATA_OFFSET = 6 * 2 + 8;
|
||||||
public static final int CALL_STACK_OFFSET = DATA_OFFSET + 0x0020 * 8;
|
public static final int DATA_STACK_SIZE = 0x0200;
|
||||||
|
public static final int CALL_STACK_OFFSET = DATA_OFFSET + DATA_STACK_SIZE * 8;
|
||||||
|
|
||||||
public TestLogger logger;
|
public TestLogger logger;
|
||||||
public TestAPI api;
|
public TestAPI api;
|
||||||
public MachineState state;
|
public MachineState state;
|
||||||
public ByteBuffer codeByteBuffer;
|
public ByteBuffer codeByteBuffer;
|
||||||
|
public ByteBuffer dataByteBuffer;
|
||||||
public ByteBuffer stateByteBuffer;
|
public ByteBuffer stateByteBuffer;
|
||||||
public int callStackSize;
|
public int callStackSize;
|
||||||
public int userStackOffset;
|
public int userStackOffset;
|
||||||
@ -35,7 +38,8 @@ public abstract class ExecutableTest {
|
|||||||
public void beforeTest() {
|
public void beforeTest() {
|
||||||
logger = new TestLogger();
|
logger = new TestLogger();
|
||||||
api = new TestAPI();
|
api = new TestAPI();
|
||||||
codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);
|
codeByteBuffer = ByteBuffer.allocate(CODE_STACK_SIZE).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
dataByteBuffer = ByteBuffer.allocate(DATA_STACK_SIZE).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
stateByteBuffer = null;
|
stateByteBuffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,15 +47,16 @@ public abstract class ExecutableTest {
|
|||||||
public void afterTest() {
|
public void afterTest() {
|
||||||
stateByteBuffer = null;
|
stateByteBuffer = null;
|
||||||
codeByteBuffer = null;
|
codeByteBuffer = null;
|
||||||
|
dataByteBuffer = null;
|
||||||
api = null;
|
api = null;
|
||||||
logger = null;
|
logger = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void execute(boolean onceOnly) {
|
protected void execute(boolean onceOnly) {
|
||||||
// version 0002, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 4, minActivation = 0
|
// version 0002, reserved 0000, code 0200 * 1, data 0200 * 8, call stack 0010 * 4, user stack 0010 * 4, minActivation = 0
|
||||||
byte[] headerBytes = hexToBytes("0200" + "0000" + "0002" + "2000" + "1000" + "1000" + "0000000000000000");
|
byte[] headerBytes = hexToBytes("0200" + "0000" + "0002" + "0002" + "1000" + "1000" + "0000000000000000");
|
||||||
byte[] codeBytes = codeByteBuffer.array();
|
byte[] codeBytes = codeByteBuffer.array();
|
||||||
byte[] dataBytes = new byte[0];
|
byte[] dataBytes = dataByteBuffer.array();
|
||||||
|
|
||||||
state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes);
|
state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes);
|
||||||
|
|
||||||
@ -93,7 +98,7 @@ public abstract class ExecutableTest {
|
|||||||
byte[] stateBytes = state.toBytes();
|
byte[] stateBytes = state.toBytes();
|
||||||
|
|
||||||
// We know how the state will be serialized so we can extract values
|
// We know how the state will be serialized so we can extract values
|
||||||
// header(6) + data(0x0020 * 8) + callStack length(4) + callStack + userStack length(4) + userStack
|
// header(6) + data(size * 8) + callStack length(4) + callStack + userStack length(4) + userStack
|
||||||
|
|
||||||
stateByteBuffer = ByteBuffer.wrap(stateBytes).order(ByteOrder.LITTLE_ENDIAN);
|
stateByteBuffer = ByteBuffer.wrap(stateBytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
callStackSize = stateByteBuffer.getInt(CALL_STACK_OFFSET);
|
callStackSize = stateByteBuffer.getInt(CALL_STACK_OFFSET);
|
||||||
|
Loading…
Reference in New Issue
Block a user